目錄 一、前景回顧 二、用C語言編寫內核 三、載入內核 四、運行測試 一、前景回顧 本回開始,我們要開始編寫內核代碼了,在此之前,先梳理一下已經完成的工作。 藍色部分是目前已經完成的部分,黃色部分是本節將要實現的。 二、用C語言編寫內核 為什麼要用C語言來編寫內核呢,其實用彙編語言也可以實現,只是對 ...
目錄
一、前景回顧
二、用C語言編寫內核
三、載入內核
四、運行測試
本回開始,我們要開始編寫內核代碼了,在此之前,先梳理一下已經完成的工作。
藍色部分是目前已經完成的部分,黃色部分是本節將要實現的。
為什麼要用C語言來編寫內核呢,其實用彙編語言也可以實現,只是對於我們來講,看C語言代碼肯定要比彙編語言更容易理解,看起來也沒那麼費勁。所以用C語言可以更加省事。
先來看看我們內核代碼的最初形態,首先在項目路徑下新建一個project/kernel的目錄,以後我們內核相關的文件都存放於此,在該目錄下新建一個名為main.c的文件,在main.c中鍵入如下代碼:
1 int main(void) 2 { 3 while(1); 4 return 0; 5 }
這就是我們的內核代碼,當然現在什麼都還沒有,就算內核成功載入進去也沒有什麼反應。這裡我們先實現一個自己的列印函數,在main函數中調用這個列印函數來列印出“HELLO KERNEL”的字元,這樣就能測試內核代碼運行是否成功。前面我們一直都是直接操作顯存段的記憶體來往屏幕上來列印字元,現在開始用C語言編程了,自然要封裝一個列印函數來列印字元。
同樣,在項目路徑下新建另一個project/lib/kernel目錄,該目錄用來存放一些供內核使用的庫文件。在該目錄下新建名為print.S和print.h的文件,在此之前,我們在project/lib目錄下新建一個名為stdint.h的文件用來定義一些數據類型。代碼如下:
1 #ifndef __LIB_STDINT_H__
2 #define __LIB_STDINT_H__
3 typedef signed char int8_t;
4 typedef signed short int int16_t;
5 typedef signed int int32_t;
6 typedef signed long long int int64_t;
7 typedef unsigned char uint8_t;
8 typedef unsigned short int uint16_t;
9 typedef unsigned int uint32_t;
10 typedef unsigned long long int uint64_t;
11 #endif
stdint.h
1 TI_GDT equ 0
2 RPL0 equ 0
3 SELECTOR_VIDEO equ (0x0003 << 3) + TI_GDT + RPL0
4
5 section .data
6 put_int_buffer dq 0
7
8 [bits 32]
9 section .text
10 ;-----------------------------------put_str--------------------------------------
11 ;功能描述:put_str通過put_char來列印以0字元結尾的字元串
12 ;----------------------------------------------------------------------------------
13 global put_str
14 put_str:
15 push ebx
16 push ecx
17 xor ecx, ecx
18 mov ebx, [esp + 12]
19 .goon:
20 mov cl, [ebx]
21 cmp cl, 0
22 jz .str_over
23 push ecx
24 call put_char
25 add esp, 4
26 inc ebx
27 jmp .goon
28 .str_over:
29 pop ecx
30 pop ebx
31 ret
32
33 ;--------------------------put_char-------------------------
34 ;功能描述:把棧中的一個字元寫入到游標所在處
35 ;---------------------------------------------------------------
36 global put_char
37 put_char:
38 pushad ;備份32位寄存器環境
39 mov ax, SELECTOR_VIDEO ;不能直接把立即數送入段寄存器中
40 mov gs, ax
41
42 ;----------------------獲取當前游標位置---------------------------------
43 ;先獲取高8位
44 mov dx, 0x03d4
45 mov al, 0x0e
46 out dx, al
47 mov dx, 0x03d5
48 in al, dx
49 mov ah, al
50
51 ;再獲取低8位
52 mov dx, 0x03d4
53 mov al, 0x0f
54 out dx, al
55 mov dx, 0x03d5
56 in al, dx
57
58 ;將游標位置存入bx
59 mov bx, ax
60
61 ;在棧中獲取待列印的字元
62 mov ecx, [esp + 36] ;pushad將8個32位寄存器都壓入棧中,再加上主調函數4位元組的返回地址,所以esp+36之後才是主調函數壓入的列印字元
63 cmp cl, 0xd ;判斷該字元是否為CR(回車),CR的ASCII碼為0x0d
64 jz .is_carriage_return
65
66 cmp cl, 0xa ;判斷該字元是否為LF(換行),LF的ASCII碼為0x0a
67 jz .is_line_feed
68
69 cmp cl, 0x8 ;判斷該字元是否為BS(空格),BS的ASCII碼為0x08
70 jz .is_backspace
71
72 jmp .put_other
73
74 ;字元為BS(空格)的處理辦法
75 .is_backspace:
76 dec bx
77 shl bx, 1
78 mov byte [gs:bx], 0x20
79 inc bx
80 mov byte [gs:bx], 0x07
81 shr bx, 1
82 jmp set_cursor
83
84 ;字元為CR(回車)以及LF(換行)的處理辦法
85 .is_line_feed:
86 .is_carriage_return:
87 xor dx, dx
88 mov ax, bx
89 mov si, 80
90 div si
91 sub bx, dx
92
93 ;CR(回車)符的處理結束
94 .is_carriage_return_end:
95 add bx, 80
96 cmp bx, 2000
97 ;LF(換行)符的處理結束
98 .is_line_feed_end:
99 jl set_cursor
100
101 .put_other:
102 shl bx, 1
103 mov [gs:bx], cl
104 inc bx
105 mov byte [gs:bx], 0x07
106 shr bx, 1
107 inc bx
108 cmp bx, 2000
109 jl set_cursor
110
111 .roll_screen:
112 cld
113 mov ecx, 960
114 mov esi, 0xc00b80a0
115 mov edi, 0xc00b8000
116 rep movsd
117
118 mov ebx, 3840
119 mov ecx, 80
120
121 .cls:
122 mov word [gs:ebx], 0x0720
123 add ebx, 2
124 loop .cls
125 mov bx, 1920
126 global set_cursor
127 set_cursor:
128 mov dx, 0x03d4
129 mov al, 0x0e
130 out dx, al
131 mov dx, 0x03d5
132 mov al, bh
133 out dx, al
134
135 mov dx, 0x03d4
136 mov al, 0x0f
137 out dx, al
138 mov dx, 0x03d5
139 mov al, bl
140 out dx, al
141 .put_char_done:
142 popad
143 ret
144 ;-----------------------------------put_int--------------------------------------
145 ;功能描述:將小端位元組序的數字變成對應的ASCII後,倒置
146 ;輸入:棧中參數為待列印的數字
147 ;輸出:在屏幕中列印十六進位數字,並不會列印首碼0x
148 ;如列印十進位15時,只會列印f,而不是0xf
149 ;----------------------------------------------------------------------------------
150 global put_int
151 put_int:
152 pushad
153 mov ebp, esp
154 mov eax, [ebp + 36]
155 mov edx, eax
156 mov edi, 7
157 mov ecx, 8
158 mov ebx, put_int_buffer
159
160 ;將32位數字按照16進位的形式從低位到高位逐個處理,共處理8個16進位數字
161 .16based_4bits: ; 每4位二進位是16進位數字的1位,遍歷每一位16進位數字
162 and edx, 0x0000000F ; 解析16進位數字的每一位。and與操作後,edx只有低4位有效
163 cmp edx, 9 ; 數字0~9和a~f需要分別處理成對應的字元
164 jg .is_A2F
165 add edx, '0' ; ascii碼是8位大小。add求和操作後,edx低8位有效。
166 jmp .store
167 .is_A2F:
168 sub edx, 10 ; A~F 減去10 所得到的差,再加上字元A的ascii碼,便是A~F對應的ascii碼
169 add edx, 'A'
170
171 ;將每一位數字轉換成對應的字元後,按照類似“大端”的順序存儲到緩衝區put_int_buffer
172 ;高位字元放在低地址,低位字元要放在高地址,這樣和大端位元組序類似,只不過咱們這裡是字元序.
173 .store:
174 ; 此時dl中應該是數字對應的字元的ascii碼
175 mov [ebx+edi], dl
176 dec edi
177 shr eax, 4
178 mov edx, eax
179 loop .16based_4bits
180
181 ;現在put_int_buffer中已全是字元,列印之前,
182 ;把高位連續的字元去掉,比如把字元000123變成123
183 .ready_to_print:
184 inc edi ; 此時edi退減為-1(0xffffffff),加1使其為0
185 .skip_prefix_0:
186 cmp edi,8 ; 若已經比較第9個字元了,表示待列印的字元串為全0
187 je .full0
188 ;找出連續的0字元, edi做為非0的最高位字元的偏移
189 .go_on_skip:
190 mov cl, [put_int_buffer+edi]
191 inc edi
192 cmp cl, '0'
193 je .skip_prefix_0 ; 繼續判斷下一位字元是否為字元0(不是數字0)
194 dec edi ;edi在上面的inc操作中指向了下一個字元,若當前字元不為'0',要恢復edi指向當前字元
195 jmp .put_each_num
196
197 .full0:
198 mov cl,'0' ; 輸入的數字為全0時,則只列印0
199 .put_each_num:
200 push ecx ; 此時cl中為可列印的字元
201 call put_char
202 add esp, 4
203 inc edi ; 使edi指向下一個字元
204 mov cl, [put_int_buffer+edi] ; 獲取下一個字元到cl寄存器
205 cmp edi,8
206 jl .put_each_num
207 popad
208 ret
print.S
1 #ifndef __LIB_KERNEL_PRINT_H
2 #define __LIB_KERNEL_PRINT_H
3 #include "stdint.h"
4 void put_char(uint8_t char_asci);
5 void put_str(char *message);
6 void put_int(uint32_t num);
7 #endif
print.h
最後輸入如下命令來編譯print.S:
nasm -f elf -o ./project/lib/kernel/print.o ./project/lib/kernel/print.S
完善了列印函數後,我們現在可以在main函數中實現列印功能了,修改main.c文件:
1 #include "print.h" 2 int main(void) 3 { 4 put_str("HELLO KERNEL\n"); 5 while(1); 6 return 0; 7 }
前面我們已經將內核代碼實現完成了,接下來按道理應該和前面一樣,將main.c文件編譯載入到硬碟中,隨後通過loader來讀取載入該文件,最終跳轉運行。的確也是如此,不過略有不同。請聽我慢慢講來。
現在我們是main.c文件,不同於彙編代碼,我們接下來要使用gcc工具將main.c文件編譯成main.o文件:
gcc -m32 -I project/lib/kernel/ -c -fno-builtin project/kernel/main.c -o project/kernel/main.o
它只是一個目標文件,也稱為重定位文件,重定位文件指的是文件裡面所用的符號還沒有安排地址,這些符號的地址將來是要與其他目標文件“組成”一個可執行文件時再重定位(編排地址),這裡的符號就是指的所調用的函數或使用的變數,看我們的main.c文件中,在main函數中調用了print.h中聲明的put_str函數,所以將來main.o文件需要和print.o文件一起組成可執行文件。
如何“組成”呢?這裡的“組成”其實就是指的C語言程式變成可執行文件下的四步驟(預處理、編譯、彙編和鏈接)中的鏈接,Linux下使用的是ld命令來鏈接,我們是在Linux平臺下的,所以自然使用ld命令:
ld -m elf_i386 -Ttext 0xc0001500 -e main -o project/kernel/kernel.bin project/kernel/main.o project/lib/kernel/print.o
最終生成可執行文件kernel.bin。它就是我們需要載入到硬碟里的那個文件。
到這裡都和前面步驟一致,只是後面loader並不是單純的將kernel.bin文件拷貝到記憶體某處再跳轉執行。這是因為我們生成的kernel.bin文件的格式為elf,elf格式的文件,在文件最開始有一個名為elf格式頭的部分,該部分詳細包含了整個文件的信息,具體內容過多我這裡不再展開講,感興趣的朋友可以參考原書《操作系統真象還原》p213~222,或者百度。所以說如果我們只是單純地跳轉到該文件的載入處,那麼必定會出現問題,因為該文件的開始部分並不是可供CPU執行的程式,我們跳轉的地址應該是該文件的程式部分。這個地址在我們前面鏈接時已經指定為0xc0001500,因為我們前面已經開啟了分頁機制,所以實際上這個地址對應的是物理地址的0x1500處。
接下來再修改loader.S文件,增加拷貝內核部分代碼以及拷貝函數代碼,為了便於閱讀,我將新代碼附在了之前的loader.S文件下,除此之外,boot.inc也有新增的內容。
1 %include "boot.inc"
2 section loader vstart=LOADER_BASE_ADDR
3 LOADER_STACK_TOP equ LOADER_BASE_ADDR
4 jmp loader_start
5
6 ;構建gdt及其內部描述符
7 GDT_BASE: dd 0x00000000
8 dd 0x00000000
9 CODE_DESC: dd 0x0000FFFF
10 dd DESC_CODE_HIGH4
11 DATA_STACK_DESC: dd 0x0000FFFF
12 dd DESC_DATA_HIGH4
13 VIDEO_DESC: dd 0x80000007
14 dd DESC_VIDEO_HIGH4
15
16 GDT_SIZE equ $-GDT_BASE
17 GDT_LIMIT equ GDT_SIZE-1
18 times 60 dq 0 ;此處預留60個描述符的空位
19
20 SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0
21 SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0
22 SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0
23
24 ;以下是gdt指針,前2個位元組是gdt界限,後4個位元組是gdt的起始地址
25 gdt_ptr dw GDT_LIMIT
26 dd GDT_BASE
27
28 ;---------------------進入保護模式------------
29 loader_start:
30 ;一、打開A20地址線
31 in al, 0x92
32 or al, 0000_0010B
33 out 0x92, al
34
35 ;二、載入GDT
36 lgdt [gdt_ptr]
37
38 ;三、cr0第0位(pe)置1
39 mov eax, cr0
40 or eax, 0x00000001
41 mov cr0, eax
42
43 jmp dword SELECTOR_CODE:p_mode_start ;刷新流水線
44
45 [bits 32]
46 p_mode_start:
47 mov ax, SELECTOR_DATA
48 mov ds, ax
49 mov es, ax
50 mov ss, ax
51 mov esp, LOADER_STACK_TOP
52 mov ax, SELECTOR_VIDEO
53 mov gs, ax
54
55 mov byte [gs:160], 'p'
56
57 ;------------------開啟分頁機制-----------------
58 ;一、創建頁目錄表並初始化頁記憶體點陣圖
59 call setup_page
60
61 ;將描述符表地址及偏移量寫入記憶體gdt_ptr,一會兒用新地址重新載入
62 sgdt [gdt_ptr]
63 ;將gdt描述符中視頻段描述符中的段基址+0xc0000000
64 mov ebx, [gdt_ptr + 2]
65 or dword [ebx + 0x18 + 4], 0xc0000000
66
67 ;將gdt的基址加上0xc0000000使其成為內核所在的高地址
68 add dword [gdt_ptr + 2], 0xc0000000
69
70 add esp, 0xc0000000 ;將棧指針同樣映射到內核地址
71
72 ;二、將頁目錄表地址賦值給cr3
73 mov eax, PAGE_DIR_TABLE_POS
74 mov cr3, eax
75
76 ;三、打開cr0的pg位
77 mov eax, cr0
78 or eax, 0x80000000
79 mov cr0, eax
80
81 ;在開啟分頁後,用gdt新的地址重新載入
82 lgdt [gdt_ptr]
83 mov byte [gs:160], 'H'
84 mov byte [gs:162], 'E'
85 mov byte [gs:164], 'L'
86 mov byte [gs:166], 'L'
87 mov byte [gs:168], 'O'
88 mov byte [gs:170], ' '
89 mov byte [gs:172], 'P'
90 mov byte [gs:174], 'A'
91 mov byte [gs:176], 'G'
92 mov byte [gs:178], 'E'
93
94 ;---------------------------------------------
95
96 ;--------------------拷貝內核文件併進入kernel--------------------------
97 mov eax, KERNEL_START_SECTOR ;kernel.bin所在的扇區號 0x09
98 mov ebx, KERNEL_BIN_BASE_ADDR ;從磁碟讀出後,寫入到ebx指定的地址0x70000
99 mov ecx, 200 ;讀入的扇區數
100
101 call rd_disk_m_32
102
103 ;由於一直處在32位下,原則上不需要強制刷新,但是以防萬一還是加上
104 ;跳轉到kernel處
105 jmp SELECTOR_CODE:enter_kernel
106
107 enter_kernel:
108 call kernel_init
109 mov esp, 0xc009f000 ;更新棧底指針
110 jmp KERNEL_ENTRY_POINT ;內核地址0xc0001500
111 ;jmp $
112 ;---------------------將kernel.bin中的segment拷貝到指定的地址
113 kernel_init:
114 xor eax, eax
115 xor ebx, ebx ;ebx記錄程式頭表地址
116 xor ecx, ecx ;cx記錄程式頭表中的program header數量
117 xor edx, edx ;dx記錄program header 尺寸,即e_phentsize
118
119 ;偏移文件42位元組處的屬性是e_phentsize, 表示program header大小
120 mov dx, [KERNEL_BIN_BASE_ADDR + 42]
121
122 ;偏移文件28位元組處的屬性是e_phoff
123 mov ebx, [KERNEL_BIN_BASE_ADDR + 28]
124
125 add ebx, KERNEL_BIN_BASE_ADDR
126 mov cx, [KERNEL_BIN_BASE_ADDR + 44]
127
128 .each_segment:
129 cmp byte [ebx + 0], PT_NULL
130 je .PTNULL
131
132 ;為函數memcpy壓入參數,參數是從右往左壓入
133 push dword [ebx + 16]
134 mov eax, [ebx + 4]
135 add eax, KERNEL_BIN_BASE_ADDR
136 push eax
137 push dword [ebx + 8]
138 call mem_cpy
139 add esp, 12
140
141 .PTNULL:
142 add ebx, edx
143 loop .each_segment
144 ret
145
146 ;-----------逐位元組拷貝mem_cpy(dst, src, size)
147 mem_cpy:
148 cld
149 push ebp
150 mov ebp, esp
151 push ecx
152 mov edi, [ebp + 8]
153 mov esi, [ebp + 12]
154 mov ecx, [ebp + 16]
155 rep movsb
156
157 pop ecx
158 pop ebp
159 ret
160 ;---------------------------------------------------
161
162 ;--------------函數聲明------------------------
163 ;setup_page:(功能)設置分頁------------
164 setup_page:
165 ;先把頁目錄