特徵:
取得 Buildroot 程式碼:
依據 semu 提供的組態來建構 root file system:
可將上方的
make
改為make -j8
,依據有效的處理器數量變更
取得 Linux 核心程式碼:
利用 Buildroot 建構的 GNU Toolchain 來編譯 Linux 核心,先設定環境變數:
編譯 Linux 核心:
可將上方的
make
改為make -j8
,依據有效的處理器數量變更
預期會得到 linux/arch/riscv/boot/Image
檔案,接著執行:
即可執行 Linux 核心。
引述 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 封包,參見下圖:
開啟二個終端機,其中 Tsemu 表示執行 semu 的終端機,Thost 表示執行本機網路設定的終端機。
在 Tsemu 執行以下命令:
預期應該要在第一行見到 allocated TAP interface: tap0
的字樣,接著切到 Thost,執行以下命令:
再切換到 Tsemu,等待以下訊息的出現:
輸入 root
之後會出現提示符號 #
,接著執行以下命令:
預期會見到以下輸出:
切換到 Thost 來檢驗:
預期會見到以下輸出:
至此,具備基本的網路設定。
在 Thost 執行以下命令:
預期會有以下輸出:
讓 tcpdump
保持執行,接著切換到 Tsemu ,執行以下命令:
切換到 Thost ,觀察封包捕捉狀況,參考輸出:
第一欄為時間戳記
研讀〈Writing a simple RISC-V emulator in plain C〉,對照 semu 原始程式碼,解釋系統模擬器之行為和原理。
一開始從命令列讀取命令與參數
接著將參數 (i.e., 執行檔名) 傳給 semu_start()
進行讀檔與設定:
當中 emu_state_t
struct 對應到文章中的 CPU struct,包含 CPU 周邊 (e.g., ram, plic, uart 等),vm_t
struct 對應到 datapath 內部結構與運作機制,包含 32 個暫存器、pc、memory fectch 等。
mmap(2) 用來設定模擬的 RAM,也就是將要讀寫的檔案內容 (i.e., Image) 映射到一段 (虛擬) 記憶體上,通過對這段記憶體的讀寫,可直接對檔案內容做修改,一般的 I/O 通常需要先將資料放進 buffer,mmap 可省略這一個步驟,提高存取速度,再來是可把檔案當成記憶體來使用,直接用指標來操作,其中第一個參數 NULL 表示由作業系統決定起始位址。
注意用詞: file 是「檔案」,document 是「文件」
ram 設定完成後,就可以 read_file_into_ram()
來將 Linux 核心映像檔案 (kernel image) 載入進模擬的 RAM 中:
TODO: 這裡可將讀檔改成 memory mapping 的方式
接著以同樣的方式載入 device tree 檔案。
TODO: 研究 device tree 並透過 virtio_blk 方式載入 disk image
設定 hart 屬性。
ISSUE: 不知道暫存器內 RV_R_A0 與 RV_R_A1 為何要這樣設定
參照 Machine-Level ISAjservImage Not Showing Possible ReasonsLearn More →
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
根據 All Aboard, Part 6: Booting a RISC-V Linux Kernel:
以及 linux/arch/riscv/kernel
/head.S 之以下組合語言程式碼:
可知 A0 和 A1 在載入 Kernel 前是用做傳遞 CPU ID 以及 DTB 位址的用途。
設定 uart 的 I/O handling 與基於 virtio 的網路存取。
TODO: 研究 virtio 與如何嵌入進這個專案中
開始模擬,首先檢查 uart 是否有請求,若有則發出中斷進行處理,若沒有就以 polling 的方式查看:
ISSUE: 不知道 peripheral_update_ctr 代表與設定成 64 的意義
推測是每經過 64 個單位時間後去 polling uart 是否需要處理。
其中內部的中斷是藉由 PLIC 來處理:
TODO: 研究 PLIC 與如何嵌入進這個專案中
如果模擬器執行期間發生 time-out 則將 sip bit 設成 1,否則設成 0:
最後就可以開始針對每個指令執行對應的操作,i.e., fetch -> decode -> execute:
TODO: 研究 SBI
運用 Buildroot 自行編譯 Linux 核心和 root file system,需要客製化 Buildroot,加入自訂的套件,如 kilo。
需要解釋 initramfs 運作原理以及 Buildroot 如何產生 cpio 檔案。
refer to:
RISC-V 和 Buildroot 介紹 by jserv
2019 年核心實作專題: RISCV with TinyEMU
buildRoot study - 建立自己的作業系統
要讓一個系統從開機到完全啟動,至少需要以下三個部分:
注意用詞:
在 POSIX 相容系統 (包含 Linux),應該用「目錄」
Buildroot 專案就能用來建構出上面三個部分,其中:
上述目錄中都至少包含以下兩個檔案:
上面章節 利用 Buildroot 建構檔案系統 中複製過來的 .config 檔案是我們要藉由 buildroot 工具去建構 kernel image + root file system 的設定檔,可透過 $ make menuconfig
去調整,下面是客製化 buildroot 的流程。
首先取得 buildroot 程式碼:
--depth=1
表示只取得最新一筆 commit,在專案很大時可加快下載時間及節省空間。
根據 semu 提供的組態來建構 root file system:
安裝必要的開發套件:
接著變更設定:
預期會看到以下畫面:
這個介面是用來選擇 build 時的特色 (features) 和參數 (parameters),其中:
上面介面中:
Busybox 是個工程程式集合,在單一的可執行檔中提供精簡的 UNIX 工具 (e.g., ls 命令),可執行於多款 POSIX 環境的作業系統 (e.g., Linux)。
cpio 是 UNIX 作業系統的檔案格式,可以從 cpio 或 tar 格式的歸檔包中存入和讀取檔案。其中歸檔包是一種包含其他檔案和有關資訊的檔案 (e.g., 檔名、存取權限等)
是個精簡的程式碼編輯器,支援語法高亮度提示和常見的編輯功能。
首先從 GitHub 下載:
進行編譯:
編譯完成後會出現 kilo 執行檔,使用 kilo 編輯檔案預期會出現以下畫面:
參考 Ztex 2019 q1 HW4 和當中提到的 The Buildroot user manual,要對 Buildroot 進行改動前可以先參照上面介紹的 Buildroot 檔案結構,第 18 章 Adding new packages to Buildroot 介紹如何加入客製化的 package (i.e., 工具或函式庫) 到 Buildroot 中,步驟如下:
詳細流程如下:
首先找到要加入的 package 位置:
進入選單後 -> Target packages -> Text editors and viewers 確認現有的 package 如下: (預設為 nano 且沒有 kilo)
再來是在 package 目錄下新增一個自訂的目錄: (這裡直接以 kilo 為例)
如果要自訂的 package 已經被分類 (e.g., x11r7、qt5 等),則需要在這些目錄下新增子目錄。
新增 kilo/Config.in 檔案,Config.in 中會包含 kilo 所需的選項和相關描述:
Config.in 內容如下:
其中 bool 是 menu 圖形化介面中會出現的字樣,help 是輸入 shift+? 會出現的提示字。
bool、help 和其他 metadata 資訊前需要以 tab 進行縮排,help 中的文字前需要再以兩個 space 進行縮排,單列不能超過 72 個字元,扣掉 tab 剩下 62 個字元,且最後需要空一行並加上專案的來源 (詳細格式請參照第 16 章 Coding style)。
將 kilo/Config.in 更新到 package/Config.in:
在 kilo/ 下新增一個 makefile 稱為 kilo.mk,這個檔案描述 package 需要如何下載、配置、建購及安裝等操作:
不同型態的 makefile 會對應到不同的寫法以及使用不同的基礎建設,可分為:
因 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 進行修改,其中包含
FOO_VERSION 可以是 tag 或完整的 commit ID
修改後的 kilo.mk 內容如下:
define 內的敘述必須以兩個 tab 縮排
不知道為何使用最新一筆 commit ID 無法下載 repo。
參考 Ztex 同學 使用的 commit ID 可解決。
完成後回到 buildroot/ 下輸入 make menuconfig
來確認及選擇 kilo:
可看到圖中出現 kilo 選項,選擇並保存後會更新到 buildroot/.config 中
更新完 buildroot/.config 後,以 make 重新建置 buildroot:
建置完成後,預期在 buildroot/output/target/usr/bin/ 下看到 kilo。
buildroot 建置完成後,至 linux/ 目錄下重新產生 Linux Kernel Image:
建置完成後,將 linux/arch/riscv/boot/Image 複製到 semu/ 並執行:
預期看到以下畫面:
文字訊息不要用圖片展現!
推測與下方 TODO 改進鍵盤輸入事件有關
refer to:
鳥哥私房菜 - 第十九章 Linux 的開機流程分析
深入理解 Linux 2.6 的 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-workspace
將上面以 git clone 下載的 linux 檔案複製到 initramfs-space/ 下
新增一個存放 kernel + initramfs 的目錄,建立 init.c
,最後再透過 semu 進行模擬
以 buildroot 中的 toolchain 編譯 init.c
這裡印出的 Hello semu !!! 就是 Early userspace
因執行時期需要 tty (terminal),所以需要在 hello-initramfs/ 下一併建立 /dev/console 的 character device
接下來開始準備 kernel
開啟 menuconfig 選擇 General setup -> 開啟 Initial RAM filesystem and RAM disk support -> 將下方的 Initramfs source file 路徑改成能找到 hello-initramfs 的路徑,保存後退出。
開始建構核心
將核心複製到 semu 下並開始執行
預期看到
可以看到在執行 init 時出現 Hello semu !!!,但因為沒有掛載其他檔案,所以無法進入 shell
在 linux/usr 下可找到 gen_init_cpio.c
,在當中的 Usage 函式中可看到
上文中的 archive
就是透過 cpio 工具產生的封裝檔案,Linux kernel 提供一個整合性工具,可一次處理目錄與檔案的封裝,封裝過後的檔案 (cpio + gzip) 即是一個完整的 initramfs image。
回到 initramfs-workspace/ 目錄下,複製 hello-initramfs
進入 hello-initramfs/ 並修改 init.c
中的內容
以 buildroot 工具鏈編譯 init.c
新增一個描述檔 desc_initramfs
以 linux 中的 cpio 工具進行封裝
usr/gen_init_cpio 工具會建構對應的 dir + device node + file 的封裝,最後以 gzip 壓縮起來,於是可得到 my_initramfs.cpio 這個新的 initramfs image
進入 initramfs-workspace/linux 修改讀取 cpio 的路徑並編譯核心
複製 image 至 semu/ 下並進行測試
預期看到
一樣因為沒有掛載其他檔案,所以無法進入 shell
上述做法都只有印出訊息,下面以整合 busybox 進行測試,首先安裝 busybox
回到 initramfs-workspace/ 目錄下,新增兩個 busybox 子目錄,並將 busybox 執行檔複製過來、擷取當中命令
進入 linux/ 下修改讀取 cpio 檔的路徑並進行建置
ISSUE:
- /init 無法執行
- /bin/sh 無法執行
- Kernel pacin - not syncing: No working init found
錯誤訊息如下:
現行 semu 所有的鍵盤輸入皆要在按下 Enter 後才發揮作用,這使得 shell 本身的 tab completion (見 Busybox 的 FEATURE_EDITING
) 無法作用,按下 Enter 後,會輸出額外的 '\n'
字元,且在 vi 中無法正確的輸入命令字元 (即按下 Esc),這是因為目前沒有正確處理輸入緩衝區。
解法:另行維護 UART 專用的緩衝區,並確保輸入事件不影響模擬器的運作 (可能需要建立執行緒)
一旦鍵盤事件能夠正確處理,就可實作 Ctrl-a x (離開模擬器) 這樣的按鍵組合。
參見:
make testdlimage
可模擬完整的 Linux 系統,鍵盤處理無誤上述 描述程式碼原理 有簡單介紹過 semu 中 uart 的運作模式,模擬器每隔 64 個單位時間就會去偵測 uart 是否需要處理,若 uart 未達需要處理的情況就進行 polling
而若此時 uart 需要處理,就發出中斷
參照「背景知識 - 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):
以 vi test.c
進行測試,進入 shell 後以 vi 編輯檔案
在能正確處理 UART 的鍵盤輸入後,就能實現組合按鍵,實作使用組合按鍵離開 semu,而非以 Ctrl-c 結束整個程式如下 (commit 90c34f9):
準備提交 pull request。參照 Linux 核心專題: 系統虛擬機器開發和改進的「TODO: 用 epoll 和 eventfd 改進 UART 實作」描述
參照 RVVM 的實作,支援 Linux 核心的 TAP 和跨平台的 userspace TAP: