---
tags: Linux, FileSystem, Kernel Module, simplefs
---
# Experiment Environment
## Hardware and OS Info
```shell
$ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
Address sizes: 39 bits physical, 48 bits virtual
CPU(s): 12
On-line CPU(s) list: 0-11
Thread(s) per core: 2
Core(s) per socket: 6
Socket(s): 1
NUMA node(s): 1
Vendor ID: GenuineIntel
CPU family: 6
Model: 167
Model name: 11th Gen Intel(R) Core(TM) i5-11400 @ 2.60GHz
Stepping: 1
CPU MHz: 2600.000
CPU max MHz: 4400.0000
CPU min MHz: 800.0000
BogoMIPS: 5184.00
Virtualization: VT-x
L1d cache: 288 KiB
L1i cache: 192 KiB
L2 cache: 3 MiB
L3 cache: 12 MiB
NUMA node0 CPU(s): 0-11
```
```shell
$ neofetch --backend off
freshliver@freshliver-K21
-------------------------
OS: Kubuntu 21.04 x86_64
Kernel: 5.11.0-49-generic
Uptime: 1 day, 23 hours, 38 mins
Packages: 3228 (dpkg)
Shell: zsh 5.8
Resolution: 1920x1080
DE: Plasma 5.21.4
WM: KWin
```
---
## Debugging Kernel via UML
### 1. Build UML
```shell
$ make mrproper
$ make defconfig ARCH=um SUBARCH=x86_64
$ make linux ARCH=um SUBARCH=x86_64 -j `nproc`
```
為了讓後面能透過 GDB 進行 debug,還需要進行以下步驟:
```shell
$ echo "CONFIG_GDB_SCRIPTS=y" > .config-fragment
$ ARCH=um scripts/kconfig/merge_config.sh .config .config-fragment
$ make scripts_gdb ARCH=um
```
### 2. Prepare Rootfs
```shell
$ cat ../rootfs.sh
# make rootfs
export REPO=http://dl-cdn.alpinelinux.org/alpine/v3.13/main
mkdir -p rootfs
curl $REPO/x86_64/APKINDEX.tar.gz | tar -xz -C /tmp/
export APK_TOOL=`grep -A1 apk-tools-static /tmp/APKINDEX | cut -c3- | xargs printf "%s-%s.apk"`
curl $REPO/x86_64/$APK_TOOL | fakeroot tar -xz -C rootfs
fakeroot rootfs/sbin/apk.static \
--repository $REPO --update-cache \
--allow-untrusted \
--root $PWD/rootfs --initdb add alpine-base
echo $REPO > rootfs/etc/apk/repositories
echo "LABEL=ALPINE_ROOT / auto defaults 1 1" >> rootfs/etc/fstab
# tini
wget -O rootfs/sbin/tini https://github.com/krallin/tini/releases/download/v0.19.0/tini-static
chmod +x rootfs/sbin/tini
```
### 3. Run UML
建立完 rootfs 後就可以準備執行 UML 了。首先參考[建構 User-Mode Linux 的實驗環境 - 客製化-UML-環境](https://hackmd.io/@sysprog/user-mode-linux-env#%E5%AE%A2%E8%A3%BD%E5%8C%96-UML-%E7%92%B0%E5%A2%83)中的步驟,寫一個 Shell Script 負責啟動 UML:
```shell=*
$ cat ../UML.sh
#!/bin/sh
./linux umid=uml0 ubd0=/dev/null \
root=/dev/root rootfstype=hostfs hostfs=./rootfs \
rw mem=64M init=/init.sh quiet
stty sane ; echo
```
:::info
TODO: [各個參數](https://www.kernel.org/doc/html/latest/admin-guide/kernel-parameters.html)意義
:::
以 `./rootfs/init.sh` 作為 initial process,其內容如下:
```shell=*
$ cat ./rootfs/init.sh
#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sys /sys
export PS1='UML:\w\ $ '
export PS1='\[\033[01;32mUML:\w\033[00m \$ '
exec /sbin/tini /bin/sh +m
```
主要是為了省去每次手動掛載 `/proc` 與 `/sys` 兩個目錄的步驟,並在 Command Line 的 `#` 字元前加上 `UML:` 字樣分辨 UML 以及 HOST。
:::info
TODO: `/proc` 與 `/sys` 用途
:::
最後嘗試透過剛剛寫的 Script 啟動 UML:
```shell
$ ../UML.sh
/init.sh: line 3: mount: not found
/init.sh: line 4: mount: not found
UML:/ # ls /bin
/bin/sh: ls: not found
UML:/ # help
Built-in commands:
------------------
. : [ [[ alias bg break cd chdir command continue echo eval exec
exit export false fg getopts hash help history jobs kill let
local printf pwd read readonly return set shift source test times
trap true type ulimit umask unalias unset wait
UML:/ #
```
可以看到 UML 順利啟動了,但是卻出現了找不到 `mount` 命令的錯誤訊息,試圖透過 `ls` 列出 `/bin` 下的執行檔,但 `ls` 也同樣找不到,因此執行 `exit` 回到 host 檢查 `./rootfs/bin` 目錄下的檔案:
```shell=*
$ ls ./rootfs/bin
bbsuid busybox rc-status sh uniso
```
結果發現只有系統應包含的基本命令的執行檔都不在該目錄下,但其中包含了一個 [busybox](https://en.wikipedia.org/wiki/BusyBox) 執行檔,而根據[官方文件的說明](https://www.busybox.net/downloads/BusyBox.html),它是一個整合了多種 UNIX 常用的工具的單一執行檔,可以透過像是 `busybox command` 的形式執行對應的命令:
```shell
$ ../UML.sh
/init.sh: line 3: mount: not found
/init.sh: line 4: mount: not found
UML:/ # busybox ls
bin etc init.sh media opt root sbin sys usr
dev home lib mnt proc run srv tmp var
UML:/ #
```
但如果每次都要透過 busybox 執行命令很麻煩,因此可以透過 `/bin/busybox --install` 這個命令,將其中包含的命令建立在 `/bin` 目錄下,讓使用者不用透過 `busybox` 執行命令:
:::info
TODO: 官方文件對這個參數的說明
:::
```shell
$ ../UML.sh
/init.sh: line 3: mount: not found
/init.sh: line 4: mount: not found
UML:/ # /bin/busybox --install
UML:/ # ls /bin
arch cp false ionice ls mv rc-status sleep uniso
ash date fatattr iostat lzop netstat reformime stat usleep
base64 dd fdflush ipcalc makemime nice rev stty watch
bbconfig df fgrep kbd_mode mkdir pidof rm su zcat
bbsuid dmesg fsync kill mknod ping rmdir sync
busybox dnsdomainname getopt link mktemp ping6 run-parts tar
cat dumpkmap grep linux32 more pipe_progress sed touch
chgrp echo gunzip linux64 mount printenv setpriv true
chmod ed gzip ln mountpoint ps setserial umount
chown egrep hostname login mpstat pwd sh uname
UML:/ #
```
### 4. Debug simplefs in UML via GDB
#### Install Kernel Modules in Rootfs
確定基本命令都能使用後,接著就要回到 host 將核心模組所需的檔案安裝到 rootfs 中。
接著依照[建構 User-Mode Linux 的實驗環境 - 準備核心模組](https://hackmd.io/@sysprog/user-mode-linux-env#%E6%BA%96%E5%82%99%E6%A0%B8%E5%BF%83%E6%A8%A1%E7%B5%84)中的命令進行 `make modules` 以及 `make modules_install`,==但 `make modules_install` 的 `MODLIB` 要改成 `INSTALL_MOD_PATH`,且指定目錄只須為 `pwd/rootfs`==:
```shell
$ make modules_install INSTALL_MOD_PATH=`pwd`/rootfs ARCH=um
INSTALL arch/um/drivers/hostaudio.ko
INSTALL block/bfq.ko
INSTALL drivers/block/loop.ko
INSTALL drivers/block/nbd.ko
INSTALL drivers/net/dummy.ko
INSTALL drivers/net/ppp/ppp_generic.ko
INSTALL drivers/net/slip/slhc.ko
INSTALL drivers/net/slip/slip.ko
INSTALL drivers/net/tun.ko
INSTALL fs/autofs/autofs4.ko
INSTALL fs/binfmt_misc.ko
INSTALL fs/isofs/isofs.ko
INSTALL sound/soundcore.ko
DEPMOD 5.12.0
$ ls rootfs/lib/modules
5.12.0
$ ls rootfs/lib/modules/5.12.0
build modules.alias modules.builtin modules.builtin.bin modules.dep modules.devname modules.softdep modules.symbols.bin
kernel modules.alias.bin modules.builtin.alias.bin modules.builtin.modinfo modules.dep.bin modules.order modules.symbols source
```
:::info
TODO: 為什麼要這些核心模組
:::
:::warning
若依照[建構 User-Mode Linux 的實驗環境 - 準備核心模組](https://hackmd.io/@sysprog/user-mode-linux-env#%E6%BA%96%E5%82%99%E6%A0%B8%E5%BF%83%E6%A8%A1%E7%B5%84)使用 `MODLIB` 的話,會發生以下錯誤:
```shell
$ make modules_install MODLIB=`pwd`/rootfs/lib/modules/VER ARCH=um
INSTALL arch/um/drivers/hostaudio.ko
INSTALL block/bfq.ko
INSTALL drivers/block/loop.ko
INSTALL drivers/block/nbd.ko
INSTALL drivers/net/dummy.ko
INSTALL drivers/net/ppp/ppp_generic.ko
INSTALL drivers/net/slip/slhc.ko
INSTALL drivers/net/slip/slip.ko
INSTALL drivers/net/tun.ko
INSTALL fs/autofs/autofs4.ko
INSTALL fs/binfmt_misc.ko
INSTALL fs/isofs/isofs.ko
INSTALL sound/soundcore.ko
DEPMOD 5.12.0
depmod: ERROR: could not open directory /lib/modules/5.12.0: No such file or directory
depmod: FATAL: could not search modules: No such file or directory
make: *** [Makefile:1484: _modinst_post] Error 1
```
從錯誤訊息的部份可以看出是 `make modules_install` 的目標目錄找不到,而該目錄位於 host 的 `/bin/modules` 目錄下,顯然與預期的目標目錄 `./rootfs/lib/modules/VER` 不合。
而檢查 `make help | grep modules_install` 的說明後會發現,核心模組的安裝位置是由 `INSTALL_MOD_PATH` 指定:
```shell
$ make help | grep modules_install
modules_install - Install all modules to INSTALL_MOD_PATH (default: /)
```
而參考[官方文件的 Building External Modules - Module Installation](https://docs.kernel.org/kbuild/modules.html#install-mod-path) 中的範例也是使用 `INSTALL_MOD_PATH` 作為參數:
```shell
$ make INSTALL_MOD_PATH=/frodo modules_install
=> Install dir: /frodo/lib/modules/$(KERNELRELEASE)/kernel/
```
然後就會根據核心的版本號建立相應的目錄結構,不用額外修改目錄名稱。
:::
```shell
UML:/lib/modules # depmod -ae `uname -r`
UML:/lib/modules # modprobe isofs
UML:/lib/modules # lsmod
Module Size Used by Not tainted
isofs 25341 0
```
:::danger
depmod? modprobe? Not tainted?
https://www.kernel.org/doc/Documentation/admin-guide/tainted-kernels.rst
:::
#### Install simplefs in UML
參考[建構 User-Mode Linux 的實驗環境 - 準備核心模組](https://hackmd.io/@sysprog/user-mode-linux-env#%E6%BA%96%E5%82%99%E6%A0%B8%E5%BF%83%E6%A8%A1%E7%B5%84) 中 Makefile 的部份,對 simplefs 的 Makefile 進行修改,但為了保持彈性,這邊選擇根據**編譯時是否指定架構**決定是否要加 `ARCHARG`,而不是直接指定架構 `ARCH=um`:
```diff
KDIR ?= /lib/modules/$(shell uname -r)/build
+ifdef ARCH
+ ARCHARG = ARCH=$(ARCH)
+endif
+
MKFS = mkfs.simplefs
all: $(MKFS)
- make -C $(KDIR) M=$(PWD) modules
+ make -C $(KDIR) M=$(PWD) modules $(ARCHARG)
...
clean:
- make -C $(KDIR) M=$(PWD) clean
+ make -C $(KDIR) M=$(PWD) clean $(ARCHARG)
```
然後再開始編譯 simplefs,但 `KDIR` 與 `PWD` 要分別設定成核心目錄以及 simplefs 目錄:
```shell
$ $ make -C simplefs KDIR=`pwd` PWD=`pwd`/simplefs ARCH=um
make: Entering directory '/home/freshliver/tmp/linux/stable/linux-5.12/simplefs'
cc -std=gnu99 -Wall -o mkfs.simplefs mkfs.c
make -C /home/freshliver/tmp/linux/stable/linux-5.12 M=/home/freshliver/tmp/linux/stable/linux-5.12/simplefs modules ARCH=um
make[1]: Entering directory '/home/freshliver/tmp/linux/stable/linux-5.12'
CC [M] /home/freshliver/tmp/linux/stable/linux-5.12/simplefs/fs.o
CC [M] /home/freshliver/tmp/linux/stable/linux-5.12/simplefs/super.o
CC [M] /home/freshliver/tmp/linux/stable/linux-5.12/simplefs/inode.o
CC [M] /home/freshliver/tmp/linux/stable/linux-5.12/simplefs/file.o
CC [M] /home/freshliver/tmp/linux/stable/linux-5.12/simplefs/dir.o
CC [M] /home/freshliver/tmp/linux/stable/linux-5.12/simplefs/extent.o
LD [M] /home/freshliver/tmp/linux/stable/linux-5.12/simplefs/simplefs.o
MODPOST /home/freshliver/tmp/linux/stable/linux-5.12/simplefs/Module.symvers
CC [M] /home/freshliver/tmp/linux/stable/linux-5.12/simplefs/simplefs.mod.o
LD [M] /home/freshliver/tmp/linux/stable/linux-5.12/simplefs/simplefs.ko
make[1]: Leaving directory '/home/freshliver/tmp/linux/stable/linux-5.12'
make: Leaving directory '/home/freshliver/tmp/linux/stable/linux-5.12/simplefs'
```
最後再將 `simplefs` **整個目錄**複製到 rootfs 中,並在 UML 中載入核心模組:
```shell
$ cp -r simplefs/ rootfs/ && ../UML.sh
UML:/ # insmod simplefs/simplefs.ko && lsmod
Module Size Used by Tainted: G
simplefs 16878 0
```
#### Make and Mount a simplefs Filesystem
首先到 `./rootfs/simplefs` 中準備一個 simplefs 的檔案系統:
```shell
$ make test.img master [084c7e7] modified
dd if=/dev/zero of=test.img bs=1M count=200
200+0 records in
200+0 records out
209715200 bytes (210 MB, 200 MiB) copied, 0.0859438 s, 2.4 GB/s
./mkfs.simplefs test.img
Superblock: (4096)
magic=0xdeadce
nr_blocks=51200
nr_inodes=51240 (istore=915 blocks)
nr_ifree_blocks=2
nr_bfree_blocks=2
nr_free_inodes=51239
nr_free_blocks=50280
Inode store: wrote 915 blocks
inode size = 72 B
Ifree blocks: wrote 2 blocks
Bfree blocks: wrote 2 blocks
```
接著依照[建構 User-Mode Linux 的實驗環境 - 搭配 GDB 進行核心追蹤和分析](https://hackmd.io/-KufbNBCRpKhBbZVWCz_CA?view#%E6%90%AD%E9%85%8D-GDB-%E9%80%B2%E8%A1%8C%E6%A0%B8%E5%BF%83%E8%BF%BD%E8%B9%A4%E5%92%8C%E5%88%86%E6%9E%90)中的說明,建制 GDB Script,並準備一個 Shell Script 自動執行 debug 所需的命令:
```shell
$ cat ../debug-UML.sh
KERNEL_ARGS="umid=uml0 root=/dev/root rootfstype=hostfs rootflags=`pwd`/rootfs rw mem=64M init=/init.sh quiet"
gdb -q \
-ex "add-auto-load-safe-path ./scripts/gdb/vmlinux-gdb.py" \
-ex "file vmlinux" \
-ex "set args $KERNEL_ARGS" \
-ex "handle SIGSEGV nostop noprint" \
-ex "handle SIGUSR1 nopass stop print" \
-ex "lx-version"
```
然後透過這個 Script 執行 UML,並掛載模組以及剛建立的檔案系統:
```shell
$ ../debug-UML.sh
Reading symbols from vmlinux...
Signal Stop Print Pass to program Description
SIGSEGV No No Yes Segmentation fault
Signal Stop Print Pass to program Description
SIGUSR1 Yes Yes No User defined signal 1
Linux version 5.12.0 (freshliver@freshliver-K21) (gcc (Ubuntu 10.3.0-1ubuntu1) 10.3.0, GNU ld (GNU Binutils for Ubuntu) 2.36.1) #2 Fri Jun 17 02:15:35 CST 2022
(gdb) r
Starting program: /home/freshliver/tmp/linux/stable/linux-5.12/vmlinux umid=uml0 root=/dev/root rootfstype=hostfs rootflags=/home/freshliver/tmp/linux/stable/linux-5.12/rootfs rw mem=64M init=/init.sh quiet
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[Detaching after fork from child process 4168879]
[Detaching after fork from child process 4168880]
[Detaching after fork from child process 4168881]
[Detaching after fork from child process 4168882]
[Detaching after fork from child process 4168883]
[New Thread 0x7ffff7d85f80 (LWP 4168884)]
[New Thread 0x7ffff7d85f80 (LWP 4168885)]
Failed to initialize ubd device 0 :Couldn't determine size of device's file
[Detaching after fork from child process 4168886]
UML:/simplefs # insmod simplefs.ko && lsmod
Module Size Used by Tainted: G
simplefs 16878 0
UML:/simplefs # mount -t simplefs -o loop test.img /test/
[Detaching after fork from child process 16651]
```
:::info
`[Detaching after fork from child process 16651]` 訊息的意義?
:::
```shell
UML:/ # df -Th
Filesystem Type Size Used Available Use% Mounted on
/dev/root hostfs 374.8G 137.0G 218.8G 39% /
devtmpfs devtmpfs 28.8M 0 28.8M 0% /dev
/dev/loop0 simplefs 200.0M 3.6M 196.4M 2% /test
UML:/ # cd /test
UML:/test # ls -a
. .. test
```
:::danger
在 host 有出現 `ls -a` 沒有顯示 `.` 以及 `..` 的問題:
```shell
$ ls -a
test
```
但是在 UML 中卻沒有出現這個問題。
:::
#### Debug simplefs via GDB
首先進入到 UML 掛載模組以及 simplefs:
```shell
UML:/ # cd simplefs/
UML:/simplefs # insmod simplefs.ko
UML:/simplefs # mount -t simplefs -o loop test.img /test/
[Detaching after fork from child process 131609]
UML:/simplefs #
```
:::danger
若先在 GDB 中執行 `lx-symbols` 命令再回到 UML 中 `insmod` 會不斷跳出的訊息?
```shell
loading @0x648b6000: /home/freshliver/tmp/linux/stable/linux-5.12/simplefs/simplefs.ko
```
:::
接著在透過 `pkill -SIGUSR1 -o vmlinux` 回到 GDB,透過 `lx-symbols` 檢查核心模組的 Debug Symbols 有載入了,因此嘗試對 `simplefs_mkdir` 函式設定中斷點:
```shell
(gdb) lx-symbols
loading vmlinux
scanning for modules in /home/freshliver/tmp/linux/stable/linux-5.12
loading @0x64942000: /home/freshliver/tmp/linux/stable/linux-5.12/drivers/block/loop.ko
loading @0x648b6000: /home/freshliver/tmp/linux/stable/linux-5.12/simplefs/simplefs.ko
(gdb) b simplefs_mkdir
Breakpoint 1 at 0x648b8033: file /home/freshliver/tmp/linux/stable/linux-5.12/simplefs/inode.c, line 707.
(gdb) c
Continuing.
```
然後再透過 `countinue` 回到 UML 的掛載目錄( `continue` 後要多按一次 ENTER 才會回到 UML 的 Command Line),並透過 `mkdir` 命令測試中斷點是否能夠正常運作:
```c
UML:/test # mkdir testdir
Thread 1 "vmlinux" hit Breakpoint 1, simplefs_mkdir (ns=0x604541e0 <init_user_ns>,
dir=0x60a44d78, dentry=0x60c1a780, mode=493)
at /home/freshliver/tmp/linux/stable/linux-5.12/simplefs/inode.c:707
707 {
(gdb) l
702 #if USER_NS_REQUIRED()
703 static int simplefs_mkdir(struct user_namespace *ns,
704 struct inode *dir,
705 struct dentry *dentry,
706 umode_t mode)
707 {
708 return simplefs_create(ns, dir, dentry, mode | S_IFDIR, 0);
709 }
710 #else
711 static int simplefs_mkdir(struct inode *dir,
(gdb) p *dentry
$2 = {d_flags = 0, d_seq = {seqcount = {sequence = 0}}, d_hash = {
next = 0x0 <loop_init>, pprev = 0x63f06bf8}, d_parent = 0x60c1acc0, d_name = {{{
hash = 1006514939, len = 7}, hash_len = 31071286011},
name = 0x60c1a7b8 "testdir"}, d_inode = 0x0 <loop_init>,
d_iname = "testdir\000.conf", '\000' <repeats 18 times>, d_lockref = {{{lock = {{
rlock = {raw_lock = {<No data fields>}}}}, count = 1}}},
...
```
可以發現中斷點如預期的運作,檢查 `dentry` 參數內容也能看到目錄名稱確實是剛剛輸入的 `"testdir"`。
---
## Debugging Kernel via QEMU
### 1. Check Hardware Virtualization
```shell
$ egrep -c '(vmx|svm)' /proc/cpuinfo
```
結果應為非 0 的數字,代表 CPU 支援虛擬化,可以從 BIOS 中開啟虛擬化並繼續以下步驟。
### 2. Install QEMU/KVM
參考 [Ubuntu 官方文件中的 KVM 安裝教學](https://help.ubuntu.com/community/KVM/Installation)進行安裝。
安裝完成後透過以下幾個命令檢查是否安裝成功,預期輸出應如下:
```shell
$ virsh list --all
Id Name State
----------------------------------
$ ls -l /dev/kvm
crw-rw----+ 1 root kvm 10, 232 May 29 22:21 /dev/kvm
$ sudo ls -la /var/run/libvirt/libvirt-sock
srwxrwxrwx 1 root root 0 May 29 22:29 /var/run/libvirt/libvirt-sock
```
### 3. Build Linux Kernel
首先因為 `git clone` 整個 kernel source 可能會花很長時間,因此改從 GitHub 上取得只有原始碼的壓縮檔,這邊使用的是最新釋出的 5.18 的版本:
```shell
$ wget https://github.com/torvalds/linux/archive/refs/tags/v5.18.tar.gz
$ tar -xf v5.18.tar.gz
$ cd linux-5.18
```
接著先透過 `make defconfig ARCH=x86_64` 產生預設的設定檔 `.config`,但由於之後要配合 GDB 使用,因此參考 Kernel 文件 [Debugging kernel and modules via gdb](https://docs.kernel.org/dev-tools/gdb-kernel-debugging.html#setup) 以及 [Bug hunting](https://www.kernel.org/doc/html/latest/admin-guide/bug-hunting.html#gdb) 中的說明調整某些設定:
- 啟動
- `CONFIG_DEBUG_INFO`
- `CONFIG_DEBUG_KERNEL`
- `CONFIG_GDB_SCRIPTS`
- 關閉
- `CONFIG_COMPILE_TEST`
- `CONFIG_DEBUG_INFO_REDUCED`
- `CONFIG_DEBUG_INFO_NONE`
:::success
各項設定可以參考 [lib/Kconfig.debug](https://github.com/torvalds/linux/blob/master/lib/Kconfig.debug) 或 [init/Kconfig](https://github.com/torvalds/linux/blob/master/init/Kconfig) 等其他 Kconfig 相關檔案中的說明。
:::
要啟動這些設定可以透過 `make menuconfig` 進行設定:
1. 選擇 "Kernel hacking"
2. 選擇 "Compile-time checks and compiler options"
3. "Kernel debugging" 設定應預設即為啟動
4. 選擇 "Debug information (Disable debug information)"
5. 按 **ENTER** 選擇 "Rely on the toolchain's implicit default DWARF version"
6. 應會自動返回 "Compile-time checks and compiler options"
7. 按 **Y** 啟動 "Provide GDB scripts for kernel debugging (NEW)"
8. 返回到最上層退出並保存更動的設定
完成這幾個步驟後應會啟動 `CONFIG_DEBUG_INFO`、`CONFIG_GDB_SCRIPTS` 並關閉 `CONFIG_DEBUG_INFO_REDUCED`、`CONFIG_DEBUG_INFO_NONE`,可以再從 `.config` 中檢查對應設定是否正確的啟動以及關閉。
:::success
在 menuconfig 中可以按下 `/` 來搜尋特定設定的路徑、dependencies。
:::
接著再透過 ``$ make -j`nproc` `` 開始編譯核心,最後應會得到兩個重要的檔案:`arch/x86_64/boot/bzImage` 與 `vmlinux`,前者是 Linux Kernel 的執行檔,能透過 QEMU 執行,而後者則包含 Debug 用的資訊,能夠配合 GDB 與前者進行互動。
### 4. Make Rootfs
基本上與前面 UML 使用的 rootfs 相同,這邊透過 QEMU 的 `-hda` 參數讓 QEMU 能把 host 的映像檔當作 rootfs 來使用,因此要先建立並掛載一個映像檔:
```shell
$ dd if=/dev/zero of=rootfs.img bs=1M count=512
512+0 records in
512+0 records out
536870912 bytes (537 MB, 512 MiB) copied, 0.504278 s, 1.1 GB/s
$ mkfs.ext4 rootfs.img
mke2fs 1.45.7 (28-Jan-2021)
Discarding device blocks: done
Creating filesystem with 131072 4k blocks and 32768 inodes
Filesystem UUID: 29f97775-0f53-4ea7-a2f2-f71c1985ecf8
Superblock backups stored on blocks:
32768, 98304
Allocating group tables: done
Writing inode tables: done
Creating journal (4096 blocks): done
Writing superblocks and filesystem accounting information: done
$ mkdir rootfs && sudo mount rootfs.img rootfs
```
需要注意的是,由於 simplefs 透過 `make test.img` 建立的映像檔預設大小有 200 MiB,所以這邊建立的大小為 512 MiB,讓需要的檔案都能放進這個映像檔,檔案系統類型則採用與 host 相同的 ext4。
然而由於掛載是用 root 權限,所以 `rootfs` 目錄只有 root 能修改:
```shell
$ ls -dl rootfs
drwxr-xr-x 3 root root 4096 Jun 27 23:26 rootfs
```
因此為了避免透過 `sudo` 操作,這邊透過 `chown` 讓當前使用者能存取該目錄:
```shell
$ sudo chown $(id -nu):$(id -ng) -R rootfs
$ mkdir rootfs/test && ls -l rootfs
total 20
drwx------ 2 freshliver freshliver 16384 Jun 27 23:38 lost+found
drwxrwxr-x 2 freshliver freshliver 4096 Jun 27 23:38 test
```
接著就直接使用前面寫的 Shell Script 把 rootfs 需要的檔案都放進映像檔的掛載目錄中:
```shell
$ ../rootfs.sh
...
```
接著是編譯 kernel module 並加入到映像檔中:
```shell
$ make modules ARCH=x86_64
...
$ make modules_install INSTALL_MOD_PATH=`pwd`/rootfs ARCH=x86_64
INSTALL /home/freshliver/tmp/linux/stable/linux-5.18/rootfs/lib/modules/5.18.0/kernel/drivers/thermal/intel/x86_pkg_temp_thermal.ko
INSTALL /home/freshliver/tmp/linux/stable/linux-5.18/rootfs/lib/modules/5.18.0/kernel/fs/efivarfs/efivarfs.ko
INSTALL /home/freshliver/tmp/linux/stable/linux-5.18/rootfs/lib/modules/5.18.0/kernel/net/ipv4/netfilter/iptable_nat.ko
INSTALL /home/freshliver/tmp/linux/stable/linux-5.18/rootfs/lib/modules/5.18.0/kernel/net/netfilter/nf_log_syslog.ko
INSTALL /home/freshliver/tmp/linux/stable/linux-5.18/rootfs/lib/modules/5.18.0/kernel/net/netfilter/xt_LOG.ko
INSTALL /home/freshliver/tmp/linux/stable/linux-5.18/rootfs/lib/modules/5.18.0/kernel/net/netfilter/xt_MASQUERADE.ko
INSTALL /home/freshliver/tmp/linux/stable/linux-5.18/rootfs/lib/modules/5.18.0/kernel/net/netfilter/xt_addrtype.ko
INSTALL /home/freshliver/tmp/linux/stable/linux-5.18/rootfs/lib/modules/5.18.0/kernel/net/netfilter/xt_mark.ko
INSTALL /home/freshliver/tmp/linux/stable/linux-5.18/rootfs/lib/modules/5.18.0/kernel/net/netfilter/xt_nat.ko
DEPMOD /home/freshliver/tmp/linux/stable/linux-5.18/rootfs/lib/modules/5.18.0
```
simplefs 則先在 kernel 目錄處理完再移動到 rootfs 中:
```shell
$ make -C simplefs KDIR=`pwd` PWD=`pwd`/simplefs
$ make -C simplefs test.img
$ cp -r simplefs rootfs/
```
最後 init script 的內容也與 UML 的內容相同:
```shell
$ cat ./rootfs/init.sh
#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sys /sys
export PS1='UML:\w\ $ '
export PS1='\[\033[01;32mUML:\w\033[00m \$ '
exec /sbin/tini /bin/sh +m
```
:::warning
這邊直接沿用 UML 的 init sciprt,所以之 command line 前面顯示的名稱也會是 **UML**。
:::
為了避免 QEMU 使用 `rootfs.img` 映像檔時修改到這個目錄的內容、造成檔案損毀,這邊直接卸載這個映像檔:
```shell
$ sudo umount rootfs
```
:::info
TODO: 系統啟動與掛載流程、ramfs, rootfs, initfs
- https://www.kernel.org/doc/html/latest/filesystems/ramfs-rootfs-initramfs.html
- https://tldp.org/HOWTO/Bootdisk-HOWTO/buildroot.html
- https://wiki.ubuntu.com/Initramfs
- [鳥哥私房菜 - 19.1 Linux 的開機流程分析](https://linux.vbird.org/linux_basic/centos7/0510osloader.php#startup)
- [gnitnaw 的 HackMD 筆記](https://hackmd.io/@3zUb5pYSS1W_VVsmvHE5MQ/H1DqYq3bl)
:::
### 5. Debug Kernel in QEMU via GDB
最後是透過 QEMU 執行核心並透過 GDB 進行 Debug,為此需要準備兩個 Terminal 分別執行 QEMU 與 GDB。
:::success
可以利用終端機分割畫面功能或是 [tmux](https://github.com/tmux/tmux) 之類的工具分割出多個畫面,以減少切換終端機的操作。
:::
#### Run Kernel in QEMU
為了讓 QEMU 能執行 Kernel,這邊須幾個參數:
- `-kernel arch/x86/boot/bzImage`
> -kernel bzImage
>
> Use bzImage as kernel image. The kernel can be either a Linux kernel or in multiboot format.
- `-m 512`
> -m [size=]megs[,slots=n,maxmem=size]
>
> Sets guest **startup RAM size** to megs **megabytes**. Default is 128 MiB. Optionally, a suffix of "M" or "G" can be used to signify a value in megabytes or gigabytes respectively. Optional pair slots, maxmem could be used to set amount of hotpluggable memory slots and maximum amount of memory. Note that maxmem must be aligned to the page size.
- `-hda ./rootfs.img`
> -hda file
>
> Use **file as hard disk** 0, 1, 2 or 3 image (see the disk images chapter in the System Emulation Users Guide).
指定的檔案則是前面建立的 rootfs 的映像檔 `rootfs.img`。
- `-s`:讓 GDB 能透過 port 1234 與 Kernel 互動
> Shorthand for -gdb tcp::1234, i.e. open a gdbserver on TCP port 1234 (see the GDB usage chapter in the System Emulation Users Guide).
- `-S`:讓 qemu 命令執行後不會馬上開始執行 Kernel
> Do not start CPU at startup (you must type 'c' in the monitor).
- `-nographic`
> -nographic
>
> ... With this option, you can totally **disable graphical output** so that QEMU is a simple command line application. The **emulated serial port is redirected on the console** and muxed with the monitor (unless redirected elsewhere explicitly). Therefore, you can still use QEMU to debug a Linux kernel with a serial console. Use C-a h for help on switching between the console and monitor.
- `-append "console=ttyS0 nokaslr root=/dev/sda rw init=/init.sh"`
> -append cmdline
>
> Use cmdline as kernel command line
要傳入的 kernel 參數:
- 由於前面加了 `nographic` 參數,所以這邊加上 `console=ttyS0` 讓訊息輸出到 `ttyS0`,而 `ttyS0` 代表原本執行命令的 Terminal。
:::info
TODO: 為什麼是 `S0`?
- https://qemu-project.gitlab.io/qemu/interop/qemu-ga.html?highlight=ttys0
- https://qemu-project.gitlab.io/qemu/system/qemu-manpage.html?highlight=ttys0
- https://www.kernel.org/doc/html/latest/admin-guide/serial-console.html
:::
- `nokaslr`
> nokaslr
>
> When CONFIG_RANDOMIZE_BASE is set, this **disables** kernel and module base offset **ASLR** (Address Space Layout Randomization).
- `root=/dev/sda rw`
前面設定的 `-hda rootfs.img` 會被掛載到 `/dev/sda`,指定它為 rootfs,並讓 qemu 中執行的 kernel 能寫入映像檔,否則將會以唯讀模式啟動。
- `init=/init.sh`
與 UML 相似,指定 init script。
而以上整合成一個 Shell Script `qemu.sh` 方便執行:
```shell
$ cat ../qemu.sh
#!/bin/sh
qemu-system-x86_64 \
-kernel arch/x86/boot/bzImage \
-hda ./rootfs.img \
-s -S -m 512 -nographic \
-append "console=ttyS0 nokaslr root=/dev/sda rw init=/init.sh"
```
然後就可以透過這個 script 執行了:
```shell
$ ../qemu.sh
WARNING: Image format was not specified for './rootfs.img' and probing guessed raw.
Automatically detecting the format is dangerous for raw images, write operations on block 0 will be restricted.
Specify the 'raw' format explicitly to remove the restrictions.
```
由於前面有設定 `-S` 讓 qemu 啟動時不會直接啟動 kernel,若沒有要透過 GDB 啟動 kernel 的話,則須先按 `Ctrl+a` 再按 `c` 切換到 qemu monitor,並輸入 `c` 啟動 kernel:
```shell
QEMU 5.2.0 monitor - type 'help' for more information
(qemu) c
...
```
等個幾秒後應該就會看到熟悉的界面以及錯誤訊息:
```sh=*
/init.sh: line 3: mount: not found
/init.sh: line 4: mount: not found
UML:/ #
```
這時再次透過 `Ctrl+a` + `c` 切回到 kernel 中,並執行一次 `# /bin/busybox --install`。
要結束 qemu 的話則需再次跳回 qemu monitor,然後輸入 `q` 結束 qemu。
:::danger
若是太快離開 qemu 的話,似乎會無法確實寫入到 rootfs 中,實測大約需要等個 15 秒左右,但並不是每次都要等那麼久。
:::
#### Debug via GDB
GDB script 則相對簡單不少,需要做的只有三件事:
1. 將 GDB script 加入到 auto-load-safe-path
2. 指定 debug 的目標為 localhost 的 1234 port
3. 讀取 vmlinux 的 symbols
以上步驟也建立一個 Shell Script `debug-qemu.sh`:
```shell
#!/bin/sh
gdb -q \
-ex "add-auto-load-safe-path ./scripts/gdb/vmlinux-gdb.py" \
-ex "target remote :1234" \
vmlinux
```
:::warning
這兩個 Script 皆應在 kernel 的最上層目錄中(`/path/to/your/linux-5.18/`)執行。
:::
接著執行這個 sciprt 來測試是否能正常透過 GDB 進行 debug:
```shell
$ ../debug-qemu.sh
Reading symbols from vmlinux...
Remote debugging using :1234
0x000000000000fff0 in exception_stacks ()
(gdb) c
Continuing.
```
:::warning
這個 script 應在 `../qemu.sh` 後執行。
:::
在 GDB 中輸入 `c` 後,qemu 應該就能正常啟動 kernel 了。啟動完成後在 kernel 中嘗試掛載 simplefs:
```sh=*
UML:/ # insmod simplefs/simplefs.ko
[ 249.489881] simplefs: loading out-of-tree module taints kernel.
[ 249.493879] simplefs: module loaded
UML:/ # mount -t simplefs -o loop simplefs/test.img /test
[ 270.671489] loop0: detected capacity change from 0 to 409600
[ 270.679379] simplefs: '/dev/loop0' mount success
UML:/ # lsmod && df -Th
Module Size Used by Tainted: G
simplefs 28672 1
Filesystem Type Size Used Available Use% Mounted on
/dev/root ext4 487.2M 220.1M 231.3M 49% /
devtmpfs devtmpfs 233.9M 0 233.9M 0% /dev
/dev/loop0 simplefs 200.0M 3.6M 196.4M 2% /test
```
接著到 GDB 的終端機按下 `Ctrl+c` 跳回到 GDB 畫面測試:
```c
(gdb) c
Continuing.
^C
Program received signal SIGINT, Interrupt.
0xffffffff81c9335b in default_idle () at arch/x86/kernel/process.c:734
734 }
(gdb) lx-symbols
loading vmlinux
scanning for modules in /home/freshliver/tmp/linux/stable/linux-5.18
loading @0xffffffffa0000000: /home/freshliver/tmp/linux/stable/linux-5.18/simplefs/simplefs.ko
(gdb) b simplefs_mkdir
Breakpoint 1 at 0xffffffffa0002430: file /home/freshliver/tmp/linux/stable/linux-5.18/simplefs/inode.c, line 711.
(gdb) c
Continuing.
```
應該會正確讀取 debug symbols,而中斷點也應該能正常運作:
```shell
UML:/ # mkdir /test/aaaaa
```
```c
(gdb) c
Continuing.
Breakpoint 1, simplefs_mkdir (ns=0xffffffff8284ee40 <init_user_ns>,
dir=0xffff8880042e0028, dentry=0xffff888003fa7600, mode=493)
at /home/freshliver/tmp/linux/stable/linux-5.18/simplefs/inode.c:711
711 return simplefs_create(ns, dir, dentry, mode | S_IFDIR, 0);
(gdb)
```
---
## Problems
### GDB with Python Support
若是沒有支援 Python 的話則無法使用 kernel 提供的 GDB Script,需要重新編譯 GDB (以 14.2 版為例):
```bash
$ cd /path/to/build/and/install/gdb
$ wget https://sourceware.org/pub/gdb/releases/gdb-14.2.tar.gz
$ tar -xf gdb-14.2.tar.gz && cd gdb-14.2/
$ ./configure --prefix=`pwd`/build # the executable will later be install to the dir
$ make -j`nproc`
$ make install # should be installed to the `pwd`/build dir
$ export PATH="`pwd`/build/bin`:$PATH"
```
編譯完成後,由於這邊將安裝目錄設定為當前目錄以方便移除,而當前目錄應不在系統路徑 `$PATH` 下,所以這邊將編譯出的執行檔所在的目錄暫時加到系統路徑下,之後透過 `which gdb` 應該可以看到 gdb 指向剛編譯出的執行檔,而不是系統的 gdb:
```bash
$ which gdb
/path/to/build/and/install/gdb/build/bin/gdb
```
### Undefined command: "lx-symbols"
出現這個原因可能是因為 `vmlinux-gdb.py` 未正確載入,可透過 `info auto-load python-scripts` 檢查,理想狀況應該可以看到:
```bash
(gdb) info auto-load python-scripts
Loaded Script
Yes /path/to/kernel/vmlinux-gdb.py
```
而以下列出幾種可能的情況:
#### No auto-load scripts
```gdb
$ gdb -q -ex "add-auto-load-safe-path ." vmlinux
Reading symbols from vmlinux...
(gdb) info auto-load python-scripts
No auto-load scripts.
```
`vmlinux-gdb.py` 這個檔案位在 `/path/to/kernel/scripts/gdb/` 目錄下,可嘗試將其 link 到 kernel 的根目錄:
```bash
$ ln -s scripts/gdb/vmlinux-gdb.py .
```
#### auto-loading has been declined
```bash
$ gdb -q -ex "add-auto-load-safe-path ." vmlinux
Reading symbols from vmlinux...
warning: File "/path/to/kernel/scripts/gdb/vmlinux-gdb.py" auto-loading has been declined by your `auto-load safe-path' set to "$debugdir:$datadir/auto-load".
To enable execution of this file add
add-auto-load-safe-path /path/to/kernel/scripts/gdb/vmlinux-gdb.py
line to your configuration file "/home/user/.config/gdb/gdbinit".
(gdb) info auto-load python-scripts
Loaded Script
No /path/to/kernel/vmlinux-gdb.py
```
出現這個錯誤代表 GDB 拒絕讀取這個檔案,這時依照指示把這個檔案的路徑加到 `$HOME/.config/gdb/gdbinit` 檔案中:
```bash
echo "add-auto-load-safe-path /path/to/kernel/scripts/gdb/vmlinux-gdb.py" >> ~/.config/gdb/gdbinit
```
### SyntaxError: invalid syntax
```bash
$ gdb -q -ex "add-auto-load-safe-path ." vmlinux
Reading symbols from vmlinux...
Traceback (most recent call last):
File "/path/to/kernel/vmlinux-gdb.py", line 25, in <module>
import linux.constants
File "/path/to/kernel/scripts/gdb/linux/constants.py", line 5
LX_SB_RDONLY = ((((1UL))) << (0))
^
SyntaxError: invalid syntax
```
在部份較舊的 kernel (如 5.15) 提供的 GDB Script 中,可能會發現 `scripts/gdb/linux/constants.py` 檔案中使用了 `1UL` 這種 C 語言的語法,這種情況下可以用 `gdb.parse_and_eval()` 包起來:
```bash
LX_SB_RDONLY = gdb.parse_and_eval("((((1UL))) << (0))")
...
```
---
## References
- General
- [鳥哥 - 第十九章、開機流程、模組管理與 Loader](https://linux.vbird.org/linux_basic/centos7/0510osloader.php)
- https://wiki.archlinux.org/title/Kernel_parameters#systemd-boot
- https://www.kernel.org/doc/html/latest/admin-guide/kernel-parameters.html
- https://docs.kernel.org/kbuild/modules.html
- Filesystem (initramfs, rootfs, tmpfs)
- [Wikipedia - Initial ramdisk](https://en.wikipedia.org/wiki/Initial_ramdisk)
- https://www.kernel.org/doc/html/latest/admin-guide/initrd.html
- https://www.kernel.org/doc/html/latest/admin-guide/init.html
- [IBM - Linux initial RAM disk (initrd) overview](https://developer.ibm.com/articles/l-initrd/)
- https://hackmd.io/@3zUb5pYSS1W_VVsmvHE5MQ/H1DqYq3bl
- KVM/QEMU
- https://help.ubuntu.com/community/KVM/Installation
- https://wiki.archlinux.org/title/QEMU
- https://docs.kernel.org/dev-tools/gdb-kernel-debugging.html
- https://nickdesaulniers.github.io/blog/2018/10/24/booting-a-custom-linux-kernel-in-qemu-and-debugging-it-with-gdb/
- https://pnx9.github.io/thehive/Debugging-Linux-Kernel.html
- https://blog.csdn.net/weixin_38227420/article/details/88402738
- UML
- https://hackmd.io/@sysprog/user-mode-linux-env