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