接上一篇 BIOS 啓動到實模式,這篇開始 loader
的編寫。首先回顧一下那張磁盤鏡像和內存分佈圖:html
目前只須要關注 1MB 一下的內存分佈,主要是黃色 mbr
和藍色 loader
部分。上一篇中已經將 mbr
加載到內存,而且程序流經過 mbr 最後一條指令 jmp LOADER_BASE_ADDR (0x8000)
已經執行到了 loader
的入口處,接下來就須要將 loader 實現。git
總的來講, loader 的工做主要有如下幾項:web
GDT(Global Descriptor Table)
,初始化內核代碼和數據段寄存器(segment registers
),帶領 CPU 進入保護模式(protection mode
);page directory
)和頁表(page tables
),打開虛擬內存(virtual memory
),進入 paging
模式;kernel
鏡像到內存,而後進入到 kernel 代碼執行,至此係統的控制權轉交到了 kernel ;能夠看到 loader 的工做是比較多的,而且已經涉及到了x86 體系架構中的一些核心部分,所以爲了讀懂並實現 loader,你必須作好如下的知識準備:shell
elf
文件格式,由於 kernel 會被編譯連接成該格式的文件;仍然和以前同樣,先給出個人項目代碼連接 src/boot/loader.S,供你參考。segmentfault
這個源代碼已經比較多了,尤爲是它仍是彙編寫成的,並且代碼裏還包含了不少工具函數和打印相關的函數。爲了不陷入混亂,這裏抽取出幾個最重要的關鍵節點(函數),分別表明了上面所述的 loader
須要作的幾項工做:多線程
# 入口 loader_start # 初始化 GDT 並進入保護模式 setup_protection_mode protection_mode_entry # 初始化 kernel 頁目錄和頁表 setup_page # 加載並進入 kernel init_kernel
接下來咱們一個一個實現這些功能。本篇咱們首先初始化 GDT,進入 32-bit 保護模式
。架構
在開始以前,咱們首先看 loader 的開始部分的代碼,和 mbr 同樣,這裏仍然首先定義了 loader 編碼的起始內存地址,爲 0x8000
,這是由於咱們預先設計好了,mbr 會將 loader 從磁盤上加載到內存 0x8000 位置處並跳轉過去,因此 loader 的編址必須從該地址開始。ide
; LOADER_BASE_ADDR = 0x8000 SECTION loader vstart=LOADER_BASE_ADDR
接下來正式進入 loader 的第一條代碼 jmp loader_start
,它是一個簡單的跳轉,咱們跳到了 loader_start
開始真正執行 loader 的工做:函數
loader_entry: jmp loader_start ; 全局數據 ; ... loader_start: call clear_screen call setup_protection_mode
若是你對這種彙編編碼的方式不熟悉,可能會以爲奇怪,爲何要 jmp
一下,中間跳過的部分是什麼?答案是,中間是咱們要定義的數據部分,相似於 .c
文件裏定義的全局變量。那裏定義了一堆用來打印的字符串,以及相當重要的 GDT
。工具
你可能已經意識到了,彙編源代碼裏的指令和數據部分是能夠自由混雜排布的,並且最終編譯出來的二進制裏它們排布順序徹底遵循源代碼的排布。因此你能夠任意安排你的指令和數據所處的位置,只要指令流能順利地流轉和執行下去,不至於跑飛就行。固然,整個 loader
的起始位置,即 0x8000
處必須是入口代碼,由於這是和 mbr
約定好的跳轉地址。至於後面所有能夠自由發揮和排布。
來到上面說的全局數據的定義部分,你能夠跳過我加入的一些打印字符串信息,直接來到 GDT 的定義處。這裏定義了 4 個 GDT entry
,每一個 entry 佔了 8 個字節即 64 bits。關於 GDT 的含義和字段格式,能夠參考這裏,也能夠參考我以前推薦的 JamesM's kernel development tutorials 。這些都是 x86 體系架構的歷史包袱,我不想浪費筆墨再解釋一遍,可是咱們的代碼必須實現並聽從它的法則。
GDT 第一個 entry 是保留項不作使用;第四個爲顯示器 video
內存段描述符,這個其實並非必須的,你能夠無視它;因此咱們只須要關注第二和第三項便可,它們是:
kernel code
)描述符;kernel data
)描述符;咱們用 dd
僞指令定義這兩個段描述符(segment descriptor
):
CODE_DESC: dd DESC_CODE_LOW_32 dd DESC_CODE_HIGH_32 DATA_DESC: dd DESC_DATA_LOW_32 dd DESC_DATA_HIGH_32
DESC_CODE_LOW_32
, DESC_CODE_HIGH_32
,DESC_DATA_LOW_32
,DESC_DATA_HIGH_32
都定義在了 src/boot/boot.inc 中,你能夠對照上面給出的手冊文檔驗證每個 bit。仍是那句話,這是一個枯燥、麻煩、細緻可是繞不開的工做,沒有什麼難點,須要的是讀文檔手冊的耐心。
爲了照顧對彙編還不是很熟悉的同窗,有必要將 dd
僞指令的做用解釋一遍。dd
的意思是 define double (4-bytes)
,與之相似的還有 db (byte)
,dw (word, 2-bytes)
,它們出如今彙編源代碼裏,就是指在編譯後的二進制裏,在該位置上寫入後面所定義的數據內容。由此你能夠再次體會一下彙編與編譯後的二進制的關係,這幾乎就是一種刻板的翻譯而已。
設置完 GDT 後,咱們就能夠進入保護模式:
; enable A20 in al, 0x92 or al, 0000_0010b out 0x92, al ; load GDT lgdt [gdt_ptr] ; open protection mode - set cr0 bit 0 mov eax, cr0 or eax, 0x00000001 mov cr0, eax ; refresh pipeline jmp dword SELECTOR_CODE:protection_mode_entry
注意這裏使用了 lgdt
指令加載 GDT
,而且打開了 cr0
寄存器的保護模式的 bit 位,正式進入保護模式。後面經過一個 far jump
,將 cs
段寄存器初始化爲 kernel code
段。注意 cs
寄存器的值不能直接經過 mov
指令設置,而是必須經過跳轉語句隱式地被設置。
跳轉後,接下來程序來到 protection_mode_entry
的執行,這裏初始化了幾個 kernel data
段寄存器:
protection_mode_entry: ; set data segments mov ax, SELECTOR_DATA mov ds, ax mov es, ax mov ss, ax ; set video segment mov ax, SELECTOR_VIDEO mov gs, ax
到此保護模式的初始化工做算是完成,而後就來到了 loader 的重點部分 setup_page
函數,開始創建 kernel 的虛擬內存,留待下一篇。