Try   HackMD

Linux 核心專題: RISC-V 系統模擬器

執行人: JiggerChuang
專題解說錄影
投影片

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
提問清單

背景知識

  • OpenSBI
  • PLIC
  • Clint
  • MMU
  • Device Tree

semu: 精簡的 RISC-V 系統模擬器

特徵:

  • RISC-V 指令集架構: RV32IMA
  • 特權等級: S mode 和 U mode
  • Control and status registers (CSR)
  • 虛擬記憶體: RV32 MMU
  • UART: 8250/16550
  • PLIC (platform-level interrupt controller): 32 interrupts, no priority
  • Standard SBI (Supervisor Binary Interface), with the timer extension
  • 支援 VirtIO: virtio-net, mapped as TAP interface
  • 原始程式碼在 2000 行左右

利用 Buildroot 建構檔案系統

RISC-V 和 Buildroot 介紹

Buildroot 相關演講

取得 Buildroot 程式碼:

$ git clone https://github.com/buildroot/buildroot.git --depth=1

依據 semu 提供的組態來建構 root file system:

$ cd buildroot
$ cp ../configs/buildroot.config .config
$ make
$ cd ..

可將上方的 make 改為 make -j8,依據有效的處理器數量變更

取得 Linux 核心程式碼:

$ git clone https://github.com/torvalds/linux.git --depth=1

利用 Buildroot 建構的 GNU Toolchain 來編譯 Linux 核心,先設定環境變數:

$ export PATH=`pwd`/buildroot/output/host/bin:$PATH
$ export CROSS_COMPILE=riscv32-buildroot-linux-gnu-
$ export ARCH=riscv

編譯 Linux 核心:

$ cd linux
$ cp ../configs/linux.config .config
$ make Image
$ cd ..

可將上方的 make 改為 make -j8,依據有效的處理器數量變更

預期會得到 linux/arch/riscv/boot/Image 檔案,接著執行:

$ ./semu Image

即可執行 Linux 核心。

網路設定

semu 支援 TAP/TUN 以存取電腦網路。

引述 Wikipedia:

TUN and TAP are kernel virtual network devices. Being network devices supported entirely in software, they differ from ordinary network devices which are backed by physical network adapters.
Though both are for tunneling purposes, TUN and TAP can't be used together because they transmit and receive packets at different layers of the network stack. TUN, namely network TUNnel, simulates a network layer device and operates in layer 3 carrying IP packets. TAP, namely network TAP, simulates a link layer device and operates in layer 2 carrying Ethernet frames. TUN is used with routing. TAP can be used to create a user space network bridge.

TUN 和 TAP 是 Linux 核心模擬出的虛擬網路裝置,TUN 處理 IP 封包,而 TAP 處理 Ethernet 封包,參見下圖:

Image Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

開啟二個終端機,其中 Tsemu 表示執行 semu 的終端機,Thost 表示執行本機網路設定的終端機。

在 Tsemu 執行以下命令:

sudo ./semu

預期應該要在第一行見到 allocated TAP interface: tap0 的字樣,接著切到 Thost,執行以下命令:

sudo ip addr add 192.168.10.1/24 dev tap0
sudo ip link set tap0 up

再切換到 Tsemu,等待以下訊息的出現:

Welcome to Buildroot
buildroot login:

輸入 root 之後會出現提示符號 #,接著執行以下命令:

ip l set eth0 up
ip a add 192.168.10.2/24 dev eth0
ping -c 3 192.168.10.1

預期會見到以下輸出:

PING 192.168.10.1 (192.168.10.1): 56 data bytes
64 bytes from 192.168.10.1: seq=0 ttl=64 time=1.021 ms
64 bytes from 192.168.10.1: seq=1 ttl=64 time=0.552 ms
64 bytes from 192.168.10.1: seq=2 ttl=64 time=0.582 ms

--- 192.168.10.1 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.552/0.718/1.021 ms

切換到 Thost 來檢驗:

ping -c 3 -I tap0 192.168.10.2

預期會見到以下輸出:

ING 192.168.10.2 (192.168.10.2) from 192.168.10.1 tap0: 56(84) bytes of data.
64 bytes from 192.168.10.2: icmp_seq=1 ttl=64 time=1.01 ms
64 bytes from 192.168.10.2: icmp_seq=2 ttl=64 time=0.957 ms
64 bytes from 192.168.10.2: icmp_seq=3 ttl=64 time=0.972 ms

至此,具備基本的網路設定。

追蹤封包

在 Thost 執行以下命令:

sudo tcpdump -i tap0

預期會有以下輸出:

tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on tap0, link-type EN10MB (Ethernet), capture size 262144 bytes

tcpdump 保持執行,接著切換到 Tsemu ,執行以下命令:

ping -c 3 192.168.10.1

切換到 Thost ,觀察封包捕捉狀況,參考輸出:

1:55:55.728344 IP 192.168.10.2 > node1: ICMP echo request, id 71, seq 0, length 64
01:55:55.728380 IP node1 > 192.168.10.2: ICMP echo reply, id 71, seq 0, length 64
01:55:57.963882 IP 192.168.10.2 > node1: ICMP echo request, id 71, seq 1, length 64
01:55:57.963904 IP node1 > 192.168.10.2: ICMP echo reply, id 71, seq 1, length 64
01:56:00.201345 IP 192.168.10.2 > node1: ICMP echo request, id 71, seq 2, length 64
01:56:00.201371 IP node1 > 192.168.10.2: ICMP echo reply, id 71, seq 2, length 64
01:56:00.922217 ARP, Request who-has 192.168.10.2 tell node1, length 28
01:56:00.922532 ARP, Reply 192.168.10.2 is-at 56:53:0c:0a:86:24 (oui Unknown), length 28
01:56:07.028471 ARP, Request who-has node1 tell 192.168.10.2, length 28
01:56:07.028487 ARP, Reply node1 is-at ce:d5:02:d4:75:e0 (oui Unknown), length 28
01:56:50.256464 IP node1.mdns > 224.0.0.251.mdns: 0 [2q] PTR (QM)? _ipps._tcp.local. PTR (QM)? _ipp._tcp.local. (45)
01:56:51.457867 IP6 node1.mdns > ff02::fb.mdns: 0 [2q] PTR (QM)? _ipps._tcp.local. PTR (QM)? _ipp._tcp.local. (45)
01:57:42.810231 IP6 node1 > ip6-allrouters: ICMP6, router solicitation, length 16

第一欄為時間戳記

TODO: 描述程式碼原理

研讀〈Writing a simple RISC-V emulator in plain C〉,對照 semu 原始程式碼,解釋系統模擬器之行為和原理。

開始流程

一開始從命令列讀取命令與參數

$ ./semu Image

檔案載入與模擬器設定

接著將參數 (i.e., 執行檔名) 傳給 semu_start() 進行讀檔與設定:

static int semu_start(int argc, char **argv)
{
    /* Init */
    emu_state_t emu;
    vm_t vm = {
        .priv = &emu,
        .mem_fetch = mem_fetch,
        ...
    };   

    /* Set up RAM */
    emu.ram = mmap(NULL, RAM_SIZE, PROT_READ | PROT_WRITE,
                   MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    
    /* Load file into RAM */
    read_file_into_ram(&ram_loc, argv[1]);
    read_file_into_ram(&ram_loc, (argc == 3) ? argv[2] : "minimal.dtb");
    
    /* Set up RISC-V hart */
    emu.timer_hi = emu.timer_lo = 0xFFFFFFFF;
    vm.s_mode = true;
    ...
        
    /* Set up peripherals */
    emu.uart.in_fd = 0, emu.uart.out_fd = 1;
#if defined(ENABLE_VIRTIONET)
    virtio_net_init(&(emu.vnet))
    emu.vnet.ram = emu.ram;
#endif
    
    /* Emulate */
    while (!emu.stopped) {
        ...
    }
}

Init

當中 emu_state_t struct 對應到文章中的 CPU struct,包含 CPU 周邊 (e.g., ram, plic, uart 等),vm_t struct 對應到 datapath 內部結構運作機制,包含 32 個暫存器、pc、memory fectch 等。

Set up RAM

mmap(2) 用來設定模擬的 RAM,也就是將要讀寫的檔案內容 (i.e., Image) 映射到一段 (虛擬) 記憶體上,通過對這段記憶體的讀寫,可直接對檔案內容做修改,一般的 I/O 通常需要先將資料放進 buffer,mmap 可省略這一個步驟,提高存取速度,再來是可把檔案當成記憶體來使用,直接用指標來操作,其中第一個參數 NULL 表示由作業系統決定起始位址。

注意用詞: file 是「檔案」,document 是「文件」

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
jserv

Load file into RAM

ram 設定完成後,就可以 read_file_into_ram() 來將 Linux 核心映像檔案 (kernel image) 載入進模擬的 RAM 中:

static void read_file_into_ram(char **ram_loc, const char *name)
{
    FILE *input_file = fopen(name, "r");
    while (!feof(input_file))
        *ram_loc += fread(*ram_loc, sizeof(char), 1024 * 1024, input_file);
}

TODO: 這裡可將讀檔改成 memory mapping 的方式

接著以同樣的方式載入 device tree 檔案。

TODO: 研究 device tree 並透過 virtio_blk 方式載入 disk image

Set up RISC-V hart

設定 hart 屬性。

ISSUE: 不知道暫存器內 RV_R_A0 與 RV_R_A1 為何要這樣設定
參照 Machine-Level ISA

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
jserv

根據 All Aboard, Part 6: Booting a RISC-V Linux Kernel:

Early Boot in Linux
When Linux boots, it expects the system to be in the following state:

a0 contains a unique per-hart ID. (下略)
a1 contains a pointer to the device tree, ... (下略)

以及 linux/arch/riscv/kernel
/head.S
之以下組合語言程式碼:

...
ENTRY(_start_kernel)
...
	/* Save hart ID and DTB physical address */
	mv s0, a0
	mv s1, a1
...

可知 A0 和 A1 在載入 Kernel 前是用做傳遞 CPU ID 以及 DTB 位址的用途。

Set up peripherals

設定 uart 的 I/O handling 與基於 virtio 的網路存取。

TODO: 研究 virtio 與如何嵌入進這個專案中

Emulate

開始模擬,首先檢查 uart 是否有請求,若有則發出中斷進行處理,若沒有就以 polling 的方式查看:

if (peripheral_update_ctr-- == 0) {
    peripheral_update_ctr = 64;
    
    u8250_check_ready(&emu.uart);
    if (emu.uart.in_ready)
        emu_update_uart_interrupts(&vm);
}

ISSUE: 不知道 peripheral_update_ctr 代表與設定成 64 的意義
推測是每經過 64 個單位時間後去 polling uart 是否需要處理。

其中內部的中斷是藉由 PLIC 來處理:

static void emu_update_uart_interrupts(vm_t *vm)
{
    emu_state_t *data = (emu_state_t *) vm->priv;
    u8250_update_interrupts(&data->uart);
    if (data->uart.pending_ints)
        data->plic.active |= IRQ_UART_BIT;
    else
        data->plic.active &= ~IRQ_UART_BIT;
    plic_update_interrupts(vm, &data->plic);
}

TODO: 研究 PLIC 與如何嵌入進這個專案中

如果模擬器執行期間發生 time-out 則將 sip bit 設成 1,否則設成 0:

if (vm.insn_count_hi > emu.timer_hi || 
    (vm.insn_count_hi == emu.timer_hi && vm.insn_count > emu.timer_lo))
    vm.sip |= RV_INT_STI_BIT;
else
    vm.sip &= ~RV_INT_STI_BIT;

最後就可以開始針對每個指令執行對應的操作,i.e., fetch -> decode -> execute:

vm_step(&vm);

if (vm.error == ERR_EXCEPTION && vm.exc_cause == RV_EXC_ECALL_S) {
    handle_sbi_ecall(&vm);
    continue;
}

if (vm.error == ERR_EXCEPTION) {
    vm_trap(&vm);
    continue;
}

TODO: 研究 SBI

TODO: 利用 Buildroot 建構系統

運用 Buildroot 自行編譯 Linux 核心和 root file system,需要客製化 Buildroot,加入自訂的套件,如 kilo

需要解釋 initramfs 運作原理以及 Buildroot 如何產生 cpio 檔案。

Buildroot

refer to:
RISC-V 和 Buildroot 介紹 by jserv
2019 年核心實作專題: RISCV with TinyEMU
buildRoot study - 建立自己的作業系統

要讓一個系統從開機到完全啟動,至少需要以下三個部分:

  • bootloader
  • kernel
  • root file system

注意用詞:

  • dirctory 是「目錄」
  • folder 是「檔案夾」

在 POSIX 相容系統 (包含 Linux),應該用「目錄」

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
jserv

Buildroot 專案就能用來建構出上面三個部分,其中:

  • toolchain/ 目錄中包含所有與交叉編譯工具鏈 (e.g., binutils, gcc, gdb、kernel-headers 等) 相關的 makefile 與軟體檔案。
  • arch/ 目錄所有 buildroot 中支援之硬體架構的定義。
  • package/ 目錄包含所有 Buildroot 可以編譯和加進目標 root filesystem 的 user-space 的工具和函式庫的 makefile 和相關檔案,且每個 package 都有一個子目錄。
  • linux/ 目錄包含 Linux 核心的 makefile 和相關檔案。
  • boot/ 目錄包含 buildroot 支援之 bootloader 的 makefile 和相關檔案。
  • system/ 目錄包含系統整合相關檔案,例如: 目標檔案系統、init 程式的選擇。
  • fs/ 目錄包含產生目標 root filesystem 的 makefile 與相關檔案。

上述目錄中都至少包含以下兩個檔案:

  • something.mk 用來下載、設定、編譯和安裝 package 的 makefile。
  • Config.in 工具配置描述檔案的一部分,描述每個 package 中的選項。

上面章節 利用 Buildroot 建構檔案系統 中複製過來的 .config 檔案是我們要藉由 buildroot 工具去建構 kernel image + root file system 的設定檔,可透過 $ make menuconfig 去調整,下面是客製化 buildroot 的流程。

首先取得 buildroot 程式碼:

$ git clone https://github.com/buildroot/buildroot.git --depth=1

--depth=1 表示只取得最新一筆 commit,在專案很大時可加快下載時間及節省空間。

根據 semu 提供的組態來建構 root file system:

$ cd buildroot
$ cp ../semu/configs/buildroot.config .config
$ make
$ cd ..

安裝必要的開發套件:

$ sudo apt install libncurses5-dev

接著變更設定:

$ make menuconfig

預期會看到以下畫面:

這個介面是用來選擇 build 時的特色 (features) 和參數 (parameters),其中:

  • Features 可使用內建、模組或直接忽略
  • Parameters 需要以十進制或十六進制的數字或文字輸入

上面介面中:

  • Target options 可選擇 ISA 架構、單精度、ABI 格式、ELF 格式等
  • Toolchain 可選擇格式 (e.g., buildroot 或外部工具鏈)、變更工具鏈名稱、要引入的函式庫 (e.g., glibc)、kernel headers 版本等
  • Build options 可選擇建構時的屬性,例如儲存 buildroot config 的位置、是否要加上除錯訊息等
  • System configuration 可選擇開機提示字、登入是否需要密碼等
  • Target packages 設定 busybox,包含是否使用原本或自訂的版本
  • Filesystem images 可建構 root filesystem 的 cpio 檔案,通常被用來初始化 RAM filesystem,並由 bootloader 傳給 kernel
  • Bootloader 用來選擇 bootloader (e.g., opensbi、U-Boot 等)

Busybox 是個工程程式集合,在單一的可執行檔中提供精簡的 UNIX 工具 (e.g., ls 命令),可執行於多款 POSIX 環境的作業系統 (e.g., Linux)。
cpio 是 UNIX 作業系統的檔案格式,可以從 cpio 或 tar 格式的歸檔包中存入和讀取檔案。其中歸檔包是一種包含其他檔案和有關資訊的檔案 (e.g., 檔名、存取權限等)

Kilo

是個精簡的程式碼編輯器,支援語法高亮度提示和常見的編輯功能。

首先從 GitHub 下載:

$ git clone https://github.com/antirez/kilo

進行編譯:

$ cd kilo
$ make

編譯完成後會出現 kilo 執行檔,使用 kilo 編輯檔案預期會出現以下畫面:

$ ./kilo kilo.c

將 Kilo 加入 Buildroot 中

參考 Ztex 2019 q1 HW4 和當中提到的 The Buildroot user manual,要對 Buildroot 進行改動前可以先參照上面介紹的 Buildroot 檔案結構,第 18 章 Adding new packages to Buildroot 介紹如何加入客製化的 package (i.e., 工具或函式庫) 到 Buildroot 中,步驟如下:

  1. 首先找到要加入的 package 位置
  2. 在 package 目錄下新增一個自訂的目錄
  3. 新增 kilo/Config.in 檔案
  4. 將 kilo/Config.in 更新到 package/Config.in
  5. 在 kilo/ 下新增一個 makefile 稱為 kilo.mk
  6. 以 make menuconfig 確認及選擇 kilo
  7. 以 make 建置 buildroot
  8. 重新產生 linux kernel image
  9. 將 Image 複製到 semu/ 並執行

詳細流程如下:

首先找到要加入的 package 位置:

$ make menuconfig

進入選單後 -> Target packages -> Text editors and viewers 確認現有的 package 如下: (預設為 nano 且沒有 kilo)

再來是在 package 目錄下新增一個自訂的目錄: (這裡直接以 kilo 為例)

$ cd buildroot/package
$ mkdir kilo

如果要自訂的 package 已經被分類 (e.g., x11r7、qt5 等),則需要在這些目錄下新增子目錄。

新增 kilo/Config.in 檔案Config.in 中會包含 kilo 所需的選項和相關描述:

$ cd kilo
$ touch Config.in

Config.in 內容如下:

config BR2_PACKAGE_KILO
    bool "kilo"
    help
      This is a comment that explains what libfoo is. The help text
      should be wrapped.
        
      https://github.com/antirez/kilo

其中 bool 是 menu 圖形化介面中會出現的字樣,help 是輸入 shift+? 會出現的提示字。

bool、help 和其他 metadata 資訊前需要以 tab 進行縮排,help 中的文字前需要再以兩個 space 進行縮排,單列不能超過 72 個字元,扣掉 tab 剩下 62 個字元,且最後需要空一行並加上專案的來源 (詳細格式請參照第 16 章 Coding style)。

將 kilo/Config.in 更新到 package/Config.in:

$ cd .. # 移動到 package/
$ vim Config.in

# 找到 menu "Text editors and viewers"
# 加上 source "package/kilo/Config.in"
# 注意加入的地方需要按照字母大小排列,也就是 kilo 需要加在 jxxx package 後

在 kilo/ 下新增一個 makefile 稱為 kilo.mk,這個檔案描述 package 需要如何下載、配置、建購及安裝等操作:

$ cd kilo
$ touch kilo.mk

不同型態的 makefile 會對應到不同的寫法以及使用不同的基礎建設,可分為:

  • generic package 不使用 autotools 或 cmake,基於類似 autotools-based package 的基礎建設,但開發者需要做的工作較少,只需要指定如何配置、編譯和安裝 package,這個基礎建設用在不使用 autotools 建置的 package。
  • autotools-based software 使用 autoconf、automake 等,Buildroot 為這些 package 提供專屬的基礎建設,且這些 package 需要依賴 autotools 來建置。
  • cmake-based software Buildroot 為這些 package 提供專屬的基礎建設,且這些 package 需要依賴 cmake 來建置。
  • Python modules Buildroot 為需要 distutils、flit、pep517、setuptools 機制來建置的 python 模組提供專屬的基礎建設。
  • Luna modules Buildroot 為需要 LuaRocks web site 來建置的 Lua 模組提供專屬的基礎建設。

因 kilo 使用 gcc 即可編譯,所以這裡選擇 generic package 方案,參考第 18.6 章 Infrastructure for packages with specific build systems,這個方案的建置方法通常使用手寫的 makfile 或 shell script 而非 autotools 或 cmake。

此外,因 kilo 是從 github 下載,這裡需要依照第 18.25.4 章 How to add a package from GitHub 和參考 stackoverflow 來對預設的 makefile 進行修改,其中包含

# Use a tag or a full commit ID
FOO_VERSION = 1.0
FOO_SITE = $(call github,<user>,<package>,v$(FOO_VERSION))
FOO_SITE_METHOD = git

FOO_VERSION 可以是 tag 或完整的 commit ID

修改後的 kilo.mk 內容如下:

################################################################################
#
# kilo
#
################################################################################

#KILO_VERSION = 69c3ce609d1e8df3956cba6db3d296a7cf3af3de
KILO_VERSION = 62b099af00b542bdb08471058d527af258a349cf
KILO_SITE = https://github.com/antirez/kilo.git
KILO_SITE_METHOD = git

define KILO_BUILD_CMDS
        $(MAKE) CC="$(TARGET_CC)" LD="$(TARGET_LD)" -C $(@D)
endef

define KILO_INSTALL_TARGET_CMDS
        $(INSTALL) -D -m 0755 $(@D)/kilo $(TARGET_DIR)/usr/bin
endef

$(eval $(generic-package))

define 內的敘述必須以兩個 tab 縮排

不知道為何使用最新一筆 commit ID 無法下載 repo。
參考 Ztex 同學 使用的 commit ID 可解決。

完成後回到 buildroot/ 下輸入 make menuconfig 來確認及選擇 kilo:

$ cd buildroot
$ make menuconfig

可看到圖中出現 kilo 選項,選擇並保存後會更新到 buildroot/.config 中

更新完 buildroot/.config 後,以 make 重新建置 buildroot:

$ make -j4

建置完成後,預期在 buildroot/output/target/usr/bin/ 下看到 kilo。

$ cd buildroot/output/target/usr/bin
$ ls

...
kilo
...

buildroot 建置完成後,至 linux/ 目錄下重新產生 Linux Kernel Image:

$ cd linux
$ make -j4 Image

建置完成後,將 linux/arch/riscv/boot/Image 複製到 semu/ 並執行:

$ cd arch/riscv/boot
$ cp -f Image ~/Documents/semu/Image

$ cd ~/Documents/semu
$ ./semu Image

預期看到以下畫面:

文字訊息不要用圖片展現!

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
jserv

ISSUE

  • kilo 無法進入編輯模式

    推測與下方 TODO 改進鍵盤輸入事件有關

initramfs 運作原理及如何產生 CPIO 檔案

refer to:
鳥哥私房菜 - 第十九章 Linux 的開機流程分析
深入理解 Linux 2.6 的 initramfs 機制

initramfs 介紹

Linux 核心可透過動態載入核心模組 (i.e., driver),這些核心模組就放在 /lib/modules 內,由於模組放置到磁碟根目錄內,因此在開機的過程中,核心必須要掛載根目錄,這樣才能讀取核心模組提供載入驅動程式的功能

一般來說,非必要的功能 (e.g., USB、SATA 等磁碟裝置驅動程式) 會被編譯成為模組等待載入,假設 Linux 是安裝在 SATA 磁碟上,可以透過 boot loader 與 kernel image 來開機,然後 kernel 會開始接管系統並且偵測硬體及嘗試掛載根目錄來取得額外的驅動程式,但問題是 kernel 在掛載 SATA 相關模組前不認得 SATA 磁碟,所以若不載入 SATA 磁碟的驅動程式就無法掛載根目錄,但 SATA 驅動程式在 /lib/modules 中,若不掛載根目錄也無法讀到 /lib/modules 中的驅動程式,在這個情況下 Linux 無法開機。

虛擬檔案系統 (Initial RAM Disk 或 Initial RAM Filesystem) 一般記為 /boot/initrd/boot/initramfs,這個檔案的特色是也能夠透過 boot loader 載入進記憶體中,然後這個檔案會被解壓縮並在記憶體中模擬成一個根目錄,且此模擬在記憶體中的檔案系統能夠提供一支載入開機過程中所需要的 kernel module (e.g., USB、SATA 等) 的程式,載入完成後,會幫助 kernel 重新呼叫 systemd 來開始後續的開機流程。

如上圖所示,boot loader 可以載入 kernel 與 initramfs,然後讓 initramfs 解壓縮成根目錄,kernel 就能藉此載入需要的驅動程式,最後釋放虛擬檔案系統,完成後 kernel 會掛載真正的 root file system 並執行 /sbin/init 程式。

實作客製化 initramfs

新增一個目錄名為 initramfs-workspace

$ mkdir initramfs-workspace
$ cd initramfs-workspace

將上面以 git clone 下載的 linux 檔案複製到 initramfs-space/ 下

$ cp -r ~/Downloads/linux ./

新增一個存放 kernel + initramfs 的目錄,建立 init.c,最後再透過 semu 進行模擬

$ mkdir -p hello-initramfs
$ cd hello-initramfs
$ cat > init.c <<EOF
> #include <stdio.h>
> int main()
> {
>     printf("Hello semu !!!\n");
>     return 0;
> }
> EOF

以 buildroot 中的 toolchain 編譯 init.c

$ riscv32-buildroot-linux-gnu-gcc -static -o init init.c

這裡印出的 Hello semu !!! 就是 Early userspace

因執行時期需要 tty (terminal),所以需要在 hello-initramfs/ 下一併建立 /dev/console 的 character device

$ mkdir -p dev
$ sudo mknod dev/console c 5 1

接下來開始準備 kernel

$ cd ../linux # 回到 initramfs-workspace/
$ make menuconfig

開啟 menuconfig 選擇 General setup -> 開啟 Initial RAM filesystem and RAM disk support -> 將下方的 Initramfs source file 路徑改成能找到 hello-initramfs 的路徑,保存後退出。

開始建構核心

$ make -j$(nproc) Image

將核心複製到 semu 下並開始執行

$ cp arch/riscv/boot/Image ~/project/riscv/semu
$ cd ~/project/riscv/semu
$ ./semu Image

預期看到

...
[    0.000000] Machine model: semu
[    0.000000] earlycon: ns16550 at MMIO 0xf4000000 (options '')
[    0.000000] printk: bootconsole [ns16550] enabled
[    0.000000] Zone ranges:
[    0.000000]   Normal   [mem 0x0000000000000000-0x000000001fffffff]
...
[    1.460666] Run /init as init process
Hello semu !!!
...

可以看到在執行 init 時出現 Hello semu !!!,但因為沒有掛載其他檔案,所以無法進入 shell

buildroot 產生 CPIO 的機制

在 linux/usr 下可找到 gen_init_cpio.c,在當中的 Usage 函式中可看到

fprintf(stderr, "Usage:\n"
        "\t%s [-t <timestamp>] [-c] <cpio_list>\n"
        "\n"
        "<cpio_list> is a file containing newline separated entries that\n"
        "describe the files to be included in the initramfs archive:\n"

上文中的 archive 就是透過 cpio 工具產生的封裝檔案,Linux kernel 提供一個整合性工具,可一次處理目錄與檔案的封裝,封裝過後的檔案 (cpio + gzip) 即是一個完整的 initramfs image

實作客製化 CPIO

回到 initramfs-workspace/ 目錄下,複製 hello-initramfs

$ cd ~/initramfs-workspace
$ cp -af hello-initramfs hello2-initramfs

進入 hello-initramfs/ 並修改 init.c 中的內容

#include <stdio.h>
int main()
{
    printf("Yat Another Hello semu !!!\n");
    return 0;
}

以 buildroot 工具鏈編譯 init.c

$ riscv32-buildroot-linux-gnu-gcc -static -o init init.c

新增一個描述檔 desc_initramfs

dir /dev 0755 0 0
nod /dev/console 0600 0 0 c 5 1
file /init /home/doublemama/initramfs-workspace/hello2-initramfs/init 0755 0 0

以 linux 中的 cpio 工具進行封裝

$ ../linux/usr/gen_init_cpio desc_initramfs > my_initramfs.cpio

# 這裡因會在 make menuconfig 下直接讀取 cpio 檔,所以不進行壓縮
($ gzip my_initramfs.cpio)

usr/gen_init_cpio 工具會建構對應的 dir + device node + file 的封裝,最後以 gzip 壓縮起來,於是可得到 my_initramfs.cpio 這個新的 initramfs image

進入 initramfs-workspace/linux 修改讀取 cpio 的路徑並編譯核心

$ cd ../linux
$ make menuconfig 
# 操作同上,將讀取 cpio 位置改成 .../hello2-initramfs/my_initramfs.cpio
$ make -j$(nproc) Image

複製 image 至 semu/ 下並進行測試

$ cp arch/riscv/boot/Image ~/project/riscv/semu/
$ cd ~/project/riscv/semu
$ ./semu Image

預期看到

...
[    0.000000] Machine model: semu
[    0.000000] earlycon: ns16550 at MMIO 0xf4000000 (options '')
[    0.000000] printk: bootconsole [ns16550] enabled
[    0.000000] Zone ranges:
[    0.000000]   Normal   [mem 0x0000000000000000-0x000000001fffffff]
[    0.000000] Movable zone start for each node
...
[    1.449265] Run /init as init process
Yat Another Hello semu !!!
...

一樣因為沒有掛載其他檔案,所以無法進入 shell

上述做法都只有印出訊息,下面以整合 busybox 進行測試,首先安裝 busybox

$ sudo apt-get install busybox-static

# 測試是否安裝成功
$ file /bin/busybox

# 預期看到
/bin/busybox: ELF 64-bit LSB executable, 
x86-64, version 1 (GNU/Linux), statically linked, 
BuildID[sha1]=36c64fc4707a00db11657009501f026401385933, 
for GNU/Linux 3.2.0, stripped

回到 initramfs-workspace/ 目錄下,新增兩個 busybox 子目錄,並將 busybox 執行檔複製過來、擷取當中命令

$ cd ~/initramfs-workspace
$ mkdir -p busybox-initramfs/bin
$ mkdir -p busybox-initramfs/proc
$ cd busybox-initramfs/bin
$ cp /bin/busybox .
./busybox --help | ruby -e 'STDIN.read.split(/functions:$/m)[1].split(/,/).each{|i|`ln -s busybox #{i.strip}` unless i=~/busybox/}'
$ cd ..
$ echo -e '#!/bin/busybox sh\nmount -t proc proc /proc\nexec busybox sh\n' > init ; chmod +x init

# 這裡一樣先不進行壓縮,也就是不執行最後的 gzip
$ find . | cpio -o -H newc | gzip > ../busybox.initramfs.cpio.gz

進入 linux/ 下修改讀取 cpio 檔的路徑並進行建置

$ cd ~/initramfs-workspace/linux
$ make menuconfig 
# 將 initramfs source file 路徑選擇 ~/initramfs-workspace/busybox.initramfs.cpio

$ make -j$(nproc) Image

ISSUE:

  • /init 無法執行
  • /bin/sh 無法執行
  • Kernel pacin - not syncing: No working init found

錯誤訊息如下:

[    3.494816] Run /init as init process
[    3.497068] Failed to execute /init (error -8)
[    3.497243] Run /sbin/init as init process
[    3.498035] Run /etc/init as init process
[    3.498744] Run /bin/init as init process
[    3.500600] Starting init: /bin/init exists but couldn't execute it (error -8)
[    3.500844] Run /bin/sh as init process
[    3.502728] Starting init: /bin/sh exists but couldn't execute it (error -8)
[    3.502987] Kernel panic - not syncing: No working init found.  Try passing init= option to kernel. See Linux Documentation/admin-guide/init.rst for guidance.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
利用 Buildroot 產生的完整 cpio,在其之上添加自訂程式,重新打包並編譯 Linux 核心以建立對應的映像檔案。熟悉 Buildroot 的關鍵在於掌握易於測試的環境,先熟悉工具再來排除上述問題。
Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
jserv

TODO: 正確處理鍵盤輸入事件

現行 semu 所有的鍵盤輸入皆要在按下 Enter 後才發揮作用,這使得 shell 本身的 tab completion (見 Busybox 的 FEATURE_EDITING) 無法作用,按下 Enter 後,會輸出額外的 '\n' 字元,且在 vi 中無法正確的輸入命令字元 (即按下 Esc),這是因為目前沒有正確處理輸入緩衝區。

解法:另行維護 UART 專用的緩衝區,並確保輸入事件不影響模擬器的運作 (可能需要建立執行緒)

一旦鍵盤事件能夠正確處理,就可實作 Ctrl-a x (離開模擬器) 這樣的按鍵組合。

參見:

上述 描述程式碼原理 有簡單介紹過 semu 中 uart 的運作模式,模擬器每隔 64 個單位時間就會去偵測 uart 是否需要處理,若 uart 未達需要處理的情況就進行 polling

void u8250_check_ready(u8250_state_t *uart)
{
    if (uart->in_ready)
        return;

    poll(&pfd, 1, 0);
    if (pfd.revents & POLLIN)
        uart->in_ready = true;
}

而若此時 uart 需要處理,就發出中斷

static void emu_update_uart_interrupts(vm_t *vm)
{
    u8250_update_interrupts(&data->uart);
    if (data->uart.pending_ints)
        data->plic.active |= IRQ_UART_BIT;
    else
        data->plic.active &= ~IRQ_UART_BIT;
    plic_update_interrupts(vm, &data->plic);
}

參照「背景知識 - PLIC」,外部裝置 (e.g., uart) 發出的中斷會傳送到 PLIC,再經由 PLIC 判斷先處理哪種中斷 (i.e., 先將哪種中斷傳給 hart 做處理)。

因 semu 原本接收鍵盤輸入的方式會在按下 Enter 後輸入額外換行字元,所以無法使用 tab completion 且無法使用 vi 進行編輯,參考 mini-rv32ima 中的做法使用 termios(3) 來解決,這個功能是用來控制終端介面非同步通訊的通訊埠,在 tcgetattr 取得該 fd 對應的設定後,取消標準輸入 (ICANON) 模式和 ECHO,完成後以 tcsetattr 做設定。

uart 處理鍵盤輸入的地方在 u8250_handle_in(),在當中加上以下程式碼即可使用 tab completion 且可使用 vi 進行編輯 (commit e8f183f):

struct termios term;
tcgetattr(0, &term);
term.c_lflag &= ~(ICANON | ECHO); // Disable echo as well
tcsetattr(0, TCSANOW, &term);

vi test.c 進行測試,進入 shell 後以 vi 編輯檔案

# vi test.c
Hello Semu
(保存後退出)
# cat te // 這裡按下 tab 可自動補齊 test.c
# cat test.c
Hello Semu

在能正確處理 UART 的鍵盤輸入後,就能實現組合按鍵,實作使用組合按鍵離開 semu,而非以 Ctrl-c 結束整個程式如下 (commit 90c34f9):

if (value == 1){ /* start of heading (Ctrl-a) */
    if (getchar() == 120){ /* keyboard x */
        printf("\n"); /* end emulator with newline */
        exit(0);
    }
}

準備提交 pull request。參照 Linux 核心專題: 系統虛擬機器開發和改進的「TODO: 用 epoll 和 eventfd 改進 UART 實作」描述

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
jserv

TODO

  • 若出現大量的鍵盤輸入,可能導致模擬器花費大量的時間在處理 IO,可以使用多執行緒解決。

TODO: 改進網路處理

參照 RVVM 的實作,支援 Linux 核心的 TAP 和跨平台的 userspace TAP: