[自制操作系统]第09回加载内核

博客 分享
0 96
张三
张三 2022-06-20 17:00:46
悬赏:0 积分 收藏

[自制操作系统] 第09回 加载内核

目录
一、前景回顾
二、用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_cursor100 101 .put_other:102         shl bx, 1103         mov [gs:bx], cl104         inc bx105         mov byte [gs:bx], 0x07106         shr bx, 1107         inc bx108         cmp bx, 2000109         jl set_cursor110 111 .roll_screen:112         cld113         mov ecx, 960114         mov esi, 0xc00b80a0115         mov edi, 0xc00b8000116         rep movsd117         118         mov ebx, 3840119         mov ecx, 80120 121 .cls:122         mov word [gs:ebx], 0x0720123         add ebx, 2124         loop .cls125         mov bx, 1920126 global set_cursor127 set_cursor:128         mov dx, 0x03d4129         mov al, 0x0e130         out dx, al131         mov dx, 0x03d5132         mov al, bh133         out dx, al134 135         mov dx, 0x03d4136         mov al, 0x0f137         out dx, al138         mov dx, 0x03d5139         mov al, bl140         out dx, al141 .put_char_done:142         popad143         ret144 ;-----------------------------------put_int--------------------------------------145 ;功能描述:将小端字节序的数字变成对应的ASCII后,倒置146 ;输入:栈中参数为待打印的数字147 ;输出:在屏幕中打印十六进制数字,并不会打印前缀0x148 ;如打印十进制15时,只会打印f,而不是0xf149 ;----------------------------------------------------------------------------------150 global put_int151 put_int:152         pushad153         mov ebp, esp154         mov eax, [ebp + 36]155         mov edx, eax156         mov edi, 7157         mov ecx, 8158         mov ebx, put_int_buffer159 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 .store167 .is_A2F:168         sub edx, 10                   ; A~F 减去10 所得到的差,再加上字符A的ascii码,便是A~F对应的ascii码169         add edx, 'A'170 171 ;将每一位数字转换成对应的字符后,按照类似“大端”的顺序存储到缓冲区put_int_buffer172 ;高位字符放在低地址,低位字符要放在高地址,这样和大端字节序类似,只不过咱们这里是字符序.173 .store:174 ; 此时dl中应该是数字对应的字符的ascii码175         mov [ebx+edi], dl               176         dec edi177         shr eax, 4178         mov edx, eax 179         loop .16based_4bits180 181 ;现在put_int_buffer中已全是字符,打印之前,182 ;把高位连续的字符去掉,比如把字符000123变成123183 .ready_to_print:184         inc edi                   ; 此时edi退减为-1(0xffffffff),加1使其为0185 .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 edi192         cmp cl, '0' 193         je .skip_prefix_0               ; 继续判断下一位字符是否为字符0(不是数字0)194         dec edi                   ;edi在上面的inc操作中指向了下一个字符,若当前字符不为'0',要恢复edi指向当前字符               195         jmp .put_each_num196 197 .full0:198         mov cl,'0'                   ; 输入的数字为全0时,则只打印0199 .put_each_num:200         push ecx                   ; 此时cl中为可打印的字符201         call put_char202         add esp, 4203         inc edi                   ; 使edi指向下一个字符204         mov cl, [put_int_buffer+edi]           ; 获取下一个字符到cl寄存器205         cmp edi,8206         jl .put_each_num207         popad208         ret
print.S
1 #ifndef  __LIB_KERNEL_PRINT_H2 #define  __LIB_KERNEL_PRINT_H3 #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_32102 103     ;由于一直处在32位下,原则上不需要强制刷新,但是以防万一还是加上104     ;跳转到kernel处105     jmp SELECTOR_CODE:enter_kernel106     107     enter_kernel:108         call kernel_init109         mov esp, 0xc009f000               ;更新栈底指针110         jmp KERNEL_ENTRY_POINT            ;内核地址0xc0001500111         ;jmp $112         ;---------------------将kernel.bin中的segment拷贝到指定的地址113         kernel_init:114             xor eax, eax115             xor ebx, ebx   ;ebx记录程序头表地址116             xor ecx, ecx    ;cx记录程序头表中的program header数量117             xor edx, edx    ;dx记录program header 尺寸,即e_phentsize118 119             ;偏移文件42字节处的属性是e_phentsize, 表示program header大小120             mov dx, [KERNEL_BIN_BASE_ADDR + 42]121             122             ;偏移文件28字节处的属性是e_phoff123             mov ebx, [KERNEL_BIN_BASE_ADDR + 28]124 125             add ebx, KERNEL_BIN_BASE_ADDR126             mov cx, [KERNEL_BIN_BASE_ADDR + 44]127     128             .each_segment: 129                     cmp byte [ebx + 0], PT_NULL130                     je .PTNULL131 132             ;为函数memcpy压入参数,参数是从右往左压入133             push dword [ebx + 16]134             mov eax, [ebx + 4]135             add eax, KERNEL_BIN_BASE_ADDR136             push eax137             push dword [ebx + 8]138             call mem_cpy139             add esp, 12140 141             .PTNULL:142                     add ebx, edx143                     loop .each_segment144             ret145 146             ;-----------逐字节拷贝mem_cpy(dst, src, size)147             mem_cpy:148                     cld149                     push ebp150                     mov ebp, esp151                     push ecx152                     mov edi, [ebp + 8]153                     mov esi, [ebp + 12]154                     mov ecx, [ebp + 16]155                     rep movsb156 157                     pop ecx158                     pop ebp159                     ret 160 ;---------------------------------------------------    161 162 ;--------------函数声明------------------------163     ;setup_page:(功能)设置分页------------164     setup_page:165         ;先把页目录占用的空间逐字节清0166         mov ecx, 4096167         mov esi, 0168         .clear_page_dir:169                 mov byte [PAGE_DIR_TABLE_POS + esi], 0170                 inc esi171         loop .clear_page_dir172         173         ;开始创建页目录项174         .create_pde:175                 mov eax, PAGE_DIR_TABLE_POS176                 add eax, 0x1000             ;此时eax为第一个页表的位置177                 mov ebx, eax178         179         ;下面将页目录项0和0xc00都存为第一个页表的地址,每个页表表示4MB内存180         ;页目录表的属性RW和P位为1,US为1,表示用户属性,所有特权级别都可以访问181         or eax, PG_US_U | PG_RW_W | PG_P182         183         ;在页目录表中的第1个目录项中写入第一个页表的地址(0x101000)和属性184         mov [PAGE_DIR_TABLE_POS + 0x0], eax185 186         mov [PAGE_DIR_TABLE_POS + 0xc00], eax187 188         ;使最后一个目录项指向页目录表自己的地址189         sub eax, 0x1000190         mov [PAGE_DIR_TABLE_POS + 4092], eax191 192         ;下面创建页表项(PTE)193         mov ecx, 256     ;1M低端内存/每页大小4K=256194         mov esi, 0195         mov edx, PG_US_U | PG_RW_W | PG_P196         .create_pte:     ;创建page table entry197                 mov [ebx + esi*4], edx198                 add edx, 4096199                 inc esi200         loop .create_pte201         202         ;创建内核其他页表的PDE203         mov eax, PAGE_DIR_TABLE_POS204         add eax, 0x2000           ;此时eax为第二个页表的位置205         or eax, PG_US_U | PG_RW_W | PG_P206         mov ebx, PAGE_DIR_TABLE_POS207         mov ecx, 254              ;范围为第769~1022的所有目录项数量208         mov esi, 769 209         .create_kernel_pde:210                 mov [ebx + esi*4], eax211                 inc esi212                 add eax, 0x1000213         loop .create_kernel_pde214         ret215         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,0x1f2224         mov al,cl225         out dx,al226         227         mov eax,esi228 229         ;第二步:将lba地址存入到0x1f3 ~ 0x1f6230                 ;lba地址7-0位写入端口0x1f3231         mov dx,0x1f3232         out dx,al233         234         ;lba地址15-8位写入端口0x1f4235         mov cl,8236         shr eax,cl237         mov dx,0x1f4238         out dx,al239         240         ;lba地址23-16位写入端口0x1f5241         shr eax,cl242         mov dx,0x1f5243         out dx,al244                 245         shr eax,cl246         and al,0x0f247         or al,0xe0248         mov dx,0x1f6249         out dx,al250 251     ;第三步:向0x1f7端口写入读命令,0x20252         mov dx,0x1f7253         mov al,0x20254         out dx,al255 256     ;第四步:检测硬盘状态257         .not_ready:258                 nop259                 in al,dx260                 and al,0x88261                 cmp al,0x08262                 jnz .not_ready263 264     ;第五步:从0x1f0端口读数据265         mov ax,di266         mov dx,256267         mul dx268         mov cx,ax269     ;di为要读取的扇区数,一个扇区共有512字节,每次读入一个字,总共需要270     ;di*512/2次,所以di*256271         mov dx,0x1f0272         .go_on_read:273                 in ax,dx274                 mov [ebx],ax275                 add ebx,2276                 loop .go_on_read277                 ret278 ;----------------------------------------------
loader.S
 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 ;使用平坦模型,所以需要将段大小设置为4GB11 DESC_G_4K equ 100000000000000000000000b     ;表示段大小为4G12 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  000000000000000b20 DESC_DPL_1  equ  010000000000000b21 DESC_DPL_2  equ  100000000000000b22 DESC_DPL_3  equ  110000000000000b23 DESC_S_CODE equ  1000000000000b  ;S等于1表示非系统段,0表示系统段24 DESC_S_DATA equ  DESC_S_CODE25 DESC_S_sys  equ  0000000000000b26 DESC_TYPE_CODE  equ  100000000000b ;x=1,c=0,r=0,a=0 代码段是可执行的,非一致性,不可读,已访问位a清027 DESC_TYPE_DATA  equ  001000000000b ;x=0,e=0,w=1,a=0 数据段是不可执行的,向上拓展,可写,已访问位a清028 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 + 0x0B33 34 35 ;------------选择子属性------------36 RPL0 equ 00b37 RPL1 equ 01b38 RPL2 equ 10b39 RPL3 equ 11b40 TI_GDT equ 000b41 TI_LDT equ 100b42 43 ;---------------页表相关属性----------------44 PG_P    equ  1b45 PG_RW_R equ  00b46 PG_RW_W equ  10b47 PG_US_S equ  000b48 PG_US_U equ  100b
boot.inc

  来看代码,首先调用函数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开始,栈向下发展,我们的内核是不会和栈发生冲突的。

四、运行测试

  首先将前面生成的可执行文件kernel.bin,也就是我们最终的内核文件使用dd命令将其写入到硬盘中去,这里记得还要重新编译loader.S以及加载loader.bin文件,因为loader.S也被修改了。

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

  这里我们count的参数为200,意为向硬盘一次性写入两百个扇区,当然我们的内核文件现在还没有这么大,Seek=9表示跳过前面9个扇区,从第10个扇区开始存放(以LBA法计算)。启动boch,最终得到如下画面:
  
  说明我们的内核文件成功写入并且加载成功了。虽然只是一小步,但是却是我们整个操作系统学习的一大步,到了这里我们整个操作系统的基本框架算是搭建完毕了,接下来就是不断地进行完善内核文件即可。

  欲知后事如何,请看下回分解。

posted @ 2022-06-20 16:38 李知行 阅读(0) 评论(0) 编辑 收藏 举报
回帖
    张三

    张三 (王者 段位)

    821 积分 (2)粉丝 (41)源码

     

    温馨提示

    亦奇源码

    最新会员