使用 QEMU 的系統模擬器來載入新的 kernel image 在開發和測試上十分便利。我們可以在修改 Linux 的原始碼後,直接重新編譯就能快速地將其執行在 QEMU 上。此外,QEMU 也支援遠端除錯的功能,因此我們可以很容易的透過此工具來觀察 Linux 執行過程的硬體暫存器、記憶體等狀態,甚至是設置斷點來追蹤 Linux 的執行。
想要安裝 QEMU,可以透過 apt 等類似工具直接下載到編譯好的執行檔。不過更建議複製(clone)一份 qemu 原始碼重新編譯,這樣必要時可以自行調整編譯設定甚至是追蹤 QEMU 的執行。
建議切換至 stable branch 進行編譯,以模擬 RISC-V 為例,可以透過以下方式編譯:
./configure --target-list=riscv64-softmmu --enable-virtfs
make -j $(nproc)
sudo make install
如果是 ARM64,可以將 configure
部份調整為:
./configure --target-list=aarch64-softmmu --enable-virtfs
enable-virtfs
方便使用 「在 QEMU 中載入 host 端的檔案」 一章提到的功能,若不需要也可選擇捨棄之
要取得 Linux 的原始程式碼,一種方法是直接從專案的 git repository 上下載。可以選擇指定 depth 只取得部分歷史資料,以減少需要下載的檔案大小進而縮短下載時間,根據實際需要也可以指定 branch 或者 commit。
git clone --depth=10 https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
如果只需要使用有版號的版本,也可以在 https://cdn.kernel.org/pub/linux/kernel/ 直接取得壓縮檔。解壓縮後即可編譯使用。
對於取得的原始程式碼,用以下的方式進行編譯:
make ARCH=riscv CROSS_COMPILE=riscv64-unknown-linux-gnu- defconfig
make ARCH=riscv CROSS_COMPILE=riscv64-unknown-linux-gnu- -j$(nproc)
我們會需要 initrd 來完整的啟動 Linux,一個常見的選擇是透過 Busybox。首先我們下載該專案,並建立編譯需要的設定檔 .config
。
git clone https://git.busybox.net/busybox
CROSS_COMPILE=riscv64-unknown-linux-gnu- make defconfig
我們需要調整 .config
,以靜態連結的方式來讓將函式庫產生到執行檔中,以利 QEMU 在載入 busybox 的檔案後,可以直接具備必要的函式庫。
+CONFIG_STATIC=y
接著就可以進行編譯:
CROSS_COMPILE=riscv64-unknown-linux-gnu- make -j$(nproc)
CROSS_COMPILE=riscv64-unknown-linux-gnu- make CONFIG_PREFIX='rootfs' install
完成後,底下會出現一個名為 rootfs
的資料夾,其中內容是將 busybox 掛載為 root file system 後會包含的檔案。首先進入並建立和更動一些內容。
cd rootfs
mv linuxrc init
mkdir -p proc sys dev etc/init.d
接著,在 etc/init.d 底下建立檔案 rcS(可透過 vim 等編輯器)。init.d 的用途是放置系統啟動時要執行的腳本。這裡我們的目的是讓系統啟動後可以自動掛載需要的虛擬檔案系統。具體作法是將以下內容複製貼上到其中。
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev
mount -t debugfs none /sys/kernel/debug
並非所有的檔案系統都需要 mount,可視實際需求而定
調整檔案權限使得該腳本得以被執行。
chmod 777 etc/init.d/rcS
然後我們可以就可以著手準備要讓 QEMU 載入的映像檔了。製作的方法可能有數種,這裡採用的方式是預先建立好 image root.img
,並先將其格式化為 Linux 可接受的檔案系統格式。接著,以 loop device 掛載該檔案並存取之,將 rootfs
中準備好的內容複製過來。複製完成並解除掛載後,root.img
就成了可以被 QEMU 載入的 initrd 了。
dd if=/dev/zero of=root.img bs=1M count=32
mkfs.ext4 -F root.img
mkdir tmpdir
sudo mount -o loop root.img tmpdir/
cd tmpdir/
sudo cp -a ../busybox/rootfs/. .
cd ..
sudo umount tmpdir
通過上述步驟可以得到 Image
和 root.img
,於是就可以通過下列命令啟動 Linux 了!
qemu-system-riscv64 \
-nographic \
-machine virt \
-kernel linux/arch/riscv/boot/Image \
-device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0 \
-drive file=root.img,if=none,format=raw,id=x0 \
-append "root=/dev/vda console=ttyS0"
其他平台的建置與啟動 QEMU 步驟原則上大致相同,雖然具體過程可能有些微差異。這裡就僅列出相關命令和簡單說明,細節請對照前面章節。
編譯 Linux Image。
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- defconfig
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc)
編譯 Busybox。這裡同樣以靜態連結連接函式庫。
ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- make defconfig
+CONFIG_STATIC=y
建立 initrd。
ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- make -j$(nproc)
ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- make CONFIG_PREFIX='rootfs' install
cd rootfs
find . | cpio -o --format=newc > ../root.img
cd ..
gzip -c root.img > root.img.gz
使用上面獲得的檔案,藉以下命令啟動 QEMU 以執行 Linux。
qemu-system-aarch64 \
-cpu cortex-a57 \
-machine virt -machine type=virt \
-nographic \
-initrd root.img.gz \
-kernel arch/arm64/boot/Image \
--append "root=/dev/ram rdinit=/bin/sh console=ttyAMA0"
如果想在模擬環境中添加新檔案,我們要去修改 rootfs
,然後再重新製作對應的 root.img
。這是一個繁瑣的過程。另一個選擇更便利的選擇是在 QEMU 中通過 9P 來存取 host 端的檔案。
首先要確認 QEMU 是以 QEMU 章節的說明配置,且一開始編譯的 Linux 版本有支援 9P 檔案系統。
+CONFIG_9P_FS=y
為方便管理,可以建立一個用來存放和 QEMU 共享檔案的資料夾。
mkdir share
接著在執行 QEMU 時加上以下的選項
+-virtfs local,path=share,mount_tag=host0,security_model=mapped,id=host0
則啟動 Linux 後,在模擬器中我們也建立一個同名的資料夾,並以 9P 掛載之。如此一來,就可以在資料夾中得到想共享的內容了!
mkdir share
mount -t 9p -o trans=virtio,version=9p2000.L host0 share
雖然透過上面提供的方法來進行測試,可以更直接的認識完整的編譯並部屬 Linux 的過程。但有時這些步驟比較繁瑣,且對於新手而言可能也相對不友善。如果我們的目標是希望可以更有效率的重複改動程式碼->編譯->執行測試,virtme-ng 是一個相當不錯的工具。在 Faster kernel testing with virtme-ng 文章中有更多對該工具的介紹,並且可以搭配 Demo 影片 來得知更多使用上的細節。
首先,有一些建議的套件可能需要安裝。
sudo apt install python3-pip python3-argcomplete flake8 pylint \
cargo rustc qemu-system-x86
安裝好上述的套件後,就可以從 GitHub 上將 virtme-ng 下載下來。
$ git clone --recurse-submodules https://github.com/arighi/virtme-ng.git
然後進入專案資料夾中並執行以下命令,安裝好所有的必要套件。
$ BUILD_VIRTME_NG_INIT=1 pip3 install --verbose -r requirements.txt .
一切大功告成後,方便使用可以把 vng
的路徑加入到環境變數中。接著我們就可以進入到 Linux 的專案資料夾底下,並執行以下命令。
$ vng -b
$ vng
rin@rin-ROG-STRIX-G10CES-G10CES:~/Linux/tip$ vng
_ _
__ _(_)_ __| |_ _ __ ___ ___ _ __ __ _
\ \ / / | __| __| _ _ \ / _ \_____| _ \ / _ |
\ V /| | | | |_| | | | | | __/_____| | | | (_| |
\_/ |_|_| \__|_| |_| |_|\___| |_| |_|\__ |
|___/
kernel version: 6.7.0-rc1-virtme-g999df3f611f5 x86_64
成功的話,應該就會看到類似上面的字樣!透過 uname -r
也可以確認到 kernel 版號應該會和原本 host 的不同(除非你的 Linux 專案資料夾恰好是用同一版)。更多詳細的操作說明可以查看 vng --help
來得知,在 virtme-ng 頁面上也可見一些操作的範例。
如果想要從 virtme-ng
的虛擬環境中退出,只要再輸入 CTRL + a x
就可以了!
使用 QEMU 測試 Linux 核心的好處之一是: 由於模擬器內建 gdbstub,這讓模擬器可以透過 GDB 設置斷點、查看暫存器、記憶體狀態等等。由此方式,
要允許完整的 GDB 支援,首先在編譯核心時,我們需要讓編譯出來的 vmlinux 中包含除錯資訊,這可以透過將 CONFIG_DEBUG_INFO_DWARF4
或CONFIG_DEBUG_INFO_DWARF5
設為 y
來達成。並且還要設置 CONFIG_GDB_SCRIPTS=y
。
如果是使用 menuconfig
,相關設定可以由以下路徑找到:
此外,確保 CONFIG_DEBUG_INFO_REDUCED=n
和 CONFIG_FRAME_POINTER=y
(如果對應處理器支援)。
之後,在 Linux 原始碼的根資料夾底下,就可以參照編譯並建置 kernel 映像檔(Linux) 一節進行編譯。在編譯完成後,額外執行以下命令:
make scripts_gdb
在 Linux 的 scripts/gdb
底下提供了許多簡化除錯的腳本,為了確保這個路徑可以在 gdb 被啟動時自動載入,我們需要建立 ~/.gdbinit
檔案,並加入以下內容:
add-auto-load-safe-path ~/linux/stable/scripts/gdb/vmlinux-gdb.py
~/linux/stable
要藉由 GDB 來對 QEMU 除錯,原則上只要在啟動 QEMU 時加上額外的 -s
選項即可。但由於 KASLR
造成的隨機位址影響,還需要 -append nokaslr
讓 GDB 可以正確將 vmlinux 對應到函式/資料的正確位置。
使用 vng
則是執行:
$ vng -o='-s' -a='nokaslr'
接著,就可以開啟另一個 console 並執行以下命令來連接到 QEMU/vng。
$ gdb vmlinux -ex "target remote :1234"
更詳細的說明可以參考 kernel 文件: