Linux内核驱动开发之hello-world
-
还是得接触一下
Linux内核驱动开发,这样才更了解Linux内核pwn。 -
首先我们给一个
C语言编写的内核驱动版本的Hello world,逐行理解这个代码,并配置环境。
1 | /* |
内核模块基础
内核模块相关概念
- 内核模块,英文名称为
Kernel Module,内核模块的可执行文件后缀通常是.ko,如hello.ko:- 内核模块是一段可以根据需要加载和卸载内核中的代码。
- 内核模块可以在不重启系统的情况下扩展内核的功能。
- 最常见的内核模块其实就是设备驱动程序,它使得内核能够访问连接到系统上的硬件。例如,鼠标其实就是一个硬件,鼠标连接到电脑上,就是由某个内核模块使得内核能与鼠标相互交互,进而操作电脑。
- 如果没有内核模块,在添加一个新的内核功能,我们就必须修改内核文件,并重新编译内核,这不仅会使得内核变得很大,而且编译内核之后还需要重启电脑,非常不方便。
注意:在具体了解内核模块之前,我将设备驱动等同于内核模块了,这是不对的,设备驱动是内核模块的其中一种。
拓展:内核模块除了有设备驱动程序,还包括了文件系统模块,网络协议模块,非硬件驱动类型的模块,内核功能拓展模块,安全模块,虚拟化或容器相关模块,伪文件系统、接口模块,其实任何可以
动态插入内核的功能单元都可以是模块。
内核模块操作
-
在
Linux中我们将介绍几个对内核模块的操作,注意在执行这些操作一般都需要root权限:- 查看已经载入到内核正在运行的内核模块
- 将未被载入的内核模块载入到内核中。
- 将被载入的内核模块从内核中移除。
-
要查看已经载入内核的内核模块,在
Linux中可以使用lsmod命令来查看。- 运行
lsmod就会显示出已经载入内核的内核模块

- 运行
lsmod命令本质上是读取/proc/modules这个文件,并输出到终端上。

- 运行
-
要将内核模块载入到内核中(其实就是运行内核模块,与运行用户程序是不一样的。)
insmod和modprobe这两个命令都可以使用。而这两个命令稍微会有区别:modprobe:最后是调用insmod命令将内核模块载入内核,该命令会自动寻找模块的默认位置,懂得如何找出依赖关系并按正确的顺序加载模块。相对来说更灵活。insmod:这个就是直接将内核模块载入内核,在载入内核模块的时候需要输入内核模块完整的路径,相对来说比较死板。- 注意:对于初学来说,就使用
insmod进行内核模块的载入即可,modprobe等之后再说。 - 假设我们这边有一个内核模块,使用
insmod载入内核模块就需要输入如下命令,如下图所示,当然图中还是没有hello.ko模块的:

1 | insmod /home/myheart/program/my_module/hello.ko |
- 要将已经载入到内核的内核模块移除,可以使用
rmmod命令,也可以使用modprobe -r命令。
1 | rmmod 模块名 |
环境准备
在编写一个内核程序之前,还需要解决几个问题,也就是环境问题:
- 内核版本问题:选择内核版本,有些魔改后的内核,或者打了补丁这就会导致一些问题,并且有些
Linux发行版/usr/src/是缺少头文件的,就导致无法编译,所以我们尽量下载、编译并启动一个全新的、纯净的Linux内核。 - 模块版本管理问题:当一个内核模块编译是给
linux kernel 5.8.1使用的,那如果在linux kernel 5.8.2载入该模块基本上会报错,因为大部分Linux发行版的stock内核都默认开启了modversioning选项。最好是编译一个关闭modversioning的内核。 - 编译问题:注意到前面的内核模块的
hello world程序,引入了与用户态的C语言不同的头文件<linux/module.h>和<linux/kernel.h>。而gcc默认引用在/usr/src/这个文件夹中的这两个头文件。如果/usr/src/文件夹中没有这两个头文件,gcc编译就会报错。 - 调试问题:内核模块不能像
printf()那样打印到屏幕上,它会记录一些信息和警告,这些信息最终只会打印到控制台中这个控制台指的是tty终端,而不是xterm等图形终端,如果使用xterm载入内核模块,只会有日志、信息和警告会被记录,而不会被输出到控制台中。我们可以输入tty命令,如果输出的是/dev/pts/tty0那么就是伪终端信息不会打印,如果是/dev/tty1那么就是真实的控制台。
我的环境:
- 使用重新编译内核后的
WSL2这样支持使用qemu,再下载一个干净的内核源码和与之对应的头文件。- 编译干净的内核源码,并关闭
modversioning,并使用busybox文件系统,配合qemu模拟出一个简单的系统,而qemu模拟出来的系统终端是/dev/tty- 在
WSL2中修改gcc编译的一些设置,使得编译时能正确引用头文件,这样实现在WSL2使用gcc编译好的内核模块,可以与内核打包成cpio文件格式,使用qemu模拟的时候放入所模拟的操作系统中,从而对内核进行调试与操作。
WSL2内核重新编译
- 重新编译
WSL2内核是为了能够使用qemu,这一步在我博客上已经写过了,这里不多说了。
编译纯净内核源码
- 由于
WSL2版本对应的是5.15.153.1-microsoft-standard-WSL2,为了兼容,同时也算是省事吧,我就选择与5.15.153.1-microsoft-standard-WSL2相近的Linux kernel 5.15版本。 - 使用
wget命令从linux kernel网站上下载对应版本的内核源码,这里选择下载的是linux kernel 5.15.1版本的:
1 | wget https://www.kernel.org/pub/linux/kernel/v5.x/linux-5.15.1.tar.gz |

- 下载完整后使用命令将源码的压缩包进行解压操作:
1 | tar -zxvf linux-5.15.1.tar.gz |
- 接着下载编译
linux kernel所需要的工具
1 | sudo apt update |
- 这里为了不多事,就直接将
WSL2的内核配置文件作为该内核编译的配置文件(不太建议这样,因为不能保证WSL2的编译配置有没很大改动)
1 | cd linux-5.15.1/ |
- 接着关闭内核模块版本管理:
1 | scripts/config --disable MODVERSIONS |
- 接着使用
make menuconfig命令,进入配置界面,把虚拟化部分给关了,可以减少一点编译时间,然后保存配置。

- 接着给编译所要运行的脚本文件添加可执行权限:
1 | chmod +x scripts/*.sh |
- 接着直接使用全部
cpu核心开始编译内核
1 | make -j$(nproc) |

- 在编译的时候还是遇到报错了,处理方案在这里

1 | scripts/config --disable DEBUG_INFO_BTF --disable DEBUG_INFO_BTF_MODULES |
- 编译完成

- 将内核复制一份过来,这样就有编译好的内核了。
1 | cp ./arch/x86/boot/bzImage ../ |

使用qemu启动内核
- 接着使用
busybox作为文件系统,配合qemu进行模拟从而启动内核。编译busybox这里也不多说了。这里先使用命令创建文件
1 | mkdir -p initramfs/{bin,sbin,proc,sys,dev,etc} |
- 接下来复制
busybox并创建符号链接,先将busybox复制到该文件夹的/bin目录下,并进入initramfs/bin目录中
1 | cp /home/myheart/busybox-1.36.1/busybox initramfs/bin/busybox |
- 接着将进行创建符号链接操作,并回到
initramfs的父目录中
1 | for cmd in $(./busybox --list); do ln -sf busybox $cmd; done |
- 在内核启动之后,会执行一个
init文件,这个了解一下即可,具体是在linux启动流程中会具体介绍,在initramfs这个目录下创建init文件
1 | cat > initramfs/init << 'EOF' |
- 接着给这个文件赋予可执行权限:
1 | chmod +x initramfs/init |
- 接着就是将文件打包为
cpio文件
1 | cd initramfs |
- 然后使用
qemu命令直接启动即可,这里我直接创建一个sh文件,就免得总是要输入这么一大串命令:
1 | # vim qemu.sh |
- 运行之后查看
tty,发现是ttyS0,而不是/dev/pts/tty0,所以控制台的环境也解决了。


编译hello_world.ko文件
- 对于编译内核的时候,
gcc编译在引用内核头文件的时候会稍微有点问题,所以我们直接采用写Makefile来构建。 - 我们直接将上面
hello world的代码复制到hello_world.c文件中。
1 | /* |
- 接下来我们要编译该文件,编译内核的时候通常不用
gcc命令,而是使用make文件,所以我们要写一个简单的make文件,先确保有make这个程序。

- 接着创建
Makefile文件,并写入这些编译信息:
1 | obj-m += hello_world.o |
- 接着输入
make命令即可进行编译操作,运行之后就会看到编译好的内核模块文件.ko
1 | make |

- 此时我们就可以将编译好的内核模块文件复制一份到
initramfs中,然后再重新打包cpio文件。
1 | cp ./hello_world.ko ./initramfs/hello_world.ko |
- 重新打包
cpio:
1 | find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../initramfs.cpio.gz |
- 接着再运行
qemu.sh脚本,尝试挂载hello_world.ko这个内核模块,可以成功挂载。


- 在内核模块中
printk,并不会将字符串输出到控制台终端,而是写入内核日志缓冲区。我们可以使用dmesg | tail查看内核日志缓冲区。

- 至此,内核模块的环境就已经搭建好了。如果我们想要在控制台中显示
Hello world 1.我们需要将printk(KERN_INFO "Hello world 1.\n");替换成printk(KERN_EMERG "Hello world 1.\n");,并重新编译内核文件打包cpio文件,挂载即可。

本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 iyheart的博客!

