1. 程式人生 > >【自制作業系統06】終於開始用 C 語言了,第一行核心程式碼!

【自制作業系統06】終於開始用 C 語言了,第一行核心程式碼!

一、整理下到目前為止的流程圖

寫到這,終於才把一些苦力活都幹完了,也終於到了我們的核心程式碼部分,也終於開始第一次用 c 語言寫程式碼了!為了這個階段性的勝利,以及更好地進入核心部分,下圖貼一張到目前為止的流程圖。(其中黃色部分是今天準備做的事情)

二、先上程式碼

loader.asm

...
;載入kernel
mov eax,0x9        ;kernel.bin所在的扇區號 0x9
mov ebx,0x70000    ;寫入的記憶體地址 0x70000
mov ecx,200        ;讀入的扇區數
call rd_disk_m_32
...

;進入核心
call kernel_init

mov byte [gs:0x280],'i'
mov byte [gs:0x282],'n'
mov byte [gs:0x284],'i'
mov byte [gs:0x286],'t'
mov byte [gs:0x28a],'k'
mov byte [gs:0x28c],'e'
mov byte [gs:0x28e],'r'
mov byte [gs:0x290],'n'
mov byte [gs:0x292],'e'
mov byte [gs:0x294],'l'

mov esp,0xc009f000
jmp 0xc0001500

; 將kernel.bin中的segment拷貝到編譯的地址
kernel_init:
    xor eax,eax
    xor ebx,ebx ;記錄程式頭表地址(核心地址+程式頭表偏移地址)
    xor ecx,ecx ;記錄程式頭中的數量
    xor edx,edx ;記錄程式頭表中每個條目的位元組大小
    
    mov dx,[0x70000+42] ;偏移檔案42位元組處是e_phentsize
    mov ebx,[0x70000+28]    ;偏移檔案28位元組處是e_phoff
    add ebx,0x70000
    mov cx,[0x70000+44] ;偏移檔案44位元組處是e_phnum
    
.each_segment:
    cmp byte [ebx+0],0  ;p_type=0,說明此頭未使用
    je .PTNULL
    
    push dword [ebx+16] ;p_filesz壓入棧(mem_cpy第三個引數)
    mov eax,[ebx+4]
    add eax,0x70000
    push eax        ;p_offset+核心地址=段地址(mem_cpy第二個引數)
    push dword [ebx+8]  ;p_vaddr(mem_cpy第一個引數)
    call mem_cpy
    add esp,12
.PTNULL:
    add ebx,edx ;ebx指向下一個程式頭
    loop .each_segment
    ret
    
;主子拷貝函式(dst,src,size)
mem_cpy:
    cld
    push ebp
    mov ebp,esp
    push ecx
    
    mov edi,[ebp+8]     ;dst
    mov esi,[ebp+12]    ;src
    mov ecx,[ebp+16]    ;size
    rep movsb
    
    pop ecx
    pop ebp
    ret

; 以下是兩個函式的具體實現,不看不影響理解主流程
; 保護模式的硬碟讀取函式
rd_disk_m_32:

    mov esi, eax
    mov di, cx

    mov dx, 0x1f2
    mov al, cl
    out dx, al

    mov eax, esi
    ; 儲存LBA地址
    mov dx, 0x1f3
    out dx, al

    mov cl, 8
    shr eax, cl
    mov dx, 0x1f4
    out dx, al

    shr eax, cl
    mov dx, 0x1f5
    out dx, al

    shr eax, cl
    and al, 0x0f
    or al, 0xe0
    mov dx, 0x1f6
    out dx, al

    mov dx, 0x1f7
    mov al, 0x20
    out dx, al

.not_ready:
    nop
    in al, dx
    and al, 0x88
    cmp al, 0x08
    jnz .not_ready

    mov ax, di
    mov dx, 256
    mul dx
    mov cx, ax
    mov dx, 0x1f0

.go_on_read:
    in ax, dx
    mov [ds:ebx], ax
    add ebx, 2
    loop .go_on_read
    ret

main.c

#include "print.h"
int main(void){
    put_str("put_str finish\n");
    while(1);
    return 0;
}

print.h

#ifndef __LIB_KERNEL_PRINT_H
#define __LIB_KERNEL_PRINT_H
#include "stdint.h"
void put_char(uint8_t char_asci);
void put_str(char* message);
#endif

print.asm

TI_GDT equ 0
RPL0 equ 0
SELECTOR_VIDEO equ (0x0003<<3)+TI_GDT+RPL0

[bits 32]
section .text

global put_str
put_str:
    push ebx
    push ecx
    xor ecx,ecx
    mov ebx,[esp+12]
.goon:
    mov cl,[ebx]
    cmp cl,0
    jz .str_over
    push ecx
    call put_char
    add esp,4
    inc ebx
    jmp .goon
.str_over:
    pop ecx
    pop ebx
    ret

global put_char
put_char:
    pushad
    ;保證gs中為正確到視訊段選擇子
    mov ax,SELECTOR_VIDEO
    mov gs,ax
    
    ;獲取當前游標位置
    ;獲得高8位
    mov dx,0x03d4   ;索引暫存器
    mov al,0x0e
    out dx,al
    mov dx,0x03d5
    in al,dx
    mov ah,al
    
    ;獲得低8位
    mov dx,0x03d4
    mov al,0x0f
    out dx,al
    mov dx,0x03d5
    in al,dx
    
    ;將游標存入bx
    mov bx,ax
    
    mov ecx,[esp+36]
    cmp cl,0xd
    jz .is_carriage_return
    cmp cl,0xa
    jz .is_line_feed
    
    cmp cl,0x8
    jz .is_backspace
    jmp .put_other
    
.is_backspace:
    dec bx
    shl bx,1
    mov byte [gs:bx],0x20
    inc bx
    mov byte [gs:bx],0x07
    shr bx,1
    jmp .set_cursor
    
.put_other:
    shl bx,1
    mov [gs:bx],cl
    inc bx
    mov byte [gs:bx],0x07
    shr bx,1
    inc bx
    cmp bx,2000
    jl .set_cursor
    
.is_line_feed:
.is_carriage_return:
;cr(\r),只要把游標移到首行就行了
    xor dx,dx
    mov ax,bx
    mov si,80
    div si
    sub bx,dx
    
.is_carriage_return_end:
    add bx,80
    cmp bx,2000
.is_line_feed_end:
    jl .set_cursor
    
.roll_screen:
    cld
    mov ecx,960
    mov esi,0xc00b80a0  ;第1行行首
    mov edi,0xc00b8000  ;第0行行首
    rep movsd
    
    ;最後一行填充為空白
    mov ebx,3840
    mov ecx,80
.cls:
    mov word [gs:ebx],0x0720
    add ebx,2
    loop .cls
    mov bx,1920 ;最後一行行首
    
.set_cursor:
;將游標設為bx值
    ;設定高8位
    mov dx,0x03d4
    mov al,0x0e
    out dx,al
    mov dx,0x03d5
    mov al,bh
    out dx,al
    
    ;再設定低8位
    mov dx,0x03d4
    mov al,0x0f
    out dx,al
    mov dx,0x03d5
    mov al,bl
    out dx,al
.put_char_done:
    popad
    ret

Makefile

mbr.bin: mbr.asm
    nasm -I include/ -o out/mbr.bin mbr.asm -l out/mbr.lst
    
loader.bin: loader.asm
    nasm -I include/ -o out/loader.bin loader.asm -l out/loader.lst
    
kernel.bin: kernel/main.c
    nasm -f elf -o out/print.o lib/kernel/print.asm
    gcc -I lib/kernel/ -c -o out/main.o kernel/main.c
    ld -Ttext 0xc0001500 -e main -o out/kernel.bin out/main.o out/print.o
    
os.raw: mbr.bin loader.bin kernel.bin
    ../bochs/bin/bximage -hd -mode="flat" -size=60 -q target/os.raw
    dd if=out/mbr.bin of=target/os.raw bs=512 count=1
    dd if=out/loader.bin of=target/os.raw bs=512 count=4 seek=2
    dd if=out/kernel.bin of=target/os.raw bs=512 count=200 seek=9
    
brun:
    make install
    make only-bochs-run

only-bochs-run:
    ../bochs/bin/bochs -f ../bochs/bochsrc.disk -q
    
install:
    make clean
    make -r os.raw

三、鳥瞰程式碼

;載入kernel
mov eax,0x9        ;kernel.bin所在的扇區號 0x9
mov ebx,0x70000    ;寫入的記憶體地址 0x70000
mov ecx,200        ;讀入的扇區數
call rd_disk_m_32
;進入核心
call kernel_init
mov esp,0xc009f000
jmp 0xc0001500

我將關鍵部分提取出來,有助於你鳥瞰本講的全部程式碼要做的事。本段程式碼實際上就做了這麼幾個事:

  1. 將硬碟第 9 扇區開始後的 200 個扇區的內容(包括 kernel.bin),複製到記憶體 0x70000 開始的地方
  2. call kernel_init 呼叫了一下這個方法,這個方法幹嘛之後再說,也是重點
  3. 棧指標賦值為 0xc009f000,並跳轉到 0xc0001500 開始執行

有一點有些不符合我們的直覺,既然 kernel.bin 被寫入記憶體第 0x70000 位置了,按照我們之前一跳二跳三跳的寫法,應該直接跳轉到 0x70000,可為什麼是 0xc0001500 呢?

下面直接解答這個問題,

kernel.bin 是用 c 語言 寫好之後編譯出來的產物,不像之前我們都是直接組合語言 .asm 編譯成 .bin。c 語言在 linux 的 gcc 工具編譯後的二進位制檔案,是一個格式為 ELF 的檔案,並不完全是從頭到尾都是可執行的機器指令。

這個格式裡肯定有某個地方指出,指令程式碼在什麼位置(相對檔案開始的偏移量),並且要求載入這種格式檔案的程式(kernel_init),將指令程式碼放在記憶體中的什麼位置(0xc0001500)。

如果是這樣的話,整個流程就說通了,kernel_init 只是將 kernel.bin 這個 ELF 格式的檔案裡的關鍵資訊提取出來,最重要的就是載入到記憶體中的什麼位置這個資訊,然後執行相應的處理操作。

那接下來,我們就該詳細看看,ELF 格式究竟是什麼?

四、詳解 ELF 格式

ELF:1999 年,被 86open 專案選為 x86 架構上的類 Unix 作業系統的二進位制檔案標準格式,用來取代 COFF,也是 Linux 的主要可執行檔案格式

為什麼要有這種格式呢?其實沒有這種格式也是完全可以的,但我們使用者寫的應用程式,是獨立與作業系統之外的。換句話說,就是需要作業系統這個 主應用程式,去呼叫那些使用者寫出來的 應用程式。如果沒有一種特定的格式當然也可以,那就讓作業系統約定俗成一個記憶體地址來存放使用者的應用程式,這樣應用程式也不能將自己的程式分成一段一段的。所以有個格式,至少是隻有好處沒有壞處。

剛剛只提到了可執行檔案,生成可執行檔案之前還要經歷一個重定位檔案的過程,連結之後才是可執行檔案。重定位檔案和可執行檔案都可以用 ELF 格式來表示,該格式有一個統一的頭,下面分成好多個段和好多個節,多個節通過連結變成一個段,具體格式如下圖。

ELF 格式鳥瞰

ELF 格式具體定義

先定義下資料型別方便後續描述

資料型別 位元組大小
Elf32_Half 無符號整數(2)
Elf32_Word 無符號整數(4)
Elf32_Addr 程式執行地址(4)
Elf32_Off 檔案偏移量(4)

ELF 頭

資料型別 名稱 位元組 含義 例子
unsigned char e_ident[16] 16 0-3魔數 4型別 5大小端 6版本 7-15保留零
Elf32_Half e_type 2 檔案型別:0未知 1可重定位 2可執行 3動態共享目標 4core 0x0002
Elf32_Half e_machine 2 處理器結構:0未知 3Intel80386 8MIPSRS3000 0x0003
Elf32_Word e_version 4 版本 0x00000001
Elf32_Addr e_entry 4 用來指明作業系統執行該程式時,將控制權轉交到的虛擬地址 0xc0001500
Elf32_Off e_phoff 4 程式頭表(program header table)在檔案內的位元組偏移量。沒有為0 0x00000034
Elf32_Off e_shoff 4 節頭表(section header table)在檔案內的位元組偏移量。沒有為0 0x0000055c
Elf32_Word e_flags 4 與處理器相關標誌 0x00000000
Elf32_Half e_enhsize 2 elf header的位元組大小 0x0034
Elf32_Half e_phentsize 2 程式頭表(program header table)中每個條目(entry)的位元組大小 0x0020
Elf32_Half e_phnum 2 程式頭表中條目的數量。實際上就是段的個數 0x0002
Elf32_Half e_shentsize 2 節頭表(section header table)中每個條目(entry)的位元組大小 0x0028
Elf32_Half e_shnum 2 程式頭表中條目的數量。實際上就是節的個數 0x0006
Elf32_Half e_shstmdx 2 用來指明string name table在節頭表中的索引index 0x0003

程式頭表

資料型別 名稱 位元組 含義 例子
Elf32_Word p_type 4 段的型別:1可載入的程式段 2動態連線資訊 3動態載入器名稱 0x00000001
Elf32_Off p_offset 4 本段在檔案內的起始偏移位元組 0x00000000
Elf32_Addr p_vaddr 4 本段在記憶體中的起始虛擬地址 0xc0001000
Elf32_Addr p_paddr 4 實體地址相關,保留,未設定 0xc0001000
Elf32_Word p_filesz 4 本段在檔案中的大小 0x0000060b
Elf32_Word p_memsz 4 本段在記憶體中的大小 0x0000060b
Elf32_Word p_flags 4 標誌 1可執行 2可寫 4可讀 0x00000005
Elf32_Word p_align 4 對其方式 0不對齊 2的冪次對齊 0x00001000

其實不用想得多複雜,就是一個格式而已,程式中需要哪個資料,就根據偏移量把它取出來用就可以了,實際上我們的程式就是這麼做的。

來看一下 kernel.bin 的具體內容

7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
02 00 03 00 01 00 00 00 [00 15 00 c0] [34 00 00 00]
64 06 00 00 00 00 00 00 34 00 [20 00] [02 00] 28 00
06 00 03 00 01 00 00 00 [00 00 00 00] [00 10 00 c0]
00 10 00 c0 [0b 06 00 00] 0b 06 00 00 05 00 00 00
00 10 00 00 51 e5 74 64 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 07 00 00 00
04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
...

按照上述的 ELF 格式表一一對應看,便能知道全部資訊,其中我們本次程式碼中用到的,都用加粗了。我們拿 ELF 檔案檢視器工具看一下(不是必須的)

程式碼中的 kernel_init 就是將 ELF 格式檔案中的 程式頭表地址、程式頭中的數量、程式頭表中每個條目的位元組大小、載入到的記憶體地址 取出,然後執行相應的拷貝操作。

kernel_init:
    xor eax,eax
    xor ebx,ebx ;記錄程式頭表地址(核心地址+程式頭表偏移地址)
    xor ecx,ecx ;記錄程式頭中的數量
    xor edx,edx ;記錄程式頭表中每個條目的位元組大小
    
    mov dx,[0x70000+42] ;偏移檔案42位元組處是e_phentsize
    mov ebx,[0x70000+28]    ;偏移檔案28位元組處是e_phoff
    add ebx,0x70000
    mov cx,[0x70000+44] ;偏移檔案44位元組處是e_phnum
    
.each_segment:
    cmp byte [ebx+0],0  ;p_type=0,說明此頭未使用
    je .PTNULL
    
    push dword [ebx+16] ;p_filesz壓入棧(mem_cpy第三個引數)
    mov eax,[ebx+4]
    add eax,0x70000
    push eax        ;p_offset+核心地址=段地址(mem_cpy第二個引數)
    push dword [ebx+8]  ;p_vaddr(mem_cpy第一個引數)
    call mem_cpy
    add esp,12
.PTNULL:
    add ebx,edx ;ebx指向下一個程式頭
    loop .each_segment
    ret

五、c 語言和組合語言相互呼叫

本章講述了 ELF 格式的可執行檔案,還講述瞭如何載入一個 ELF 可執行檔案,並跳轉到相應的地址去執行。

本章還隱含講述了組合語言如何呼叫 c 語言(約定好跳轉地址,以及傳參方式),以及 C 語言如何調用匯編語言。

c 語言調用匯編

print.asm

global put_str
put_str:
    ...
    ret

main.c

#include "print.h"
int main(void){
    put_str();
    return 0;
}

print.h

#ifndef __LIB_KERNEL_PRINT_H
#define __LIB_KERNEL_PRINT_H
void put_str();
#endif

寫在最後:開源專案和課程規劃

如果你對自制一個作業系統感興趣,不妨跟隨這個系列課程看下去,甚至加入我們,一起來開發。

參考書籍

《作業系統真相還原》這本書真的贊!強烈推薦

專案開源

專案開源地址:https://gitee.com/sunym1993/flashos

當你看到該文章時,程式碼可能已經比文章中的又多寫了一些部分了。你可以通過提交記錄歷史來檢視歷史的程式碼,我會慢慢梳理提交歷史以及專案說明文件,爭取給每一課都準備一個可執行的程式碼。當然文章中的程式碼也是全的,採用複製貼上的方式也是完全可以的。

如果你有興趣加入這個自制作業系統的大軍,也可以在留言區留下您的聯絡方式,或者在 gitee 私信我您的聯絡方式。

課程規劃

本課程打算出系列課程,我寫到哪覺得可以寫成一篇文章了就寫出來分享給大家,最終會完成一個功能全面的作業系統,我覺得這是最好的學習作業系統的方式了。所以中間遇到的各種坎也會寫進去,如果你能持續跟進,跟著我一塊寫,必然會有很好的收貨。即使沒有,交個朋友也是好的哈哈。

目前的系列包括

  • 【自制作業系統01】硬核講解計算機的啟動過程
  • 【自制作業系統02】環境準備與啟動區實現
  • 【自制作業系統03】讀取硬碟中的資料
  • 【自制作業系統04】從真實模式到保護模式
  • 【自制作業系統05】開啟記憶體分頁機制。