C语言编写内核代码的最初形态,你知道吗?

目录

一、前景回顾

在这个时候开始,我们将开始编写内核代码。在此之前,让我们梳理一下已经完成的工作。

蓝色部分是到目前为止已经完成的部分,黄色部分是本节将要实现的部分。

二、用C编写内核

为什么要用C语言编写内核?其实也可以用汇编语言来实现,但是对于我们来说,C语言代码肯定比汇编语言更容易看懂,而且看起来也没有那么费劲。所以使用C语言会更方便。

让我们看一下我们的内核代码的初始形式。首先,在项目路径下新建一个项目/内核目录。之后,我们所有的内核相关文件都存储在这里。在此目录中创建一个名为 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

标准字符集

  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 

图片[1]-C语言编写内核代码的最初形态,你知道吗?-唐朝资源网

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

印刷

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

打印.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 KERNELn");
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可以执行的程序,而我们跳转到的地址应该是程序的程序部分文件。

接下来修改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     

图片[2]-C语言编写内核代码的最初形态,你知道吗?-唐朝资源网

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 ;先把页目录占用的空间逐字节清0 166 mov ecx, 4096 167 mov esi, 0 168 .clear_page_dir: 169 mov byte [PAGE_DIR_TABLE_POS + esi], 0 170 inc esi 171 loop .clear_page_dir 172 173 ;开始创建页目录项 174 .create_pde: 175 mov eax, PAGE_DIR_TABLE_POS 176 add eax, 0x1000 ;此时eax为第一个页表的位置 177 mov ebx, eax 178 179 ;下面将页目录项0和0xc00都存为第一个页表的地址,每个页表表示4MB内存 180 ;页目录表的属性RW和P位为1,US为1,表示用户属性,所有特权级别都可以访问 181 or eax, PG_US_U | PG_RW_W | PG_P 182 183 ;在页目录表中的第1个目录项中写入第一个页表的地址(0x101000)和属性 184 mov [PAGE_DIR_TABLE_POS + 0x0], eax 185 186 mov [PAGE_DIR_TABLE_POS + 0xc00], eax 187 188 ;使最后一个目录项指向页目录表自己的地址 189 sub eax, 0x1000 190 mov [PAGE_DIR_TABLE_POS + 4092], eax 191 192 ;下面创建页表项(PTE) 193 mov ecx, 256 ;1M低端内存/每页大小4K=256 194 mov esi, 0 195 mov edx, PG_US_U | PG_RW_W | PG_P 196 .create_pte: ;创建page table entry 197 mov [ebx + esi*4], edx 198 add edx, 4096 199 inc esi 200 loop .create_pte 201 202 ;创建内核其他页表的PDE 203 mov eax, PAGE_DIR_TABLE_POS 204 add eax, 0x2000 ;此时eax为第二个页表的位置 205 or eax, PG_US_U | PG_RW_W | PG_P 206 mov ebx, PAGE_DIR_TABLE_POS 207 mov ecx, 254 ;范围为第769~1022的所有目录项数量 208 mov esi, 769 209 .create_kernel_pde: 210 mov [ebx + esi*4], eax 211 inc esi 212 add eax, 0x1000 213 loop .create_kernel_pde 214 ret 215 216 ;rd_disk_m_32:(功能)读取硬盘n个扇区------------ 217 rd_disk_m_32: 218 mov esi,eax ;备份eax,eax中存放了扇区号 219 mov di,cx ;备份cx,cx中存放待读入的扇区数 220 221 ;读写硬盘: 222 ;第一步:设置要读取的扇区数 223 mov dx,0x1f2 224 mov al,cl 225 out dx,al 226 227 mov eax,esi 228 229 ;第二步:将lba地址存入到0x1f3 ~ 0x1f6 230 ;lba地址7-0位写入端口0x1f3 231 mov dx,0x1f3 232 out dx,al 233 234 ;lba地址15-8位写入端口0x1f4 235 mov cl,8 236 shr eax,cl 237 mov dx,0x1f4 238 out dx,al 239 240 ;lba地址23-16位写入端口0x1f5 241 shr eax,cl 242 mov dx,0x1f5 243 out dx,al 244 245 shr eax,cl 246 and al,0x0f 247 or al,0xe0 248 mov dx,0x1f6 249 out dx,al 250 251 ;第三步:向0x1f7端口写入读命令,0x20 252 mov dx,0x1f7 253 mov al,0x20 254 out dx,al 255 256 ;第四步:检测硬盘状态 257 .not_ready: 258 nop 259 in al,dx 260 and al,0x88 261 cmp al,0x08 262 jnz .not_ready 263 264 ;第五步:从0x1f0端口读数据 265 mov ax,di 266 mov dx,256 267 mul dx 268 mov cx,ax 269 ;di为要读取的扇区数,一个扇区共有512字节,每次读入一个字,总共需要 270 ;di*512/2次,所以di*256 271 mov dx,0x1f0 272 .go_on_read: 273 in ax,dx 274 mov [ebx],ax 275 add ebx,2 276 loop .go_on_read 277 ret 278 ;----------------------------------------------

装载机

 1 ;--------------------loader和kernel ---------------
 2 LOADER_BASE_ADDR    equ 0x900
 3 LOADER_START_SECTOR equ 0x2
 4 PAGE_DIR_TABLE_POS  equ 0x100000
 5 KERNEL_START_SECTOR equ 0x9
 6 KERNEL_BIN_BASE_ADDR equ 0x70000 
 7 KERNEL_ENTRY_POINT equ 0xc0001500
 8 PT_NULL equ 0
 9 ;-------------------gdt描述符属性------------------
10 ;使用平坦模型,所以需要将段大小设置为4GB
11 DESC_G_4K equ 100000000000000000000000b     ;表示段大小为4G
12 DESC_D_32 equ 10000000000000000000000b      ;表示操作数与有效地址均为32位
13 DESC_L    equ 0000000000000000000000b       ;表示32位代码段
14 DESC_AVL  equ 000000000000000000000b        ;忽略
15 DESC_LIMIT_CODE2  equ  11110000000000000000b   ;代码段的段界限的第2部分
16 DESC_LIMIT_DATA2  equ  DESC_LIMIT_CODE2            ;相同的值  数据段与代码段段界限相同
17 DESC_LIMIT_VIDEO2 equ    00000000000000000000b      ;第16-19位 显存区描述符VIDEO2 书上后面的0少打了一位 这里的全是0为高位 低位即可表示段基址
18 DESC_P      equ  1000000000000000b      ;p判断段是否在内存中,1表示在内存中
19 DESC_DPL_0  equ  000000000000000b
20 DESC_DPL_1  equ  010000000000000b
21 DESC_DPL_2  equ  100000000000000b
22 DESC_DPL_3  equ  110000000000000b
23 DESC_S_CODE equ  1000000000000b  ;S等于1表示非系统段,0表示系统段
24 DESC_S_DATA equ  DESC_S_CODE
25 DESC_S_sys  equ  0000000000000b
26 DESC_TYPE_CODE  equ  100000000000b ;x=1,c=0,r=0,a=0 代码段是可执行的,非一致性,不可读,已访问位a清0
27 DESC_TYPE_DATA  equ  001000000000b ;x=0,e=0,w=1,a=0 数据段是不可执行的,向上拓展,可写,已访问位a清0
28 
29 DESC_CODE_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_CODE2 + DESC_P + DESC_DPL_0 + DESC_S_CODE + DESC_TYPE_CODE + 0x00 ;代码段的高四个字节内容
30 DESC_DATA_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_DATA2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x00 ;数据段的高四个字节内容
31 
32 DESC_VIDEO_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_VIDEO2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x0B
33 
34 
35 ;------------选择子属性------------
36 RPL0 equ 00b
37 RPL1 equ 01b
38 RPL2 equ 10b
39 RPL3 equ 11b
40 TI_GDT equ 000b
41 TI_LDT equ 100b
42 
43 ;---------------页表相关属性----------------
44 PG_P    equ  1b
45 PG_RW_R equ  00b
46 PG_RW_W equ  10b
47 PG_US_S equ  000b
48 PG_US_U equ  100b

引导程序

看代码,首先调用函数rd_disk_m_32将kernel.bin文件从硬盘拷贝到地址KERNEL_BIN_BASE_ADDR,即0x70000。

enter_kernel 是一个进入内核的函数。首先,调用 kernel_init 函数。在这个函数中,解析复制到地址0x70000的kernel.bin文件,将程序部分复制到地址0xc0001500,然后跳转到那里。

这就是为什么它在地址0xc0001500,物理内存中的0x900是loader.bin的加载地址,这个地址的开头是GDT。GDT 将一直使用,不能被覆盖。预计loader.bin的大小不会超过2000字节,前面我们提到内核要放在loader之上,因为内核会不断增长,所以我们可选的物理地址为0x900+2000= 0x10d0,我们在组成整数时选择0x1500作为内核。入口地址,你不必奇怪为什么是这个地址,只是凭感觉设计的。因为我们的记忆比较松散,没必要这么紧凑。

进入内核后,我们修改了栈顶指针,不再是之前的0x900。查看内存布局可知,地址 0x7E00~0x9FBFF 之间大约有 630KB 的未使用空间,因此我们选择地址 0x9F000 作为栈顶。考虑到未来内核的扩展,预计只有70KB。我们的内核从 0x1500 开始,栈向下发展。我们的内核不会与堆栈冲突。

四、运行测试

首先,使用dd命令将之前生成的可执行文件kernel.bin,也就是我们最终的内核文件,写入硬盘。记得重新编译loader.S并加载loader.bin文件,因为loader.s。S也进行了修改。

dd if=./project/kernel/kernel.bin of=./hd60M.img bs=512 count=200 seek=9 conv=notrunc

这里我们count的参数是200,意思是一次向硬盘写入200个扇区。当然,我们的内核文件现在没有那么大了。Seek=9 表示跳过前 9 个扇区,从第 10 个扇区开始。开始存储区域(通过 LBA 方法计算)。启动boch,最终得到如下画面:

说明我们的内核文件写入成功,加载成功。虽然只是一小步,但却是我们整个操作系统学习的一大步。至此,我们整个操作系统的基本框架就完成了,接下来就是不断完善内核文件。

要了解接下来发生的事情,请参阅下一个细分。

© 版权声明
THE END
喜欢就支持一下吧
点赞191赞赏 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容