從零開始寫 OS 內核 - GDT 與保護模式

系列目錄

從 mbr 到 loader

接上一篇 BIOS 啓動到實模式,這篇開始 loader 的編寫。首先回顧一下那張磁盤鏡像和內存分佈圖:html

目前只須要關注 1MB 一下的內存分佈,主要是黃色 mbr 和藍色 loader 部分。上一篇中已經將 mbr 加載到內存,而且程序流經過 mbr 最後一條指令 jmp LOADER_BASE_ADDR (0x8000) 已經執行到了 loader 的入口處,接下來就須要將 loader 實現。git

loader 的工做

總的來講, loader 的工做主要有如下幾項:web

  • 創建 GDT(Global Descriptor Table),初始化內核代碼和數據段寄存器(segment registers),帶領 CPU 進入保護模式(protection mode);
  • 創建 kernel 頁目錄(page directory)和頁表(page tables),打開虛擬內存(virtual memory),進入 paging 模式;
  • 加載 kernel 鏡像到內存,而後進入到 kernel 代碼執行,至此係統的控制權轉交到了 kernel ;

能夠看到 loader 的工做是比較多的,而且已經涉及到了x86 體系架構中的一些核心部分,所以爲了讀懂並實現 loader,你必須作好如下的知識準備:shell

  • GDT,段內存尋址,段寄存器,保護模式;
  • 虛擬內存,頁目錄,頁表;
  • elf 文件格式,由於 kernel 會被編譯連接成該格式的文件;

loader 實現

仍然和以前同樣,先給出個人項目代碼連接 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

在開始以前,咱們首先看 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 表

來到上面說的全局數據的定義部分,你能夠跳過我加入的一些打印字符串信息,直接來到 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_32DESC_CODE_HIGH_32DESC_DATA_LOW_32DESC_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 的虛擬內存,留待下一篇。

相關文章
相關標籤/搜索