使用 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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM