# Linux 核心專題: RISC-V 系統模擬器 > 執行人: JiggerChuang > [專題解說錄影](https://youtu.be/CtZ5QiDcjPE) > [投影片](https://docs.google.com/presentation/d/1phJa4XvK6FdXHxEj_Z0bGUq1r5rqZR2mNwmFC_AA2Vc/edit?usp=sharing) :::success :question: 提問清單 * 這是一個"minimalist RISC-V emulator",請問作為一個minimalist他至少需要完成哪些事情呢??可以詳細說明這部分跟對照spec(https://riscv.org/technical/specifications/)補充嗎?? ::: ## [背景知識](https://hackmd.io/@y8jRQNyoRe6WG-qekloIlA/S1VWjwt4n) * OpenSBI * PLIC * Clint * MMU * Device Tree ## [semu](https://github.com/jserv/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](https://github.com/riscv-non-isa/riscv-sbi-doc/)), with the timer extension * 支援 [VirtIO](https://docs.oasis-open.org/virtio/virtio/v1.2/virtio-v1.2.html): virtio-net, mapped as TAP interface * 原始程式碼在 2000 行左右 ## 利用 Buildroot 建構檔案系統 [RISC-V 和 Buildroot 介紹](https://hackmd.io/@sysprog/ryHaBkrOE) * [2019 年專題: RISCV with TinyEMU](https://hackmd.io/@johnnylord/HkeMrEARE) > [Buildroot 相關演講](https://hackmd.io/@0xff07/rJkMw37mP) 取得 Buildroot 程式碼: ```shell $ git clone https://github.com/buildroot/buildroot.git --depth=1 ``` 依據 semu 提供的組態來建構 root file system: ```shell $ cd buildroot $ cp ../configs/buildroot.config .config $ make $ cd .. ``` > 可將上方的 `make` 改為 `make -j8`,依據有效的處理器數量變更 取得 Linux 核心程式碼: ```shell $ git clone https://github.com/torvalds/linux.git --depth=1 ``` 利用 Buildroot 建構的 GNU Toolchain 來編譯 Linux 核心,先設定環境變數: ```shell $ export PATH=`pwd`/buildroot/output/host/bin:$PATH $ export CROSS_COMPILE=riscv32-buildroot-linux-gnu- $ export ARCH=riscv ``` 編譯 Linux 核心: ```shell $ cd linux $ cp ../configs/linux.config .config $ make Image $ cd .. ``` > 可將上方的 `make` 改為 `make -j8`,依據有效的處理器數量變更 預期會得到 `linux/arch/riscv/boot/Image` 檔案,接著執行: ```shell $ ./semu Image ``` 即可執行 Linux 核心。 ## 網路設定 [semu](https://github.com/jserv/semu) 支援 [TAP/TUN](https://www.kernel.org/doc/html/latest/networking/tuntap.html) 以存取電腦網路。 引述 [Wikipedia](https://en.wikipedia.org/wiki/TUN/TAP): > 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 封包,參見下圖: ![](https://hackmd.io/_uploads/SkI_aOkH2.png) 開啟二個終端機,其中 T~semu~ 表示執行 semu 的終端機,T~host~ 表示執行本機網路設定的終端機。 在 T~semu~ 執行以下命令: ``` sudo ./semu ``` 預期應該要在第一行見到 `allocated TAP interface: tap0` 的字樣,接著切到 T~host~,執行以下命令: ``` sudo ip addr add 192.168.10.1/24 dev tap0 sudo ip link set tap0 up ``` 再切換到 T~semu~,等待以下訊息的出現: ``` 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 ``` 切換到 T~host~ 來檢驗: ``` 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 ``` 至此,具備基本的網路設定。 ## 追蹤封包 在 T~host~ 執行以下命令: ``` 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` 保持執行,接著切換到 T~semu~,執行以下命令: ``` ping -c 3 192.168.10.1 ``` 切換到 T~host~,觀察封包捕捉狀況,參考輸出: ``` 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](https://fmash16.github.io/content/posts/riscv-emulator-in-c.html)〉,對照 semu 原始程式碼,解釋系統模擬器之行為和原理。 ### 開始流程 一開始從命令列==讀取命令與參數== ```shell $ ./semu Image ``` ### 檔案載入與模擬器設定 接著將參數 (i.e., 執行檔名) 傳給 `semu_start()` 進行==讀檔與設定==: ```c 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)](https://man7.org/linux/man-pages/man2/mmap.2.html) 用來**設定模擬的 RAM**,也就是將要讀寫的檔案內容 (i.e., Image) 映射到一段 (虛擬) 記憶體上,通過對這段記憶體的讀寫,可直接對檔案內容做修改,一般的 I/O 通常需要先將資料放進 buffer,mmap 可省略這一個步驟,提高存取速度,再來是可把檔案當成記憶體來使用,直接用指標來操作,其中第一個參數 NULL 表示由作業系統決定起始位址。 :::warning 注意用詞: file 是「檔案」,document 是「文件」 :notes: jserv ::: #### Load file into RAM ram 設定完成後,就可以 `read_file_into_ram()` 來將 Linux 核心映像檔案 (kernel image) 載入進模擬的 RAM 中: ```c 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](https://www.five-embeddev.com/riscv-isa-manual/latest/machine.html) :notes: jserv 根據 [All Aboard, Part 6: Booting a RISC-V Linux Kernel](https://www.sifive.com/blog/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](https://github.com/torvalds/linux/blob/master/arch/riscv/kernel/head.S#L292) 之以下組合語言程式碼: ``` ... 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 的方式查看: ```c 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** 來處理: ```c 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: ```c 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: ```c 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](https://github.com/antirez/kilo)。 需要解釋 [initramfs](http://xstarcd.github.io/wiki/Linux/ShengRuLiJie_linux_2.6_initramfs.html) 運作原理以及 Buildroot 如何產生 cpio 檔案。 ### Buildroot > refer to: > [RISC-V 和 Buildroot 介紹 by jserv](https://hackmd.io/@sysprog/ryHaBkrOE) > [2019 年核心實作專題: RISCV with TinyEMU](https://hackmd.io/@johnnylord/HkeMrEARE) > [buildRoot study - 建立自己的作業系統](http://fichugh.blogspot.com/2016/02/buildroot-study.html) 要讓一個系統從==開機到完全啟動==,至少需要以下三個部分: * **bootloader** * **kernel** * **root file system** :::warning 注意用詞: * dirctory 是「目錄」 * folder 是「檔案夾」 在 POSIX 相容系統 (包含 Linux),應該用「目錄」 :notes: 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 建構檔案系統](https://hackmd.io/@sysprog/Skuw3dJB3#%E5%88%A9%E7%94%A8-Buildroot-%E5%BB%BA%E6%A7%8B%E6%AA%94%E6%A1%88%E7%B3%BB%E7%B5%B1) 中複製過來的 \.config 檔案是我們要藉由 buildroot 工具去建構 kernel image + root file system 的設定檔,可透過 `$ make menuconfig` 去調整,下面是客製化 buildroot 的流程。 首先取得 buildroot 程式碼: ```shell $ git clone https://github.com/buildroot/buildroot.git --depth=1 ``` > `--depth=1` 表示只取得最新一筆 commit,在專案很大時可加快下載時間及節省空間。 根據 semu 提供的組態來建構 root file system: ```shell $ cd buildroot $ cp ../semu/configs/buildroot.config .config $ make $ cd .. ``` 安裝必要的開發套件: ```shell $ sudo apt install libncurses5-dev ``` 接著變更設定: ```shell $ make menuconfig ``` 預期會看到以下畫面: ![](https://hackmd.io/_uploads/H1FlJj182.png) 這個介面是用來選擇 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 下載: ```shell $ git clone https://github.com/antirez/kilo ``` 進行編譯: ```shell $ cd kilo $ make ``` 編譯完成後會出現 kilo 執行檔,使用 kilo 編輯檔案預期會出現以下畫面: ``` $ ./kilo kilo.c ``` ![](https://hackmd.io/_uploads/rkGq-ik8h.png) ### 將 Kilo 加入 Buildroot 中 參考 [Ztex 2019 q1 HW4](https://hackmd.io/@ztex/HyDMZyJtE?type=view) 和當中提到的 [The Buildroot user manual](https://buildroot.org/downloads/manual/manual.html#_getting_started),要對 Buildroot 進行改動前可以先參照上面介紹的 Buildroot 檔案結構,第 18 章 Adding new packages to Buildroot 介紹如何==加入客製化的 package== (i.e., 工具或函式庫) 到 Buildroot 中,步驟如下: 0. 首先找到要加入的 package 位置 1. 在 package 目錄下新增一個自訂的目錄 2. 新增 kilo/Config.in 檔案 3. 將 kilo/Config.in 更新到 package/Config.in 4. 在 kilo/ 下新增一個 makefile 稱為 kilo.mk 5. 以 make menuconfig 確認及選擇 kilo 6. 以 make 建置 buildroot 7. 重新產生 linux kernel image 8. 將 Image 複製到 semu/ 並執行 ==詳細流程==如下: 首先**找到要加入的 package 位置**: ```shell $ make menuconfig ``` 進入選單後 -> Target packages -> Text editors and viewers 確認現有的 package 如下: (預設為 nano 且沒有 kilo) ![](https://hackmd.io/_uploads/Sy1EPdM8h.png) 再來是在 package 目錄下**新增一個自訂的目錄**: (這裡直接以 kilo 為例) ```shell $ cd buildroot/package $ mkdir kilo ``` > 如果要自訂的 package 已經被分類 (e.g., x11r7、qt5 等),則需要在這些目錄下新增子目錄。 **新增 kilo/Config.in 檔案**,Config.in 中會包含 kilo 所需的選項和相關描述: ```shell $ cd kilo $ touch Config.in ``` Config.in 內容如下: ```shell 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**: ```shell $ cd .. # 移動到 package/ $ vim Config.in # 找到 menu "Text editors and viewers" # 加上 source "package/kilo/Config.in" # 注意加入的地方需要按照字母大小排列,也就是 kilo 需要加在 jxxx package 後 ``` 在 kilo/ 下新增**一個 makefile** 稱為 kilo.mk,這個檔案描述 package 需要如何下載、配置、建購及安裝等操作: ```shell $ 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](https://buildroot.org/downloads/manual/manual.html#generic-package-tutorial),這個方案的建置方法通常使用手寫的 makfile 或 shell script 而非 autotools 或 cmake。 此外,因 kilo 是從 github 下載,這裡需要依照[第 18.25.4 章 How to add a package from GitHub](https://buildroot.org/downloads/manual/manual.html#_tips_and_tricks) 和參考 [stackoverflow](https://stackoverflow.com/questions/8014991/how-do-i-add-a-a-package-to-buildroot-which-is-available-in-a-git-repository) 來對預設的 makefile 進行修改,其中包含 ```shell # 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 內容如下: ```shell ################################################################################ # # 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 縮排 :::warning 不知道為何使用最新一筆 commit ID 無法下載 repo。 參考 [Ztex 同學](https://hackmd.io/@ztex/HyDMZyJtE) 使用的 commit ID 可解決。 ::: 完成後回到 buildroot/ 下輸入 `make menuconfig` 來確認及選擇 kilo: ```shell $ cd buildroot $ make menuconfig ``` ![](https://hackmd.io/_uploads/BymFvuGLn.png) 可看到圖中出現 kilo 選項,選擇並保存後會更新到 buildroot/.config 中 更新完 buildroot/.config 後,以 make **重新建置 buildroot**: ```shell $ make -j4 ``` 建置完成後,預期在 buildroot/output/target/usr/bin/ 下看到 kilo。 ```shell $ cd buildroot/output/target/usr/bin $ ls ... kilo ... ``` buildroot 建置完成後,至 linux/ 目錄下**重新產生 Linux Kernel Image**: ```shell $ cd linux $ make -j4 Image ``` 建置完成後,將 linux/arch/riscv/boot/Image 複製到 semu/ 並執行: ```shell $ cd arch/riscv/boot $ cp -f Image ~/Documents/semu/Image $ cd ~/Documents/semu $ ./semu Image ``` 預期看到以下畫面: ![](https://hackmd.io/_uploads/Hy4cvOfLh.png) :::danger 文字訊息不要用圖片展現! :notes: jserv ::: #### ISSUE * kilo 無法進入編輯模式 ![](https://hackmd.io/_uploads/BJz5F_zIn.png) > 推測與下方 TODO 改進鍵盤輸入事件有關 ### initramfs 運作原理及如何產生 CPIO 檔案 > refer to: > [鳥哥私房菜 - 第十九章 Linux 的開機流程分析](https://linux.vbird.org/linux_basic/centos7/0510osloader.php#startup) > [深入理解 Linux 2.6 的 initramfs 機制](http://xstarcd.github.io/wiki/Linux/ShengRuLiJie_linux_2.6_initramfs.html) #### 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 來開始後續的開機流程。 ![](https://hackmd.io/_uploads/Syuxa49I2.png) 如上圖所示,boot loader 可以載入 kernel 與 initramfs,然後讓 initramfs 解壓縮成根目錄,kernel 就能藉此載入需要的驅動程式,最後釋放虛擬檔案系統,完成後 **kernel 會掛載真正的 root file system** 並執行 \/sbin\/init 程式。 #### 實作客製化 initramfs 新增一個目錄名為 `initramfs-workspace` ```shell $ mkdir initramfs-workspace $ cd initramfs-workspace ``` 將上面以 git clone 下載的 linux 檔案複製到 initramfs-space/ 下 ```shell $ cp -r ~/Downloads/linux ./ ``` 新增一個存放 kernel + initramfs 的目錄,建立 `init.c`,最後再透過 semu 進行模擬 ```shell $ 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` ```shell $ riscv32-buildroot-linux-gnu-gcc -static -o init init.c ``` > 這裡印出的 Hello semu !!! 就是 Early userspace 因執行時期需要 tty (terminal),所以需要在 hello-initramfs/ 下一併建立 /dev/console 的 character device ```shell $ mkdir -p dev $ sudo mknod dev/console c 5 1 ``` 接下來開始準備 kernel ```shell $ cd ../linux # 回到 initramfs-workspace/ $ make menuconfig ``` > 開啟 menuconfig 選擇 General setup -> 開啟 Initial RAM filesystem and RAM disk support -> 將下方的 Initramfs source file 路徑改成能找到 hello-initramfs 的路徑,保存後退出。 開始建構核心 ```shell $ make -j$(nproc) Image ``` 將核心複製到 semu 下並開始執行 ```shell $ cp arch/riscv/boot/Image ~/project/riscv/semu $ cd ~/project/riscv/semu $ ./semu Image ``` 預期看到 ```shell ... [ 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 函式中可看到 ```shell 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 ```shell $ cd ~/initramfs-workspace $ cp -af hello-initramfs hello2-initramfs ``` 進入 hello-initramfs/ 並修改 `init.c` 中的內容 ```c #include <stdio.h> int main() { printf("Yat Another Hello semu !!!\n"); return 0; } ``` 以 buildroot 工具鏈編譯 `init.c` ```shell $ riscv32-buildroot-linux-gnu-gcc -static -o init init.c ``` 新增一個描述檔 desc_initramfs ```shell 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 工具進行封裝 ```shell $ ../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 的路徑並編譯核心 ```shell $ cd ../linux $ make menuconfig # 操作同上,將讀取 cpio 位置改成 .../hello2-initramfs/my_initramfs.cpio $ make -j$(nproc) Image ``` 複製 image 至 semu/ 下並進行測試 ```shell $ cp arch/riscv/boot/Image ~/project/riscv/semu/ $ cd ~/project/riscv/semu $ ./semu Image ``` 預期看到 ```shell ... [ 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 ```shell $ 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 執行檔複製過來、擷取當中命令 ```shell $ 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 檔的路徑並進行建置 ```shell $ 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. > ``` :::warning :top: 利用 Buildroot 產生的完整 cpio,在其之上添加自訂程式,重新打包並編譯 Linux 核心以建立對應的映像檔案。熟悉 Buildroot 的關鍵在於掌握易於測試的環境,先熟悉工具再來排除上述問題。 :notes: jserv ::: ## TODO: 正確處理鍵盤輸入事件 現行 semu 所有的鍵盤輸入皆要在按下 Enter 後才發揮作用,這使得 shell 本身的 tab completion (見 Busybox 的 `FEATURE_EDITING`) 無法作用,按下 Enter 後,會輸出額外的 `'\n'` 字元,且在 vi 中無法正確的輸入命令字元 (即按下 Esc),這是因為目前沒有正確處理輸入緩衝區。 解法:另行維護 UART 專用的緩衝區,並確保輸入事件不影響模擬器的運作 (可能需要建立執行緒) 一旦鍵盤事件能夠正確處理,就可實作 Ctrl-a x (離開模擬器) 這樣的按鍵組合。 參見: * [mini-rv32ima](https://github.com/cnlohr/mini-rv32ima): 執行 `make testdlimage` 可模擬完整的 Linux 系統,鍵盤處理無誤 * [kvm-host](https://github.com/sysprog21/kvm-host/blob/master/src/serial.c) * [xv6 branch](https://github.com/jserv/semu/blob/xv6/semu.c) * [yarisc](https://github.com/fgssfgss/yariscv/blob/main/src/console.c) * [rvc](https://github.com/PiMaker/rvc/blob/master/src/uart.h) * [poll(2)](https://man7.org/linux/man-pages/man2/poll.2.html) 上述 [描述程式碼原理](https://hackmd.io/@sysprog/Skuw3dJB3#TODO-%E6%8F%8F%E8%BF%B0%E7%A8%8B%E5%BC%8F%E7%A2%BC%E5%8E%9F%E7%90%86) 有簡單介紹過 semu 中 uart 的運作模式,模擬器每隔 64 個單位時間就會去偵測 uart 是否需要處理,若 uart 未達需要處理的情況就進行 polling ```c 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 需要處理,就發出中斷 ```c 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](https://github.com/cnlohr/mini-rv32ima) 中的做法使用 [termios(3)](https://man7.org/linux/man-pages/man3/termios.3.html) 來解決,這個功能是用來控制終端介面非同步通訊的通訊埠,在 `tcgetattr` 取得該 fd 對應的設定後,取消標準輸入 (ICANON) 模式和 ECHO,完成後以 `tcsetattr` 做設定。 uart 處理鍵盤輸入的地方在 `u8250_handle_in()`,在當中加上以下程式碼即可使用 tab completion 且可使用 vi 進行編輯 ([commit e8f183f](https://github.com/JiggerChuang/semu/commit/e8f183f32b5360b93c5fa34aca22809420466b57)): ```c struct termios term; tcgetattr(0, &term); term.c_lflag &= ~(ICANON | ECHO); // Disable echo as well tcsetattr(0, TCSANOW, &term); ``` 以 `vi test.c` 進行測試,進入 shell 後以 vi 編輯檔案 ```shell # vi test.c Hello Semu (保存後退出) # cat te // 這裡按下 tab 可自動補齊 test.c # cat test.c Hello Semu ``` 在能正確處理 UART 的鍵盤輸入後,就能實現組合按鍵,實作使用組合按鍵離開 semu,而非以 Ctrl-c 結束整個程式如下 ([commit 90c34f9](https://github.com/JiggerChuang/semu/commit/90c34f9f1d5a70093270c1f7e2608dfbae09147c)): ```shell if (value == 1){ /* start of heading (Ctrl-a) */ if (getchar() == 120){ /* keyboard x */ printf("\n"); /* end emulator with newline */ exit(0); } } ``` :::warning 準備提交 pull request。參照 [Linux 核心專題: 系統虛擬機器開發和改進](https://hackmd.io/@sysprog/rkro_FeSh)的「TODO: 用 epoll 和 eventfd 改進 UART 實作」描述 :notes: jserv ::: #### TODO * 若出現大量的鍵盤輸入,可能導致模擬器花費大量的時間在處理 IO,可以使用多執行緒解決。 ## TODO: 改進網路處理 參照 [RVVM](https://github.com/LekKit/RVVM) 的實作,支援 Linux 核心的 TAP 和跨平台的 userspace TAP: * [src/devices/tap_api.h](https://github.com/LekKit/RVVM/blob/staging/src/devices/tap_api.h) * [src/devices/tap_linux.c](https://github.com/LekKit/RVVM/blob/staging/src/devices/tap_linux.c) * [src/devices/tap_user.c](https://github.com/LekKit/RVVM/blob/staging/src/devices/tap_user.c)