OS架構整理
- 引導啟動部分
- bios bootloader區別
- 啟動流程(x86 BIOS 啟動):
- bios
- boot_loader
- 3.切換進保護模式
- 實模式的限制
- 如何切換進保護模式
- 加載kernel到內存地址1M
- 加載內核映像文件
- elf
- 一些基礎知識
- 鏈接腳本與代碼數據段
- 創建GDT表
- 段頁式內存管理
- 顯示字符串
- bios中斷向量表中來顯示
- 內聯匯編來顯示
引導啟動部分
bios bootloader區別
[BIOS] (硬件開機,里面有寫好的固件,上電自檢)↓ 讀取硬盤第0扇區 → 加載到內存地址 0x7C00
[boot] (512字節以內的 MBR/bootloader階段1)(因為只有512字節,太小了所以用了二級引導)↓ 加載并跳轉到更復雜的 loader(通常是 2階)
[loader] (多段程序,支持文件系統、內核加載等)↓ 加載操作系統內核(kernel)
[OS內核] (操作系統正式啟動)
前三步是我們自己控制不了的
我的代碼是從磁盤加載
啟動流程(x86 BIOS 啟動):
這個第0扇區的代碼需要自己去寫
首先Bios上電自檢,然后將磁盤的第一個扇區512字節放入到內存地址0x7c00,檢查是否以0x55 0xaa結束,從而判斷這是否是有效的MBR
,如果是的話就進行引導
bios
- 加電啟動(Power On)
- BIOS 執行 POST(Power-On Self-Test)
- 搜索可啟動設備(硬盤、U盤、光盤…)
- 找到啟動設備后:
- 讀取該設備的 第 1 個扇區(LBA 0,大小 = 512 字節)
- 把它加載到內存地址
0x0000:0x7C00
(實地址 = 0x7C00)
- 檢查最后兩個字節是否為
0x55AA
(有效的引導扇區簽名) - 如果正確 → 跳轉到
0x7C00
執行
在 BIOS 啟動模式中,BIOS 會將硬盤的 第 0 扇區(MBR)加載到 0x7C00,并從那里開始執行。
讀取磁盤又兩種方式
一種是int13(bios)
一種是LAB(復雜一點,在loader中實現)
x86在上電后自動進入實模式,1m內存 無分頁機制 寄存器也只能用16位
但是1m內存的話要訪問完需要20位地址,我們就是將段基址<<4+偏移構成20位
段基址的值是存在CS/DS/SS/ES/FS/GS(段寄存器)中
這個實模式下1M大小的內存映射情況:
我的boot是在start.s中寫的
boot的初始化: 主要就是將段寄存器先賦初值0,簡化代碼,棧頂指針賦值0x7c00,表示我的boot在0x7c00地址以下的棧區,大概30kb左右是滿足這個大小的
boot跳轉到loader二級引導:
讀取多個磁盤加載到內存地址上:用了bios中斷向量表 0x13 從第一個扇區開始 分配64個扇區(大概32kb) 如果讀取磁盤正確后進入C環境并跳轉到loader(jmp或者call)
.extern C函數名字(boot的一個跳轉函數)
boot_loader
cmake的鏈接器表示我loader 加載到0x8000的地址,start.s放在cmake加入的工程文件的最開頭,這樣就可以保證加載到0x8000時在start.s
因為512字節顯然比較小,沒辦法完成這么多功能,所以我做了一個二級引導
1.內聯匯編顯示字符串
2.檢測內存容量 0x15(boot_info)
檢測10塊可用內存區域
3.切換進保護模式
實模式的限制
1.只能訪問1MB內存,內核寄存器最大為16位寬
2.所有的操作數最大為16位寬
3.沒有任何保護機制
4.沒有特權級支持
5.沒有分頁機制和虛擬內存的支持
如何切換進保護模式
首先保證過程原子性,禁用中斷,然后打開A20地址線讓其訪問1m以上的內存地址,然后初始化加載GDT表保證開啟保護模式寄存器值正常,再設置CR0 PE位,開啟保護模式。
這個禁用中斷的函數我也寫了一個函數,保存關中斷前的各個寄存器的狀態(eflags等),完成實模式到保護模式切換后恢復到原來的狀態,這個函數再后面的也可以用到。函數中我也用到了內聯匯編函數 sti cli進行開關中斷.
加載kernel到內存地址1M
在loader中實現LAB讀取磁盤,(一次兩字節讀取)(通常512字節一次性讀取)
#define SYS_KERNEL_LOAD_ADDR (1024*1024) // 內核加載的起始地址(此時打開了A20地址線和保護模式,可以訪問1M以上空間
static void read_disk(int sector, int sector_count, uint8_t * buf)
名稱 | 含義 | 單位 |
---|---|---|
sector | 起始扇區號(LBA) | 扇區(512 字節) |
sector_count | 連續讀取的扇區數量 | 扇區(512 字節) |
創建kernel文件夾,同樣cmake設置起始位置1M
棧的作用:C的局部變量,函數調用中的參數
在保護模式下 push pop的一個棧都是4個字節
esp是棧頂指針 ebp相對比較固定。ESP 指向當前棧頂,EBP 保存的是上一個調用幀的基地址(上一個函數的棧底基準)
我在loader32.c中寫了 ((void (*)(boot_info_t *))kernel_entry)(&boot_info);
然后進入kernel1M地址,
push %ebpmov %esp, %ebpmov 0x8(%ebp), %eaxpush %eaxcall kernel_init
取出boot_info
參數傳給kernel_init函數void kernel_init (boot_info_t * boot_info)(函數指針)
我沒用全局變量不依賴任何外部狀態,bootloader 和 kernel解耦,可移植性強。
我再講解下這個loader的這個函數指針:(為了跳到kernel_entry
(裸地址0x100000)并傳入參數boot_info
部分 | 解釋 |
---|---|
void (*)(boot_info_t *) | 一個函數指針類型,指向接受一個 boot_info_t * 參數、返回 void 的函數 |
(void (*)(boot_info_t *))kernel_entry | 把 kernel_entry 強制轉換為這種函數指針類型 |
((...))(&boot_info) | 把這個函數指針當成函數調用,傳入參數 &boot_info |
加載內核映像文件
改一下kernel.lds和loader32.c中跳轉到kernel的函數指針的裸地址改一下0x100000 改為0x1000
elf
elf更小,并且可以進行權限設置。
elf是一種通用的可執行文件格式,操作系統內核是程序邏輯,elf是其封裝形式,方便bootloader加載執行。
這個elf文件是我的c、匯編寫的內核編譯出來的最終可執行映像。
查看手冊把elf_header和program_header的結構放在elf.h中
在loader32.c中寫了一個加載elf的函數,首先要進行魔術驗證,0x7F ‘E’ ‘L’ 'F’開頭,判斷其是否合法。將elf里面的內容提取到指定的物理地址,做好初始化(比如bss通常是未初始化的參數,初始化為0),最后跳轉到elf的入口地址(return elf_hdr->e_entry;)
。然后將e_entry這個elf入口地址作為loader的跳轉地址(剛剛我們改的裸地址就可以改成e_entry)
從而跳轉到kernel內核。
一些基礎知識
鏈接腳本與代碼數據段
先看一個具體的
有下面的一個順序
為什么會這么放呢?
下面是一個測試
當我們建立了kernel.lds就不用在每個文件夾下的cmake -text進行設置 告訴編譯器這些分別放在哪
創建GDT表
段描述符 結構的主要是和cpu有關,就放在kernel的include文件下cpu.h里面。
GDT表就放在cpu.c中
段頁式內存管理
我們先關注分段存儲
段頁式內存管理第一部分:邏輯地址 → 線性地址
我采用的是平坦模型(base address = 0,offset的limit可以覆蓋整個4GB空間),選擇子在GDT查表得到基地址,然后和偏移量相加得到線性地址。
換而言之:線性地址 = offset (因為 base = 0)
但是我仍需要設置GDT表:
gdt:(參考下面段描述符).quad 0 ; 空描述符.quad 0x00CF9A000000FFFF ; 代碼段:base=0, limit=4GB.quad 0x00CF92000000FFFF ; 數據段:base=0, limit=4GB
其中limit有20位(高 4 + 低 16),雖然 limit 字段最大只能表示 0xFFFFF(1MB),但段描述符中還有一個 “粒度位(G bit)”,它決定 limit 的單位是字節還是 4KB。如果是4KB*1MB=4GB 因此段限長可以設置為4GB,從而實現平坦系統。
總結一下內存訪問整體流程:
下面是對段描述符,GDT結構 處理上的詳細介紹:
段描述符:
/*** GDT描述符(其實就是段描述符)*/
typedef struct _segment_desc_t {uint16_t limit15_0;uint16_t base15_0;uint8_t base23_16;uint16_t attr;uint8_t base31_24;
}segment_desc_t;
主要分成三個部分limit base attr
/*** 設置段描述符*/
void segment_desc_set(int selector, uint32_t base, uint32_t limit, uint16_t attr) {segment_desc_t * desc = gdt_table + (selector >> 3);// 如果界限比較長,將長度單位換成4KBif (limit > 0xfffff) {attr |= 0x8000;limit /= 0x1000;}desc->limit15_0 = limit & 0xffff;desc->base15_0 = base & 0xffff;desc->base23_16 = (base >> 16) & 0xff;desc->attr = attr | (((limit >> 16) & 0xf) << 8);desc->base31_24 = (base >> 24) & 0xff;
}/*** 然后利用set函數初始化GDT*/
void init_gdt(void) {// 全部清空for (int i = 0; i < GDT_TABLE_SIZE; i++) {segment_desc_set(i << 3, 0, 0, 0);}//數據段segment_desc_set(KERNEL_SELECTOR_DS, 0x00000000, 0xFFFFFFFF,SEG_P_PRESENT | SEG_DPL0 | SEG_S_NORMAL | SEG_TYPE_DATA| SEG_TYPE_RW | SEG_D | SEG_G);// 只能用非一致代碼段,以便通過調用門更改當前任務的CPL執行關鍵的資源訪問操作segment_desc_set(KERNEL_SELECTOR_CS, 0x00000000, 0xFFFFFFFF,SEG_P_PRESENT | SEG_DPL0 | SEG_S_NORMAL | SEG_TYPE_CODE| SEG_TYPE_RW | SEG_D | SEG_G);// 調用門gate_desc_set((gate_desc_t *)(gdt_table + (SELECTOR_SYSCALL >> 3)),KERNEL_SELECTOR_CS,(uint32_t)exception_handler_syscall,GATE_P_PRESENT | GATE_DPL3 | GATE_TYPE_SYSCALL | SYSCALL_PARAM_COUNT);// 加載gdtlgdt((uint32_t)gdt_table, sizeof(gdt_table));
}
其中selector >> 3才能表示GDT的索引,因為選擇子selector 是16位,結構包括 Index(段描述符索引)、TI(表選擇標志1位)和 RPL(請求者特權級2位),所以通過選擇子查找GDT中的索引需要右移3位。
有的同學可能會疑惑,進入保護模式CS/DS/SS等端寄存器不是從16位變成32位了嗎?
事實上每個段寄存器(如CS、DS、SS、ES、FS、GS)在保護模式下實際上分為兩部分,其中“隱藏部分”,保存了段基址、限長、權限等完整段描述符信息,這部分可能是32 位甚至 64 位。
部分 | 說明 |
---|---|
可見部分 | 16 位選擇子(selector) |
隱藏部分 | 段描述符緩存(base、limit、access rights) |
顯示字符串
bios中斷向量表中來顯示
0x10
內聯匯編來顯示
也是用的bios中斷進行顯示的