3

[自制操作系统] 第09回 加载内核 - 李知行

 1 year ago
source link: https://www.cnblogs.com/Lizhixing/p/16393527.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

目录
一、前景回顾
二、用C语言编写内核
三、加载内核
四、运行测试

一、前景回顾

  本回开始,我们要开始编写内核代码了,在此之前,先梳理一下已经完成的工作。

2593960-20220620153709087-1919766914.png

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

二、用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的文件用来定义一些数据类型。代码如下:

ContractedBlock.gifExpandedBlockStart.gif

stdint.h

ContractedBlock.gifExpandedBlockStart.gif

print.S

ContractedBlock.gifExpandedBlockStart.gif

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也有新增的内容。

ContractedBlock.gifExpandedBlockStart.gif

loader.S

ContractedBlock.gifExpandedBlockStart.gif

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,最终得到如下画面:

2593960-20220620163054612-616494611.png

  说明我们的内核文件成功写入并且加载成功了。虽然只是一小步,但是却是我们整个操作系统学习的一大步,到了这里我们整个操作系统的基本框架算是搭建完毕了,接下来就是不断地进行完善内核文件即可。

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK