當(dāng)前位置:首頁 > IT技術(shù) > 其他 > 正文

操作系統(tǒng)實現(xiàn)-loader
2022-05-11 11:02:47

博客網(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)系

image-20220427214535140

因此如何描述這一段內(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;

image-20220427214816129

則全局描述符表就有8192項,每一項都是指示一片內(nèi)存的全局描述符,且表的第0項是NULL。有一個特殊寄存器GDT register指向它,只要讀取這個寄存器的值,就可以找到這個表,然后通過段選擇子就可以知道是哪一個下標。GDT register有48位,結(jié)構(gòu)如下,0-15位共16位標識GDT界限,共65536字節(jié),每個全局描述符8字節(jié),所以一共65536/8=8192個

image-20220427215152160

下面給出相關(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/

開通會員,享受整站包年服務(wù)立即開通 >