# 第三講:透過 User-Mode Linux 建構實驗環境
> 本筆記僅為個人紀錄,相關教材之 Copyright 為[jserv](http://wiki.csie.ncku.edu.tw/User/jserv)及其他相關作者所有
* 直播:==[Linux 核心設計 : 以 User Mode Linux 進行開發及分析 - 2020/2/8](https://www.youtube.com/watch?v=ivqO-yCgk2o)==
* 詳細共筆:[建構 User-Mode Linux 的實驗環境](https://hackmd.io/@sysprog/user-mode-linux-env)
* 主要參考資料:
* [Advanced testing with UserModeLinux](https://static.sched.com/hosted_files/osseu19/ca/slides.pdf)
* [User-Mode Linux (Politehnica University)](https://elf.cs.pub.ro/soa/res/lectures/lecture-05-uml.pdf)
---
本筆記旨在闡述 User-Mode Linux (UML) 的概念、建構方法、客製化以及如何利用 UML 進行 Linux 核心的學習與偵錯。
---
## User-Mode Linux 概況
### 簡介
User-Mode Linux (以下簡稱 UML) 是一種將 Linux 核心移植到使用者空間 (user-space) 的技術。這使得 Linux 核心可以像一個普通的 Linux 行程 (process) 一樣執行。從技術分類上講,UML 屬於半虛擬化 (para-virtualization)。在 Kernel-based Virtual Machine (KVM) 以及 Intel/AMD 硬體虛擬化加速擴展普及之前 (即 x86 virtualization 技術成熟前),UML 是 Linux 核心內建的唯一虛擬化機制。
與 KVM 等依賴硬體虛擬化擴充 (如 Intel VT-x 或 AMD-V,這些技術大約在 2006 年後開始普及) 的虛擬化技術不同,UML 不需要特定的硬體支援。KVM 能夠利用這些硬體特性提供高效能的完全虛擬化 (full virtualization),通常不需要修改客體作業系統 (guest OS)。而 UML 作為一種半虛擬化技術,它執行的 Linux 核心是經過修改的,使其能夠在另一個宿主 (host) Linux 系統的使用者空間中執行,可以理解為 "Linux on Linux"。

### UML 的主要優勢和應用場景:
* **偵錯與快速測試**:可用於對與硬體架構無關的一般性 Linux 程式進行偵錯和快速測試。由於 UML 本身就是一個行程,可以使用如 GDB 等標準偵錯工具直接進行核心層級的追蹤。
* **檔案系統檢驗**:方便檢驗客製化檔案系統的完整性與正確性,特別是與 init scripts 相關的部分。
* **虛擬網路環境**:可以在單機上建構虛擬網路環境,利用多個 UML 實例進行網路模擬操作。
* **核心開發與教學**:可以搭配 GDB 追蹤 Linux 核心主體流程,快速測試新的演算法或引入改進,是一個易於部署的 Linux 教學環境。
* **輕量級隔離**:UML 執行的檔案系統對宿主 Linux 而言僅是普通檔案,提供了一個類似沙盒 (sandbox) 的受限封閉測試機制,可以在不損害真實硬體與系統的情況下進行實驗。
### UML 採用案例:
* **[KUnit](https://www.kernel.org/doc/html/latest/dev-tools/kunit/)**:
Kernel unit-testing framework (KUnit) 也採用 UML,因為它允許在不需要完整虛擬機器或實際硬體的情況下執行核心單元測試。對 KUnit 而言,UML 的限制反而成為優點,讓核心開發者能專注於測試硬體無關的邏輯。
* **Android**:
Android 5.0 之後也使用 UML 來測試核心和網路連線,例如其 [Kernel Networking Unit Tests](https://source.android.com/devices/architecture/kernel/network_tests)。
* **[PCAP 整合](http://user-mode-linux.sourceforge.net/old/UserModeLinux-HOWTO-6.html)**:
UML 可直接連結 libpcap 函式庫,使得在 UML 內部運行的網路驅動程式或應用程式能透過標準的 libpcap 介面進行封包的擷取與注入。此方式無需真實網路硬體或複雜的硬體模擬,簡化了網路功能的測試。其實際網路連線功能有限,但用於監控主機流量則非常適合。
* **時間旅行 (Timetravel)**:
UML 提供「時間旅行」功能 (透過 `CONFIG_UML_TIME_TRAVEL_SUPPORT` 啟用)。此功能允許測試框架**控制 UML 內部的虛擬時鐘**,例如跳過冗長的等待、模擬時間快轉或無限 CPU 算力,從而加速測試執行並有效驗證時間敏感的程式碼路徑。
### UML 核心觀念:
* UML 本身是一個**全功能的 Linux 核心**,擁有專屬的虛擬環境。
* 對**硬體**的支援僅依賴於**宿主 Linux 系統**。
* **UML 的運作機制**:
* 核心被編譯成一個普通的執行檔,並連結到 C 函式庫 (libc)。
* 擁有一個真實的 `main()` 函數作為程式進入點。
* 在 UML 內執行的每個使用者程式都運行在 [ptrace()](http://man7.org/linux/man-pages/man2/ptrace.2.html) 系統呼叫的監控之下,使得 **GDB 等工具可以對其進行偵錯**。
* **系統呼叫 (syscall)**:
* UML 內部的 syscall 會在宿主系統上**轉為無操作 (no-op)**,然後呼叫 UML 核心內的**系統呼叫處理常式**。
* **信號 (signal)**:
* 分頁錯誤 (Page faults) 通過 `SIGSEGV` 信號處理。
* 中斷 (Interrupts),如計時器中斷,則通過信號 (signals) 來模擬。
* `earlyprintk` (早期核心訊息輸出) 在 UML 中直接**透過宿主系統的 `fprintf()` 實現**,避免了早期硬體初始化未完成時訊息遺失的問題。
* **Linux Kernel Library**:UML 也預計會重用 [Linux Kernel Library (LKL)](https://github.com/lkl/linux) 的成果,LKL 旨在讓 Linux 核心的程式碼更容易被其他專案複用。
> 延伸閱讀: [LKL: 重用 Linux 核心的成果](https://hackmd.io/@sysprog/linux-lkl)
---
## 建構 User-Mode Linux 和搭配的檔案系統
以下步驟說明如何從 Linux 核心原始程式碼編譯出 UML 並建構其所需的檔案系統。
### 編譯準備
首先,在宿主 Linux 系統 (如 Ubuntu) 上安裝必要的編譯工具與函式庫:
```shell
$ sudo apt install build-essential libncurses-dev flex bison
$ sudo apt install xz-utils wget ca-certificates bc
```
### 獲取與解壓核心原始碼
以 Linux 核心 v6.14.6 為例:
```shell
$ wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.14.6.tar.xz
$ tar xvf linux-6.14.6.tar.xz
$ cd linux-6.14.6
```
建議設定一個**環境變數**指向此**工作目錄**:
```shell
export WS=`pwd`
```
### 編譯 User-Mode Linux
設定核心組態並編譯,關鍵在於指定 `ARCH=um`:
```shell
$ make mrproper
$ make defconfig ARCH=um SUBARCH=x86_64
$ make menuconfig ARCH=um # 可選,用於自訂配置,例如啟用 Host FS、Magic SysRq 等
$ make linux ARCH=um SUBARCH=x86_64 -j `nproc`
```
編譯成功後,會在當前目錄下產生一個名為 `linux` 的可執行檔。這就是 UML 核心。
```shell
$ file linux # 預期輸出類似:linux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/l, for GNU/Linux 3.2.0
$ ./linux --help # 可以看到 UML 核心的命令列選項
```
### 準備 Root File System (rootfs)
UML 核心需要一個 **根檔案系統 (root file system, rootfs)** 才能啟動。這裡我們使用輕量級的 [Alpine Linux](https://alpinelinux.org/) 來製作 rootfs。
由於建立檔案系統時可能涉及權限操作,但為了**避免直接使用 `sudo` 影響開發環境**,可以安裝 `fakeroot`:
```shell
$ sudo apt install fakeroot
```
**步驟**:(使用 Alpine Linux 的套件管理系統 APK (Alpine Package Keeper) 建立 rootfs)
1. 設定環境變數 REPO 為 APK 套件庫的 URL
```shell
$ export REPO=http://dl-cdn.alpinelinux.org/alpine/v3.21/main
$ mkdir -p rootfs
```
2. 下載該 repo 的索引檔 APKINDEX.tar.gz,並解壓縮到 /tmp/,以便後面解析有哪些套件版本可用:
```shell
$ curl $REPO/x86_64/APKINDEX.tar.gz | tar -xz -C /tmp/
```
3. 從剛才解壓縮的索引裡,找到 apk-tools-static 這個套件的名稱和版本,並存在環境變數 APK_TOOL:
```shell
$ export APK_TOOL=`grep -A1 apk-tools-static /tmp/APKINDEX | cut -c3- | xargs printf "%s-%s.apk"`
```
4. 下載剛剛找出的 apk-tools-static 套件 (包含 `apk.static` 執行檔),並直接解壓縮到 `/rootfs`:
```shell
$ curl $REPO/x86_64/$APK_TOOL | fakeroot tar -xz -C rootfs
```
5. 呼叫剛解壓縮出的 `apk.static`,在還沒正式安裝到系統的狀態下就能用
```shell
$ fakeroot rootfs/sbin/apk.static \
--repository $REPO --update-cache \
--allow-untrusted \
--root $PWD/rootfs --initdb add alpine-base
```
若出現 `Operation not permitted` 或 `script exited with error 127` 等錯誤,可暫時忽略。
6. 把 repository 設定寫入到 rootfs/etc/apk/repositories,之後在 guest 裡若要安裝新套件就能直接用 apk:
```shell
$ echo $REPO > rootfs/etc/apk/repositories
```
7. 在 `fstab` 裡加入一行:「開機時自動把 root (/) 掛到標籤為 `ALPINE_ROOT` 的裝置上」。只是在 guest 中做個紀錄 (對於 `hostfs` 掛載並非必要,但有助未來用真硬碟映像時能自動掛載)。
```shell
$ echo "LABEL=ALPINE_ROOT / auto defaults 1 1" >> rootfs/etc/fstab
```
### 啟動 User-Mode Linux
1. **建立一個啟動腳本 `UML.sh`**:
```shell
#!/bin/sh
# 獲取包含此腳本的目錄的絕對路徑
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
"$SCRIPT_DIR/linux" umid=uml0 \
root=/dev/root rootfstype=hostfs rootflags="$SCRIPT_DIR/rootfs" \
rw mem=64M init=/bin/sh quiet
```
* **參數說明**:
* `umid=uml0`:設定 UML 實例的識別碼。
* `root=/dev/root rootfstype=hostfs`:指定根檔案系統類型為 `hostfs`,表示使用宿主機的檔案系統。
* `rootflags=/absolute/path/to/rootfs`:指定 `hostfs` 的實際路徑為 `/rootfs` 的絕對路徑。指定相對路徑的方式是 `hostfs=./rootfs`。
* `rw`:以可讀寫模式掛載根檔案系統。
* `mem=64M`:分配給 UML 的記憶體大小。
* `init=/bin/sh`:指定 UML 啟動後執行的第一個使用者層級程式為 `/bin/sh`。
* `quiet`:抑制核心啟動訊息的輸出。
* `ubd0=/dev/null`:抑制 `Failed to initialize ubd device 0` 的錯誤訊息。
2. **賦予執行權限並啟動 UML**:
```shell
$ chmod +x UML.sh
$ ./UML.sh
```
成功啟動後,會看到類似 `/bin/sh: can't access tty; job control turned off` 的訊息,以及 `# `提示符,表示已進入 UML 環境。
> [!Warning] 注意
> 為了區分宿主機和 UML 環境中的命令,UML 環境中的提示符後續以 `UML # ` 表示。
### UML 環境基本設定
1. **設定 [BusyBox](https://busybox.net/) Symbolic Links**:
由於使用的是 Alpine Linux 提供的最小系統,其中很多常用命令是透過 BusyBox 提供的。需要執行一次安裝命令來建立 symbolic link :
```shell
UML # /bin/busybox --install
```
2. **掛載 [procfs](https://zh.wikipedia.org/zh-tw/Procfs)**:
為了能使用 `ps` 等命令,需要手動掛載 `/proc` 檔案系統:
```shell
UML # mount -t proc none /proc
```
之後便可在 UML 中執行 `ps`、`uname -a` 等命令。
:::info
procfs 是 (process file system) 的縮寫,包含一個偽檔案系統 (啟動時動態生成的檔案系統),用於通過 kernel 訪問 process 的資訊。由於 /proc 不是一個真正的檔案系統,它也就不占用儲存空間,只是占用有限的主記憶體。
:::
3. **觀察 `cpuinfo`**:
編譯時指定的 `ARCH=um` 會體現在 `/proc/cpuinfo` 中:
```shell
UML # cat /proc/cpuinfo
```
預期會看到 `vendor_id: User Mode Linux` 以及 `mode: skas` [(Separate Kernel Address Space)](http://user-mode-linux.sourceforge.net/old/skas.html) 等資訊。
4. **退出 UML**:
在 UML 的 shell 中執行 `exit` 命令即可終止 UML。
```shell
UML # exit
```
此時宿主機會顯示類似 `Kernel panic - not syncing: Attempted to kill init!` 的訊息。若終端機游標消失,可在宿主機執行 `reset` 命令恢復。為了避免每次手動 `reset`,可以在 `UML.sh` 腳本末尾加入:
```shell
stty sane ; echo
```
* `stty sane`:set teletypewriter 是一個用於設定和顯示終端機線路特性的命令,將終端機的設定恢復到一個合理 (sane) 的預設狀態,解決可能出現的游標消失、輸入不回顯、控制鍵失靈等問題。
* `echo`:輸出一個換行符。確保在 UML 會話結束後,host 終端機提示字元會顯示在新的一行,保持介面整潔。
---
## 網路設定
以下步驟說明如何在 UML 中**設定網路**並**與宿主機通訊**。
### 宿主機設定 (Host)
1. **新增 TUN/TAP 介面**:
`tuntap` 是一種虛擬網路設備,`tap` 運作在 OSI 第二層 (資料鏈路層),**模擬乙太網路設備**。
```shell
$ sudo ip tuntap add dev tap0 mode tap
```
2. **啟用 TAP 介面**:
```shell
$ sudo ip link set tap0 up
```
3. **設定 IP 位址**:
為 `tap0` 介面設定一個 IP 位址,此位址將作為 UML 的通訊對象。
```shell
$ sudo ip address add 192.168.100.100/24 dev tap0
```

### UML 啟動參數修改
修改 `UML.sh` 腳本,在 `./linux` 命令的參數中加入網路設定,將 UML 的 `eth0` 介面對應到宿主機的 `tap0`:
```diff
#!/bin/sh
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
"$SCRIPT_DIR/linux" umid=uml0 \
+ hostname=uml1 eth0=tuntap,tap0 \
root=/dev/root rootfstype=hostfs rootflags="$SCRIPT_DIR/rootfs" \
rw mem=64M init=/bin/sh quiet ubd0=/dev/null
stty sane ; echo
```
* `hostname=uml1`:設定 UML 的主機名稱。
* `eth0=tuntap,tap0`:表示 UML 內的 `eth0` 網路介面使用宿主機的 `tap0` (透過 `tuntap` 驅動)。
### UML 內部網路設定與測試 (Guest)
重新啟動 UML (`./UML.sh`) 後,在 UML 環境中執行以下命令:
1. **啟用 `eth0` 介面**:
```shell
UML # ip link set eth0 up
```
2. **設定 IP 位址**:
為 UML 的 `eth0` 介面設定一個與宿主機 `tap0` 同網段的 IP 位址。
```shell
UML # ip address add 192.168.100.101/24 dev eth0
```
3. **測試連線**:
Ping 宿主機的 `tap0` IP 位址。
```shell
UML # ping -c 3 192.168.100.100
```
如果設定正確,應該能成功收到類似以下的回應:
```shell
PING 192.168.100.100 (192.168.100.100): 56 data bytes
64 bytes from 192.168.100.100: seq=0 ttl=64 time=0.225 ms
64 bytes from 192.168.100.100: seq=1 ttl=64 time=1.378 ms
64 bytes from 192.168.100.100: seq=2 ttl=64 time=0.488 ms
--- 192.168.100.100 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.225/0.697/1.378 ms
```
---
## 客製化 UML 環境
### 使用 Tini 作為 Init Process
[tini](https://github.com/krallin/tini) 是一個極簡的 init 程序,主要用於**容器環境中**正確處理信號並避免 [殭屍行程 (zombie processes)](https://en.wikipedia.org/wiki/Zombie_process) 的產生。
1. **下載 Tini 到 rootfs,並賦予執行權限**:
```shell
$ wget -O rootfs/sbin/tini https://github.com/krallin/tini/releases/download/v0.19.0/tini-static
$ chmod +x rootfs/sbin/tini
```
2. **建立 `rootfs/init.sh`**:
此腳本將作為 UML 啟動後執行的第一個程序 (`init` 程序)。
```shell
#!/bin/sh
# 掛載必要的虛擬檔案系統
mount -t proc proc /proc
mount -t sysfs sys /sys
# 設定命令提示字元 (可選)
export PS1='\[\033[01;32mUML:\w\033[00m \$ '
# 使用 tini 執行 /bin/sh
# +m 參數用於抑制 "can't access tty; job control turned off" 訊息
exec /sbin/tini /bin/sh +m
```
賦予執行權限:
```shell
$ chmod +x rootfs/init.sh
```
3. **修改 `UML.sh` 指定新的 `init` 程序**:
```diff
#!/bin/sh
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
"$SCRIPT_DIR/linux" umid=uml0 \
hostname=uml1 eth0=tuntap,tap0 \
root=/dev/root rootfstype=hostfs rootflags="$SCRIPT_DIR/rootfs" \
- rw mem=64M init=/bin/sh quiet ubd0=/dev/null
+ rw mem=64M init=/init.sh quiet ubd0=/dev/null
stty sane ; echo
```
重新執行 `UML.sh` 後,UML 環境將自動掛載 `/proc` 和 `/sys`,並且命令提示符會變為綠色的 `UML:~ #`。
> 更多顏色設定:
> * [How to Change the Color of your Linux Prompt](https://linuxhostsupport.com/blog/how-to-change-the-color-of-your-linux-prompt/)
> * [Bash Profile Generator](http://xta.github.io/HalloweenBash/)
---
## 準備核心模組
UML 同樣支援 Linux 核心模組的載入與卸載。
### 編譯核心模組
在 Linux 核心原始碼頂層目錄執行:
```shell
$ make ARCH=um SUBARCH=x86_64 modules
```
### 安裝核心模組到 rootfs
將編譯好的模組安裝到先前準備的 `rootfs` 中。模組通常安裝在 `/lib/modules/$(uname -r)/` 目錄下。
```shell
$ make modules_install MODLIB=`pwd`/rootfs/lib/modules/VER ARCH=um
```
這裡 `VER` 是一個暫時的目錄名,稍後進入 UML 環境後會將其更名為實際的核心版本號。
### UML 環境中設定模組
啟動 UML (`./UML.sh`) 後,在 UML 環境中執行:
1. **更名模組目錄**:
`uname -r` 會輸出 UML 核心的版本號。
```shell
UML # cd /lib/modules
UML # mv VER `uname -r`
```
2. **更新模組依賴**:
`depmod` 命令用於產生模組依賴檔案。
```shell
UML # depmod -ae `uname -r`
```
3. **測試載入模組**:
嘗試載入一個內建模組,如 `isofs` (ISO 檔案系統模組)。
```shell
UML # modprobe isofs
UML # lsmod
```
預期輸出中應包含 `isofs` 模組。
### 撰寫與測試 自訂核心模組
#### 範例一:Hello World 模組 (單純版)
1. **建立原始碼檔案**:
在宿主機的 Linux 核心原始碼目錄下建立 `tests` 子目錄,並在其中建立 `tests/hello.c`:
```c=
#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);
```
2. **建立 Makefile**:
在 `tests` 目錄下建立 `Makefile`:
```makefile
obj-m += hello.o
PWD := $(shell pwd)
KDIR := $(PWD)/..
default:
$(MAKE) -C $(KDIR) M=$(PWD) modules ARCH=um
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean ARCH=um
```
> 注意:Makefile 中縮排必須使用 Tab 字元。
3. **編譯並複製模組**:
```shell
$ make -C tests
$ cp tests/hello.ko rootfs/
```
4. **在 UML 中測試**:
重新啟動 UML (`./UML.sh`)。
```shell
UML # insmod /hello.ko
# 預期輸出: Hello World! - init
UML # lsmod
UML # rmmod hello
# 預期輸出: Hello World! - exit
```
#### 範例二:Ticks 模組 (透過 sysfs 揭露 jiffies 和 HZ) (進階版)
此模組將 Linux 核心內部的 `jiffies` (系統啟動以來的 tick 計數) 和 `HZ` (每秒的 tick 數) 透過 `/sys` 檔案系統暴露給使用者空間。
1. **建立原始碼檔案**:
在宿主機建立 `tests/ticks/ticks.c`:
```cpp
#include <linux/module.h>
#include <linux/init.h>
#include <linux/sysfs.h>
#include <linux/kobject.h>
#include <linux/kernel.h> // 為了 jiffies 和 HZ
static ssize_t jiffies_show(struct kobject *kobj, struct kobj_attribute *attr,
char *buf) {
return sprintf(buf, "%lu", jiffies);
}
static ssize_t hz_show(struct kobject *kobj, struct kobj_attribute *attr,
char *buf) {
return sprintf(buf, "%u", HZ);
}
// 0444: read-only for all
static struct kobj_attribute jiffies_attr = __ATTR(jiffies, 0444, jiffies_show, NULL);
static struct kobj_attribute hz_attr = __ATTR(hz, 0444, hz_show, NULL);
static struct attribute *ticks_attrs[] = { &jiffies_attr.attr, &hz_attr.attr,
NULL }; // 屬性陣列以 NULL 結尾
static struct attribute_group ticks_grp = { .attrs = ticks_attrs };
static struct kobject *ticks_kobj; // kobject 指標
static int __init ticks_init(void) {
int retval;
// 在 /sys 下建立名為 "ticks" 的目錄 (kobject)
ticks_kobj = kobject_create_and_add("ticks", NULL);
if (!ticks_kobj) // 父 kobject 為 NULL,表示在 /sys 下
return -ENOMEM; // 改為 -ENOMEM,表示記憶體不足
// 在 "ticks" kobject 下建立屬性群組
retval = sysfs_create_group(ticks_kobj, &ticks_grp);
if (retval)
kobject_put(ticks_kobj); // 若建立群組失敗,釋放先前建立的 kobject
return retval;
}
module_init(ticks_init);
static void __exit ticks_exit(void) {
// 確保 ticks_kobj 已被初始化
if (ticks_kobj) {
sysfs_remove_group(ticks_kobj, &ticks_grp);
kobject_put(ticks_kobj); // 釋放 kobject
}
}
module_exit(ticks_exit);
MODULE_LICENSE("GPL");
```
2. **建立 Makefile**:
在 `tests/ticks` 目錄下建立 `Makefile`:
```makefile
obj-m := ticks.o
KDIR := ../../ # 相對路徑指向核心源碼根目錄
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules ARCH=um
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean ARCH=um
```
3. **編譯並複製模組**:
```shell
$ pushd tests/ticks # 進入 tests/ticks 目錄
$ make
$ popd # 返回先前目錄
$ cp tests/ticks/ticks.ko rootfs/
```
4. **在 UML 中測試**:
重新啟動 UML (`./UML.sh`)。
```shell
UML # insmod /ticks.ko
UML # ls /sys/ticks/
# 預期看到 jiffies hz
UML # cat /sys/ticks/jiffies
# 顯示當前的 jiffies 值
UML # cat /sys/ticks/hz
# 顯示 HZ 的值 (通常是 100 或 250, 1000等,取決於核心配置)
```
---
## 搭配 GDB 進行核心追蹤和分析
UML 的一個強大之處在於可以直接使用 GDB (GNU Debugger) 進行核心層級的追蹤與分析。
### GDB 環境準備
1. **檢查 GDB Python 整合**:
Linux 核心提供的 GDB scripts 是用 Python 撰寫的。
```shell
$ gdb -q -ex "python print(1+1)" -ex "quit"
```
若輸出 `2`,表示 GDB 已整合 Python。
2. **建構核心 GDB Scripts**:
首先,確保核心配置中啟用了 GDB scripts:
```shell
$ echo "CONFIG_GDB_SCRIPTS=y" > .config-fragment
$ ARCH=um scripts/kconfig/merge_config.sh .config .config-fragment
$ make ARCH=um SUBARCH=x86_64 # 重新編譯核心以使配置生效
$ make ARCH=um scripts_gdb # 生成 GDB scripts
```
注意:`ARCH=um` 不可省略。`make ARCH=um scripts_gdb` 會在 `scripts/gdb` 目錄下產生 `vmlinux-gdb.py` 等腳本。
3. **載入 GDB Scripts**:
啟動 GDB 時指定載入核心提供的 GDB script:
```shell
$ gdb -ex "add-auto-load-safe-path scripts/gdb/vmlinux-gdb.py" \
-ex "file vmlinux" \
-ex "lx-version" -q
```
* `add-auto-load-safe-path`:將 `scripts/gdb/vmlinux-gdb.py` 所在路徑加入安全載入列表。
* `file vmlinux`:載入帶有除錯符號的 UML 核心執行檔 (`vmlinux` 通常位於核心源碼根目錄,或 `make ARCH=um` 生成的 `linux` 檔案的帶符號版本)。
* `lx-version`:執行 GDB script 提供的自訂命令,用於顯示核心版本。
若成功,會顯示 Linux 核心版本訊息。若失敗,可能提示 `Undefined command: "lx-version"`。
在 GDB 提示符 `(gdb) ` 後輸入 `lx` 並按 Tab 鍵,可以查看所有 `lx-` 開頭的自訂命令。使用 `apropos lx` 可以獲取這些命令的詳細描述。
4. **準備 `gdbinit` 檔案**:
為了方便,可以建立一個 `gdbinit` 檔案,GDB 啟動時會自動執行其中的命令。在核心源碼根目錄建立 `gdbinit`:
```gdb
python gdb.COMPLETE_EXPRESSION = gdb.COMPLETE_SYMBOL
add-auto-load-safe-path scripts/gdb/vmlinux-gdb.py
file vmlinux
lx-version
# 注意:FULLPATH 需要被替換為 rootfs 的絕對路徑
set args umid=uml0 root=/dev/root rootfstype=hostfs rootflags=FULLPATH/rootfs rw mem=64M init=/init.sh quiet
# UML 內部通訊使用 SIGSEGV (如 page fault),GDB 預設會攔截給除錯器。
# nostop noprint: 收到 SIGSEGV 時不停止,不印出訊息。
handle SIGSEGV nostop noprint
# SIGUSR1 用於從外部中斷 UML 執行並返回 GDB。
# nopass: 不將信號傳遞給被除錯程式。 stop: 停止執行。 print: 印出信號訊息。
handle SIGUSR1 nopass stop print
```
使用 `sed` 將 `FULLPATH` 替換為實際的絕對路徑:
```shell
$ sed -i 's|FULLPATH|'"$PWD"'|' gdbinit
```
### 使用 GDB 執行與分析 UML
1. **啟動 GDB**:
```shell
$ gdb -q -x gdbinit
```
GDB 會載入 `vmlinux`,顯示核心版本,並等待命令。
2. **在 GDB 中執行 UML**:
```
(gdb) run
```
UML 將會啟動,並最終停在 UML 的 shell 提示符 (`UML:/ # `)。
3. **中斷 UML 並返回 GDB**:
由於 UML 正在執行,直接在 GDB 視窗按 Ctrl+C 可能無效。需要開啟另一個終端機視窗,執行以下命令向 UML 行程 (執行檔名為 `vmlinux`,因 `file vmlinux` 而定) 發送 `SIGUSR1` 信號:
```shell
$ pkill -SIGUSR1 -f "vmlinux umid=uml0"
```
* -f:根據完整命令列匹配行程 (由 gdb 啟動的 UML 實例)。
* -o:只匹配最舊的那個名為 vmlinux 的行程。
切換回 GDB 視窗,會看到類似 `Thread 2 received signal SIGUSR1, User defined signal 1.` 的訊息,表示 UML 已暫停,控制權回到 GDB。
4. **使用 GDB script 命令進行分析**:
```
(gdb) lx-mounts # 查看已掛載的檔案系統
(gdb) lx-cmdline # 查看核心啟動命令列
(gdb) lx-ps # 列出 UML 內部的行程
(gdb) lx-dmesg # 查看核心訊息緩衝區
(gdb) lx-lsmod # 列出已載入的核心模組
```
若已在 UML 中載入 `hello.ko` 模組:
```
(gdb) print $lx_module("hello") # 查看 "hello" 模組的資訊
```
`$` 符號表示 GDB script 定義的便利函式 (convenience function)。
```
(gdb) print $lx_task_by_pid(1).comm # 查看 PID 為 1 的行程的名稱 (通常是 init)
(gdb) print $lx_task_by_pid(1).cred # 查看 PID 為 1 的行程的憑證資訊
(gdb) print *(struct task_struct *)0xADDRESS_OF_INIT_TASK # 查看 init_task 的完整結構 (地址從 lx-ps 獲取)
(gdb) p $container_of(init_task.tasks.next, "struct task_struct", "tasks") # 獲取 PID=1 的 task_struct 地址
(gdb) lx-list-check init_task.tasks # 檢查行程列表的 linked list 一致性
```
> 延伸閱讀:[Debugging kernel and modules via gdb](https://www.kernel.org/doc/html/v4.10/dev-tools/gdb-kernel-debugging.html)
5. **設定中斷點與自動化命令**:
例如,在核心模組載入函數 `do_init_module` 設定中斷點:
```
(gdb) break do_init_module
# Breakpoint 1 at 0x...: file kernel/module.c, line ...
```
為了只在載入特定模組 (如 `hello`) 時才真正中斷,可以使用 GDB 的 `command` 功能:
```
(gdb) command 1 # 針對中斷點 1 設定命令
Type commands for breakpoint(s) 1, one per line.
End with a line saying just "end".
> py if gdb.parse_and_eval("mod->name").string() != "hello": gdb.execute("continue", to_string=False)
> end
(gdb) continue # 繼續執行 UML
```
當在 UML 中執行 `insmod /hello.ko` 時,GDB 會在中斷點 `do_init_module` 處停下。
可以使用 `(gdb) list` 查看原始碼。
---
## 總結
User-Mode Linux 提供了一個輕量級、易於設定和使用的環境,非常適合進行 Linux 核心的學習、開發和偵錯,尤其是在不需要完整硬體虛擬化或希望快速迭代測試的場景下。透過與 GDB 的緊密整合,開發者可以深入洞察核心的內部運作。
> 延伸閱讀:
> * [Docker in User Mode Linux](https://github.com/weber-software/diuid)
> * [User Mode Linux HOWTO (kernel.org)](https://www.kernel.org/doc/Documentation/virt/uml/UserModeLinux-HOWTO.txt)
> * [Linux as a Hypervisor](https://www.linuxsecrets.com/kdocs/ols/2006/ols2006v1-pages-225-234.pdf)
> * [Getting the best from your server with User-Mode Linux](https://blog.bytemark.co.uk/wp-content/uploads/2012/04/GettingTheBest.pdf)
> * [Running virtualized native drivers in User Mode Linux](http://folk.uio.no/paalee/referencing_publications/ref-nr-guffens-presentation05.pdf)
> * [Advanced Network Simulation under User-Mode Linux](https://www.strongswan.org/uml/DFN_UML.pdf)
> * [WiFi Layer for User Mode Linux](https://sites.google.com/site/vincentguffens/wifi4uml)
---
回[主目錄](https://hackmd.io/@Jaychao2099/Linux-kernel)