# 利用 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`