本文主要介紹在 MacOS 上使用 qemu 搭建 Linux Kernel 的開發環境。(在開始之前需要注意的是,本文中的 Linux 開發環境是一個遠程服務器,而 qemu 被安裝在本地的 MacOS 上。通常並不需要這樣折騰,直接將 qemu 安裝在 Linux 中更加方便,而且 qemu 是可以 -nographic
無圖形界面運行的。)
1. 為什么需要 qemu?
qemu 是一個硬件虛擬化程序( hypervisor that performs hardware virtualization),與傳統的 VMware / VirtualBox 之類的虛擬機不同,它可以通過 binary translation 模擬各種硬件平台(比如在 x86 機器上模擬 ARM 處理器)。而 VirtualBox 等更多是通過虛擬化來進行資源隔離,以便在其上運行多個 guest os。
基於 qemu 的硬件模擬能力,我們可以輕松搭建指定硬件平台的運行實驗環境。
qemu 與 VirtualBox 另一個不同點在於,在 VirtualBox 上必須安裝一個完整的操作系統套件,而通過 qemu 我們可以通過參數直接啟動到一個裸的 Linux Kernel,連 bootloader 都不需要關心。在此之外,按需配置相關工具套件與啟動好的 Kernel 一起工作即可。
qemu 提供的這種高度可定制化的『白盒』能力,使得我們可以按需構建快速、輕量級的開發環境,提供流暢的開發體驗。
2. 環境准備
首先,為了進行內核開發,需要一個現成的 Linux 操作系統環境。可以是一個通過 ssh 工作的遠程 Linux Server,或者也可以在 MacOS 上通過 VirtualBox (或者使用 qemu 也可以)安裝一個虛擬機用於開發。VirtualBox 的安裝和 Linux Guest OS 的安裝配置此處略過不提。
接下來,安裝 qemu。在 MacOS 上可以使用 Homebrew 包管理工具進行安裝(本文使用的 qemu 版本為 2.9.0_2):
brew install qemu
安裝完成后,可以看到系統中有很多個 qemu-system-
開頭的命令,用於模擬各種硬件平台,比如 qemu-system-x86_64
。運行其中一個命令來驗證安裝是否成功:
qemu-system-x86_64
上述命令會啟動一個類似 VirtualBox 虛擬機啟動時的窗口。當然,由於我們沒有指定任何設備,最終會提示找不到可啟動設備。
3. 編譯內核
按需編譯內核,此處只進行簡單說明(基於內核 v4.13)。
3.1 內核編譯配置
可以先執行 make help
可以查看 make 支持哪些 target。
通常先進行內核編譯配置:
make menuconfig
會啟動一個基於文本的配置界面進行各種選項、模塊、驅動等配置。或者也可以直接使用目標平台默認的配置,如針對 x86_64 平台(后續平台相關的地方均以 x86_64 為例進行說明)可以使用:
make x86_64_defconfig
配置完成后相應的配置項會保存在 .config
文件中。下一次執行 make menuconfig
時可以 load 這份配置文件,在此基礎上進行修改。
3.2 編譯內核和模塊
我們構建一個壓縮過的內核鏡像:
make bzImage
編譯成功后,bzImage 文件將出現在 arch/x86_64/boot/bzImage
。記住文件路徑或者拷貝到一個方便的路徑,便於后續啟動時使用。
接下來,編譯在配置階段選擇的內核模塊:
make modules
編譯好的內核模塊 *.ko
文件存在於模塊對應的源碼目錄中。
4. 啟動內核
編譯好內核以后,我們就可以使用 qemu 啟動內核了。只需要使用 -kernel
參數告訴 qemu 內核文件的位置即可:
qemu-system-x86_64 \
-m 512M \ # 指定內存大小
-smp 4\ # 指定虛擬的 CPU 數量
-kernel ./bzImage # 指定內核文件路徑
上述命令假設編譯好的 bzImage 內核文件就存放在當前目錄下。因為之前編譯好的內核文件是在 VirtualBox 的虛擬機中(或者在遠程服務器上),而 qemu 在本地 MacOS 上,可以通過 VirtualBox 的 share folder 來共享目錄,或者使用 NFS 共享,甚至簡單使用 rsync 來在兩者之間同步文件。后續關於文件同步與共享不再贅述。
不出意外的話,就可以在啟動窗口中看到內核的啟動日志了。在內核啟動的最后,會出現一條 panic 日志:
Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0, 0)
從日志內容可以看出,內核啟動到一定階段后嘗試加載根文件系統,但我們沒有指定任何磁盤設備,所以無法掛載根文件系統。而且上一節中編譯出來的內核模塊現在也沒有用上,內核模塊也需要存放到文件系統中供內核需要的時候進行加載。
所以,接下來需要制作一個磁盤鏡像文件供內核作為根文件系統加載。
5. 制作磁盤鏡像
如上一節所述,需要制作一個磁盤鏡像文件作為根文件系統供內核加載,同時也用於存放編譯好的內核模塊,以及后續所需的各種配套工具程序。
5.1 創建磁盤鏡像文件
使用 qemu-img
創建一個 512M 的磁盤鏡像文件:
qemu-img create -f raw disk.raw 512M
現在 disk.raw 文件就相當於一塊磁盤,為了在里面存儲文件,需要先進行格式化,創建文件系統。比如在 Linux 系統中使用 ext4 文件系統進行格式化:
mkfs -t ext4 ./disk.raw
5.2 掛載磁盤鏡像文件
格式化完成之后,可以在 Linux 系統中以 loop 方式將磁盤鏡像文件掛載到一個目錄上,這樣就可以操作磁盤鏡像文件中的內容了。
下面的命令將磁盤鏡像文件掛載到 img 目錄上:
sudo mount -o loop ./disk.raw ./img
5.3 安裝內核模塊
現在可以將之前編譯好的內核模塊安裝到磁盤鏡像中了。命令如下:
sudo make modules_install \ # 安裝內核模塊
INSTALL_MOD_PATH=./img # 指定安裝路徑
執行完成后即可在 ./img/lib/modules/
下看到安裝好的內核模塊。
5.4 使用磁盤鏡像文件作為根文件系統
准備好磁盤鏡像文件后,使用下面的命令再次啟動 qemu:
qemu-system-x86_64 \
-m 512M \
-smp 4\
-kernel ./bzImage \
-drive format=raw,file=./disk.raw \ # 指定文件作為磁盤
-append "root=/dev/sda" # 內核啟動參數,指定根文件系統所在設備
這一次,內核不再報根文件系統找不到了。但是報了另一個錯誤:
Kernel panic - not syncing: No working init found. Try passing init= option to Kernel. See Linux Documentation/admin-guide/init.rst for guidance.
這說明內核啟動已經接近完成了,准備啟動 1 號進程,也就是 init 進程。但我們的啟動參數里面沒有指定 init 選項,而且磁盤鏡像中也沒有相應的 init 程序。因此,接下來需要准備一個 init 程序供內核啟動。
6. 准備 init 程序
常用的 init 程序有下面幾種:
- sysv init:傳統 Linux 系統中最常用的 init 程序
- systemd:目前最流行的 init 程序,很多主流發行版都已經切換到 systemd。systemd 針對 sysv init 啟動速度慢、無法並行以及管控能力弱等問題進行了重新設計。參見 Rethinking PID 1
- busybox init:通知用在嵌入式等小型系統中。除了 init 程序外,busybox 還包含了很多常用的命令工具,比如
ls
、cat
等。busybox 非常輕量級,可以編譯出完全獨立無依賴的 busybox 套件。
這里選用 busybox 作為 init 程序及其它命令工具的提供者。
6.1 編譯 busybox
下載 busybox 的源碼到 Linux 系統中,准備進行編譯,這里使用的 busybox 版本為 1.27.2。
busybox 的編譯流程與內核很像,這里我們基於默認配置進行編譯。首先,執行如下命令讓默認配置生效:
make defconfig
接下來,在默認配置的基礎上進行定制:
make menuconfig
這里有一個重要的配置,因為 busybox 將被用作 init 程序,而且我們的磁盤鏡像中沒有任何其它庫,所以 busybox 需要被靜態編譯成一個獨立、無依賴的可執行文件,以免運行時發生鏈接錯誤。配置路徑如下:
Busybox Settings --->
--- Build Options
[*] Build BusyBox as a static binary (no shared libs)
最后,配置完成后執行編譯:
make
編譯完成后在當前目錄下可以看到 busybox
可執行文件,查看大小才 2.5M 左右。整個 busybox 套件只有這一個可執行文件,里面包含了若干工具。比如:
./busybox ls -l
./busybox ps
6.2 安裝 busybox 到磁盤鏡像
編譯好 busybox 之后需要將其安裝到磁盤鏡像中以供使用。執行如下命令進行安裝:
make CONFIG_PREFIX=<path_to_disk_img_mount_point> install
CONFIG_PREFIX
用於指定安裝路徑,需要指定到之前磁盤鏡像文件的掛載目錄,比如 ./img
。進入磁盤鏡像掛載目錄查看,常見的文件系統結構已經建立起來了。查看 bin 和 sbin 目錄下的命令,可以看到都是鏈接到 bin/busybox
的,busybox 會根據執行時的文件名來執行不同的功能。
6.3 使用 busybox 作為 init 程序
busybox 安裝完成之后,使用內核啟動參數 init=
來指定 busybox 作為 init 程序,再次嘗試啟動。
qemu-system-x86_64 \
-m 512M \
-smp 4\
-kernel ./bzImage \
-drive format=raw,file=./disk.raw \
-append "init=/linuxrc root=/dev/sda"
上述命令通過 init=/linuxrc
指定了 init 程序為根目錄下的 linuxrc,實際上是一個指向 busybox 的軟鏈接。
這一次內核成功找到了 init 程序並且創建出 init 進程,但是 init 執行過程中出現如下報錯:
can't run '/etc/init.d/rcS': No such file or directory
can't open /dev/tty3: No such file or directory
can't open /dev/tty4: No such file or directory
看樣子,init 程序需要一些配置才能正常運行起來。
6.4 配置 busybox init
參考 busybox 代碼中的 文檔 可知,init 啟動后會掃描 /etc/inittab
配置文件,這個配置文件決定了 init 程序的行為。而 busybox init 在沒有 /etc/inittab
文件的情況下也能工作,因為它有默認行為。它的默認行為相當於如下配置:
::sysinit:/etc/init.d/rcS
::askfirst:/bin/sh
::ctrlaltdel:/sbin/reboot
::shutdown:/sbin/swapoff -a
::shutdown:/bin/umount -a -r
::restart:/sbin/init
tty2::askfirst:/bin/sh
tty3::askfirst:/bin/sh
tty4::askfirst:/bin/sh
參考文檔,我們提供一份 /etc/inittab
配置文件如下:
::sysinit:/etc/init.d/rcS
::askfirst:/bin/ash
::ctrlaltdel:/sbin/reboot
::shutdown:/sbin/swapoff -a
::shutdown:/bin/umount -a -r
::restart:/sbin/init
並且根據配置,我們創建可執行文件 /etc/init.d/rcS
,內容如下(暫時什么事都不做):
#!/bin/sh
配置完成以后再次嘗試啟動,這次將成功啟動,並且出現如下提示:
Please press Enter to activate this console.
按提示按下 Enter 鍵之后將會啟動 shell,進行到我們熟悉的環境,可以執行各種常用命令了。
6.5 掛載 /dev, /proc, /sys 文件系統
查看當前系統環境,會發現當前文件系統結構是不完整的。比如沒有 /dev, /proc 以及 /sys 掛載點。這樣我們無法通過 /dev 查看系統中的設備,如果執行 df
命令也會因為沒有 /proc 掛載點而報錯:
df: /proc/mounts: No such file or directory
因此,我們需要手工創建 /dev, /proc, /sys 這三個目錄。/dev 目錄創建完成后重啟系統即可工作,但 /proc 和 /sys 需要執行掛載才可工作,可以將 /proc 和 /sys 的掛載動作放到 /etc/init.d/rcS
中,每次系統啟動時自動掛載。修改 /etc/init.d/rcS
內容如下:
#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
重新啟動系統查看,可以看到 /dev, /proc, /sys 掛載點都相應有了內容。
7. 小結
本文介紹了通過 qemu 作為模擬器,自己動手編譯內核,並從頭配置 init 進程,構建出一個最小的可運行系統,可用於驗證對內核的改動。
通過這次開發環境搭建,對系統的啟動過程有了一個粗略的了解。但這只是邁出了第一步,后續還有長路漫漫。