现在将通过阅读代码,看看redboot是如何启动的,这是每个系统执行的第一步,也是不可缺少的一步。这部分会分几篇完成,这是第一部分,主要是一个概要介绍。
由于系统启动跟硬件的紧密关系,所以在不同的硬件平台下,这部分都会有相应不同的处理。
下面这幅图来自《EMBEDDED SOFTWARE DEVELOPMENT WITH ECOS》书中,介绍了一款PowerPC的设备启动流程。
而这里,仍将介绍PC下的启动流程,通过阅读代码可以了解其具体的执行步骤,也可以通过修改代码重新编译、运行前面介绍的实验,查看结果。在阅读代码的过程中,最好结合实验,这样能够证实系统是否象我们理解的那样运行。
在今后,会进一步分析eCos操作系统的其他部分代码结构,比如:调度,驱动等等。
也会对其他不同体系架构(如 mips arm)下的代码进行分析阅读。
在Redboot最先执行的文件通常命名为 vectors.S
因为我们是i386架构下,所以可以在下面目录下找到相应文件:
package/hal/i386/arch/v2_0/src/vectors.S
程序执行的入口是 _start。
从 _start 处开始执行,至 call cyg_start 完成启动部分。
在函数 cyg_start 中,将进入 Redboot的执行流程。
这并不是很长的一段代码,却完成了整个PC的启动和初始化的部分。其中相当部分代码为汇编,所以要阅读这些代码必须首先对i386体系结构有基本的了解。
虽然 Redboot的启动过程已经相对简单,它没有使用分页机制,也没有区分内核态和用户态。
所有的代码都被放在一个段中统一编址。只使用了两个段(selector)一个为code,另一个为data。这个对于redboot的size(大概在100k左右吧)已经足够了。
如果对于一个操作系统来讲,这是一个相当程度简化的操作系统。即便如此,启动过程也不是一目了然的清晰。所以,这里仍然会忽略掉一些不太必要的内容,比如对SMP,FPU,GDB的支持等等。
整个启动过程中大概分为以下几个部分:
1. boot-loader: 这部分的工作就是将代码加载(load)到内存中,对于不同的设备,代码会装在不同的地方,通常PC会放在硬盘里面,某些嵌入式设备会把代码放在ROM(一般是flash)里面,而这里是放在软盘里面。boot-loader就是把软盘的代码load至内存,然后运行它们。
2. 进入保护模式,对于i386体系架构的cpu上电后缺省时实模式,内存访问空间也只有1M,这只是为了跟最初的8086兼容,所以要进入保护模式。
3. 初始化中断向量和异常。
4. 初始化基本的输入输出设备,如显示器,键盘,串行口等等。
5. 其他还有一些零散的初始化工作。
从下次开始将按运行次序对启动过程进行介绍。
_start之后的第一行是一个宏 hal_cpu_init, 该宏定义在package/hal/i386/pcmb/v2_0/include/pcmb.inc 文件中。 由于从软盘引导,所以 hal_cpu_init分为两部分: 一部分是引导分区部分(以0xAA55结束),另一部分的功能是使CPU进入保护模式。
解释:PC上电后会进入BIOS, BIOS会检测磁盘(软盘和硬盘),如果软盘(或硬盘)的第一个分区是引导分区(以 0xAA55结束为标志)则将软盘的第一个分区内容加载到内存 0x7c00处,并运行它。
引导分区被运行之后,将整个redboot逐个分区地加载到0x3000为起始地址的内存空间。
注意:这个过程,引导分区内容会再次被加载到0x3000处,且马上会跳转到0x3000处执行引导分区的代码。而0x7c00处的内容会在后面被覆盖掉。
在执行加载工作前,会先执行两个小任务:
1. 设置栈,栈顶为0x3000
2. 通过BIOS提供的中断,得到扩展内存和标准内存大小,压入栈中
引导分区代码结束后,程序将进入保护模式。大概次序如下:
1. 关中断
2. 初始化GDT和IDT
3. 进入保护模式
4. 设置数据段
5. 重新设置栈段(因为保护模式和实模式,内存编址方式不同)
GDT主要用两个Selector分别是 0x08 (code) 和 0x10 (data)。
寻址空间都是从0x00000000 - 0xFFFFFFFF
特权级(DPL)为 0
IDT的地址为:0x1000
这部分比较复杂,这里不在详细说明,如果有时间,会在讨论i386架构的文章里,详细说明。而有关这部分的资料也相当丰富,可以在网上搜寻。
到这里hal_cpu_init已经执行结束了 ,不过进入保护模式还需要 激活A20地址线 。不激活A20地址线只能访问1M空间内的内存,激活A20才能访问所有内存。这个任务在另外一个宏 hal_memc_init 中实现,它位于文件 packages/hal/i386/arch/v2_0/include/arch.inc
这样,就完成了加载和进入保护模式的任务。
进入保护模式之后做以下工作:
-
设置中断栈空间,在之前栈定义在小于0x3000的内存空间中。通过
movl $__interrupt_stack, %esp
将栈重新设置在__interrupt_stack处,并分配了4096kB的栈空间。
栈大小也可以通过改变cdl文件进行调整。
-
hal_idt_init(packages/hal/i386/pc/v2_0/include/platform.inc)
设置idt向量, 回头会在介绍中断的文章中介绍这部分。
-
BSS段初始化(清零)
解释: BSS(Block Started by Symbol)用来存放程序中未初始化的全局变量的一块內存区域,属于静态內存分配。
-
hal_platform_init(packages/hal/i386/pc/v2_0/src/plf_misc.c)
这部分是内容比较多,也比较重要。主要为 初始化 中断,异常 以及虚拟向量表。
初始化虚拟向量表的工作通过调用函数 hal_if_init 来完成
在eCos中设置了3张向量表,idt, hal_vsr_table, hal_virtual_vector_table
内存地址如下:
idtStart = 0x00001000;
hal_vsr_table = 0x00001800;
hal_virtual_vector_table = 0x00001c00;
idt 是硬件实际设置的中断区域,当中断发生后进入调用idt的中断向量。而eCos将idt的中断向量有指向 hal_vsr_table
(猜测是为了隔离不同的cpu中断机制不同,专门设置了hal_vsr_table,使其中断处理方式一致)
在 hal_platform_init 函数中,初始化所有中断(和异常),设定为缺省的中断处理例程,以防止有中断意外发生,造成程序进入未知状态。
有关中断,异常,虚拟向量等相关处理机制内容比较多,会在以后的文章具体介绍。
-
cyg_hal_invoke_constructors
这个函数主要执行相关的构造函数,因为ecos一部分代码是用c++写的,所以有些类具有构造函数,需要在首先执行,这里面做这部分的处理。
-
cyg_start
最后则进入主函数 cyg_start, 这个函数中,首先执行外围设备的初始化,之后就开始轮询处理用户输入的命令。
以上就是Redboot启动的全过程。由于篇幅有限,对很多部分没有深入讨论。在之后计划对以下部分进行讨论:
1 eCos设备驱动结构和编写
2 PCI设备的初始化过程
3 中断和异常处理过程
4 虚拟向量(hal_virtual_vector_table)介绍