- x86 開機時,會先呼叫位於主機板上的 BIOS。
- BIOS 的工作:準備硬體,將控制權轉給 OS (XV6)。
- 準確的說,控制權轉給 boot sector,位於開機碟的第一個磁碟扇區(512 byte)。
- Boot sector 包含 boot loader—負責將 kernel 載入記憶體。
- BIOS 將 boot sector 寫入 0x7c00 的位置,並跳至該位址(透過設定暫存器 %ip)。
- XV6 boot loader 包含兩個檔案:bootasm.s、bootmain.c。
Code: Assembly bootstrap#
File: bootasm.s#
1 | #include "asm.h" |
- 第一行指令:
cli
,禁止處理器中斷。 - 硬體可以透過中斷觸發中斷處理,進而操作系統的功能。BIOS 為了初始化硬體,可能設置了自己的中斷處理。但控制權已經給 boot loader 了,所以現在處理中斷是不安全的;當 XV6 準備完成後會重新允許中斷。
9 | 16-bit mode # Assemble for |
- 處理器在模擬 Intel 8088 的 real mode 狀態下,有 8 個 16 位元的通用暫存器,但處理器傳送的是20位元的地址給記憶體;因此多出來的四個位元由段暫存器(
%cs
,%ds
,%es
,%ss
)提供。 %cs
取指令用%ds
讀寫資料用%ss
讀寫堆疊用- BIOS 完成工作後
%ds
,%es
,%ss
是未知的,所以將其設為 0
13 | # DS, ES, and SS. data registers |
- XV6 假設 x86 的指令是使用虛擬地址,但實際上使用的是邏輯地址。
- 一個邏輯地址包含一個段選擇器及一個差值,有時表示為 segment:offset。
- 更多時候,段是固定的,所以程式只會使用差值。
- 分段硬體負責將邏輯地址翻譯成線性地址。
- 如果分頁硬體是啟用的,它會把線性地址轉成物理地址;若未啟用,處理器會把線性地址當作物理地址。
- 一個 segment:offset 可能產生 21-bit 的物理地址,但在模擬 Intel 8088 下只能使用 20 bits 的記憶體位置,IBM 提出了一個方法:如果鍵盤控制器輸出端的第二位高於第一位,則第 21 個 bit 可以正常使用,反之則歸零。
- boot loader 用 I/O 指令控制鍵盤控制器端 0x64、0x60 來確保第 21 個 bit 正常運作。
18 | # Physical address line A20 is tied to so that the first PCs |
- 由於 real mode 只有 16-bit 的暫存器,導致一個程式如果要使用超過 65536 bytes 的記憶體會很困難,也不可能使用超過 1MB 的記憶體。
- x86 從 80286 開始有 protected mode,允許物理位置能擁有更多 bits,從 80386 後有 32-bit 模式。
- Boot loader 接著開啟 protected mode 和 32-bit 模式。
- 在 protected mode 下的段暫存器保存著段描述符表的索引。
- Limits 代表最大的虛擬地址
- 段描述符表包含一個權限(被 protected mode 保護),kernel 可以使用這個權限確保一個程式只會使用自己的記憶體。
XV6 幾乎不用段,取而代之的是分頁。
- Boot loader 設定段描述符表 gdt,每一段的基址為 0,且 limit 為 4GB (2^32)。
- Flag 使程式碼會在 32-bit 中執行。
- 由上述設定能確保當 boot loader 進入 protected mode 時,邏輯地址映射到物理地址會是 1-1 的。
lgdt
指令將 GDT 暫存器(指向 gdt) 載入 gdtdesc 的值。
GDT 補充
在創建 GDT 的時候,第一項須為空(規定),接著我們為此臨時的 GDT 設立 code 及 data 段。
# Line 78 in bootasm.S |
35 | # Switch from real to protected mode. Use a bootstrap GDT that makes |
- Boot loader 將
%cr0
中的CRO_PE
設為 1,來啟用 protected mode。 - 啟用 protceted mode 不會立即改變處理器轉譯邏輯地址的過程;只有當段暫存器載入了新的值,處理器讀取 GDT 改變其內部的斷設定。
39 | movl %cr0, %eax |
ljmp
指令語法:ljmp segment offset
,此時段暫存器為SEG_KCODE<<3
,即 8 (SEG_KCODE == 1
,定義於mmu.h
)ljmp
指令跳至 start32 執行。
42 | //PAGEBREAK! |
- 進入 32 位元後的第一個動作:用
SEG_KDATA
初始化數據段暫存器
47 | 32-bit code now. # Tell assembler to generate |
- 最後建立一個 stack,跳至 bootmain.c。
- 記憶體 0xa0000 至 0x100000 為設備區,XV6 kernel 放在 0x100000。
- Boot loader 位於 0x7c00 至 0x7e00 (512 bytes),所以其他位置都能拿來建立堆疊;這裡選擇 0x7c00 當作 top (
$start
),堆疊向下延伸,直到 0x0000。
57 | # Set and call into C. the stack pointer |
- 如果出錯了,會向 0x8a00 端輸出一些字。
- 實際上沒有設備連接到 0x8a00。
- 如果使用模擬器,boot loader 會把控制權還給模擬器。
真正的 boot loader 會印出一些錯誤訊息。
60 | # If bootmain returns (it shouldnt), trigger a Bochs |
- 接著進入無限迴圈
67 | spin: |
Code: C bootstrap#
File: bootmain.c#
- bootmain 的工作:載入並執行 kernel。
- Kernel 為 ELF 格式的二進位檔。
ELF(Executable and Linking Format) ,為 UNIX 中的目錄檔格式。
功能 | 回傳值 |
---|---|
開機程序(執行 kernel) | void |
1 | // Boot loader. |
- 為了存取 ELF 開頭,bootmain 載入 ELF 文件的前 4096 bytes,並拷貝到 010000 中。
24 | elf = (struct elfhdr*)0x10000; // scratch space |
- 接著確認是否為 ELF 文件。
正常情況下 bootmain 不會return
,這裡return
會跳回 bootasm.S 中,由 bootasm.S 來處理此錯誤。
28 | // Is this an ELF executable? |
- bootmain 從 ELF 開頭之後的 off bytes 讀取內容,並存入 paddr 中(呼叫
readseg
)。 - 呼叫
stosb
將段的剩餘部分設 0
31 | // Load each program segment (ignores ph flags). |
- Boot loader 最後一項工作:呼叫 kernel 的進入指令,即 kernel 第一條執行指令的地址(0x10000c)。
- entry.S 中定義的
_start
即為 ELF 入口。 - XV6 虛擬記憶體尚未建立,因此 entry 為物理地址。
40 | // Call the entry point from the ELF header. |
函數指標的補充[1]:
上述用一個 void (*entry)(void)
指標即為一個函數指標,此指標指向一個函數,於上述 42 行將此指標指向 elf->entry
,此動作將 entry
指標指向一個函數的進入點位置(elf->entry
)。
此時呼叫 entry()
會進入此指標位置,並當作一個副函式執行;因此執行完上述程式碼會進入 entry.S,並執行其中的程式碼。
功能 | 回傳值 |
---|---|
等待磁碟 | void |
45 | void |
功能 | 回傳值 |
---|---|
讀取一個磁碟區 | void |
*dst |
offset |
---|---|
目標磁碟 | 目標磁碟區 |
52 | // Read a single sector at offset into dst. |
功能 | 回傳值 |
---|---|
讀取一段磁碟資料 | void |
*pa |
`count | offset |
---|---|---|
目標位址 | 數量 | 目標磁碟區 |
69 | // Read 'count' bytes at 'offset' from kernel into physical address 'pa'. |
Reference