其实,我们等了这一刻好久好久,即使我不说,大家也有这样的认识,linux内核是用c 语言写的,咱们肯定也要用c语言。其实...说点伤感情的话,今后的工作只是大部分(99%)都要用c语言来写,还有一些要用到汇编的地方。大家也不要因此气馁心灰(其实突然不用汇编还会想它呢,这不是玩笑),我在此过程中一定会尽我所能让内容简单易接受。
我们的内核文件是kernel.bin,这个文件是由loader将其从硬盘上读出并加载到内存中的,到此,接力棒传到了最后一个选手的手里。也就是说,咱们需要事先把kernel.bin定入硬盘。好久不往虚拟硬盘上写东西了,甭说是大家,我都有点陌生了呢,不过好在操作很简单,写之前让我们先看看这块虚拟硬盘上的文件布局吧。
MBR是写在了硬盘的第0扇区,第1扇区是空着的,原因是个人喜好,其实不空着也行,不过硬盘那么大,何必搞得那么拥挤呢。因此loader是写在硬盘的第2扇区,由于loader.bin目前的大小是1342字节,占用3个扇区,所以第2~4扇区不能再用啦,从第5扇区起我们可以自由使用。但此时我的强迫症又发作啦,我这里并没有接着第5扇区写,而是选的第9扇区(要是起始为1的话算是第10个扇区)。一是为了loader万一哪天要扩展,得预留出硬盘空间,二是您可能已经预计到了,隔开点显得更放心,这纯属是出于个人喜好做出的选择。
好,既然已经确定了写入扇区的位置,我们还是要通过dd命令往磁盘上写,命令如下:
dd if= kernel.bin of=/your_path/hd60M.img bs=512 count=200 seek=9 conv=notrunc回车
seek为9,目的是跨过前9个扇区(第0~8个扇区),我们是在第9个扇区写入。
count为200,目的是一次往参数of指定的文件中写入200个扇区。
至于为什么把count设成这么大,原因是这样的:每次写完内核后,咱们要往磁盘中同步内核文件,这样才能验证内核的正确性。按理说,咱们现在的内核文件不足4扇区,count=4最合适。不过,内核发展越来越大时,每次都要根据实际内核文件大小去改写count参数,这样就难免会有忘记修改的情况。之前我就深受其苦,内核文件变大了,而count忘记调整,造成写入硬盘中的内核文件不完整,所以到后来,程序运行不受控制,以至于调试的时候都调晕啦,看着cpu中跑的指令我完全蒙圈了,根本不是自己写的。恍然大悟之后,我就干脆一步到位,因为我们将来的内核大小不会超过100KB,所以直接把count改为200块扇区。另外请大家不用担心,dd命令会自己判断写入的数据量,如果参数if指定的文件体积小于count*bs,只按实际文件大小写入。
不过,估计您也觉得参数太多了,为了方便,我通常是把下面三个命令,编译、链接、再写入硬盘一起完成,您可以将它们写成一个脚本,脚本内容如下:
gcc -c -o main.o main.c && ld main.o -Ttext 0xc0001500 -e main -o kernel.bin && dd if= kernel.bin of=/your_path/hd60M.img bs=512 count=200 seek=9 conv=notrunc
好啦,上面命令在回车之后,这样我们的内核文件就成功写进磁盘了。
菜配好啦,就等下锅啦,我们的内核是由loader加载的,所以我们还要去修改下loader.S。
loader.S需要修改两个地方:
- λ加载内核:需要把内核文件加载到内存缓冲区。
- λ初始化内核:需要在分页后,将加载进来的elf内核文件安置到相应的虚拟内存地址,然后跳过去执行,从此loader的工作结束。
先说第一个加载内核,这里所说的加载内核只是把内核从硬盘上拷贝到内存中,并不是运行内核代码。这项工作在开启分页前后都可以,不过为了简单,咱们把它安排在分页开启之前加载。
话说内核加载到内存中,得有个加载地址,也就是缓冲区。其实开发经验少的同学对缓冲区这个概念总是觉得有点“只可意会不可言传”的意思。借此机会多说两句。缓冲区,buffer,意味存放物品的地点,也就是用于加工处理中暂存数据的地方。生活中的缓冲区例子有很多,比如水杯是水的缓冲区,水不是直接入口的,总有个中间载体做为中转,然后才入口。而且,水杯的作用相当于暖瓶或水房的缓存,咱们不是喝一口水就跑到水房接一口水,而是一次接一大杯,回来慢慢喝,这样就减少了去水房的次数。由此可见,缓冲区,既有存放数据的空间之意,又有提高效率的缓存之意。换在计算机世界里,缓冲区必然也是个能存储数据的介质,比如咱们这里所说的内存。
好啦,不能扯太远啦,咱们的缓冲区在设在哪里呢,这不是乱放的,得参考下目前内存中哪个地方还有可用的空间,千万不能覆盖了重要数据。也许大家首先想到的是很久之前说到的那个内存布局图,赞,答对啦,不过,大家不用往前翻看啦,一向体贴的我已经将其重点部分摘到这里啦,大家请看图
内核被加载到内存后,loader还要通过分析其elf结构将其展开到新的位置,所以说,内核在内存中是有两份拷贝,一份是elf格式的原文件kernel.bin,另一份是loader解析elf格式的kernel.bin后在内存中生成的内核映像(也就是将程序中的各种段segment复制到内存后的程序体),这个映像才是真正运行的内核。
将来内核肯定是越来越大,为了多预留出生长空间,咱们要将内核文件kernel.bin加载到地址较高的空间,而内核映像要放置到较低的地址。内核文件经过loader解析后就没用啦,这样内核映像将来往高地址处扩展时,也可以覆盖原来的内核文件kernel.bin。所以咱们的结论是,在0x7e00~0x9fbff这片区域的高地址中找一亩地给kernel.bin,这里我擅自做主啦,帮大家选的是0x70000。为什么?没有为什么,随意选的,取了个整而已,就是觉得0x70000~0x9fbff有0x2fbff=190KB字节的空间,而我们的内核不超过100KB,够用就行。
好,万事俱备啦,代码走起,请大家过目代码
147 ; ------------------------- 加载kernel ----------------------
148 mov eax, KERNEL_START_SECTOR ; kernel.bin所在的扇区号
149 mov ebx, KERNEL_BIN_BASE_ADDR; 从磁盘读出后,写入到ebx指定的地址
150 mov ecx, 200 ; 读入的扇区数
151
152 call rd_disk_m_32
153
154 ; 创建页目录及页表并初始化页内存位图
155 call setup_page
代码属于loader的一部分,它的作用是把内核文件从硬盘上加载到内存中,下面简要说一下。
第148~149行的KERNEL_START_SECTOR和KERNEL_BIN_BASE_ADDR是在boot/include/boot.inc中定义,其值分别为0x9和0x70000。
第150行的ecx为200,这是读入的扇区数,这里应该同前面用dd命令往硬盘上写入内核文件时的参数count保持一致,原因你懂的不解释。
以上的eax、ebx、ecx是函数rd_disk_m_32的三个参数,是为调用下面的函数做准备。
第152行的函数是rd_disk_m_32,用于从硬盘上读取文件。它的三个参数已经在上面赋值了。由于目前已经在32位保护模式下,所以相比之前位于mbr中的函数rd_disk_m_16,rd_disk_m_32只是版本由16位变成了32位的,函数实现原理相差无几,主要体现在里面所用的寄存器变成了32位。所以,就不细说啦,大家一看就明白啦。
接下来的第155行就是开始创建页表啦,把它放在这是为了让大家知道代码是加到了哪里,承上启下。setup_page函数实现没变,无须多说。
内核加载到缓冲区中后,现在该说要修改的第二处啦,也就是初始化内核。