【自制操做系統06】終於開始用 C 語言了,第一行內核代碼!

1、整理下到目前爲止的流程圖

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

2、先上代碼

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

3、鳥瞰代碼

;加載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

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

  1. 將硬盤第 9 扇區開始後的 200 個扇區的內容(包括 kernel.bin),複製到內存 0x70000 開始的地方
  2. call kernel_init 調用了一下這個方法,這個方法幹嗎以後再說,也是重點
  3. 棧指針賦值爲 0xc009f000,並跳轉到 0xc0001500 開始執行

有一點有些不符合咱們的直覺,既然 kernel.bin 被寫入內存第 0x70000 位置了,按照咱們以前一跳二跳三跳的寫法,應該直接跳轉到 0x70000,可爲何是 0xc0001500 呢?git

下面直接解答這個問題,架構

kernel.bin 是用 c 語言 寫好以後編譯出來的產物,不像以前咱們都是直接彙編語言 .asm 編譯成 .bin。c 語言在 linux 的 gcc 工具編譯後的二進制文件,是一個格式爲 ELF 的文件,並不徹底是從頭至尾都是可執行的機器指令。ide

這個格式裏確定有某個地方指出,指令代碼在什麼位置(相對文件開始的偏移量),而且要求加載這種格式文件的程序(kernel_init),將指令代碼放在內存中的什麼位置(0xc0001500)。函數

若是是這樣的話,整個流程就說通了,kernel_init 只是將 kernel.bin 這個 ELF 格式的文件裏的關鍵信息提取出來,最重要的就是加載到內存中的什麼位置這個信息,而後執行相應的處理操做。工具

那接下來,咱們就該詳細看看,ELF 格式到底是什麼?oop

4、詳解 ELF 格式

ELF:1999 年,被 86open 項目選爲 x86 架構上的類 Unix 操做系統的二進制文件標準格式,用來取代 COFF,也是 Linux 的主要可執行文件格式學習

爲何要有這種格式呢?其實沒有這種格式也是徹底能夠的,但咱們用戶寫的應用程序,是獨立與操做系統以外的。換句話說,就是須要操做系統這個 主應用程序,去調用那些用戶寫出來的 應用程序。若是沒有一種特定的格式固然也能夠,那就讓操做系統約定俗成一個內存地址來存放用戶的應用程序,這樣應用程序也不能將本身的程序分紅一段一段的。因此有個格式,至少是隻有好處沒有壞處。ui

剛剛只提到了可執行文件,生成可執行文件以前還要經歷一個重定位文件的過程,連接以後纔是可執行文件。重定位文件可執行文件均可以用 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

5、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 私信我您的聯繫方式。

課程規劃

本課程打算出系列課程,我寫到哪以爲能夠寫成一篇文章了就寫出來分享給你們,最終會完成一個功能全面的操做系統,我以爲這是最好的學習操做系統的方式了。因此中間遇到的各類坎也會寫進去,若是你能持續跟進,跟着我一塊寫,必然會有很好的收貨。即便沒有,交個朋友也是好的哈哈。

目前的系列包括

相關文章
相關標籤/搜索