Try   HackMD

Experiment Environment

Hardware and OS Info

$ 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
$ 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

$ make mrproper
$ make defconfig ARCH=um SUBARCH=x86_64
$ make linux ARCH=um SUBARCH=x86_64 -j `nproc`

為了讓後面能透過 GDB 進行 debug,還需要進行以下步驟:

$ 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

$ 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-環境中的步驟,寫一個 Shell Script 負責啟動 UML:

$ 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

TODO: 各個參數意義

./rootfs/init.sh 作為 initial process,其內容如下:

$ 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。

TODO: /proc/sys 用途

最後嘗試透過剛剛寫的 Script 啟動 UML:

$ ../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 目錄下的檔案:

$ ls ./rootfs/bin                
bbsuid  busybox  rc-status  sh  uniso

結果發現只有系統應包含的基本命令的執行檔都不在該目錄下,但其中包含了一個 busybox 執行檔,而根據官方文件的說明,它是一個整合了多種 UNIX 常用的工具的單一執行檔,可以透過像是 busybox command 的形式執行對應的命令:

$ ../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 執行命令:

TODO: 官方文件對這個參數的說明

$ ../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 的實驗環境 - 準備核心模組中的命令進行 make modules 以及 make modules_installmake modules_installMODLIB 要改成 INSTALL_MOD_PATH,且指定目錄只須為 pwd/rootfs

$ 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

TODO: 為什麼要這些核心模組

若依照建構 User-Mode Linux 的實驗環境 - 準備核心模組使用 MODLIB 的話,會發生以下錯誤:

$ 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 指定:

$ make help | grep modules_install                                          
  modules_install - Install all modules to INSTALL_MOD_PATH (default: /)

而參考官方文件的 Building External Modules - Module Installation 中的範例也是使用 INSTALL_MOD_PATH 作為參數:

$ make INSTALL_MOD_PATH=/frodo modules_install
=> Install dir: /frodo/lib/modules/$(KERNELRELEASE)/kernel/

然後就會根據核心的版本號建立相應的目錄結構,不用額外修改目錄名稱。

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

Install simplefs in UML

參考建構 User-Mode Linux 的實驗環境 - 準備核心模組 中 Makefile 的部份,對 simplefs 的 Makefile 進行修改,但為了保持彈性,這邊選擇根據編譯時是否指定架構決定是否要加 ARCHARG,而不是直接指定架構 ARCH=um

 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,但 KDIRPWD 要分別設定成核心目錄以及 simplefs 目錄:

$ $ 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 中載入核心模組:

$ 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 的檔案系統:

$ 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 進行核心追蹤和分析中的說明,建制 GDB Script,並準備一個 Shell Script 自動執行 debug 所需的命令:

$ 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,並掛載模組以及剛建立的檔案系統:

$ ../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]

[Detaching after fork from child process 16651] 訊息的意義?

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

在 host 有出現 ls -a 沒有顯示 . 以及 .. 的問題:

$ ls -a
test

但是在 UML 中卻沒有出現這個問題。

Debug simplefs via GDB

首先進入到 UML 掛載模組以及 simplefs:

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 #

若先在 GDB 中執行 lx-symbols 命令再回到 UML 中 insmod 會不斷跳出的訊息?

loading @0x648b6000: /home/freshliver/tmp/linux/stable/linux-5.12/simplefs/simplefs.ko

接著在透過 pkill -SIGUSR1 -o vmlinux 回到 GDB,透過 lx-symbols 檢查核心模組的 Debug Symbols 有載入了,因此嘗試對 simplefs_mkdir 函式設定中斷點:

(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 命令測試中斷點是否能夠正常運作:

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

$ egrep -c '(vmx|svm)' /proc/cpuinfo

結果應為非 0 的數字,代表 CPU 支援虛擬化,可以從 BIOS 中開啟虛擬化並繼續以下步驟。

2. Install QEMU/KVM

參考 Ubuntu 官方文件中的 KVM 安裝教學進行安裝。

安裝完成後透過以下幾個命令檢查是否安裝成功,預期輸出應如下:

$ 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 的版本:

$ 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 以及 Bug hunting 中的說明調整某些設定:

  • 啟動
    • CONFIG_DEBUG_INFO
    • CONFIG_DEBUG_KERNEL
    • CONFIG_GDB_SCRIPTS
  • 關閉
    • CONFIG_COMPILE_TEST
    • CONFIG_DEBUG_INFO_REDUCED
    • CONFIG_DEBUG_INFO_NONE

各項設定可以參考 lib/Kconfig.debuginit/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_INFOCONFIG_GDB_SCRIPTS 並關閉 CONFIG_DEBUG_INFO_REDUCEDCONFIG_DEBUG_INFO_NONE,可以再從 .config 中檢查對應設定是否正確的啟動以及關閉。

在 menuconfig 中可以按下 / 來搜尋特定設定的路徑、dependencies。

接著再透過 $ make -j`nproc` 開始編譯核心,最後應會得到兩個重要的檔案:arch/x86_64/boot/bzImagevmlinux,前者是 Linux Kernel 的執行檔,能透過 QEMU 執行,而後者則包含 Debug 用的資訊,能夠配合 GDB 與前者進行互動。

4. Make Rootfs

基本上與前面 UML 使用的 rootfs 相同,這邊透過 QEMU 的 -hda 參數讓 QEMU 能把 host 的映像檔當作 rootfs 來使用,因此要先建立並掛載一個映像檔:

$ 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 能修改:

$ ls -dl rootfs
drwxr-xr-x 3 root root 4096 Jun 27 23:26 rootfs

因此為了避免透過 sudo 操作,這邊透過 chown 讓當前使用者能存取該目錄:

$ 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 需要的檔案都放進映像檔的掛載目錄中:

$ ../rootfs.sh
...

接著是編譯 kernel module 並加入到映像檔中:

$ 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 中:

$ make -C simplefs KDIR=`pwd` PWD=`pwd`/simplefs
$ make -C simplefs test.img
$ cp -r simplefs rootfs/

最後 init script 的內容也與 UML 的內容相同:

$ 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

這邊直接沿用 UML 的 init sciprt,所以之 command line 前面顯示的名稱也會是 UML

為了避免 QEMU 使用 rootfs.img 映像檔時修改到這個目錄的內容、造成檔案損毀,這邊直接卸載這個映像檔:

$ sudo umount rootfs

5. Debug Kernel in QEMU via GDB

最後是透過 QEMU 執行核心並透過 GDB 進行 Debug,為此需要準備兩個 Terminal 分別執行 QEMU 與 GDB。

可以利用終端機分割畫面功能或是 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 參數:

而以上整合成一個 Shell Script qemu.sh 方便執行:

$ 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 執行了:

$ ../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:

QEMU 5.2.0 monitor - type 'help' for more information
(qemu) c
...

等個幾秒後應該就會看到熟悉的界面以及錯誤訊息:

/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。

若是太快離開 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

#!/bin/sh
gdb -q \
    -ex "add-auto-load-safe-path ./scripts/gdb/vmlinux-gdb.py" \
    -ex "target remote :1234" \
    vmlinux

這兩個 Script 皆應在 kernel 的最上層目錄中(/path/to/your/linux-5.18/)執行。

接著執行這個 sciprt 來測試是否能正常透過 GDB 進行 debug:

$ ../debug-qemu.sh
Reading symbols from vmlinux...
Remote debugging using :1234
0x000000000000fff0 in exception_stacks ()
(gdb) c
Continuing.

這個 script 應在 ../qemu.sh 後執行。

在 GDB 中輸入 c 後,qemu 應該就能正常啟動 kernel 了。啟動完成後在 kernel 中嘗試掛載 simplefs:

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 畫面測試:

(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,而中斷點也應該能正常運作:

UML:/ # mkdir /test/aaaaa

(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 版為例):

$ 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:

$ which gdb
/path/to/build/and/install/gdb/build/bin/gdb

Undefined command: "lx-symbols"

出現這個原因可能是因為 vmlinux-gdb.py 未正確載入,可透過 info auto-load python-scripts 檢查,理想狀況應該可以看到:

(gdb) info auto-load python-scripts
Loaded  Script
Yes     /path/to/kernel/vmlinux-gdb.py

而以下列出幾種可能的情況:

No auto-load scripts

$ 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 的根目錄:

$ ln -s scripts/gdb/vmlinux-gdb.py .

auto-loading has been declined

$ 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 檔案中:

echo "add-auto-load-safe-path /path/to/kernel/scripts/gdb/vmlinux-gdb.py" >> ~/.config/gdb/gdbinit

SyntaxError: invalid syntax

$ 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() 包起來:

LX_SB_RDONLY = gdb.parse_and_eval("((((1UL))) << (0))")
...

References