[Little OS Book 筆記] 手榨作業系統:The First Step

這一系列是我讀 The little book about OS development 的一些實作紀錄,此篇為介紹一些基礎概念,以及利用這些簡化的流程,實作一個非常簡單的奈米級作業系統。


我選擇的作業系統是 Ubuntu 22.04 LTS(之後改用 16.04,後續會說明原因)。

準備好後需要先下載一些 package:
sudo apt-get install build-essential nasm genisoimage bochs bochs-sdl

針對上方指令安裝的東西稍稍解釋。

nasm,NASM 全稱是 Netwide Assembler,是一款組譯與反組譯工具,算是 Linux 平台上最受歡迎的組譯工具之一。另外這本書主要使用 C 語言作為開發語言,為的是能更直接控制記憶體。

genisoimage,是 generate ISO image 的縮寫,如其名就是建立 ISO 映像檔案。

bochs,Bochs,發音同 box,開放原始碼的 x86、x86-64IBM PC 相容機模擬器以及除錯工具,主要用於作業系統開發,也可以用來執行不相容的舊軟體。

Booting 基本概念

Booting 又稱啟動程式,算是電腦中的一個初始化過程,當我們啟動一個作業系統,可以看作是一個一個小程式的串聯,而作業系統就是最後一個程式。所以簡單來看,可以將 boot 簡化為:

BIOS → GRUB1 → GRUB2 → OS

BIOS

BIOS(Basic Input Output System),也稱作基本輸入輸出系統,通常儲存在主機板上的唯讀晶片,主要作用是在啟動時載入的第一個軟體。人如其名,控制著電腦的基本輸出及輸入裝置,在開機時對各系統測試和初始化裝置,會對電腦的設備進行檢驗和測試,若有問題可能會發出提示音或在螢幕中提示。

我們的系統軟體大部分會放在硬碟,我們必須要有一個開機管理程式來處理核心載入的問題,這個程式就是 boot loader。

GRUB

GRUB 是 GRand Unified Bootloader 的縮寫,是 Linux 中最常見的 Bootloader。老實說我一直不理解 boot 和 loader 這兩個字到底要分開還是黏一起。

Bootloader

Loader 最主要的功能是認識作業系統的檔案格式,並且依此載入核心到主記憶體中去執行。在不同的作業系統中,必須使用自己的 loader 才能夠載入屬於自己的作業系統核心,這是因為每種作業系統的格式都不一樣,因此每個都有自己的 bootloader。

Bootloader 這個程式會放在 MBR,Master Boot Record,也就是主要開機紀錄區(開機裝置的第一個 sector 內)。使用 GRUB,可以將操作系統建置為 ELF 可執行檔,再來由 GRUB 將它載入至正確的記憶體位置,kernel 的編譯會使用特定的方式將程式碼發佈到記憶體中。

Operating System

再來就是我們的作業系統啦。GRUB 會尋找一個 magic number 以確保有正確跳轉到作業系統,之後作業系統就會完全接管電腦。


迷你的類作業系統

講述完基本概念後,接下來要嘗試用組合語言寫一個非常迷你的作業系統(有些人甚至覺得不叫作業系統)。這是我有生以來第一次寫組合語言,果然是另外一個世界XD

主要目的很簡單,要將數字 0xCAFEBABE 寫入 eax 暫存器中(最後可以看 log 驗證有沒有成功)。

loader

global loader

MAGIC_NUMBER equ 0x1BADB002
FLAGS        equ 0x0
CHECKSUM     equ -MAGIC_NUMBER

section .text:
align 4
    dd MAGIC_NUMBER
    dd FLAGS
    dd CHECKSUM

loader:
    mov eax, 0xCAFEBABE

loop:
    jmp .loop

畢竟是第一次寫,來個不負責任解說,說錯了可以在下方留言指正:

第一行的 global loader 有一點像是我們宣告

再來是 MAGIC_NUMBERFLAGS 以及 CHECKSUM。我才剛聽完有關網路的課程,說了各種封包的 header,對於這部分感到很熟悉XD MAGIC_NUMBER 會被用來偵測特定格式的資料。這次 Boot loader 不需要做任何事,也不需要要求任何東西,所以不需要設定,也就是 0x0。如果是非 ELF 另外的格式,就必須要加上 loading address。而 CHECKSUM 是校驗碼,一個 32-bit unsigned 的值,是 Multiboot header 的必填項目,會被用來測試資料的完整性。

MAGIC_NUMBERFLAGS 三者加起來必須為 0。

equ 大概就是 equal?總而言之可以指派常數,且不能改變。

section .bss 用於宣告變數、section .data 用於宣告初始化資料,另外 section .text 用於保存程式碼,以上三者算是組合語言的三個部分。

align 4 主要功用是 4 byte 對齊編譯,對齊的概念對於系統來說很重要,也能夠提升執行效率。

dd 是一個偽指令,定義操作數占用的字節數。結合上面的 align 4,就可以對齊好資料,寫進 section .text

loader 區塊中,可以看到我們這次程式的主要目的:將 0xCAFEBABE 寫到 eax 中。Loader 給 linker script 作為入口,接著執行程式。

接下來是 jmp,進入某段程式。.loop 則是會循環,不會結束。

再來是將 loader.s 編譯為 32 位元 elf 檔的指令:

nasm -f elf32 loader.s

指令下完後,同一個路徑中就會出現檔案 loaders.o


linker

另外我們要把寫好的 loader.s 放到剛才提到的 linker script 中。在此新增一個檔案叫做 link.ld

ENTRY(loader)

SECTIONS {
    . = 0x00100000;

    .text ALIGN (0x1000) :
    {
        *(.text)
    }

    .rodata ALIGN (0x1000) :
    {
        *(.rodata*)
    }

    .data ALIGN (0x1000) :
    {
        *(.data)
    }

    .bss ALIGN (0x1000) :
    {
        *(COMMON)
        *(.bss)
    }
}

Linker script 用於規定如何把輸入檔案內的 section 放到輸出檔案內,並且控制輸出檔案內記憶體位置。而 linker 是把一到多個輸入檔案合成一個輸出檔案。

ld -T link.ld -melf_i386 loader.o -o kernel.elf

操作後將產生 kernel.elf 檔案。


GRUB Config、產生映像檔

接下來建立一個 GRUB 的設定檔 menu.lst,像是 kernel 的位置等等。

default=0
timeout=0

title os
kernel /boot/kernel.elf

輸入以下指令:
mkdir -p iso/boot/grub
cp stage2_eltorito iso/boot/grub
cp kernel.elf iso/boot/
cp menu.lst iso/boot/grub

如果用圖形介面搞不好自己移一移比較快。第一列指令是建立資料夾、第二、三、四個指令是將 stage2_eltoritokernel.elfmenu.lst 三個檔案分別放進指定路徑中。

最後回到 iso 資料夾的上一層,並且輸入以下指令:

genisoimage -R                              \
            -b boot/grub/stage2_eltorito    \
            -no-emul-boot                   \
            -boot-load-size 4               \
            -A os                           \
            -input-charset utf8             \
            -quiet                          \
            -boot-info-table                \
            -o os.iso                       \
            iso

這麼長的指令主要是用來生成 os.iso 檔,也就是所謂的映像檔。


Bochs Config

在使用 Bochs 之前,我們也要先設定關於 Bochs 的設定檔:

megs:            32
display_library: sdl
romimage:        file=/usr/share/bochs/BIOS-bochs-latest
vgaromimage:     file=/usr/share/bochs/VGABIOS-lgpl-latest
ata0-master:     type=cdrom, path=os.iso, status=inserted
boot:            cdrom
log:             bochslog.txt
clock:           sync=realtime, time0=local
cpu:             count=1, ips=1000000

最後利用 Bochs 執行映像檔 os.iso

bochs -f bochsrc.txt -q

若出現以下錯誤訊息:

Bochs is exiting with the following message:
[ ] bochsrc.txt:2: display library 'sdl' not available

可以試著將 bochsrc.txt 設定檔中的 display_library 後方內容改成 sdl2 或是 x。不太確定是不是因為作業系統版本所造成的差異,我在 Ubuntu 16.04 沒有遇到,一切順利,但是在 22.04 出現錯誤,最後甚至無法順利執行,之前在 GitHub 上也有不少人發 Issue 並提到版本問題。

另外有一個問題是 Bochs 執行時,出現視窗顯示 "bochs-bin" is not responding

目前這題…… 我這邊無解,因為剛好我手邊有版本較舊的 Ubuntu,又是實體機器,所以虛擬機的 22.04 版遇到的問題就乾脆先放著不理會。


若一切順利,或是錯誤皆避開後,可以接續以下步驟。我們這部分主要目的是將數字 0xCAFEBABE 寫入 eax 暫存器中,所以接下來啟動後應該可以在紀錄檔中驗證這件事情。

首先應該會看到 Bochs 視窗被開啟但是沒有反應,我們必須回到終端機輸入 c 並按 Enter 後,讓 Bochs 繼續執行。沒錯,c 代表的是 continue,所以要搞威一點輸入 continue 也行。

應該可以看到 Bochs 有開始動作的樣子,看到之後可以按右上角的電源鍵將視窗關閉。

接著我們查看剛產生的紀錄檔 bochslog.txt,或是在命令列輸入:

cat bochslog.txt | grep EAX

搜尋 EAX 是否已改成 0xCAFEBABE

若看到表示成功執行!也就完成了一個非常微小的作業系統了。


參考資料

讓我知道你在想什麼!