# 利用 QEMU 與 BusyBox 建立 Linux 測試環境 :::info :pencil: 這篇筆記是從 [The Linux Foundation](https://www.linuxfoundation.org/) 的 Webinar "[Setting Up an Environment for Writing Linux Kernel Modules in Rust](https://www.linuxfoundation.org/webinars/setting-up-an-environment-for-writing-linux-kernel-modules-in-rust)" 整理而來,有興趣的可以觀看錄影存檔 > :movie_camera: [Setting Up an Environment for Writing Linux Kernel Modules in Rust](https://www.youtube.com/watch?v=tPs1uRqOnlk) [color=#33b2ff] ::: :::success :bulb: 在本篇筆記的命令列範例中,若前綴為 `$` 者,表示其執行在 host 端;而前綴為 `#` 者,表示其須執行在 guest 端 ::: ## Compile Linux 從 GitHub 下載 linux 的原始碼, ```bash $ git clone https://github.com/torvalds/linux.git ``` 使用 qemu-busybox-min.config 作為設定檔的基底 ```bash $ make allnoconfig qemu-busybox-min.config ``` 設定完畢後開始編譯核心 ```bash $ make -j$(nproc) ``` 完成後,在 linux 專案目錄底下會產生 `vmlinux` 輸出檔,這是我們後續開機時要載入虛擬機的系統執行檔。 ## Build Busybox 接著我們開始著手製作 initrd,initrd 的全稱是 initial ram disk,作用是在開機的早期提供系統一些必要的設定檔與工具程式,以利系統進一步開啟與掛載像是檔案系統等重要的子系統。 而這邊挑選的是 busybox 我們一樣可以從 GitHub 上下載 busybox 的原始碼 ```bash $ git clone https://github.com/mirror/busybox.git ``` 而這邊可以直接使用預設的設定檔 ```bash $ make defconfig ``` 需要調整的地方是,我們希望產生出來的執行檔,使用靜態連結的方式來連結函式庫。而這樣的好處就是系統不需要先載入動態函式庫,就可以使用 busybox 的執行檔。 ```diff --- a/.config +++ b/.config @@ -40,7 +40,7 @@ # # Build Options # -# CONFIG_STATIC is not set +CONFIG_STATIC=y # CONFIG_PIE is not set # CONFIG_NOMMU is not set # CONFIG_BUILD_LIBBUSYBOX is not set ``` 設定完成後,就可以開始編譯專案 ```bash $ make -j$(nproc) ``` 完成後,可以使用以下命令建立一個簡易的 ramdisk 架構 ``` $ make install ``` 上述的指令會在專案目錄底下生成一個 `_install` 的目錄,其中包含 ramdisk 的目錄結構、shell、ls、ps 等工具程式和基本的設定檔。 ### Package an initrd image 進入 `busybox/_install` 中,並使用下列命令產生 ramdisk 映像檔 ```bash $ find . | cpio -H newc -o | gzip > ../ramdisk.img ``` ## Run qemu 首先我們先在電腦上安裝 QEMU,這邊使用 `apt-get` 來進行安裝 ```bash $ sudo apt install qemu-kvm ``` 安裝完畢後,就可以使用我們在先前建置的核心映像檔來啟動虛擬,, ```bash $ qemu-system-x86_64 -nographic -kernel linux/vmlinux ``` 進入 console 畫面之後,可以發現系統並未順利啟動。這是正常的狀況,因為我們並未引入 ramdisk,因此 kernel 發現無法掛載檔案系統與其他程式 ``` <...> [ 0.664064] IPI shorthand broadcast: enabled [ 0.664598] sched_clock: Marking stable (634657050, 28781546)->(665613312, -2174716) [ 0.674702] List of all partitions: [ 0.675135] No filesystem could mount root, tried: [ 0.675154] [ 0.675500] Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0) [ 0.676127] CPU: 0 PID: 1 Comm: swapper/0 Not tainted 6.0.0-rc7+ #3 [ 0.676502] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.15.0-1 04/01/2014 <...> ``` 使用 <kbd>Ctrl</kbd> - <kbd>A</kbd> + <kbd>X</kbd> 結束 QEMU 執行,並在啟動的參數中加入先前打包好的 ramdisk 映像檔 ```bash $ qemu-system-x86_64 -nographic -kernel linux/vmlinux -initrd busybox/ramdisk.img ``` ### Fix tty warning 再次開啟虛擬機時,可以發現這次 shell 可以被順利開啟,且可以開始鍵入命令來執行各項操作。但是卻會不斷的跳出 tty 無法掛載的訊息。原因就在 BusyBox 的 `init` 程式預設會開啟 tty[2-5],但是這在的測試環境中並不會被使用,因此我們回到 `busybox/_install` 目錄來指定我們欲使用的設定 首先需要先在 ramdisk 中建立 `etc` 目錄,並將範例的設定檔複製到其中 ```bash # At busybox/_install $ mkdir etc $ cp ../examples/inittab etc/. ``` 接著編輯 `etc/inittab`,將 tty[2-5] 的設定刪除 ```diff --- a/etc/inittab +++ b/etc/inittab @@ -72,13 +72,6 @@ # Start an "askfirst" shell on the console (whatever that may be) ::askfirst:-/bin/sh # Start an "askfirst" shell on /dev/tty2-4 -tty2::askfirst:-/bin/sh -tty3::askfirst:-/bin/sh -tty4::askfirst:-/bin/sh - -# /sbin/getty invocations for selected ttys -tty4::respawn:/sbin/getty 38400 tty5 -tty5::respawn:/sbin/getty 38400 tty6 # Example of how to put a getty on a serial line (for a terminal) #::respawn:/sbin/getty -L ttyS0 9600 vt100 ``` 重複前述打包的動作,重新製作 ramdisk 後,開啟虛擬機,可以發現不會再出現 tty 無法開啟的錯誤訊息。 ### Fix proc mounting 即使我們修復了 tty 開啟異常的錯誤,我們仍可以在開機的過程中看到如下的錯誤訊息: ``` can't run '/etc/init.d/rcS': No such file or directory ``` 且我們也無法使用 `ps` 來查看當前運行中的程序有哪些 ```shell # ps PID USER TIME COMMAND ps: can't open '/proc': No such file or directory ``` 而原因就如同 `ps` 的錯誤訊息所述,就是目前我們的系統並不會掛載 `/proc` 的目錄。 我們可以直接用以下的命令來手動掛載 procfs ```shell # mkdir /proc # mount -t proc none /proc ``` 但考慮到每次開啟虛擬機都需要這樣手動掛載檔案系統,實在是不太實際,因此我們回到 busybox 的目錄底下,透過新增 `etc/init.d/rcS` 來自動化整個掛載的流程 ```bash ## At busybox/_install $ mkdir -p etc/init.d $ echo "mkdir /proc mount -t proc none /proc" > etc/init.d/rcS $ chmod +x etc/init.d/rcS ``` ### Fix network 最後一個我們想要解決的問題是,虛擬機無法連線到網路的問題。首先我們先使用 `ifconfig` 啟動 loopback interface ```ifconfig # ifconfig lo up ``` 這樣如果我們有執行一些簡易的伺服器服務,就可以透過 `127.0.0.1` 來與服務連線。 而連線到像是 google.com 或是 facebook.com 等外部網路顯然是無法透過啟動 loopback address 就可以達成的,因此我們關閉虛擬機,並在 QEMU 的參數加入網路介面卡的設定 ```bash $ qemu-system-x86_64 -nographic -kernel linux-rust/arch/x86/boot/bzImage -initrd busybox/ramdisk.img -nic user,model=rtl8139 ``` 重新啟動虛擬機後,我們就可以看到新的網路界面出現在 `ip link` 的結果中: ```shell # ip link 1: lo: <LOOPBACK> mtu 65536 qdisc noop qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 2: eth0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop qlen 1000 <--- here link/ether 52:54:00:12:34:56 brd ff:ff:ff:ff:ff:ff ``` 接著我們就可以使用 `udhcpc` 向 DHCP Server 請求配置 IP 位址 ```shell # udhcp -i eth0 udhcpc: started, v1.36.0.git Clearing IP addresses on eth0, upping it [ 453.448315] 8139cp 0000:00:03.0 eth0: link up, 100Mbps, full-duplex, lpa 0x05E1 udhcpc: broadcasting discover udhcpc: broadcasting select for 10.0.2.15, server 10.0.2.2 udhcpc: lease of 10.0.2.15 obtained from 10.0.2.2, lease time 86400 Setting IP address 10.0.2.15 on eth0 Deleting routers route: SIOCDELRT: No such process Adding router 10.0.2.2 Recreating /etc/resolv.conf Adding DNS server 10.0.2.3 ``` 從輸出的結果中,我們可以知道我們所取得的 IP 位址是 10.0.2.3 :::info :bulb: 上面使用到的命令一樣可以放到 `/etc/init.d/rcS` 中,這樣開機時就會自動請求動態 IP 位址 ::: 並可以順利連上外部網路 ```shell # ping google.com PING google.com (142.251.42.238): 56 data bytes 64 bytes from 142.251.42.238: seq=0 ttl=255 time=7.807 ms 64 bytes from 142.251.42.238: seq=1 ttl=255 time=6.290 ms 64 bytes from 142.251.42.238: seq=2 ttl=255 time=6.741 ms 64 bytes from 142.251.42.238: seq=3 ttl=255 time=6.689 ms ``` ```bash $ qemu-system-x86_64 -nographic -kernel linux-rust/arch/x86/boot/bzImage -initrd busybox/ramdisk.img -nic user,model=rtl8139,hostfwd=tcp::5555-:23 ``` ###### tags: `linux`