前言

写文前搜了下CSDN的资源,要么质量不行或者太过时,没有自己想要的。笔者很早就想学习内核了,然而曾多次尝试。这次以实验室的一个项目为契机,鼓励自己完善下去。
本文将面向同为新手的你,记录学习的一些值得注意的地方。你只需要准备一台Linux操作系统的计算机即可(以Ubuntu为例),并预留一定的硬盘空间。
因为笔者水平有限,文章质量会尽量完善。

编译内核

Linux内核官网 (https://www.kernel.org/) 下载内核源码。
官网在线文档
在线阅读Linux源码的网址1,函数跳转好用
在线阅读Linux源码的网址2,各个版本均有

在这里插入图片描述

关于版号的认识:
如上图最新的release版本是5.17.4,其版号格式如x.y.z
要注意的是,以-rc结尾的是测试版。

mkdir kernel_source_code && cd kernel_source_code
#download linux-5.17.4.tar.xz
tar xvf linux-5.17.4.tar.xz
:~/source_code/kernel_source_code$ tree linux-5.17.4 -L 1
linux-5.17.4
├── arch
├── block
├── certs
├── COPYING
├── CREDITS
├── crypto
├── Documentation	#文档,推荐去[官网文档在线看]
├── drivers
├── fs				#文件系统,比如ext4,btrfs
├── include
├── init
├── ipc
├── Kbuild
├── Kconfig
├── kernel
├── lib
├── LICENSES
├── MAINTAINERS
├── Makefile
├── mm
├── net
├── README
├── samples
├── scripts			#一些脚本,比如后面调试需要用到
├── security
├── sound
├── tools
├── usr
└── virt

编译内核是必经之路,然而现代的内核编译步骤非常简单。第一次编大约需要1个小时,3GB左右硬盘空间。期间make可能出错,一般是缺乏lib库,按提示安装后,重新make即可。

sudo apt install flex bison libncurses-dev libelf-dev libssl-dev

推荐第一次使用make defconfig。不推荐第一次直接使用make menuconfig,它会拷贝当前ubuntu系统的/boot目录下的配置文件作为默认配置,会遇到额外的问题。

编译器的版本不能太旧,不然会出现奇怪的问题。
我是Ubuntu20.04,默认的gcc版本是gcc (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0

cd linux-5.17.4
#---简单版
#make defconfig	#默认配置
#make -j4
#---

#---本文以此为例:编一个arm内核
sudo apt install gcc-aarch64-linux-gnu #安装交叉编译器

#在当前目录下生成.config文件
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- defconfig 

vi .config #可直接编辑 
#	CONFIG_DEBUG_INFO=y	#开启DEBUG信息
#	CONFIG_GDB_SCRIPTS=y #方便后面用GDB调试

#可选,KASAN是一个BUG检测工具,之后我们写个驱动如有BUG,它能检测并帮助你调试。
#如果你为了学习内核,或者不知道KASAN干什么的,那么不要开启KASAN。
#	CONFIG_KASAN=y

#如果你改了什么,应当make oldconfig让它检查并备份一下
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- oldconfig

make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j4
#---

安装QEMU

要玩内核,还是把它放到虚拟机里吧,BUG了也没事。首先,需要一个虚拟机qemu,qemu能虚拟出一个计算机基本的硬件。
你可以下载qemu源码并编译,默认情况下会编译出我们需要的二进制文件。
或者直接使用apt安装,但是版本较老。

sudo apt install qemu-system

qemu-system-aarch64 --version
QEMU emulator version 4.2.1 (Debian 1:4.2-3ubuntu6.21)
Copyright (c) 2003-2019 Fabrice Bellard and the QEMU Project developers

为了让qemu运行的更快,最好开启CPU虚拟化支持。在未开启的情况下qemu启动大约2分钟,开启后只需几秒,差别非常大。

grep -E '(svm|vmx)' /proc/cpuinfo
# svm/vmx 是x86系列cpu的虚拟化技术缩写,如果在cpuinfo里的flag出现,
# 说明当前已经开启了CPU虚拟化支持。

创建一个硬盘镜像文件

只有qemu和内核是不够的,还需要一个基本的应用程序的环境,里面最好有一些基本让你执行cd、ls、cat的命令行工具。这里有多种选择,你可以构造一个极简的如BusyBox做一个ramdisk文件系统,或者构建一个比较丰富的如Ubuntu这样的系统。

  • 使用BusyBox构建ramdisk。方便源码单步调试内核,便于学习内核。参考教程
  • 使用buildroot构建ext4格式根文件系统。方便丰富其上的应用程序,便于在虚拟机上做一些其他工作,本文以这个方法进行实际操作。参考教程

上面两个教程写得挺好的,推荐看看。下面我会提出其中一些重要的地方,并且补充和qemu配合的内容。

使用BusyBox构建ramdisk

参考: 根文件系统及Busybox简介
首先看看kernel里与init进程有关的代码。kernel最开始的代码在汇编中,与架构相关。在汇编中做一些保存参数与初始话的工作,最后跳转到C代码。最先进入init/main.c里的start_kerenl()。最后会开启一个线程,并执行kernel_init(),如下。其中run_init_process()会以类似execv()的方式执行该文件路径下的程序,执行成功则不会return。

static int __ref kernel_init(void *unused)
{
	...
	if (ramdisk_execute_command) { //ramdisk_execute_command默认值是 "/init"
		ret = run_init_process(ramdisk_execute_command);
		...
	}
	...
	if (!try_to_run_init_process("/sbin/init") ||
	    !try_to_run_init_process("/etc/init") ||
	    !try_to_run_init_process("/bin/init") ||
	    !try_to_run_init_process("/bin/sh"))
		return 0;

	panic("No working init found.  Try passing init= option to kernel. "
	      "See Linux Documentation/admin-guide/init.rst for guidance.");
}

关于initrd文档可以看看linux-5.17.4/Documentation/admin-guide/initrd.rst
这里先编好busybox,然后加一些必要的东西,再使用qemu跑起来。

默认情况下,编好busybox后会在源码目录下出现_install文件夹,里面有bin sbin usr等常见的在根目录下的文件夹名,如下。

$ tree _install/ -L 1
_install/
├── bin
├── linuxrc -> bin/busybox
├── sbin
└── usr

默认编译的busybox是动态链接,所以为了能跑在qemu里,需要把它需求的共享库也加进来。如果当时编busybox时选择静态链接那就不必准备共享库。

$ ldd bin/busybox 
	linux-vdso.so.1 (0x00007fff46188000)
	libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f02a62c7000)
	libresolv.so.2 => /lib/x86_64-linux-gnu/libresolv.so.2 (0x00007f02a62ab000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f02a60b9000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f02a6545000)
# cd your workspace
cp -r /path/to/_install . #拷贝过来
cd _install
mkdir -p lib/x86_64-linux-gnu lib64

a=(libm.so.6 libresolv.so.2 libc.so.6);for i in ${a[@]}; do sudo cp /lib/x86_64-linux-gnu/$i lib/x86_64-linux-gnu; done

sudo cp /lib64/ld-linux-x86-64.so.2 lib64
#至此准备好了共享库,然后准备/init程序
echo -e '#!/bin/sh\n /bin/sh' > init #init程序只是简单的开一个shell
chmod +x ./init		#必须让init有可执行权限

#准备好共享库后,把当前文件夹的内容打包成cpio。注意必须进入打包的文件夹目录下执行。
find . | cpio -o --format=newc > ../rootfs.cpio
cd ..

qemu-system-x86_64 \
-m 1G \
-kernel /path/to/your_kernel_src/arch/x86/boot/bzImage \ 
-initrd  ./rootfs.cpio \
-nographic  \
-append "console=ttyS0"

这样就可以开启一个qemu了。注意在这样的虚拟机的写入操作,在关闭qemu后都会丢失。如果你想给虚拟机上传一个文件,那么应该复制到_install文件夹内并重新使用cpio进行打包。这样的特性对学习内核比较好,每次重启都是一个崭新的环境。如果你不想这样,希望有个可以写入持久化的文件系统,那么请参考下面使用buildroot构建ext4格式根文件系统例子。

可以看到/init文件啥也没干,只是开启一个shell,这样是不够的,应该准备基本的环境,比如希望能够通过网络连到host,甚至通过host连接互联网。在这里,busybox已经具有该有的功能,不过需要我们自己配置。

sudo qemu-system-x86_64 \
-m 1G \
-kernel /path/to/your_kernel_src/arch/x86/boot/bzImage \ 
-initrd  ./rootfs.cpio \
-nographic  \
-append "console=ttyS0" \
-net tap,ifname=tap0,script=no,downscript=no \
-net nic,macaddr=de:de:de:de:de:22 \

# -net tap 会在host里创建tap0虚拟网卡(qemu关闭会自动删除该网卡),在vm中会出现eth0网卡
sudo ifconfig tap0 192.168.100.1/24 up
# /init 文件
#!/bin/sh
mkdir -p /proc && mount -t proc proc /proc
mkdir -p /dev && mount -t devtmpfs none /dev
mkdir -p /tmp && mount -t tmpfs -o size=16m tmpfs /tmp
mkdir -p /sys && mount -t sysfs sysfs /sys
mkdir -p /dev/pts && mount -t devpts devpts /dev/pts

echo /sbin/mdev > /proc/sys/kernel/hotplug
#根据/sys/class和/sys/block自动动态创建dev
/sbin/mdev -s

ifconfig eth0 192.168.100.111 up

setsid /bin/cttyhack setuidgid 0 /bin/sh

这样自然可以ping到host的ip了。如下所示。然而希望能够连接host的其他ip,或者互联网,只需要把tap0网卡当成正常网卡配置,在host与qemu中配置路由即可,即完成NAT配置。

/ # ping 192.168.100.1
PING 192.168.100.1 (192.168.100.1): 56 data bytes
64 bytes from 192.168.100.1: seq=0 ttl=64 time=0.947 ms
64 bytes from 192.168.100.1: seq=1 ttl=64 time=0.590 ms

想在ramdisk安装一个些工具包,比如python,可以选择源码编译,在make install阶段修改prefix变量,如下

./configure #--prefix默认是/usr/local
make
make prefix=/path/to/yourfs/usr/local install

需要注意的是,通过在./configure --prefix=/path/to/yourfs/usr设置的方式并不好。该方法可能在make阶段写死一些路径,会带来错误。

使用buildroot构建ext4格式根文件系统

参考: syzkaller, setupbuildroot详解和分析

使用buildroot就比较简单,它把需要做的事情都整理好了,只需要简单配置一下后,make,完成后就能得到一个可用的文件系统。去buildroot官网下一份最新版本。
直接make menuconfig,然后进行如下配置。本文以此步骤继续进行,使用buildroot构建一个aarch64文件系统。

Target options
    Target Architecture - Aarch64 (little endian)
System Configuration
[*] Run a getty (login prompt) after boot  --->
    TTY port - ttyAMA0
Target packages
    [*]   Show packages that are also provided by busybox
    Networking applications
        [*] dhcpcd
        [*] iproute2
        [*] openssh
Filesystem images
    [*] ext2/3/4 root filesystem
        ext2/3/4 variant - ext4
        exact size in blocks 256M
    [*] tar the root filesystem

make menuconfig配置完成后,make。
make完成后将生成output/images/rootfs.ext4,这就是build完成的文件系统了。

$ file output/images/rootfs.ext2
rootfs.ext2: Linux rev 1.0 ext4 filesystem data, UUID=963fc3c3-9ebc-41a2-a9b2-5a806e317038, volume name "rootfs" (extents) (large files) (huge files)

qemu-system-aarch64 \
-machine virt \
-cpu cortex-a57 \
-m 1G \
-nographic \
-kernel /path/to/linux-5.17.4/arch/arm64/boot/Image \
-hda  /path/to/buildroot-2022.02.1/output/images/rootfs.ext4 \
-append "console=ttyAMA0 root=/dev/vda oops=panic panic_on_warn=1 panic=-1 ftrace_dump_on_oops=orig_cpu debug earlyprintk=serial slub_debug=UZ nokaslr" \
-net user,hostfwd=tcp::10023-:22 -net nic

开启一个qemu后,等待加载。你如果没有更改上文的提到的配置的话,默认账号root,默认密码为空。接下来进行ssh的配置,方便正常操作。
ctrl+a+c + q可以退出qemu。

#---in qemu
ping baidu.com	#这时应当可以正常联网
passwd			#设置你自己的密码
vi /etc/ssh/sshd_config
#编辑,新增允许PermitRootLogin
#PermitRootLogin yes

sync	#让写操作刷到磁盘上
reboot	#重启
#---


ssh root@localhost -p 10023 #可以连上虚拟机

内核驱动开发环境

这里不去介绍怎么写驱动,而是撘一个方便开发的环境,基于vscode,有基本的代码提示与错误检查,可像写用户态C程序一样编写驱动。

  • 安装vscode。你可以直接使用apt,或者Ubuntu Software商店。vscode是有linux版本的,非常适合。
  • 创建vscode工作目录,新建文件mydriver.c。贴上一个简单hello world代码与Makefile,该代码只有一个无脑打印hello world的功能。
#include <linux/module.h>
#include <linux/proc_fs.h>

static const char msg[] = "Hello World!\n";
static ssize_t my_read(struct file* filp, char __user* ubuf, size_t count, loff_t * offp) {
    int slen = strlen(msg);
    int need = (count + *offp) >= slen ? slen - *offp : count;
    if (*offp >= slen) return 0;
    if(copy_to_user(ubuf, &msg[*offp], need)) return -EFAULT;
    *offp += need;
    return need;
}
static struct proc_dir_entry* my_proc_entry = NULL;
static const struct proc_ops my_ops = {
    .proc_read = my_read,
};
static int __init monitor_init(void) {
    printk(KERN_ERR "mydriver: monitor_init\n");
    my_proc_entry = proc_create("hello_world", 0, 0, &my_ops);
    return 0;
}

static void __exit monitor_exit(void) {
    printk(KERN_ERR "mydriver: monitor_exit\n");
    proc_remove(my_proc_entry);
}
module_init(monitor_init);
module_exit(monitor_exit);
MODULE_LICENSE("GPL v2");
KDIR ?= /path/to/linux-5.17.4
BDIR ?= $(PWD)/build
ccflags-y += -g -DDEBUG

default: $(BDIR)
	make -C $(KDIR) M=$(BDIR) src=$(PWD)

$(BDIR):
	mkdir -p "$@"
	touch "$@"/Makefile

.PHONY:clean
clean:
	make -C $(KDIR) M=$(BDIR) src=$(PWD) clean
	rm -rf $(BDIR)

obj-m := mydriver.o
  • 配置include与宏定义,让vscode能正确处理内核项目。创建.vscode文件夹,在其中创建c_cpp_properties.json文件。至此,当前目录是这样的:
$ tree . -a
.
├── Makefile
├── mydriver.c
└── .vscode
    └── c_cpp_properties.json

1 directory, 3 files

其中c_cpp_properties.json文件内容如下。需要留意defines内的宏定义,__KERNELMODULE务必要包含在其中。includePath内的路径指向内核源码,如果你是x86编译的,需要把路径中arm64替换成x86。

{
    "configurations": [
        {
            "name": "Linux",
            "includePath": [
                "/path/to/linux-5.17.4/include",
                "/path/to/linux-5.17.4/include/generated/uapi",
                "/path/to/linux-5.17.4/arch/arm64/include/generated",
                "/path/to/linux-5.17.4/arch/arm64/include",
                "/path/to/linux-5.17.4/arch/arm64/include/uapi",
                "/path/to/linux-5.17.4/include/uapi"

            ],
            "defines": [
                "__GNUC__",
                "__KERNEL__",
                "MODULE",
                "KBUILD_MODNAME=\"empty\""
            ]
        }
    ],
    "version": 4
}

最后的效果应当是没有任何vscode的报错,并且能有正常的代码提示,如下图。
在这里插入图片描述

  • 之后,示例驱动能够成功编译,并成功运行。
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-

#编译成功,生成ko文件
$ file build/mydriver.ko 
build/mydriver.ko: ELF 64-bit LSB relocatable, ARM aarch64, version 1 (SYSV), BuildID[sha1]=f1cfe92e4b0ef29293eb0b9579edc7a03d1988b2, with debug_info, not stripped

#把ko文件拷贝到虚拟机里
scp -P 10023 build/mydriver.ko root@localhost:~
# ls
mydriver.ko
# insmod mydriver.ko 
[   64.956126] mydriver: loading out-of-tree module taints kernel.
[   64.976412] mydriver: monitor_init
# cat /proc/hello_world 
Hello World!
# rmmod mydriver
[  159.358676] mydriver: monitor_exit

调试环境

与调试相关的kernel官方文档有简单的介绍,我实际处理的时候还是遇到一些问题,并且仍有问题没有解决,感觉是个大坑。

一般pc是intel的CPU,x86架构的,所以需要调试aarch64的虚拟机需要gdb支持,现在也很方便,安装gdb-multiarch即可

sudo apt install gdb-multiarch

实测linux-5.17.14版本的aarch64的gdb相关脚本是炸的,原因不明。 实际上其他版本也有点问题,官网的文档也描述不明确,坑先放在这。
所以下面参考stackoverflow上的某个答案,手动添加符号信息。

# 在qemu启动命令中,添加-s选项
qemu-system-aarch64 \
-machine virt \
-cpu cortex-a57 \
-m 1G \
-nographic \
-kernel /path/to/linux-5.17.4/arch/arm64/boot/Image \
-hda  /path/to/buildroot-2022.02.1/output/images/rootfs.ext4 \
-append "console=ttyAMA0 root=/dev/vda oops=panic panic_on_warn=1 panic=-1 ftrace_dump_on_oops=orig_cpu debug earlyprintk=serial slub_debug=UZ nokaslr" \
-net user,hostfwd=tcp::10023-:22 -net nic \
-s
--- in qemu ---
# insmod mydriver.ko
[   53.715766] mydriver: loading out-of-tree module taints kernel.
[   53.733192] mydriver: monitor_init
# cat /proc/modules
mydriver 16384 0 - Live 0xffff8000015d0000 (O)
------

如此以来,得到mydriver驱动的代码段的基地址。
如果你也想有全局变量的符号,那么.data .bss段的基地址也要记录下来。

# cat /sys/module/mydriver/sections/.text
0xffff8000015d0000
# cat /sys/module/mydriver/sections/.data
0xffff8000015d2000
# cat /sys/module/mydriver/sections/.bss
0xffff8000015d2500
#另一个窗口
gdb-multiarch -ex 'target remote :1234' /path/to/linux-5.17.4/vmlinux
(gdb) add-symbol-file /home/xkt/learn-kernel/kernel-debug/driver_test/build/mydriver.ko 0xffff8000015d0000
Reading symbols from /home/xkt/learn-kernel/kernel-debug/driver_test/build/mydriver.ko...
(gdb) b mydriver.c:my_read 
Breakpoint 1 at 0xffff8000015d0000: file /home/xkt/learn-kernel/kernel-debug/driver_test/mydriver.c, line 7.
(gdb) c
Continuing
--- in qemu
cat /proc/hello_world
---

可以看到,已经在这里断了下来

Breakpoint 1, my_read (filp=0xffff0000096af440, ubuf=0xffff80000cd97d70 "", count=18446462598926772328, offp=0xffff0000096af440) at /home/xkt/learn-kernel/kernel-debug/driver_test/mydriver.c:7
7	    int need = (count + *offp) >= slen ? slen - *offp : count;
(gdb)lay asm

在这里插入图片描述
看汇编以及调用链,似乎是对了。单步调试基本能确定应该是断下来了。
这里因为编了ASAN,所以代码比较混乱,有一些__asan开头的插桩。

Logo

更多推荐