(接前一博文)
Stage2的代码通常用C语言来实现,以便于实现更复杂的功能和取得更好的代码可读性和可移植性效果。但是与普通C语言应用程序不同的是,在编译和链接bootloader这样的程序时,不能使用glib库中的任何支持函数。那么从哪里跳转进main()函数呢?
最直接的想法就是,直接把main()函数的起始地址作为整个stage2执行映像的入口点,但是这样处理有两个问题,一是无法通过main()函数传递函数参数,二是无法处理main()函数返回的情况。有一种更为巧妙的方法就是利用trampoline(弹簧床)的概念:用汇编语言写一段trampoline小程序,并将它来作为stage2可执行映像的执行入口点。也即在trampoline中用CPU跳转指令跳入main()函数中去执行,当main()函数返回时,CPU执行路径显然再次回到trampoline程序。简言之,就是用这段trampoline小程序作为main()函数的外部包裹(external wrapper)。
Trampoline程序的简单示例(来自Blob Bootloader)
.text
.globl _trampoline
_trampoline:
Bl main
b _trampoline
可以看出,当main()函数返回后,我们又用一条跳转指令重新执行trampoline程序——当然也就要重新执行main()函数,这也是trampoline(弹簧床)一词的意思所在。
Bootloader 的stage2通常包括以下步骤:
1.初始化本阶段使用到底硬件设备。这通常包括:初始化至少一个串口,以便和终端用户进行I/O输出信息;初始化计时器等。初始化这些设备之前,也可以重新把LED灯点亮,以表明现在已进入main()函数执行。设备初始化完成后,可以输出一些打印信息,如程序名字字符串、版本号等。
2.检测系统内存映射(memory map)。所谓内存映射就是指整个4GB物理地址空间(32位机)中有哪些地址范围已经被分配用来寻址系统的RAM单元。比如,在SA-1100 CPU中,从0xC000 0000开始的512M被用作系统的RAM地址空间;Samsung S3C44B0X CPU中,从0x0C00 0000到0x1000 0000间的64M被用作系统的RAM地址空间。
注意,虽然CPU通常预留出一大段足够的地址空间给系统RAM,但是在搭建具体的嵌入式系统时却不一定会实现CPU预留的全部RAM地址空间,也即具体的嵌入式系统往往只把CPU预留的全部RAM地址空间中的一部分映射到RAM单元上,而让剩下的那部分预留RAM地址空间处于未使用状态。因此,Bootloader的stage2必须在它想做点什么(比如,将存储在FLASH上帝内核映像读到RAM空间中)之前检测整个昔日的内存映射情况。也即它必须知道CPU预留的全部RAM地址空间中的哪些被真正映射到RAM地址单元,哪些处于unused状态。
一般用如下数据结构描述RAM地址空间的一段连续的地址范围:
Type struct memory_area_struct
{
u32
start;
//内存区域的起始地址
u32
size;
//内存区域的大小(字节数)
int
used;
//内存区域的状态
}memory_area_t;
其中,used = 0/1,1表示这段地址范围已经被实现,也即真正的被映射到RAM单元上,0表示这段地址范围并未被系统实现,处于未使用状态。
整个CPU预留的RAM地址空间可以用一个memory_area_t 类型数组来表示,如
Memory_area_t memory_map[NUM_MEM_AREAS]=
{
[0...(NUM_MEM_AREAS)]=
{
.start = 0,
.size = 0,
.used = 0
//表示检测内存映射之前的初始状态
},
};
内存检测用到如下算法:
1)数组初始化,每个区域的used标志设为0;
2)将整个空间中所有页面的钱32位(4字节)写为0;
3)依次检测每个页面是否有效(使用test_mempage算法):
3.1)若当前页面无效
若当前区域已映射,则当前区域检测结束;
3.2)若当前页面有效
判断该页面是否由其他页面映射而来,若是同3.1;
否则若当前区域已映射,则增加有效页面到当前区域中;
若当前区域为一个新的区域,则初始化该区域并增加当前页面到当前区域中。
在用上述算法检测完系统的内存映射情况后,Bootloader也可以将内存映射到详细信息打印到串口。
3.将kernel映像和跟文件系统映像从flash上读到RAM空间中。
在这个过程中需要规划内存占用的布局,包括内存映像所占用的内存范围和根文件系统所占用的内存范围。实际上只要考虑基地址和映像大小就行了,例如,对内核映像,一般考虑从(MEN_START + 0x8000)开始约1MB的内存范围内。嵌入式Linux的内核一般都不会超过1MB,为什么要把MEN_START 到MEN_START + 0x8000 这一段32KB大小的内存出来呢?这是因为 Linux 内核要在这段内存中放置一些全局数据结构,如:启动参数和内核页表等信息。对根文件系统映像,一般从 MEM_START+0x0010,0000 开始。如果用 Ramdisk 作为根文件系统映像,则其解压后的大小一般是1MB。
加载映像:从 Flash 上拷贝。像 ARM 这样的嵌入式 CPU 通常都在统一的内存地址空间中寻址 Flash 等固态存储设备;从 Flash 上读取数据与从 RAM 单元中读取数据并没有什么不同。用一个简单的循环就可完成从 Flash 设备上拷贝映像的工作:
While (count)
{
* dest ++ = * src ++;
Count - = 4;
}
4.为内核设置启动参数
在嵌入式Linux系统中,需要有boot_loader设置的参数有:
内核参数,如页面大小、根设备;
内存映射情况;
命令行参数;
Initrd映像参数,包括起始地址、大小;
Ramdisk 参数,包括解压后的大小。
5.调用内核
内核调用的一般方法是直接跳转到内核的第一条指令处,也即RAM中内核被加载的地址处。对于ARM Linux系统,在跳转前必须满足一下几个条件:
1)CPU寄存器的设置:
R0 = 0;
R1 = 机器类型ID;
R2 = 传递给内核的启动参数其实地址。
2)CPU模式:
必须禁止中断(IRQs 和FIQs);
CPU必须处于SVC模式。
3)Cache和MMU的设置:
MMU必须关闭;
指令Cache可以打开也可以关闭;
数据Cache必须关闭。
从此stage2的工作结束,系统完全交给操作系统。