基于Ubuntu编译内核并添加内核模块

First Post:

Last Update:

Word Count:
1.6k

Read Time:
6 min

本文详细记录在 Ubuntu 24.10 系统上编译 Linux 内核、创建最小化运行环境,并添加自定义内核模块的全过程。所有操作均在 VMware 虚拟机环境下验证通过。

使用的系统镜像是 ubuntu-24.10-desktop-amd64.iso

一、环境准备与内核编译

首先第一步是在桌面创建一个文件夹,这里我就取名为 linux ,在 linux 文件夹中打开终端

接着我们需要获取内核的源码,去 官网 https://kernel.org 下载最新的稳定版,这里我们就下载 stable:6.13.8 ,复制 tarball 的链接后,到前面在linux文件夹中打开的终端,输入:

1
2
# 下载稳定版内核源码(以6.13.8为例)
wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.13.8.tar.xz

在下载完成之后我们需要对文件夹进行解压缩,输入:

1
2
# 解压源码包
tar -xf linux-6.13.8.tar.xz

解压完成后进入该文件夹,输入:

1
2
# 进入源码目录
cd linux-6.13.8

此时内核的源代码中已经有 Makefile ,因此可以直接 make

这里我们使用默认配置,输入:

1
2
# 生成默认配置(使用x86_64架构的默认配置)
make defconfig

接着就开始内核的编译,因为我的虚拟机就两核因此使用双线程,大家可以根据自己的配置进行调整,输入:

1
2
# 开始编译内核(-j参数根据CPU核心数设置,双核示例)
make -j 2

接着就是漫长的等待,编译完成后会生成 arch/x86/boot/bzImage 内核文件

二、最小化环境测试

接着我们使用 QEMU 这个模拟器进行内核功能的测试,输入:

1
qemu-system-x86_64 -kernel arch/x86/boot/bzImage

但是只有内核本身是跑不起来的,此时我们要返回 linux 文件夹,并在 linux 文件夹内创建 shell 文件夹,然后在 shell 文件夹内创建 shell.c 文件,输入:

1
2
3
4
5
6
7
# 返回工程目录并创建shell环境
cd ..
mkdir shell
cd shell

# 编写测试程序(使用vim或任意编辑器)
vim shell.c

接着可以编写一个简单的 c 程序试验一下,输入:

1
2
3
4
5
6
7
8
9
10
11
12
/* 最小化交互程序 */
#include<stdio.h>
int main()
{
char a;
while(1)
{
printf("Are you OK?");
scanf("%c",&a);
}
return 0;
}

接着编译并运行,输入:

1
2
gcc shell.c
./a.out

现在已经可以顺利运行该 shell.c 文件了,为了使编译生成的文件不被动态地链接到其他无关的库,则在编译时使用 -static ,输入:

1
2
# 静态编译(避免动态链接依赖)
gcc shell.c -static

接着将可运行文件重命名为 init ,这是 linux 内核默认搜索的一个文件名,输入:

1
2
mv ./a.out init
# 也可以在上一步直接 gcc shell.c -static -o init

然后再将其打包成一个 cpio 格式的压缩包,输入:

1
2
# 打包为initramfs(需包含名为init的可执行文件)
echo "init" | cpio -H newc -o > init.cpio

在文件压缩成功之后再使用 qemu 尝试启动,输入:

1
2
# 使用QEMU启动自定义内核
qemu-system-x86_64 -kernel ../linux-6.13.8/arch/x86/boot/bzImage -initrd init.cpio

运行后输出是:

此时 QEMU 已成功启动自定义内核,编译内核至此已经完成了,接下来将是添加内核模块

三、添加内核模块

此时的目录结构应为:

1
2
3
4
5
6
~/桌面/linux/
├── linux-6.13.8/
├── shell/
│ ├── init
│ ├── init.cpio
│ └── shell.c

接着开始编写内核模块,在 shell/ 目录下新建文件 hello.c,编写模块的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Linux kernel module");

/* 模块加载函数 */
static int __init hello_init(void) {
printk(KERN_INFO "Hello, Kernel Module loaded!\n"); // 内核日志输出
return 0;
}

/* 模块卸载函数 */
static void __exit hello_exit(void) {
printk(KERN_INFO "Goodbye, Kernel Module unloaded.\n");
}

/* 注册模块入口/出口 */
module_init(hello_init); // 模块加载时调用 hello_init
module_exit(hello_exit); // 模块卸载时调用 hello_exit

然后是编写模块的 Makefile ,在同一目录下创建 Makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 指定模块名称(需与源码文件名一致)
obj-m := hello.o

# 内核源码路径(根据实际位置修改)
KDIR := ~/桌面/linux/linux-6.13.8 # 指向你的内核源码目录

# 当前模块路径
PWD := $(shell pwd)

all:
make -C $(KDIR) M=$(PWD) modules

clean:
make -C $(KDIR) M=$(PWD) clean

接着开始编译内核模块,进入shell/ 目录并编译,输入:

1
2
3
4
cd ~/桌面/linux/shell

# 执行编译(生成hello.ko内核模块)
make

此时若成功则会输出:

1
2
3
4
5
make -C ~/桌面/linux/linux-6.13.8 M=~/桌面/linux/shell modules
CC [M] ~/桌面/linux/shell/hello.o
MODPOST 1 modules
CC ~/桌面/linux/shell/hello.mod.o
LD [M] ~/桌面/linux/shell/hello.ko

检查模块信息,应包含许可证、作者等信息,输入:

1
2
# 查看模块信息
modinfo hello.ko

四、集成与完整测试

修改用户态 init 程序,更新 shell.c ,使其加载内核模块并交互:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
printf("=== My Minimal Shell ===\n");

// 1. 加载内核模块
system("insmod /hello.ko");
printf("Kernel module loaded. Check dmesg.\n");

// 2. 模拟用户交互
int input;
while (1) {
printf("Enter '0' to exit: ");
scanf("%d", &input);
if (input == 0) break;
}

// 3. 卸载模块
system("rmmod hello");
printf("Kernel module unloaded.\n");

return 0;
}

接着编译并打包新的 initramfs ,输入:

1
2
3
4
5
6
7
8
9
10
# 静态编译
gcc -static shell.c -o init

# 确保可执行
chmod +x init

cd ~/桌面/linux/shell

# 打包包含模块的initramfs(必须包含init和hello.ko)
echo -e "init\nhello.ko" | cpio -o -H newc > init.cpio

最后是通过 QEMU 启动完整测试,输入:

1
2
3
4
5
# 带控制台输出的启动方式
qemu-system-x86_64 \
-kernel ~/桌面/linux/linux-6.13.8/arch/x86/boot/bzImage \
-initrd ~/桌面/linux/shell/init.cpio \
-nographic -append "console=ttyS0"

输出:

1
2
3
=== My Minimal Shell ===
Kernel module loaded. Check dmesg.
Enter '0' to exit:

输入 0 退出程序后,检查卸载日志,输入:

1
2
# 过滤内核日志中的卸载信息
dmesg | grep Goodbye

输出:

1
Goodbye, Kernel Module unloaded.

运行后的输出是:

通过以上步骤,即可完成从内核编译到模块开发的完整流程。

但是目前仍存在部分小问题,等待后续学习修正。