Bootloader两个阶段流程简介
扫描二维码
随时随地手机看文章
通用的linux内核,启动时需要很多参数 ,这些参数必须通过Bootloader传递。而且内核一半是压缩存放在外存上的,从外存到内存的复制也是由Bootloader完成。从Bootloader的第二个功能就知道,Bootloader时不能与内核放在一起的。由于Bootloader的实现依赖于CPU的体系结构,因此大多数的Bootloader都分为Stage1和Stage2l辆大部分。
依赖于CPU体系结构的代码,比如设备初始化代码等,通常都放在Stage1中,而且通常用汇编语言来实现,以达到短小精悍的目的。而Stage2则通常用C语言来实现,这样可以实现复杂的功能,并且代码会具有更好的可读性和可移植性。
(1)Bootloader的Stage1通常包括以下步骤:
1:硬件设备初始化
2:为加载Bootloader的Stage2准备RAM空间
3:将Bootoader的Stage2复制到RAM空间中
4:设置好堆栈
5:跳转到Stage2的C入口点
(2)Bootloader的Stage2通常包括以下步骤:
1:初始化本阶段要使用到的硬件设备
2:检测系统内存映射(Memory Map)
3:将Kernel映像和根文件系统映像从Flash上读到RAM空间中
4:为内核设置启动参数
5:调用内核。
Stage1:汇编阶段
1:基本硬件初始化
(1)屏蔽所有中断。
(2)设置CPU的速度和时钟频率。
(3)RAM初始化。包括正确地设置系统的内存控制器的功能寄存器以及各内存控制寄存器等。
(4)初始化LED。(或者初始化串口,用于表示当前状态)
(5)关闭CPU内部指令/数据Cache。
2:为加载Stage2准备RAM空间
为了获得更快的执行速度,通常把Stage2加载到RAM空间中来执行**,因此必须为加载Bootloader的Stage2准备好一段可用的RAM空间范围。主要工作是对预备空间进行读写测试。
3:复制Stage2到RAM中
复制时注意两点:Stage2的可执行映像在固态存储设备的存放起始位地址和终止地址。RAM空间的起始地址。
4:设置堆栈指针sp
堆栈指针设置是为执行C语言代码做准备。通常可把sp的值设置为(stage2_end-4),也即1MB的RAM空间的最顶端(堆栈向下生长)。此外,在设置堆栈指针sp之前,也可以关闭LED灯,以提示用户我们准备跳转到Stage2。经过上述这些执行步骤后,系统的内存物理布局如下图所示。
5:跳转到Stage2的C入口点
在上述一切都就绪后,就可以跳转到Bootloader的Stage2去执行了。比如,在ARM系统中,这可以通过修改PC寄存器为合适的地址来实现。
Stage2:C语言阶段
正如之前所说,Stage2代码通常用C语言来实现,以便于实现更加复杂的功能和取得更好的代码可读性和可移植性。但是与通常C语言应用程序不同的是,在编译和链接Bootloader这样的程序时,我们不能使用glibc库中国年的任何支持函数。原因显而易见。
这就给我们带来一个问题,那就是从哪里跳转进main()函数呢?直接把main()函数的起始地址作为整个Stage2执行映像的入口点或许是最直接的想法。但是这样做有两个缺点:
- 无法通过mian()函数传递函数参数
- 无法处理main()函数的返回值情况
一种更为巧妙的方法是利用Trampoline(弹簧床)的概念。也即,用汇编语言写一段Trampoline小程序,并将这段小程序作为Stage2可执行映像的执行入口点。然后我们可以在Trampoline汇编小程序中用CPU跳转指令跳入main()函数中去执行;而当main()函数返回时,CPU执行路径显然再次回到我们的Trampoline程序。简而言之,这种方法的思想就是用这段Trampoline小程序最为mian()函数的外部包裹(External Wrapper)。
下面给出一个简单的Trampoline程序示例(来自Blob):
.text
.globl _trampoline
_trampoline:
bl main
/*if main ever returens we just call it again */
b _trampoline
可以看到,当main()函数返回之后,我们又重新调用trampoline,也就相当于循环调用mian(),这就是弹簧床的概念。
1:初始化系统本阶段需要用到的硬件
需要初始化的设备包括:
- 至少一个串口,以便于终端用户进行I/O信息输出
- 计时器等
2:检测系统的内存映射(Memory Map)
所谓内存 映射,就是指在整个4GB物理地址空间中有哪些地址范围被分配用来寻址系统的RAM单元。比如,在Samsung S3C44B0X中,从0x0c000000到0x10000000之间的64MB地址空间被用作系统的RAM地址空间。
虽然CPU通常预留出一大段足够的地址空间给系统RAM,但是在搭建具体的嵌入式系统时却不一定会实现CPU预留的全部RAM地址空间。也就是说,具体的嵌入式系统往往只把CPU预留的全部RAM空间的一部分映射到RAM单元上,而让剩下的那部分预留RAM地址空间处于未使用状态,不挂满可节省成本。由于上述事实,因此Bootloader的Stage2必须在它想干点什么(比如,将存储在Flash上的内核映像独到RAM空间中)之前检测整个系统的内存映射情况,也即它必须知道CPU预留的全部RAM地址空间中的哪些呗真正映射到物理RAM地址单元,哪些是处于unused状态的。
3:加载内核映像和根文件系统映像
规划内存占用的布局。这里面就包括两个方面:
-内核映像所占用的内存范围。
-根文件系统所占用的内存范围。在规划内存范围占用的布局时,主要考虑基地址和映像的大小两个方面。
对于内核映像,一般将其复制到从(MEM_START + 0x8000)这个地址开始的大约1MB大小的内存范围内(嵌入式LINUX的内核一般都不超过1MB)。
为什么把从MEM_START到MEM_START + 0x8000这段32KB大小的内存空出来呢?这是因为LINUX内核要在这段内存中放置一些全局数据结构,如启动参数和内核页表等信息。
而对于根文件系统映像,则一般将其复制到MEM_START + 0x0010 0000开始的地方。如果用Ramdisk作为根文件系统映像,则其解压后的大小一般是1MB。
4:从Flash上复制
由于像ARM这样的嵌入式CPU通常都是在同一的内存地址空间中国年寻址Flash等固态存储设备的,因此从Flash上读取数据与从RAM单元中读取数据并没有什么不同。用一个简单的循环就可以完成从Flash设备上复制映像的工作:
while(count){
*dest++ = *src++; /*they are all aligned with word boundary */
count -= 4; /* byte number */
};
5:设置内核的启动参数
应该说,在讲内核映像和根文件系统映像复制到RAM空间中后,就可以准备启动Linux内核了。但是在调用内核之前,应该做一步准备工作,即设置Linux内核的启动参数。
Linux 2.4以后的内核期望以标记列表(Tagged List)的形式来传递启动参数。启动参数标记列表以标记ATAG_CORE开始,以标记ATAG_NONE结束。每个标记由北传递参数tag_header结构以及随后的参数值数据结构来组成。数据结构tag和tag_header定义在Linux内核源码的include/asm/setup.h头文件中。
在嵌入式Linux系统中,通常要由Bootloader设置的常用启动参数有ATAG_CORE、ATAG_MEM、ATAG_CMDLINE、ATAG_RAMDISK、ATAG_INITRD等。
Linux内核在启动时可以以命令行参数的形式来接受信息,利用这一点我们可以向内核提供哪些内核不能自己检测的硬件参数信息,活着重载(override)内核自己检测到的信息。比如,我们用这样一条命令行参数字符串“console=ttyS0,115200n8”来通知内核以ttyS0作为控制台,且用串口采用“115200bps,无奇偶校验、8位数据位”这样的设置。
6:掉用内核
Bootloader调用Linux内核的方法是直接跳转到内核的第一条指令处,也即直接跳转到MEM_START + 0x8000地址处。在跳转时要满足下列条件。
- CPU寄存器的设置:R0=0; R1=机器类型ID; R2=启动参数标记列表在RAM中的起始基地址。
- CPU模式:必须禁止中断(IRQs和FIQs);CPU必须处于SVC模式。
- Cache和MMU的设置:MMU必须关闭;指令缓存可以打开也可以关闭;数据缓存必须关闭。