--- tags: Linux Kernel Internals, 作業系統 --- # Linux 核心設計: 開發與測試環境 ## 安裝 QEMU 使用 [QEMU](https://zh.wikipedia.org/zh-tw/QEMU) 的系統模擬器來載入新的 kernel image 在開發和測試上十分便利。我們可以在修改 Linux 的原始碼後,直接重新編譯就能快速地將其執行在 QEMU 上。此外,QEMU 也支援遠端除錯的功能,因此我們可以很容易的透過此工具來觀察 Linux 執行過程的硬體暫存器、記憶體等狀態,甚至是設置斷點來追蹤 Linux 的執行。 想要安裝 QEMU,可以透過 apt 等類似工具直接下載到編譯好的執行檔。不過更建議複製(clone)一份 [qemu](https://github.com/qemu/qemu) 原始碼重新編譯,這樣必要時可以自行調整編譯設定甚至是追蹤 QEMU 的執行。 建議切換至 stable branch 進行編譯,以模擬 RISC-V 為例,可以透過以下方式編譯: ```shell ./configure --target-list=riscv64-softmmu --enable-virtfs make -j $(nproc) sudo make install ``` 如果是 ARM64,可以將 `configure` 部份調整為: ```shell ./configure --target-list=aarch64-softmmu --enable-virtfs ``` :::info `enable-virtfs` 方便使用 「[在 QEMU 中載入 host 端的檔案](#在-QEMU-中載入-host-端的檔案)」 一章提到的功能,若不需要也可選擇捨棄之 ::: ## 下載 Linux 原始碼 要取得 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/ 直接取得壓縮檔。解壓縮後即可編譯使用。 ## 測試環境: 以 RISCV 為例 ### 編譯並建置 kernel 映像檔(Linux) 對於取得的原始程式碼,用以下的方式進行編譯: ``` make ARCH=riscv CROSS_COMPILE=riscv64-unknown-linux-gnu- defconfig make ARCH=riscv CROSS_COMPILE=riscv64-unknown-linux-gnu- -j$(nproc) ``` ### 編譯並建置 initrd(Busybox) 我們會需要 initrd 來完整的啟動 Linux,一個常見的選擇是透過 [Busybox](https://zh.wikipedia.org/zh-tw/BusyBox)。首先我們下載該專案,並建立編譯需要的設定檔 `.config`。 ``` git clone https://git.busybox.net/busybox CROSS_COMPILE=riscv64-unknown-linux-gnu- make defconfig ``` 我們需要調整 `.config`,以靜態連結的方式來讓將函式庫產生到執行檔中,以利 QEMU 在載入 busybox 的檔案後,可以直接具備必要的函式庫。 ```diff +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 ``` :::warning 並非所有的檔案系統都需要 mount,可視實際需求而定 ::: 調整檔案權限使得該腳本得以被執行。 ``` chmod 777 etc/init.d/rcS ``` 然後我們可以就可以著手準備要讓 QEMU 載入的映像檔了。製作的方法可能有數種,這裡採用的方式是預先建立好 image `root.img`,並先將其格式化為 Linux 可接受的檔案系統格式。接著,以 [loop device](https://zh.wikipedia.org/zh-tw/Loop%E8%AE%BE%E5%A4%87) 掛載該檔案並存取之,將 `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 ``` ### 啟動 QEMU 通過上述步驟可以得到 `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" ``` ## 測試環境: 以 ARM64 為例 其他平台的建置與啟動 QEMU 步驟原則上大致相同,雖然具體過程可能有些微差異。這裡就僅列出相關命令和簡單說明,細節請對照前面章節。 ### Linux 編譯 Linux Image。 ``` make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- defconfig make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- -j$(nproc) ``` ### Busybox 編譯 Busybox。這裡同樣以靜態連結連接函式庫。 ``` ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- make defconfig ``` ```diff +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 使用上面獲得的檔案,藉以下命令啟動 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" ``` > * [Box for ARM on QEMU](https://jasonblog.github.io/note/arm_emulation/busybox_for_arm_on_qemu.html) ## 在 QEMU 中載入 host 端的檔案 如果想在模擬環境中添加新檔案,我們要去修改 `rootfs`,然後再重新製作對應的 `root.img`。這是一個繁瑣的過程。另一個選擇更便利的選擇是在 QEMU 中通過 [9P](https://zh.wikipedia.org/zh-tw/9P) 來存取 host 端的檔案。 首先要確認 QEMU 是以 [QEMU](#QEMU) 章節的說明配置,且一開始編譯的 Linux 版本有支援 9P 檔案系統。 ```diff +CONFIG_9P_FS=y ``` 為方便管理,可以建立一個用來存放和 QEMU 共享檔案的資料夾。 ``` mkdir share ``` 接著在執行 QEMU 時加上以下的選項 ```diff +-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 ``` ## virtme-ng 雖然透過上面提供的方法來進行測試,可以更直接的認識完整的編譯並部屬 Linux 的過程。但有時這些步驟比較繁瑣,且對於新手而言可能也相對不友善。如果我們的目標是希望可以更有效率的重複改動程式碼->編譯->執行測試,[virtme-ng](https://github.com/arighi/virtme-ng) 是一個相當不錯的工具。在 [Faster kernel testing with virtme-ng](https://lwn.net/Articles/951313/) 文章中有更多對該工具的介紹,並且可以搭配 [Demo 影片](https://youtu.be/3sDkVuXVw9A?si=NABha8kabcY2XuXZ) 來得知更多使用上的細節。 ### 安裝方式 首先,有一些建議的套件可能需要安裝。 ``` sudo apt install python3-pip python3-argcomplete flake8 pylint \ cargo rustc qemu-system-x86 ``` 安裝好上述的套件後,就可以從 GitHub 上將 [virtme-ng](https://github.com/arighi/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](https://github.com/arighi/virtme-ng) 頁面上也可見一些操作的範例。 如果想要從 `virtme-ng` 的虛擬環境中退出,只要再輸入 `CTRL + a x` 就可以了! ## 使用 GDB 對 QEMU/vng 進行除錯 使用 QEMU 測試 Linux 核心的好處之一是: 由於模擬器內建 [gdbstub](https://wiki.qemu.org/Features/gdbstub),這讓模擬器可以透過 GDB 設置斷點、查看暫存器、記憶體狀態等等。由此方式, ### 前置準備 要允許完整的 GDB 支援,首先在編譯核心時,我們需要讓編譯出來的 vmlinux 中包含除錯資訊,這可以透過將 `CONFIG_DEBUG_INFO_DWARF4` 或`CONFIG_DEBUG_INFO_DWARF5` 設為 `y` 來達成。並且還要設置 `CONFIG_GDB_SCRIPTS=y`。 如果是使用 `menuconfig`,相關設定可以由以下路徑找到: * Kernel hacking --> Compile-time checks and compiler options --> Debug information --> Generate DWARF Version 4/5 debuginfo * Kernel hacking --> Compile-time checks and compiler options --> Provide GDB scripts for kernel debugging 此外,確保 `CONFIG_DEBUG_INFO_REDUCED=n` 和 `CONFIG_FRAME_POINTER=y`(如果對應處理器支援)。 之後,在 Linux 原始碼的根資料夾底下,就可以參照[編譯並建置 kernel 映像檔(Linux)](#編譯並建置-kernel-映像檔(Linux)) 一節進行編譯。在編譯完成後,額外執行以下命令: ``` make scripts_gdb ``` 在 Linux 的 [`scripts/gdb`](https://github.com/torvalds/linux/tree/master/scripts/gdb) 底下提供了許多簡化除錯的腳本,為了確保這個路徑可以在 gdb 被啟動時自動載入,我們需要建立 `~/.gdbinit` 檔案,並加入以下內容: ``` add-auto-load-safe-path ~/linux/stable/scripts/gdb/vmlinux-gdb.py ``` * 假設 Linux 的根目錄在 `~/linux/stable` ### 使用方式 要藉由 GDB 來對 QEMU 除錯,原則上只要在啟動 QEMU 時加上額外的 `-s` 選項即可。但由於 [`KASLR`](https://en.wikipedia.org/wiki/Address_space_layout_randomization) 造成的隨機位址影響,還需要 `-append nokaslr` 讓 GDB 可以正確將 vmlinux 對應到函式/資料的正確位置。 使用 `vng` 則是執行: ``` $ vng -o='-s' -a='nokaslr' ``` 接著,就可以開啟另一個 console 並執行以下命令來連接到 QEMU/vng。 ``` $ gdb vmlinux -ex "target remote :1234" ``` 更詳細的說明可以參考 kernel 文件: > * [Debugging kernel and modules via gdb](https://docs.kernel.org/dev-tools/gdb-kernel-debugging.html) ## Reference * [Running 64- and 32-bit RISC-V Linux on QEMU](https://risc-v-getting-started-guide.readthedocs.io/en/latest/linux-qemu.html) * [Linux Kernel Debugging Tricks of the Trade](https://www.linuxfoundation.org/webinars/linux-kernel-debugging-tricks-of-the-trade)