virtme 是 Linux 核心開發者利用 QEMU 所建立一個輕量級的 Linux 核心測試環境,和 Linux 核心原始程式碼有很好的整合。
以 Ubuntu Linux 22.04 來說,需要安裝以下套件:
virtme 以 Python3 撰寫,可用 pip 安裝,注意 Python 版本需要大於 3.3
$PATH
環境變數需要一併更新:
或者列於 $HOME/.bashrc
中。
首先,找一個容納 6 GB 空間的目錄,你需要記住絕對路徑,之後我們還用得到。為了行文的便利,我們用 /home/ubuntu
來稱呼。
root
帳號
執行以下命令來取得 Linux 核心原始程式碼:
使用 virtme 選取預設核心組態並編譯:
針對 Arm64 處理器架構,改為
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc)
命令
CONFIG_DEBUG_INFO
在組態中有被開啟,以利後續實驗使用。方法是在 $ make
命令前,執行 $ grep CONFIG_DEBUG_INFO .config
,預期要看到 CONFIG_DEBUG_INFO=y
在編譯結束後,預期可見以下訊息:
針對 Arm64 處理器架構,是
arch/arm64/boot/Image
透過以下命令,在 QEMU 虛擬機器中啟動 Linux 核心:
virtme-init
開頭的訊息時,請保持耐心等待
上方命令的
--mods=auto
指定自動掛載 Linux 核心模組,若不需要這機制,可改為$ virtme-run --kimg arch/x86/boot/bzImage
預期可見以下訊息:
這時你可在 virtme 輸入一些命令,例如:
為了區隔模擬環境和宿主 (host,即 Ubuntu Linux) 端的終端機操作,我們約定 $
開頭的命令是執行於宿主端環境,而 #
開頭的命令則執行於 virtme
當你見到 Linux (none) 6.1.21
一類的訊息,就表示成功運作 virtme。
倘若要離開測試環境,你可按下 Ctrl-A
放開再按下 X
按鍵。
virtme
的選項除了用 -kdir
選項指定核心映像檔,尚可指定若干選項,例如:
-a
: 附加 Linux 核心啟動參數
-a "nokaslr"
抑制 ASLR--disk
: 指定磁碟,測試檔案系統或 I/O 操作很好用
之後在模擬環境中可執行 fdisk -l /dev/sda
,隨後亦可掛載檔案系統:
--kimg
: 指定核心映像檔,例如 -kimg arch/x86/boot/bzImage
使用 virtme 的 kimg
參數啟動核心映像檔後,若我們想要使用 modprobe
載入與核心一同編譯的核心模組時,會因為與 Ubuntu Linux 共用檔案系統,而無法從預設路徑 /lib/modules/$(uname -r)
中讀取相關設定檔。可以透過以下的方式來進行設定:
我們需要將核心模組安裝到一個暫存的目錄中
/tmp
目錄之下,因為宿主的 /tmp
與模擬環境中的 /tmp
無法共通
接著啟動模擬環境,並將前述放置核心模組的目錄掛載到 /lib/modules/
設定完畢後,就能夠使用 modprobe
載入預先編譯好的核心模組。例如載入 TLS 模組
並且可以使用 lsmod
列出目前載入的所有核心模組及其相依性。
virtme 建立的虛擬環境所用的檔案系統是共用 Ubuntu Linux 的檔案系統 (透過 9P over VirtIO),因此你可以在裡頭編譯核心模組!
還記得之前記住的絕對路徑吧?Linux 核心原始程式碼就在 /home/ubuntu/linux
中,不過在 virtme 創造的虛擬環境中,該路徑是唯讀。為了便利起見,我們就在 /tmp
目錄實驗。你可以研讀 virtme 文件,以得知檔案分享和權限處理的機制。
依據 UNIX 慣例,我們用 Hello World 核心模組來示範:首先在 /tmp
建立實驗用的目錄:
建立檔案 /tmp/hello/hello.c
,其內容為:
還要有對應的 /tmp/hello/Makefile
,內容如下: (記得要更換 /home/ubuntu
這個路徑)
注意在 all:
和 $(MAKE)
之間不是空白字元,而是 Tab,同理,clean:
和 $(MAKE)
之間也以 Tab 區隔。
接著在 virtme 編譯核心模組:
預期可見以下輸出:
萬事俱備,就來測試:
預期可見以下訊息:
移除核心模組也行:
我們故意在 Linux 核心原始程式碼做以下更動:
透過上述的 $ make ARCH=x86 CROSS_COMPILE=x86_64-linux-gnu-
命令來編譯核心模組,接著用 $ virtme-run --kimg arch/x86/boot/bzImage
啟動 Linux 核心,預期可見到以下錯誤訊息:
我們在即將啟動 init
程式之前,故意做非預期的記憶體操作,這時就觸發 Linux 核心註冊的例外處理機制。
上述實驗結束後,請還原程式碼並編譯核心:
許多時候,除了在觸發 kernel panic 時系統會提供對應的 call trace 外,我們也需要交叉比對其他的資訊,諸如 dmesg
或是所有行程,方可定位出問題所在,而 crash 就是一款可用來偵錯的工具。
簡單來說,crash 是一種針對核心偵錯特化的一種 GDB
crash
的維護者,Red Hat 工程師 David Aderson 在〈Whilte Paper: Crash Utility〉提到:
While gdb is an incredibly powerful tool, it is designed to debug user programs, and is not at all "kernel-aware".
雖然 Ubuntu Linux 提供預先編譯好的 crash 套件,但因為較新的核心 (v5.17 以上) 內部結構已有調整,因此需要 v8.0.1 以上之 crash 才能解析。
我們可以在 crash-utility 下載 8.0.2 版本的 crash 並進行編譯與安裝:
crash
需要較長的時間,因為要從 GDB 原始程式碼建構,後者又有一系列相依的套件要編譯。
因為 virtme 使用到 QEMU 來建立虛擬環境,因此我們可用 QEMU 的功能選項來產生執行時的 kernel dump。
首先我們需要在啟動虛擬環境的命令中加入 --qemu-opts -qmp tcp:localhost:4444,server,nowait
這樣的參數,啟動 QEMU 的 QEMU Machine Protocol (QMP) 功能,命令如下:
啟動虛擬環境後,我們使用 Linux Magic System Request 來觸發 kernel panic
預期可以看到系統發生 kernel panic 並印出相關訊息。
維持虛擬環境繼續執行的情況下,我們回到宿主系統,使用 QMP 來與現行 QEMU 環境通訊,擷取目前虛擬環境的 kernel dump。
依照以下的步驟產生 kernel dump
準備好含有 debug symbol 的 vmlinux 以及 kernel dump,就可以開始使用 crash 偵錯
進入 crash 後,除了會先印出當前使用核心的基本訊息以及 panic 的原因之外,我們也可以透過 shell 風格的命令來印出發生 panic 時 kernel ring buffer 的內容
下面示範印出 ring buffer 最後 30 行的內容:
從上述的結果,我們可以得知 panic 是被 PID 為 137
的 bash 所觸發
同時我們也可以利用 ps
查看該核心所有行程和狀態
於是我們可以使用 bt
調查編號 137 的行程對應的 call stack 及其暫存器狀態
如果我們懷疑其中的 sysrq_handle_crash
函式可能是觸發 panic 的元兇,則可以使用類似 GDB 的命令 list
列出該函式對應的原始碼
從上面的結果我們可以看到在第 155 行的程式碼實際呼叫 panic
,導致核心進入 panic 的處理機制。