Linux内核设计与实现总结(1) —— qemu内核调试环境搭建(macOS)

宿主机系统

  1. macOS 12.4 (x86)

安装qemu

qemu相比于vmware,简单、轻便,可以直接指定linux内核,再指定一个文件系统,即可调试。硬盘、网络设备可以后加。

首先我尝试了utmapp/UTM,这是个适用于iOS/macOS的虚拟机,基于qemu。但是在指定内核启动镜像bzImage和initramfs后,遇到了问题,一直报错:

1
The bootindex 0 has already been used

搞了一会没搞出来,索性不用UTM了,直接上qemu。

使用homebrew可以直接安装qemu。

1
➜  ~ brew install qemu

目前安装的是7.0.0_2。

安装完成之后验证一下:

1
2
3
4
5
6
7
➜  ~ qemu-system-x86_64 --help
QEMU emulator version 7.0.0
Copyright (c) 2003-2022 Fabrice Bellard and the QEMU Project developers
usage: qemu-system-x86_64 [options] [disk_image]

'disk_image' is a raw hard disk image for IDE hard disk 0
....

编译Linux内核

这里使用的是虚拟机,因为我macOS文件系统使用的是APFS,不区分大小写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 下载v5.19源码
$ wget https://github.com/torvalds/linux/archive/refs/tags/v5.19.zip
# 解压
$ unzip v5.19.zip
# 进入源码目录
$ cd linux-5.19
# 安装编译依赖
$ sudo apt install git fakeroot build-essential ncurses-dev xz-utils libssl-dev bc flex libelf-dev bison
# 使用默认x86_64配置
$ make defconfig
# 修改配置打开CONFIG_DEBUG_INFO
$ make menuconfig
# 编译内核bzImage
$ make -j $(nproc) bzImage

生成的内核位于 arch/x86/boot/bzImage (怎么arch/x86_64/boot/bzImage链接到这里?)

准备rootfs

什么是ramdisk、ramfs、tmpfs、initrd、initramfs?

这几个东西是比较绕的,有必要记录一下。

先来个总结:以一些嵌入式设备举例,根文件系统可以存放在nor flash或者nand flash中,那么可以把对应的块设备驱动以及指定的文件系统驱动编译到内核中,就可以在内核启动时直接挂载根文件系统。但是对于x86的设备来说,它可以把根文件系统放软盘、硬盘、甚至网络硬盘上,还需要支持不同的文件系统,把这些驱动编译到内核中会导致内核大小膨胀。initrd和initramfs就是用来解决这个问题的,这两个都是基于内存的,所以不需要额外的驱动,因此可以编译到内核中,也可以在外部由bootloader加载到内存中。initrd和initramfs会存放一些挂载真正根文件系统所需的驱动,在挂载后,通过chroot切换到根上。这样既解决了“先有鸡还是先有蛋“问题,又减少了内核的体积。

ramdisk

ramdisk是一种基于内存的块设备,因此ramdisk具有块设备的一切属性,存在于内存,与真实的块设备无异,甚至访问的时候还需要缓存到内存中,也就是从内存里缓存到内存里。ramdisk需要文件系统驱动。

ramfs

ramfs是基于内存的文件系统,是一种简单的文件系统,访问ramfs的文件也需要分配页缓存,但是它在对应的页修改后不会进行回写,为它分配的页永远不可以置为可用,所以ramfs可能会出现不断写入导致最后占满物理内存的情况,因此ramfs只有root可写。另外,ramfs不是一定作为临时rootfs使用,还可以作为其他用途。

tmpfs

tmpfs基于ramfs,相比于ramfs支持限制空间大小,而且可以把不需要的文件写入到swap中,比如我们平时用来存放运行时信息的/run目录就是tmpfs类型的文件系统。

initrd

initrd全称Bootloader Init Ram Disk,它基于ramdisk,在2.4及更早版本时,内核使用initrd挂载临时根文件系统。

initramfs

initramfs由2.6内核引入,取代了initrd?网上说的。所以,initrd已经上升成了一种机制,可以指代内核启动过程中的一个阶段。在我的Kali系统上,可以看到/boot目录下存在一个initrd.img镜像:

1
2
3
4
5
6
7
8
9
10
11
12
┌──(kali㉿kali)-[/run]
└─$ ls -lh /boot
total 133M
-rw-r--r-- 1 root root 240K Jan 31 2022 config-5.15.0-kali3-amd64
-rw-r--r-- 1 root root 243K Apr 1 08:03 config-5.16.0-kali7-amd64
drwxr-xr-x 6 root root 4.0K Apr 21 09:03 grub
-rw-r--r-- 1 root root 59M Apr 21 09:23 initrd.img-5.15.0-kali3-amd64
-rw-r--r-- 1 root root 60M Apr 21 09:23 initrd.img-5.16.0-kali7-amd64
-rw-r--r-- 1 root root 83 Jan 31 2022 System.map-5.15.0-kali3-amd64
-rw-r--r-- 1 root root 83 Apr 1 08:03 System.map-5.16.0-kali7-amd64
-rw-r--r-- 1 root root 7.0M Jan 31 2022 vmlinuz-5.15.0-kali3-amd64
-rw-r--r-- 1 root root 7.1M Apr 1 08:03 vmlinuz-5.16.0-kali7-amd64

实际上这些镜像都是initramfs:

1
2
3
┌──(kali㉿kali)-[~/Workspace]
└─$ file initrd.img-5.15.0-kali3-amd64.gz
initrd.img-5.15.0-kali3-amd64.gz: ASCII cpio archive (SVR4 with no CRC)

initrd在制作时需要关联到loopback设备上,然后格式化设备的文件系统,挂载到目录,再把需要的文件复制进去,比较麻烦。
initramfs本身就是文件系统,制作时把文件打包成cpio,再压缩成gz。内核启动时直接将文件populate即可即可。

但是,initrd在内核启动引导完就会释放空间,而initramfs的空间则是一直被保留的。

创建initramfs

使用initramfs作为临时rootfs。

step 1: 下载busybox源码

1
$ wget https://busybox.net/downloads/busybox-1.32.1.tar.bz2

step2: 解压

1
$ tar -jxvf busybox-1.32.1.tar.bz2

step3: 设置成静态链接

1
$ make menuconfig

step4: 编译

1
$ make -j $(nproc)

step5: 安装到_install

1
$ sudo make install

step6: 准备根文件系统

1
2
3
4
5
cd _install
sudo mkdir dev
sudo mknod dev/console c 5 1
sudo mknod dev/ram b 1 0
sudo touch init

step7: 准备init

将以下内容写入init,我在网上找的,不知道busybox有没有example。

1
2
3
4
5
6
7
8
9
10
#!/bin/sh
echo "INIT SCRIPT"
mkdir /proc
mkdir /sys
mkdir /tmp
mount -t proc none /proc
mount -t sysfs none /sys
mount -t tmpfs none /tmp
echo -e "\nThis boot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
exec /bin/sh

这里init脚本最后执行了/bin/sh,也就是pid为1的用户进程是sh。

step8: 生成rootfs

1
2
$ sudo chmod +x init
$ find . -print0 | cpio --null -ov --format=newc | gzip -9 > initramfs-busybox-x64.cpio.gz

step9: 启动虚拟机

从编译机取出initramfs和bzImage,在macOS下启动:

1
2
3
4
➜  linux qemu-system-x86_64 -s -nographic \
-kernel ./bzImage \
-initrd initramfs-busybox-x64.cpio.gz \
--append "nokaslr console=ttyS0"

另一种方式:直接从虚拟机的磁盘启动

可以创建一个N size的img,然后从img创建回环设备,格式化成ext4文件系统,再将busybox安装到img的目录。这样可以得到一个硬盘镜像,再把镜像设置为虚拟机的硬盘,可以直接从硬盘挂载rootfs。

step1: 创建空镜像

1
2
3
4
5
┌──(kali㉿kali)-[~/Workspace]
└─$ dd if=/dev/zero of=rootfs.img bs=1M count=2048
2048+0 records in
2048+0 records out
2147483648 bytes (2.1 GB, 2.0 GiB) copied, 1.84944 s, 1.2 GB/s

step2: 格式化文件系统

1
2
3
4
5
6
7
8
9
10
11
12
13
┌──(kali㉿kali)-[~/Workspace]
└─$ mkfs.ext4 rootfs.img
mke2fs 1.46.5 (30-Dec-2021)
Discarding device blocks: done
Creating filesystem with 524288 4k blocks and 131072 inodes
Filesystem UUID: fcdbe231-cd7d-4c37-b8a9-054496e50413
Superblock backups stored on blocks:
32768, 98304, 163840, 229376, 294912

Allocating group tables: done
Writing inode tables: done
Creating journal (16384 blocks): done
Writing superblocks and filesystem accounting information: done

step3: 创建挂载点

1
2
┌──(kali㉿kali)-[~/Workspace]
└─$ mkdir ./rootfs

step4: 创建回环设备并挂载

1
2
┌──(kali㉿kali)-[~/Workspace]
└─$ sudo mount -t ext4 -o loop rootfs.img ./rootfs

step5: 安装rootfs到挂载点

1
2
3
4
5
┌──(kali㉿kali)-[~/Workspace]
└─$ cd busybox-1.32.1

┌──(kali㉿kali)-[~/Workspace/busybox-1.32.1]
└─$ sudo make install CONFIG_PREFIX=../rootfs

step6: 配置init

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
┌──(kali㉿kali)-[~/Workspace/busybox-1.32.1]
└─$ sudo mkdir ../rootfs/proc ../rootfs/dev ../rootfs/etc ../rootfs/sys ../rootfs/tmp

┌──(kali㉿kali)-[~/Workspace/linux-learn/rootfs]
└─$ sudo mkdir etc/init.d

┌──(kali㉿kali)-[~/Workspace/linux-learn/rootfs]
└─$ sudo vim /etc/fstab
proc /proc proc defaults 0 0
tmpfs /tmp tmpfs defaults 0 0
sysfs /sys sysfs defaults 0 0

┌──(kali㉿kali)-[~/Workspace/linux-learn/rootfs]
└─$ sudo vim etc/init.d/rcS
#!/bin/sh

/bin/mount -a
echo -e "Remounting the root filesystem"
mount -o remount,rw /
mkdir -p /dev/pts
mount -t devpts devpts /dev/pts
mdev -s

┌──(kali㉿kali)-[~/Workspace/linux-learn/rootfs]
└─$ sudo chmod +x etc/init.d/rcS

┌──(kali㉿kali)-[~/Workspace/linux-learn/rootfs]
└─$ sudo vim etc/inittab
::sysinit:/etc/init.d/rcS
::respawn:-/bin/sh
::askfirst:-/bin/sh
::ctrlaltdel:/bin/umount -a -r

┌──(kali㉿kali)-[~/Workspace/linux-learn/rootfs]
└─$ sudo chmod +x etc/inittab

┌──(kali㉿kali)-[~/Workspace/linux-learn/rootfs/dev]
└─$ cd dev

┌──(kali㉿kali)-[~/Workspace/linux-learn/rootfs/dev]
└─$ sudo mknod console c 5 1

┌──(kali㉿kali)-[~/Workspace/linux-learn/rootfs/dev]
└─$ sudo mknod null c 1 3

┌──(kali㉿kali)-[~/Workspace/linux-learn/rootfs/dev]
└─$ sudo mknod tty1 c 4 1

┌──(kali㉿kali)-[~/Workspace/busybox-1.32.1]
└─$ sudo cp -r examples/bootfloppy/* ../rootfs/etc

这里内核在启动阶段会直接调用/sbin/init,而不再是initramfs时在根目录下的init。

原因见内核源码main.c#L1525

  1. 如果bootloader指定了initrd从initramfs挂载临时rootfs,且cmdline未指定rdinit,则默认执行/init;
  2. 如果没有指定initrd,但cmdline指定了init,则执行指定的init;
  3. 如果1和2都没有满足,但存在编译时配置的CONFIG_DEFAULT_INIT,则执行它设置的值;
  4. 如果都没有,则依次执行,/sbin/init/etc/init/bin/init/bin/sh
  5. 如果4也失败了,那就panic。

step9: 启动虚拟机

从编译机取出rootfs.img和bzImage,在macOS下启动:

1
2
3
4
➜  qemu-system-x86_64 -s -nographic \
-kernel ./bzImage \
-hda rootfs.img \
--append "nokaslr root=/dev/sda rw console=ttyS0"

第一个内核模块

hello.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
/*
* hello.c First kernel module
*/

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

static int hello_init(void)
{
printk(KERN_ALERT "Hello World!\n");
return 0;
}

static void hello_exit(void)
{
printk(KERN_ALERT "exit of Hello World\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("lyq1996");
MODULE_DESCRIPTION("A Hello, World Module");

这里选择直接在内核源码外编译

1
2
3
4
5
6
7
8
9
10
11
12
┌──(kali㉿kali)-[~/Workspace/linux-learn/helloworld]
└─$ cat Makefile
obj-m := hello.o

┌──(kali㉿kali)-[~/Workspace/linux-learn/helloworld]
└─$ make -C ../linux-5.19 M=$PWD modules
make: Entering directory '/home/kali/Workspace/linux-learn/linux-5.19'
CC [M] /home/kali/Workspace/linux-learn/helloworld/hello.o
MODPOST /home/kali/Workspace/linux-learn/helloworld/Module.symvers
CC [M] /home/kali/Workspace/linux-learn/helloworld/hello.mod.o
LD [M] /home/kali/Workspace/linux-learn/helloworld/hello.ko
make: Leaving directory '/home/kali/Workspace/linux-learn/linux-5.19'

ko文件已生成。

一般情况下,需要继续使用make modules_install安装到/lib/modules/version/kernel下,然后使用depmod生成所有模块的依赖关系/lib/modules/version/module.dep,就可以使用modprobe来更好的加载模块,但我们这里需要在虚拟机挂载,所以不安装了,拷贝到虚拟机内。

1
2
3
/ # insmod hello.ko 
[ 24.472542] hello: loading out-of-tree module taints kernel.
[ 24.477124] Hello World!

GDB调试内核

qemu启动时加入了-s参数,因此gdb远程调试也是没有问题的。

首先在mac上安装gdb:

1
➜  ~ brew install gdb

然后使用gdb加载vmlinux可执行文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
➜  linux gdb vmlinux 
GNU gdb (GDB) 12.1
Copyright (C) 2022 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-apple-darwin21.3.0".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from vmlinux...
(gdb) target remote localhost:1234

qemu会在1234端口打开gdb远程调试,直接连即可。

至此,内核调试环境搭建完成。

Linux内核设计与实现总结(1) —— qemu内核调试环境搭建(macOS)

https://lyq.blogd.club/2022/08/03/LKD-conclusion-1/

Author

lyq1996

Posted on

2022-08-03

Updated on

2022-10-29

Licensed under

Comments