【自制操作系統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 私信我您的聯系方式。

課程規划

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

目前的系列包括


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM