写文前搜了下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
#---
要玩内核,还是把它放到虚拟机里吧,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这样的系统。
上面两个教程写得挺好的,推荐看看。下面我会提出其中一些重要的地方,并且补充和qemu配合的内容。
参考: 根文件系统及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阶段写死一些路径,会带来错误。
参考: syzkaller, setup,buildroot详解和分析
使用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程序一样编写驱动。
#include
#include
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
c_cpp_properties.json
文件。至此,当前目录是这样的:$ tree . -a . ├── Makefile ├── mydriver.c └── .vscode └── c_cpp_properties.json 1 directory, 3 files
其中c_cpp_properties.json
文件内容如下。需要留意defines内的宏定义,__KERNEL
和MODULE
务必要包含在其中。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开头的插桩。