寫到這,終於才把一些苦力活都幹完了,也終於到了咱們的內核代碼部分,也終於開始第一次用 c 語言寫代碼了!爲了這個階段性的勝利,以及更好地進入內核部分,下圖貼一張到目前爲止的流程圖。(其中黃色部分是今天準備作的事情)html
... ;加載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
#include "print.h" int main(void){ put_str("put_str finish\n"); while(1); return 0; }
#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
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
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
我將關鍵部分提取出來,有助於你鳥瞰本講的所有代碼要作的事。本段代碼實際上就作了這麼幾個事:linux
有一點有些不符合咱們的直覺,既然 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
ELF:1999 年,被 86open 項目選爲 x86 架構上的類 Unix 操做系統的二進制文件標準格式,用來取代 COFF,也是 Linux 的主要可執行文件格式學習
爲何要有這種格式呢?其實沒有這種格式也是徹底能夠的,但咱們用戶寫的應用程序,是獨立與操做系統以外的。換句話說,就是須要操做系統這個 主應用程序,去調用那些用戶寫出來的 應用程序。若是沒有一種特定的格式固然也能夠,那就讓操做系統約定俗成一個內存地址來存放用戶的應用程序,這樣應用程序也不能將本身的程序分紅一段一段的。因此有個格式,至少是隻有好處沒有壞處。ui
剛剛只提到了可執行文件,生成可執行文件以前還要經歷一個重定位文件的過程,連接以後纔是可執行文件。重定位文件和可執行文件均可以用 ELF 格式來表示,該格式有一個統一的頭,下面分紅好多個段和好多個節,多個節經過連接變成一個段,具體格式以下圖。
數據類型 | 字節大小 |
---|---|
Elf32_Half | 無符號整數(2) |
Elf32_Word | 無符號整數(4) |
Elf32_Addr | 程序運行地址(4) |
Elf32_Off | 文件偏移量(4) |
數據類型 | 名稱 | 字節 | 含義 | 例子 |
---|---|---|---|---|
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
本章講述了 ELF 格式的可執行文件,還講述瞭如何加載一個 ELF 可執行文件,並跳轉到相應的地址去執行。
本章還隱含講述了彙編語言如何調用 c 語言(約定好跳轉地址,以及傳參方式),以及 C 語言如何調用匯編語言。
global put_str put_str: ... ret
#include "print.h" int main(void){ put_str(); return 0; }
#ifndef __LIB_KERNEL_PRINT_H #define __LIB_KERNEL_PRINT_H void put_str(); #endif
若是你對自制一個操做系統感興趣,不妨跟隨這個系列課程看下去,甚至加入咱們,一塊兒來開發。
《操做系統真相還原》這本書真的贊!強烈推薦
當你看到該文章時,代碼可能已經比文章中的又多寫了一些部分了。你能夠經過提交記錄歷史來查看歷史的代碼,我會慢慢梳理提交歷史以及項目說明文檔,爭取給每一課都準備一個可執行的代碼。固然文章中的代碼也是全的,採用複製粘貼的方式也是徹底能夠的。
若是你有興趣加入這個自制操做系統的大軍,也能夠在留言區留下您的聯繫方式,或者在 gitee 私信我您的聯繫方式。
本課程打算出系列課程,我寫到哪以爲能夠寫成一篇文章了就寫出來分享給你們,最終會完成一個功能全面的操做系統,我以爲這是最好的學習操做系統的方式了。因此中間遇到的各類坎也會寫進去,若是你能持續跟進,跟着我一塊寫,必然會有很好的收貨。即便沒有,交個朋友也是好的哈哈。
目前的系列包括