--- tags: LINUX KERNEL, LKI --- # 測試 Linux 核心的虛擬化環境 [virtme](https://git.kernel.org/pub/scm/utils/kernel/virtme/virtme.git) 是 Linux 核心開發者利用 QEMU 所建立一個輕量級的 Linux 核心測試環境,和 Linux 核心原始程式碼有很好的整合。 :::warning [virtme](https://git.kernel.org/pub/scm/utils/kernel/virtme/virtme.git) 已不再維護,請改用 [virtme-ng](https://github.com/arighi/virtme-ng) ::: ## 預先安裝套件 以 Ubuntu Linux 22.04 來說,需要安裝以下套件: ```shell $ sudo apt -y -q install \ bc \ flex \ bison \ build-essential \ expect \ git \ libncurses-dev \ libssl-dev \ libelf-dev \ u-boot-tools \ wget \ xz-utils \ qemu-kvm \ iproute2 \ python3 \ python3-pip ``` ## 安裝 [virtme](https://git.kernel.org/pub/scm/utils/kernel/virtme/virtme.git) [virtme](https://git.kernel.org/pub/scm/utils/kernel/virtme/virtme.git) 以 Python3 撰寫,可用 pip 安裝,注意 Python 版本需要大於 `3.3` ```shell $ pip3 install --user \ git+https://git.kernel.org/pub/scm/utils/kernel/virtme/virtme.git ``` :::success :warning: `$PATH` 環境變數需要一併更新: ```shell $ export PATH=$HOME/.local/bin:$PATH ``` 或者列於 `$HOME/.bashrc` 中。 ::: ## 利用 [virtme](https://git.kernel.org/pub/scm/utils/kernel/virtme/virtme.git) 建立 Linux 核心測試環境 首先,找一個容納 6 GB 空間的目錄,你需要記住絕對路徑,之後我們還用得到。為了行文的便利,我們用 `/home/ubuntu` 來稱呼。 :::success :warning: 以下命令都該用一般使用者權限來執行,切勿用 `root` 帳號 ::: 執行以下命令來取得 Linux 核心原始程式碼: ```shell $ git clone https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux $ cd linux $ git checkout -b linux-6.1.y origin/linux-6.1.y ``` 使用 [virtme](https://git.kernel.org/pub/scm/utils/kernel/virtme/virtme.git) 選取預設核心組態並編譯: ```shell $ virtme-configkernel --defconfig $ make ARCH=x86 CROSS_COMPILE=x86_64-linux-gnu- -j$(nproc) ``` > 針對 Arm64 處理器架構,改為 `make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc)` 命令 :::success :warning: 這邊需要確保 `CONFIG_DEBUG_INFO` 在組態中有被開啟,以利後續實驗使用。方法是在 `$ make` 命令前,執行 `$ grep CONFIG_DEBUG_INFO .config`,預期要看到 `CONFIG_DEBUG_INFO=y` ::: 在編譯結束後,預期可見以下訊息: ``` Kernel: arch/x86/boot/bzImage is ready ``` > 針對 Arm64 處理器架構,是 `arch/arm64/boot/Image` 透過以下命令,在 QEMU 虛擬機器中啟動 Linux 核心: ```shell $ virtme-run --kdir . --mods=auto ``` :::success :warning: 看到 `virtme-init` 開頭的訊息時,請保持耐心等待 > 上方命令的 `--mods=auto` 指定自動掛載 Linux 核心模組,若不需要這機制,可改為 `$ virtme-run --kimg arch/x86/boot/bzImage ` ::: 預期可見以下訊息: ``` virtme-init: console is ttyS0 root@(none):/# ``` 這時你可在 [virtme](https://git.kernel.org/pub/scm/utils/kernel/virtme/virtme.git) 輸入一些命令,例如: ```shell # uname -a ``` :::success 為了區隔模擬環境和宿主 (host,即 Ubuntu Linux) 端的終端機操作,我們約定 ==`$ `== 開頭的命令是執行於宿主端環境,而 ==`# `== 開頭的命令則執行於 [virtme](https://git.kernel.org/pub/scm/utils/kernel/virtme/virtme.git) ::: 當你見到 `Linux (none) 6.1.21` 一類的訊息,就表示成功運作 [virtme](https://git.kernel.org/pub/scm/utils/kernel/virtme/virtme.git)。 倘若要離開測試環境,你可按下 `Ctrl-A` 放開再按下 `X` 按鍵。 ### `virtme` 的選項 除了用 `-kdir` 選項指定核心映像檔,尚可指定若干選項,例如: * `-a` : 附加 Linux 核心啟動參數 * 例如: `-a "nokaslr"` 抑制 ASLR * `--disk` : 指定磁碟,測試檔案系統或 I/O 操作很好用 ```shell $ dd if=/dev/zero of=/tmp/disk.img bs=1M count=1024 $ virtme-run --kimg arch/x86/boot/bzImage --disk "disk1=/tmp/disk.img" ``` 之後在模擬環境中可執行 `fdisk -l /dev/sda`,隨後亦可掛載檔案系統: ```shell # mkdir -p /tmp/disk # mkfs.ext4 /dev/sda # mount /dev/sda /tmp/disk ``` * `--kimg` : 指定核心映像檔,例如 `-kimg arch/x86/boot/bzImage` ## 載入核心模組 使用 [virtme](https://git.kernel.org/pub/scm/utils/kernel/virtme/virtme.git) 的 `kimg` 參數啟動核心映像檔後,若我們想要使用 `modprobe` 載入與核心一同編譯的核心模組時,會因為與 Ubuntu Linux 共用檔案系統,而無法從預設路徑 `/lib/modules/$(uname -r)` 中讀取相關設定檔。可以透過以下的方式來進行設定: 我們需要將核心模組安裝到一個暫存的目錄中 ```shell $ make modules_install INSTALL_MOD_PATH=/home/ubuntu/test-kmod ``` :::success :warning: 暫存目錄需避免設定於 `/tmp` 目錄之下,因為宿主的 `/tmp` 與模擬環境中的 `/tmp` 無法共通 ::: 接著啟動模擬環境,並將前述放置核心模組的目錄掛載到 `/lib/modules/` ```shell # mount --bind /home/ubuntu/test-kmod/lib/modules /lib/modules ``` 設定完畢後,就能夠使用 `modprobe` 載入預先編譯好的核心模組。例如載入 TLS 模組 ```shell # modprobe tls ``` 並且可以使用 `lsmod` 列出目前載入的所有核心模組及其相依性。 ## 編譯和測試核心模組 [virtme](https://git.kernel.org/pub/scm/utils/kernel/virtme/virtme.git) 建立的虛擬環境所用的檔案系統是共用 Ubuntu Linux 的檔案系統 (透過 [9P over VirtIO](https://www.linux-kvm.org/page/9p_virtio)),因此你可以在裡頭編譯核心模組! 還記得之前記住的絕對路徑吧?Linux 核心原始程式碼就在 `/home/ubuntu/linux` 中,不過在 [virtme](https://git.kernel.org/pub/scm/utils/kernel/virtme/virtme.git) 創造的虛擬環境中,該路徑是唯讀。為了便利起見,我們就在 `/tmp` 目錄實驗。你可以研讀 [virtme](https://git.kernel.org/pub/scm/utils/kernel/virtme/virtme.git) 文件,以得知檔案分享和權限處理的機制。 依據 UNIX 慣例,我們用 Hello World 核心模組來示範:首先在 `/tmp` 建立實驗用的目錄: ```shell # mkdir -p /tmp/hello ``` 建立檔案 `/tmp/hello/hello.c`,其內容為: ```cpp #include <linux/init.h> #include <linux/module.h> MODULE_LICENSE("GPL"); static int hello_init(void) { printk(KERN_ALERT "Hello World! - init\n"); return 0; } static void hello_exit(void) { printk(KERN_ALERT "Hello World! - exit\n"); } module_init(hello_init); module_exit(hello_exit); ``` 還要有對應的 `/tmp/hello/Makefile`,內容如下: (記得要更換 `/home/ubuntu` 這個路徑) ```cpp KDIR=/home/ubuntu/linux obj-m += hello.o PWD := $(shell pwd) # KDIR := $(PWD)/.. # 上面這行會導致錯誤,會使 KDIR 導引到錯誤位置 by hankTaro all: $(MAKE) -C $(KDIR) M=$(PWD) modules clean: $(MAKE) -C $(KDIR) M=$(PWD) clean ``` 注意在 `all:` 和 `$(MAKE)` 之間不是空白字元,而是 Tab,同理,`clean:` 和 `$(MAKE)` 之間也以 Tab 區隔。 接著在 [virtme](https://git.kernel.org/pub/scm/utils/kernel/virtme/virtme.git) 編譯核心模組: ```shell # make ``` :::success :warning: 在虛擬環境中,要等待一陣子,請保持耐心。之後應該在宿主端編譯核心及其模組。 ::: 預期可見以下輸出: ``` make[1]: Entering directory '/home/ubuntu/linux' CC [M] /tmp/hello/hello.o MODPOST 1 modules CC [M] /tmp/hello/hello.mod.o LD [M] /tmp/hello/hello.ko make[1]: Leaving directory '/home/ubuntu/linux' ``` 萬事俱備,就來測試: ```shell # insmod hello.ko ``` 預期可見以下訊息: ``` [ 2096.143949] hello: loading out-of-tree module taints kernel. [ 2096.160912] Hello World! - init ``` 移除核心模組也行: ```shell # rmmod hello ``` ## 透過 [virtme](https://git.kernel.org/pub/scm/utils/kernel/virtme/virtme.git) 來測試 Linux 核心 我們故意在 Linux 核心原始程式碼做以下更動: ```diff --- a/init/main.c +++ b/init/main.c @@ -1372,6 +1372,7 @@ static int __ref kernel_init(void *unused) rcu_end_inkernel_boot(); + *(char *) NULL = 0; if (ramdisk_execute_command) { ret = run_init_process(ramdisk_execute_command); if (!ret) ``` 透過上述的 `$ make ARCH=x86 CROSS_COMPILE=x86_64-linux-gnu-` 命令來編譯核心模組,接著用 `$ virtme-run --kimg arch/x86/boot/bzImage` 啟動 Linux 核心,預期可見到以下錯誤訊息: ``` [ 1.976785] Call Trace: [ 1.977534] ret_from_fork+0x22/0x40 [ 1.977835] Modules linked in: [ 1.978013] CR2: 0000000000000000 [ 1.978486] ---[ end trace eeb768630587adc1 ]--- [ 1.978687] RIP: 0010:kernel_init+0x3f/0x100 [ 1.978838] Code: 00 0f 84 d2 00 00 00 e8 3f 7a 5d ff e8 ba 86 55 ff e8 95 2d 56 ff c7 05 bf 47 a1 00 02 00 00 00 e8 b6 0c 6d ff e8 11 2c 5d ff <c6> 04 25 00 00 00 00 00 48 8b 3d 4a 4b fd 00 48 85 ff 74 21 e8 28 [ 1.979560] RSP: 0018:ffffbbf280013f50 EFLAGS: 00000246 [ 1.979770] RAX: 0000000000000000 RBX: ffffffff9bb72490 RCX: 0000000000000000 [ 1.980045] RDX: 0000000000000005 RSI: ffff9bb447498018 RDI: 000000000002e6b0 [ 1.980333] RBP: 0000000000000000 R08: 0000000000000163 R09: 0000000000000049 [ 1.980613] R10: fffff56d00147b88 R11: 4b363735203a7972 R12: 0000000000000000 [ 1.980902] R13: 0000000000000000 R14: 0000000000000000 R15: 0000000000000000 [ 1.981133] FS: 0000000000000000(0000) GS:ffff9bb447a00000(0000) knlGS:0000000000000000 [ 1.981433] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033 [ 1.981644] CR2: 0000000000000000 CR3: 000000000520a000 CR4: 00000000000006f0 [ 1.982090] Kernel panic - not syncing: Attempted to kill init! exitcode=0x00000009 [ 1.982676] Kernel Offset: 0x1a000000 from 0xffffffff81000000 (relocation range: 0xffffffff80000000-0xffffffffbfffffff) [ 1.983192] ---[ end Kernel panic - not syncing: Attempted to kill init! exitcode=0x00000009 ]--- ``` 我們在即將啟動 `init` 程式之前,故意做非預期的記憶體操作,這時就觸發 Linux 核心註冊的例外處理機制。 :::warning 上述實驗結束後,請還原程式碼並編譯核心: ```shell $ git reset --hard HEAD $ make ARCH=x86 CROSS_COMPILE=x86_64-linux-gnu- -j$(nproc) ``` ::: ## 搭配 [crash](https://crash-utility.github.io) 進行核心偵錯 許多時候,除了在觸發 kernel panic 時系統會提供對應的 call trace 外,我們也需要交叉比對其他的資訊,諸如 `dmesg` 或是所有行程,方可定位出問題所在,而 crash 就是一款可用來偵錯的工具。 簡單來說,[crash](https://crash-utility.github.io) 是一種針對核心偵錯特化的一種 GDB `crash` 的維護者,Red Hat 工程師 David Aderson 在〈[Whilte Paper: Crash Utility](https://crash-utility.github.io/crash_whitepaper.html)〉提到: > While gdb is an incredibly powerful tool, it is designed to debug user programs, and is not at all "kernel-aware". ### 安裝 [crash](https://crash-utility.github.io) 雖然 Ubuntu Linux 提供預先編譯好的 crash 套件,但因為較新的核心 (v5.17 以上) 內部結構已有調整,因此需要 v8.0.1 以上之 crash 才能解析。 我們可以在 [crash-utility](https://crash-utility.github.io) 下載 8.0.2 版本的 crash 並進行編譯與安裝: ```shell $ wget https://github.com/crash-utility/crash/archive/8.0.2.tar.gz -O crash.tar.gz $ tar xf crash.tar.gz $ cd crash-8.0.2 $ make -j$(nproc) $ sudo make install ``` :::success :warning: 編譯 `crash` 需要較長的時間,因為要從 GDB 原始程式碼建構,後者又有一系列相依的套件要編譯。 ::: ### 產生 kernel dump 因為 [virtme](https://git.kernel.org/pub/scm/utils/kernel/virtme/virtme.git) 使用到 QEMU 來建立虛擬環境,因此我們可用 QEMU 的功能選項來產生執行時的 kernel dump。 首先我們需要在啟動虛擬環境的命令中加入 `--qemu-opts -qmp tcp:localhost:4444,server,nowait` 這樣的參數,啟動 QEMU 的 [QEMU Machine Protocol (QMP)](https://wiki.qemu.org/Documentation/QMP) 功能,命令如下: ```shell $ virtme-run --kimg arch/x86/boot/bzImage \ --qemu-opts -qmp tcp:localhost:4444,server,nowait ``` 啟動虛擬環境後,我們使用 [Linux Magic System Request](https://www.kernel.org/doc/html/latest/admin-guide/sysrq.html) 來觸發 kernel panic ```shell # echo c > /proc/sysrq-trigger ``` 預期可以看到系統發生 kernel panic 並印出相關訊息。 :::danger :rotating_light: 請確保該命令執行在**模擬環境**之中,若執行在宿主系統上會導致目前使用的系統崩潰 ::: 維持虛擬環境繼續執行的情況下,我們回到宿主系統,使用 [QMP](https://wiki.qemu.org/Documentation/QMP) 來與現行 QEMU 環境通訊,擷取目前虛擬環境的 kernel dump。 依照以下的步驟產生 kernel dump 1. 使用 telnet 與 QEMU 連線,預期可以看到 [QMP](https://wiki.qemu.org/Documentation/QMP) 的歡迎訊息 ```shell $ telnet localhost 4444 {"QMP": {"version": {"qemu": {"micro": 0, "minor": 6, "major": 1}, "package": ""}, "capabilities": []}} ``` 2. 輸入以下的命令進入 [QMP](https://wiki.qemu.org/Documentation/QMP) 的命令模式: ```shell telnet> { "execute": "qmp_capabilities" } ``` 3. 執行命令將虛擬環境的記憶體內容傾到於指定的檔案中 ```shell telnet> { "execute": "dump-guest-memory", "arguments": {"paging": false, "protocol": "file:$out_file" }} ``` ### 執行 crash 並分析 kernel dump 準備好含有 debug symbol 的 vmlinux 以及 kernel dump,就可以開始使用 crash 偵錯 ```shell $ crash /path/to/your/vmlinux /path/to/your/vmcore.img <...> KERNEL: vmlinux DUMPFILE: vmcore.img CPUS: 1 DATE: Mon Mar 20 22:24:12 CST 2023 UPTIME: 00:00:32 LOAD AVERAGE: 1.00, 0.27, 0.09 TASKS: 57 NODENAME: (none) RELEASE: 6.2.7 VERSION: #2 SMP PREEMPT_DYNAMIC Sat Mar 18 20:58:35 CST 2023 MACHINE: x86_64 (1991 Mhz) MEMORY: 127.5 MB PANIC: "Kernel panic - not syncing: sysrq triggered crash" PID: 137 COMMAND: "bash" TASK: ffff8fd4c2520000 [THREAD_INFO: ffff8fd4c2520000] CPU: 0 STATE: TASK_RUNNING (PANIC) crash> ``` 進入 crash 後,除了會先印出當前使用核心的基本訊息以及 panic 的原因之外,我們也可以透過 shell 風格的命令來印出發生 panic 時 kernel ring buffer 的內容 下面示範印出 ring buffer 最後 30 行的內容: ```shell crash> dmesg | tail -30 [ 32.542695] sysrq: Trigger a crash [ 32.542986] Kernel panic - not syncing: sysrq triggered crash [ 32.543488] CPU: 0 PID: 137 Comm: bash Not tainted 6.2.7 #2 [ 32.543848] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.15.0-1 04/01/2014 [ 32.544343] Call Trace: [ 32.545231] <TASK> [ 32.545606] dump_stack_lvl+0x37/0x50 [ 32.546117] panic+0x2fd/0x320 [ 32.546275] ? _printk+0x57/0x80 [ 32.546440] sysrq_handle_crash+0x15/0x20 [ 32.546622] __handle_sysrq+0xa1/0x180 [ 32.546794] write_sysrq_trigger+0x23/0x40 [ 32.546969] proc_reg_write+0x54/0xa0 [ 32.547134] ? preempt_count_add+0x4c/0xa0 [ 32.547281] vfs_write+0xc4/0x3c0 [ 32.547407] ? __do_sys_newfstatat+0x35/0x60 [ 32.547532] ksys_write+0x5e/0xe0 [ 32.547664] do_syscall_64+0x3f/0x90 [ 32.547800] entry_SYSCALL_64_after_hwframe+0x72/0xdc [ 32.548174] RIP: 0033:0x7f4ebe5bba37 [ 32.548608] Code: 10 00 f7 d8 64 89 02 48 c7 c0 ff ff ff ff eb b7 0f 1f 00 f3 0f 1e fa 64 8b 04 25 18 00 00 00 85 c0 75 10 b8 01 00 00 00 0f 05 <48> 3d 00 f0 ff ff 77 51 c3 48 83 ec 28 48 89 54 24 18 48 89 74 24 [ 32.549317] RSP: 002b:00007ffda4922dd8 EFLAGS: 00000246 ORIG_RAX: 0000000000000001 [ 32.549692] RAX: ffffffffffffffda RBX: 0000000000000002 RCX: 00007f4ebe5bba37 [ 32.550009] RDX: 0000000000000002 RSI: 000056527820daf0 RDI: 0000000000000001 [ 32.550323] RBP: 000056527820daf0 R08: 0000000000000000 R09: 000056527820daf0 [ 32.550641] R10: 0000000000000077 R11: 0000000000000246 R12: 0000000000000002 [ 32.550929] R13: 00007f4ebe6c1780 R14: 00007f4ebe6bd600 R15: 00007f4ebe6bca00 [ 32.551330] </TASK> [ 32.552166] Kernel Offset: 0x17000000 from 0xffffffff81000000 (relocation range: 0xffffffff80000000-0xffffffffbfffffff) [ 32.552936] ---[ end Kernel panic - not syncing: sysrq triggered crash ]--- ``` 從上述的結果,我們可以得知 panic 是被 PID 為 `137` 的 bash 所觸發 同時我們也可以利用 `ps` 查看該核心所有行程和狀態 ```shell crash> ps PID PPID CPU TASK ST %MEM VSZ RSS COMM 0 0 0 ffffffff99a14a00 RU 0.0 0 0 [swapper/0] 1 0 0 ffff8fd4c1128000 IN 1.7 4492 2228 virtme-init 2 0 0 ffff8fd4c1128e40 IN 0.0 0 0 [kthreadd] 3 2 0 ffff8fd4c1129c80 ID 0.0 0 0 [rcu_gp] 4 2 0 ffff8fd4c112aac0 ID 0.0 0 0 [rcu_par_gp] 5 2 0 ffff8fd4c112b900 ID 0.0 0 0 [slub_flushwq] 6 2 0 ffff8fd4c112c740 ID 0.0 0 0 [netns] 7 2 0 ffff8fd4c112d580 ID 0.0 0 0 [kworker/0:0] 8 2 0 ffff8fd4c112e3c0 ID 0.0 0 0 [kworker/0:0H] <...> 83 74 0 ffff8fd4c1b6b900 IN 2.6 22988 3464 systemd-udevd 84 74 0 ffff8fd4c1b6c740 IN 2.6 22988 3388 systemd-udevd 85 74 0 ffff8fd4c1b6d580 IN 2.5 22988 3308 systemd-udevd 86 74 0 ffff8fd4c1b6e3c0 IN 2.8 22988 3612 systemd-udevd > 137 1 0 ffff8fd4c2520000 RU 2.0 4644 2656 bash ``` 於是我們可以使用 `bt` 調查編號 137 的行程對應的 call stack 及其暫存器狀態 ```shell crash> bt 137 PID: 137 TASK: ffff8fd4c2520000 CPU: 0 COMMAND: "bash" #0 [ffff9d09c026bd90] panic at ffffffff98084829 #1 [ffff9d09c026be10] sysrq_handle_crash at ffffffff985efac5 #2 [ffff9d09c026be18] __handle_sysrq at ffffffff985f00f1 #3 [ffff9d09c026be48] write_sysrq_trigger at ffffffff985f0703 #4 [ffff9d09c026be58] proc_reg_write at ffffffff982fd284 #5 [ffff9d09c026be70] vfs_write at ffffffff98274cf4 #6 [ffff9d09c026bf00] ksys_write at ffffffff982751ae #7 [ffff9d09c026bf38] do_syscall_64 at ffffffff98e2175f #8 [ffff9d09c026bf50] entry_SYSCALL_64_after_hwframe at ffffffff990000ae RIP: 00007f4ebe5bba37 RSP: 00007ffda4922dd8 RFLAGS: 00000246 RAX: ffffffffffffffda RBX: 0000000000000002 RCX: 00007f4ebe5bba37 RDX: 0000000000000002 RSI: 000056527820daf0 RDI: 0000000000000001 RBP: 000056527820daf0 R8: 0000000000000000 R9: 000056527820daf0 R10: 0000000000000077 R11: 0000000000000246 R12: 0000000000000002 R13: 00007f4ebe6c1780 R14: 00007f4ebe6bd600 R15: 00007f4ebe6bca00 ORIG_RAX: 0000000000000001 CS: 0033 SS: 002b ``` 如果我們懷疑其中的 `sysrq_handle_crash` 函式可能是觸發 panic 的元兇,則可以使用類似 GDB 的命令 `list` 列出該函式對應的原始碼 ```shell crash> gdb list sysrq_handle_crash 146 #else 147 #define sysrq_unraw_op (*(const struct sysrq_key_op *)NULL) 148 #endif /* CONFIG_VT */ 149 150 static void sysrq_handle_crash(int key) 151 { 152 /* release the RCU read lock before crashing */ 153 rcu_read_unlock(); 154 155 panic("sysrq triggered crash\n"); ``` 從上面的結果我們可以看到在第 155 行的程式碼實際呼叫 `panic`,導致核心進入 panic 的處理機制。 ## 參考資料 * [Debugging the Linux kernel with GDB](https://sergioprado.blog/debugging-the-linux-kernel-with-gdb/)