Chinaunix首页 | 论坛 | 博客
  • 博客访问: 9670
  • 博文数量: 5
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 60
  • 用 户 组: 普通用户
  • 注册时间: 2014-10-09 17:57
文章分类
文章存档

2016年(5)

我的朋友
最近访客

分类: LINUX

2016-02-08 19:46:10

在开机加电之后,cs=F000h,ip=fff0h,(应该是这样的), 该处是固化在主板上的BIOS程序,该程序将从0地址处建立中断表,并准备中断服务程序,然后探测一些硬件的信息,诸如硬盘,显卡,光驱等等。最后会执行int 0x19中断指令,该中断服务程序就是启动加载服务程序,负责把硬盘或者是软盘的第零号扇区(0磁头0磁道1扇区,也就是引导扇区)中的程序加载至内存的0x7c00处,然后跳转到此处执行代码。引导扇区的工作就是用代码实现文件系统中说的手工查找,找到Loader.bin文件,然后跳到内核的开始位置,开始初始化工作。

为什么不是直接跳转到内核呢,还要中转一下loader呢?因为在开始内核工作之前,肯定是有很多工作要完成的,而引导扇区512个字节跑完的时候保护模式需要的环境可能还没有建立起来;如果把建立保护模式环境的共工作放在内核中,我猜又对内核的移植性不好,说不定有的情况下就不用重建环境,只要真正的内核代码呢。具体原因是什么,我也不清楚,可能就是出于便于维护吧,如果由于loader出了点问题不至于整个内核都要重新编译一下。

先凭着感觉和记忆想一想,Loader在最开始的时候要做什么工作呢?肯定要建立一个临时的IDTGDT,要知道内存的大小,要初始化分页工作,加载好gdtrldtrcr0cr3寄存器的值,对8259A进行重新编程,还要为必要的中断绑定上相应的处理程序,如果想界面友好一点,还可以在屏幕上输出一些流程信息。(上述没有考虑到先后顺序)

还是按照作者写的代码来说好了。要是让我自己写,一时半会肯定写不出来,自己又没有很多时间,唉,惭愧,一路是边抄袭作者的想法边捎带点私货。

1.首先是调用int 15中断,循环读取可用的内存区域。这是为了建立页表时候用的。如果不获取可用的内存大小,按照4G来建立,那么对于我们这个实验就态浪费,总共内存大小才16M,建立页表就花费了4M,岂不是太浪费了。

2.按照在文件系统中所分析的思路,将在引导扇区寻找loader的思路变成寻找kernel的代码,基本上都是一样的。不过在这里有个地方要注意,因为此时我们还是处于实模式下,一个段的最大空间是64K(说实在的以前啊一直没能真切体会到这句话的含义,超了就超了呗)。知道这次跟着作者写了一个超过64K的程序,又要用一个扇区一个扇区的加载的方式读到实模式的一个段里面的时候才算是有了体会。实模式下,寄存器都是16位的,我们的程序使用int 13h中断将kernel读到es:bx指明的地方,每读一个扇区,bx加上一个扇区的大小,如果我们的程序大于64K,那么bx会回退到0,再读代码就把原来的代码的覆盖了,运气好点覆盖的偏差大些,内核还能跑两步,运气不好直接从0开始覆盖,内核完全都是错误的,上来就崩溃了。

对于这个情况的处理是bx加上扇区的大小如果产生了进位,就说明超过一个段了,那么es0x1000,表示下一个段了。这样加载的内核就不会因为bx的回退而被覆盖了。

3.等完全加载完kernel后,再加载我们临时准备好的gdtr,然后关闭中断,开启32位地址线,然后开启cr0中的PE标志位,注意此时虽然置位PE进入保护模式,但是只是开始分段模式,也就是说开启cs代表的是GDT中索引的效果,此时还没有开启分页标志,分页标志是PG。作者对于这个GDT表的安排,是第0个是全零项;然后是代码段和数据段,这两个段长度都是4G,一贯的linux风。对于第三个段,作者的安排是分给了显存。这里准备这几个段描述符不过是为了后续跳转到

4.最后就是跳转到保护模式下,指明了段选择子和偏移。然后在这里初始化页表和将内核里面的数据加载到相对应的地方。关于加载内核就是通常程序加载器干的事,根据elf文件格式将代码段和数据段等需要加载进内存的段放到指定的内存地址。

5.说到这,我想说说自己当初的一个疑问,多任务是如何进行内核的共享的,作者是将内核在每个任务的空间内都复制了一份,而linux0.12已经实现了写时复制的机制,两者的方式是不一样的。而且对于那个在开启分页后,改变cs值的一个长跳转,虽然此时我们直接这样是完全正确的,但是对于内存大一点的,一次不会初始化4G的分页形式,要考虑到恒等映射的问题(留个坑)。

先说说建立页表

此时的可用内存大小应该已经确定下来了,我们用这个大小除以1024*4K,得到需要几个页目录项,因为一个页目录项对应一张二级页表,这个二级页表包含有1024个页表的首地址,所以一个页目录项覆盖4M的范围。我们除以4M就得到可用内存一共要多少个页目录项。如果有余数,那么说明可用内存要超过4M的整数倍,没办法只能再加上一个页目录项了。

作者将一级页目录表的首地址放在0x100000,而一张一级的页目录表恰好可以容纳4G的空间,不得不说设计者的才华让人赞叹,似乎到了造物主的巧合地步。二级页表的首地址紧跟在下一个物理内存页面,也就是0x101000,我们先将4个(在这个试验中一共分配了16M的内存,也就是占用了4个二级页表和四个页目录项)二级页表的首地址依次存放在对应的页目录项中。然后开始初始化每个二级页表,对于二级页表的初始化,不过是从0地址开始开始循环,每次加一个页面的大小(4096个字节),然后依序填入二级页表的相对应的项中,顺便加上页表的初始属性就可以了。

最后将页目录表基址填入cr3,然后设置cr0PG标志位,开启分页机制。

再说内核加载

Elf文件格式说复杂也复杂,不过好在此时没有其他程序来干扰内核的加载,想怎么放就怎么放,完全是一家独大。

1.首先要知道该文件要有几个段要放进内存里面,也就是pELFHdr->e_phnum,如果用汇编获取这个值,那距离文件头就是0x2C个偏移。这个值就指明了段的重新分布要执行几次。

2.得到第一个段相对于文件头的偏移,pELFHdr->e_phoff,这个值距离文件头的偏移是0x1C。我们就从这里作为遍历的开始。

3.e_phoff指定的地址是一个段的描述性结构。我们需要的是其中三个值,分别是:p_vaddr指明了本段要被加载到以哪个内存为开始的空间(这是链接脚本指定的地址,可人工指定),p_offset指明了本段的数据放在距离文件头有多少偏移,p_filesz指明了本段的大小。有这几个数据,我们直接用自己实现的memcpy把这个段落赋值过去就好了。

4.读取下一个段的描述,直到描述段的信息为0,说明到了段描述表的最后了,要加载进内存的段都重新放置完成。

做完上述的工作后,内核需要的环境算是暂时能够满足了。在跳转到内核代码前,还在一个地方保存了一些值,不过现在还用不到,这些值是在实现fork调用的时候才用的上,到时候再回过头来看,自然明了。

关于IDT表的是交给内核来建立的,我想之所以这么做,是因为只有内核才知道每个中断是如何安排的。

阅读(783) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~