1. 程式人生 > >使用 qemu 搭建核心開發環境

使用 qemu 搭建核心開發環境

本文主要介紹在 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 還包含了很多常用的命令工具,比如 lscat 等。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 程序,構建出一個最小的可執行系統,可用於驗證對核心的改動。
通過這次開發環境搭建,對系統的啟動過程有了一個粗略的瞭解。但這只是邁出了第一步,後續還有長路漫漫。

同步釋出:https://hellogc.net/archives/121