x86 linux系统内核引导流程梳理

来源:互联网 发布:eclipse新建教程 java 编辑:程序博客网 时间:2024/05/18 01:58

引言:在上节提到双系统关键技术之一关于系统启动问题,即在执行内核代码前做了些什么?是怎样从汇编代码过渡到c代码执行?arm芯片和x86芯片启动有什么不同?

结合这些问题去研究,这里推荐赵炯的《Linux内核完全注释》v3.0,内核版本0.11,内核版本虽然比较低,但理解起来较为容易,更重要的是该书对引导程序的注解非常到位。有些结论就直接引用该书,他山之石可以攻玉。
计算机系统的粗略启动流程:
上电 –> BIOS自检 –> bootloader –> kernel
对Linux x86启动有几个问题值得关注:

  1. 上电后PC指针如何跳转到BIOS程序处?
  2. bootloader的代码放在何处?如何执行bootloader的代码?
  3. bootloader具体做了什么操作?

问题1,书中给出实模式即cpu访问的是实际内存地址,上电和复位后,cpu自动设置了CS和IP的值,此时IP指向0xfffffff0,此处是系统ROM BIOS存放的位置。
对于这样的回答应该不算满意,比如cpu自动设置能修改吗?BIOS代码在何处?此处0xfffffff0如何映射到BIOS的位置?
继续探索得知,BIOS代码存放在ROM(flash)中,而这块ROM的BIOS地址通常放在CPU能寻址的内在最高端,长度256K~2M。这样的确可以解释BIOS程序的执行,那么这个映射是如何完成的?
这里必须提一下32位cpu最大可寻到的物理址4G,这部分地址主要给RAM用(低端地址),少部分给其它总线设备用(高端 地址)。所以BIOS的映射是由硬件设计决定,通过统一编址后,可由cpu直接寻址到。

理清这些问题后,开始研究问题2,对于bootloader要求掉电仍在,否则kernel无法启动。所以 bootloader很可能存放在硬盘或ROM里。至于具体放在哪儿,在BIOS里已经写好相应的地址。因此,也可以肯定BIOS的功能微量除系统自检、初始化中断量外,还应该将bootloader程序复制到RAM里,书中给出地址为0x7C00(31KB)处。此后,就开始进入bootloader程序流程。

这里有几个问题,RAM是什么时候准备好的?为什么不直接加载到0x0000处,而要加载到0x7C00(31KB)处?
很容易想到RAM设备在硬件设计时其地址范围已经确定,上电后经过初始化就已经可用。为何不加载到0x0000,这个就需要了解bootloader的执行过程,如下:
bootsect.S –> setup.s –> head.s –> main.c

一般说来,bootsect.S加载setup.s代码和kernel代码(head.s,init),然后setup.s负责移动kernel代码,利用BIOS中断,读取各硬件系统参数,然后开起32位保护模式,并跳转到head.s代码处。

随着这三个疑问,逐步揭开linux从引导程序过度到内核启动,并切换到用户空间程序的神秘面纱。
重新从bootsect.S开始探索,代码中指定了根文件系统设备ROOT_DEV=0x306(一种老式的硬盘设备命名方式,即第2个硬盘的第一个分区),可以根据实际情况修改。

第二个疑问,32位保护模式很容易想到加载全局描述符表(GDT)寄存器,和中断描述符表(IDT)寄存器,还需要开启A20地址线,当然最关键的一步是设置CPU的控制寄存器CR0,让通知CPU进入 32位保护模式。这些都好还理解,唯独这个开启A20地址线让人费解。不开启就不能进入保护模式?

这里提一点,A20主要是为了兼容80286和8086、8088对于内存的访问,8086/8088只有20根地址线,能够访问1M内存,80286地址线为24根,能够访问16M内存。8086在访问1M以上的内在时,由于地址非法,此时地址对1M取模,重新从0开始。而80286在访问1M以上内存时,地址不非法,这样与8086就不兼容。为了解决这个问题,IBM提出A20地址线来控制,对于1M以上的内在地址,如果A20关闭,系统使用8086方式;如果开启,系统真正访问这些地址。不过这些内存访问都在实模式下,那么这又和保护模式有什么关系???
按理说80286在实模式下可以访问16M全部内存,但实际不能访问1M以上的内存地址。只能通过保护进入保护模式,并开启A20,才能访问所有地址空间。这样设计唯一的理由就是兼容8086,那还能有什么?关于A20还有三个问题,一是80286在保护模式下,关闭A20,能访问的内存为多少?二是如何开启或关闭A20地址线?三是如何确定真的开启了A20地址线?这些问题就不再这里解答了。

继续关注第三个疑问,如何从内核空间切换到用户空间?
setup.s后内存布局

先看下此时的内存布局,head.s在0x0000处,所以setup.s肯定有代码跳到此处,开始执行head.s,那么head.s有什么作用?

  1. 初始化中断描述符表,检查A20地址线是否已打开
  2. 初始化内存页目录表,为分页管理做好准备
  3. 跳转到init/main.c代码处开始执行

对于初始化内存页目录,必须要提一点的是内存页表在页目录之后,如一个内存页表映射4M内存空间,16M的内存需要4个页表。内存的分页功能也需要启动,即设置寄存器cr0的第31位。页目录表是系统所有进程公用的,而随后的4个页表为内核专用。那么有一个问题,用户空间的进程页表怎么安排?后边涉及到MM(内存管理)时,再说明 。
经过head.s的内存布局如下图:
head.s后内存布局
可以清晰地看到内核init代码进行前的内存使用情况,head.s的作用就是在于此。那么下一个关键环节,main.c的代码是如何进入和执行的?
是head.s在程序中利用汇编返回指令ret将/init/main.c::_main()的地址弹出,从而开始了main函数的生命周期。根据c程序运行环境,在弹出前需要初始化内核的堆栈。还有可能传入一些参数给main函数。

至此,对x86处理器linux系统从开机上电内核c程序执行,进行了一个简单的梳理。内核的初始化也很有必要梳理下,不过对于Android系统,还是更多的关注ARM架构下的系统启动流程,相信会有其它惊喜的发现。

0 0
原创粉丝点击