博客網(wǎng)址:www.shicoder.top
微信:18223081347
歡迎加群聊天 :452380935
大家好呀,終于我們到了操作系統(tǒng)的loader部分了,loader也是操作系統(tǒng)中最重要的一個部分,承接上面的boot,啟下下面的kernel,那我們就開始吧!!!
內(nèi)存檢測
在loader中,最重要的一點就是檢測內(nèi)存,檢測一些系統(tǒng)參數(shù),到時候給kernel使用,那么下面我們就介紹下loader中如何檢測內(nèi)存。還是一樣,我們先看下檢測內(nèi)存的代碼
detect_memory:
; 置為0
xor ebx, ebx
; es:di賦值
mov ax, 0
mov es, ax
mov edi, ards_buffer
mov edx, 0x534d4150 ;固定簽名
.next:
mov eax, 0xe820
mov ecx, 20
; 執(zhí)行系統(tǒng)調(diào)用
int 0x15
; 檢測cf標志位
jc error
; 將緩存指針指向下一個結(jié)構(gòu)體
add di, cx
; 將結(jié)構(gòu)體數(shù)量+1
inc word [ards_count]
; 檢測ebx是否為0
cmp ebx, 0
jnz .next
mov si, detecting
call print
注意,我們這里獲取內(nèi)存的方式是采用BIOS中int 0x15中子功能0xE820。我們先給出int 0x15下3個子功能的具體描述
- EAX=0xE820 :遍歷主機上全部內(nèi)存
- AX=0xE801:分別檢測第15MB和16MB-4GB的內(nèi)存
- AH=0x88:最多檢測出64MB內(nèi)存
內(nèi)存的相關(guān)值共同組成一個結(jié)構(gòu)體:ARDS(地址范圍描述符),共20字節(jié)如下
- BaseAddrLow(4字節(jié)):基地址的低32位
- BaseAddrHigh(4字節(jié)):基地址的高32位
- LengthLow(4字節(jié)):內(nèi)存長度的低32位,以字節(jié)為單位
- LengthHigh(4字節(jié)):內(nèi)存長度的高32位,以字節(jié)為單位
- Type(4字節(jié)):本段內(nèi)存的類型
返回值如下
- CF位:若CF位為0表示調(diào)用未出錯
- EAX:0x534d4150
- ED:DI:ARDS的地址
- ECX:寫入到ARDS的字節(jié)數(shù),一般為20字節(jié)
- EBX:下一個ARDS的地址,當(dāng)CF=0,且EBX=0,表示結(jié)束
通過上述代碼,就可以將ARDS的個數(shù)存在ards_count中,將每一個ARDS的值放在ards_buffer中。
準備進入保護模式
進入保護模式需要三個步驟
- 打開A20
- 加載GDT
- 將cr0的pe位置1
全局描述符表
在實模式下,訪問一個地址的方式為
段地址 << 4 + 偏移地址
但是在進入保護模式后,地址線是足夠的,共32條,所以并不需要上面的方式,其尋址方式為
段選擇子(16位):段內(nèi)偏移(32位)
我們來說下段選擇子。段選擇子有16位,3-15位為描述符索引(13位可表示8192個),第2位為TI位,TI=0,表示從全局描述符表中取,TI=1,表示從局部描述符表中取。第0-1位為特權(quán)級RPL(熟悉的特權(quán)級0-3級,用2位描述),代碼如下
typedef struct selector
{
unsigned char RPL : 2; // Request PL
unsigned char TI : 1; // 0 全局描述符 1 局部描述符 LDT Local
unsigned short index : 13; // 全局描述符表索引
} __attribute__((packed)) selector;
上面出現(xiàn)了一個全局描述符表的東西(GDT),全局描述符表中每一項都是一個全局描述符,每個全局描述符都指向內(nèi)存中的一個位置,下面的圖展示了其關(guān)系
因此如何描述這一段內(nèi)存,就變得尤為重要,全局描述符的結(jié)構(gòu)如下
typedef struct descriptor /* 共 8 個字節(jié) */
{
unsigned short limit_low; // 段界限 0 ~ 15 位
unsigned int base_low : 24; // 基地址 0 ~ 23 位 16M
unsigned char type : 4; // 段類型
unsigned char segment : 1; // 1 表示代碼段或數(shù)據(jù)段,0 表示系統(tǒng)段
unsigned char DPL : 2; // Descriptor Privilege Level 描述符特權(quán)等級 0 ~ 3
unsigned char present : 1; // 存在位,1 在內(nèi)存中,0 在磁盤上
unsigned char limit_high : 4; // 段界限 16 ~ 19;
unsigned char available : 1; // 該安排的都安排了,送給操作系統(tǒng)吧
unsigned char long_mode : 1; // 64 位擴展標志
unsigned char big : 1; // 32 位 還是 16 位;
unsigned char granularity : 1; // 粒度 4KB 或 1B
unsigned char base_high; // 基地址 24 ~ 31 位
} __attribute__((packed)) descriptor;
則全局描述符表就有8192項,每一項都是指示一片內(nèi)存的全局描述符,且表的第0項是NULL。有一個特殊寄存器GDT register指向它,只要讀取這個寄存器的值,就可以找到這個表,然后通過段選擇子就可以知道是哪一個下標。GDT register有48位,結(jié)構(gòu)如下,0-15位共16位標識GDT界限,共65536字節(jié),每個全局描述符8字節(jié),所以一共65536/8=8192個
下面給出相關(guān)代碼
memory_base equ 0 ; 內(nèi)存開始的位置
; 32位下,內(nèi)存為4G,然后選用的粒度為4KB
memory_limit equ ((1024 * 1024 * 1024 * 4) / (1024 * 4) - 1) ; 內(nèi)存界限 4G / 4k -1
; 準備進入保護模式
prepare_protected_mode:
cli; 關(guān)閉中斷
...
; 加載GDT
lgdt [gdt_ptr]
...
gdt_ptr:
dw (gdt_end-gdt_base)-1
dd gdt_base
gdt_base:
; dd 4個字節(jié),全局描述符表中第一個8字節(jié)為null描述符
dd 0,0 ;null描述符
gdt_code:
dw memory_limit & 0xffff ; 段界限 0 ~ 15 位
dw memory_base & 0xffff ; 基地址 0 ~ 15 位
db (memory_base >> 16) & 0xff ; 基地址 16 ~ 23 位
; 存在位,1 在內(nèi)存中
; 特權(quán)等級 00
; 1 表示代碼段或數(shù)據(jù)段
; 段類型 | X | C/E | R/W | A | 1 0 1 0 代碼段-非依從-可讀-沒有訪問
db 0b_1_00_1_1_0_1_0
; 1 粒度 4KB
; 1 32 位
; 0 非64 位擴展標志
; 0 available 隨意
; 段界限 16 ~ 19
db 0b_1_1_0_0_0000 | (memory_limit >> 16) & 0xf
; 基地址 24 ~ 31 位
db (memory_base >> 24) & 0xff
gdt_data:
dw memory_limit & 0xffff ; 段界限 0 ~ 15 位
dw memory_base & 0xffff ; 基地址 0 ~ 15 位
db (memory_base >> 16) & 0xff ; 基地址 16 ~ 23 位
; 存在位,1 在內(nèi)存中
; 特權(quán)等級 00
; 1 表示代碼段或數(shù)據(jù)段
; 段類型 | X | C/E | R/W | A | 0 0 1 0 數(shù)據(jù)段-向上-可寫-沒有訪問
db 0b_1_00_1_0_0_1_0
; 1 粒度 4KB
; 1 32 位
; 0 非64 位擴展標志
; 0 available 隨意
; 段界限 16 ~ 19
db 0b_1_1_0_0_0000 | (memory_limit >> 16) & 0xf
; 基地址 24 ~ 31 位
db (memory_base >> 24) & 0xff
gdt_end:
我們重點來看重點部分,其他我們后續(xù)來說
我們前面說過,在進入保護模式前,我們要加載GDT,以便在保護模式后,其他地方要用到,所以使用如下命令
lgdt [gdt_ptr]; 加載GDT 將gdt_ptr所指向的區(qū)域加載到GDT register中
sgdt [gdt_ptr]; 保存 gdt 將GDT register中的內(nèi)容保存到gdt_ptr所指向的區(qū)域
然后我們構(gòu)建代碼段和數(shù)據(jù)段的段選擇子,通過選擇子的結(jié)構(gòu)進行構(gòu)建
; 構(gòu)建代碼段和數(shù)據(jù)段的段選擇子
; 1 << 3 => 0001 根據(jù)段選擇子的結(jié)構(gòu),第0-1位為 RPL ,第2位為TI ,后面為index
code_selector equ (1 << 3)
data_selector equ (2 << 3)
A20線
其實就是為了在保護模式下可以使用更大的尋址線,因此打開A20線,方式很簡單,就是將端口0x92的第1位置1就可以,代碼如下:
; 打開A20線
in al, 0x92
or al, 0b10 ; 第1位置1
out 0x92, al
CR0寄存器
我們需要將CR0寄存器的第0位(PE位)Protection Enable打開,方式如下
mov eax, cr0
or eax, 1 ; 第0位置1
mov cr0, eax
刷新流水線
我們可以看到一條很奇怪的jmp指令
; 用跳轉(zhuǎn)來刷新緩存,啟用保護模式
jmp dword code_selector:protect_mode
; 提醒編譯器,到了32位的保護模式
[bits 32]
protect_mode:
因為我們知道在跳轉(zhuǎn)前是實模式,可能是16位,但是跳轉(zhuǎn)到保護模式后,需要在32位下進行,那么CPU指令卻不知道,仍然可能用16位的方式去解析32位指令,就會出錯,因此采用1個jmp模式進行
進入保護模式
經(jīng)過前面的步驟,我們終于來到了保護模式protect_mode
。這個版本的操作系統(tǒng)我們設(shè)置的保護模式很簡單,代碼如下:
[bits 32]
protect_mode:
mov ax, data_selector
; 初始化段寄存器
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
; 在0x7e00-0x9fbff可用區(qū)域間隨便找一個位置
mov esp, 0x10000 ;修改棧頂
; 因為system.bin(kernel文件夾里面的程序編譯的)是從第10個扇區(qū)開始寫入,寫了200個扇區(qū)
mov edi, 0x10000;讀取的目標內(nèi)存
mov ecx, 10 ;起始扇區(qū)
mov bl, 200 ;扇區(qū)數(shù)量
call read_disk
; 內(nèi)核代碼被放在0x10000處,所以跳轉(zhuǎn)到這里執(zhí)行內(nèi)核代碼
jmp dword code_selector: 0x10000
我們在編譯的時候,先將system.bin寫入到磁盤的第10個扇區(qū),命令如下
dd if=system.bin of=master.img bs=512 count=200 seek=10 conv=notrunc
終于我們可以編寫c語言了,前面寫匯編實在難受,哈哈哈。
內(nèi)核的主程序在main.c中,先簡單實現(xiàn)下把,后續(xù)再補充
void kernel_init(){
char *video = (char*)0xb8000; // 文本顯示器的內(nèi)存位置
for (int i = 0; i < sizeof(message); i++)
{
// 第一個位是字符,第二個位是該字符的特性,比如是閃爍還是不閃爍等,所以每個字符要在內(nèi)存位置占2個位
video[i*2] = message[i];
}
}
注意下0xb8000,在這個系列的第2章中,有如下代碼
; 0xb8000 文本顯示器的內(nèi)存區(qū)域
mov ax, 0xb800
mov ds, ax
mov byte [0], 'H'
0xb8000已經(jīng)超過16位了,所以在實模式下,需要使用( 16 位段基址 << 4 ) + 16 位偏移地址
方式,而在保護模式下有32位,所以可以直接訪問
本文摘自 :https://www.cnblogs.com/