Bootloader 的架构和功能
1.
BootLoader的概念
BootLoader是系统加电启运行的第一段软件代码。回忆一下PC的体系结构我们可以知道,PC机中的引导加载程序由BIOS(其本质就是一段固件程序)和位于硬盘MBR中的引导程序(比如,LILO和GRUB等)一起组成。BIOS在完成硬件检测和资源分配后,将硬盘MBR中的引导程序读到系统的RAM中,然后将控制权交给引导程序。引导程序的主要运行任务就是将内核映象从硬盘上读到RAM中
然后跳转到内核的入口点去运行,也即开始启动操作系统。
而在嵌入式系统中,通常并没有像BIOS那样的固件程序(有的嵌入式系统也会内嵌一段短小的启动程序),因此整个系统的加载启动任务就完全由BootLoader来完成。比如在一个基于 ARM7TDMI core的嵌入式系统中,系统在上电或复位时都从地址 0x00000000开始执行。而在这个地址处安排的通常就是系统的BootLoader程序。
简单地说BootLoader就是在操作系统内核或用户应用程序运行之前运行的一段小程序。通过这段小程序,我们可以初始化硬件设备、建立内存空间的映射图(有的CPU没有内存映射功能如 S3C44B0),从而将系统的软硬件环境带到一个合适的状态,以便为最终调用操作系统内核或用户应用程序准备好正确的环境。对于一个嵌入式系统来说,可能有的包括操作系统,有的小型系统也可以只包括应用程序,但是在这之前都需要BootLoader为它准备一个正确的环境。通常,BootLoader是依赖于硬件而实现的,特别是在嵌入式领域,为嵌入式系统建立一个通用的BootLoader是很困难的。当然,我们可以归纳出一些通用的概念来,以便我们了解特定BootLoader的设计与实现。
2.几种常见的bootloader
Redboot
Redboot是Redhat公司随eCos发布的一个BOOT方案,是一个开源项目。
当前Redboot的最新版本是Redboot-2.0.1,Redhat公司将会继续支持该项目,其官方发布网址为:http://sources.redhat.com/redboot/
Redboot支持的处理器构架有ARM,MIPS,MN10300,PowerPC, Renesas SHx,v850,x86等,是一个完善的嵌入式系统Boot Loader。
Redboot是在ECOS的基础上剥离出来的,继承了ECOS的简洁、轻巧、可灵活配置、稳定可靠等品质优点。它可以使用X-modem或Y-modem协议经由串口下载,也可以经由以太网口通过BOOTP/DHCP服务获得IP参数,使用TFTP方式下载程序映像文件,常用于调试支持和系统初始化(Flash下载更新和网络启动)。Redboot可以通过串口和以太网口与GDB进行通信,调试应用程序,甚至能中断被GDB运行的应用程序。Redboot为管理FLASH映像,映像下载,Redboot配置以及其他如串口、以太网口提供了一个交互式命令行接口,自动启动后,REDBOOT用来从TFTP服务器或者从Flash下载映像文件加载系统的引导脚本文件保存在Flash上。当前支持单板机的移植版特性有:
- 支持ECOS,Linux操作系统引导
- 在线读写Flash
- 支持串行口kermit,S-record下载代码
- 监控(minitor)命令集:读写I/O,内存,寄存器、内存、外设测试功能等
Redboot是标准的嵌入式调试和引导解决方案,支持几乎所有的处理器构架以及大量的外围硬件接口,并且还在不断地完善过程中。
ARMboot
ARMboot是一个ARM平台的开源固件项目,它特别基于PPCBoot,一个为PowerPC平台上的系统提供类似功能的姊妹项目。鉴于对PPCBoot的严重依赖性,已经与PPCBoot项目合并,新的项目为U-Boot。
ARMboot发布的最后版本为ARMboot-1.1.0,2002年ARMboot终止了维护,其发布网址为:http://sourceforge.net/projects/armboot
ARMboot支持的处理器构架有StrongARM ,ARM720T ,PXA250 等,是为基于ARM或者StrongARM CPU的嵌入式系统所设计的。
ARMboot的目标是成为通用的、容易使用和移植的引导程序,非常轻便地运用于新的平台上。ARMboot是GPL下的ARM固件项目中唯一支持Flash闪存,BOOTP、DHCP、TFTP网络下载,PCMCLA寻线机等多种类型来引导系统的。特性为:
- 支持多种类型的FLASH
- 允许映像文件经由BOOTP、DHCP、TFTP从网络传输;
- 支持串行口下载S-record或者binary文件
- 允许内存的显示及修改
- 支持jffs2文件系统等
Armboot对S3C44B0板的移植相对简单,在经过删减完整代码中的一部分后,仅仅需要完成初始化、串口收发数据、启动计数器和FLASH操作等步骤,就可以下载引导uClinux内核完成板上系统的加载。总得来说,ARMboot介于大、小型Boot Loader之间,相对轻便,基本功能完备,缺点是缺乏后续支持。
U-Boot
U-Boot是由开源项目PPCBoot发展起来的,ARMboot并入了PPCBoot,和其他一些arch的Loader合称U-Boot。2002年12月17日第一个版本U-Boot-0.2.0发布,同时PPCBoot和ARMboot停止维护。
U-Boot自发布以后已更新6次,最新版本为U-Boot-1.1.1,U-Boot的支持是持续性的。其发布网址为:
http://sourceforge.net/projects/u-boot/
U-Boot支持的处理器构架包括PowerPC (MPC5xx,MPC8xx,MPC82xx,MPC7xx,MPC74xx,4xx), ARM (ARM7,ARM9,StrongARM,Xscale),MIPS (4Kc,5Kc),x86等等, U-Boot(Universal Bootloader)从名字就可以看出,它是在GPL下资源代码最完整的一个通用Boot Loader。
U-Boot提供两种操作模式:启动加载(Boot loading)模式和下载(Downloading)模式,并具有大型Boot Loader的全部功能。主要特性为:
-SCC/FEC以太网支持
-BOOTP/TFTP引导
-IP,MAC预置功能
- 在线读写FLASH,DOC, IDE,IIC,EEROM,RTC
-支持串行口kermit,S-record下载代码
-识别二进制、ELF32、pImage格式的Image,对Linux引导有特别的支持
-监控(minitor)命令集:读写I/O,内存,寄存器、内存、外设测试功能等
-脚本语言支持(类似BASH脚本)
-支持WatchDog,LCD logo,状态指示功能等
U-Boot的功能是如此之强大,涵盖了绝大部分处理器构架,提供大量外设驱动,支持多个文件系统,附带调试、脚本、引导等工具,特别支持Linux,为板级移植做了大量的工作。U-Boot1.1.1版本特别包含了对SA1100和44B0芯片的移植,所以44B0移植主要是针对Board 的移植,包括FLASH、内存配置以及串口波特率等等。U-Boot的完整功能性和后续不断的支持,使系统的升级维护变得十分方便。
Blob
Blob(Boot Loader Object)是由Jan-Derk Bakker and Erik Mouw发布的,是专门为StrongARM 构架下的LART设计的Boot Loader。
Blob的最后版本是blob-2.0.5,其发布网址为:
http://www.lart.tudelft.nl/lartware/blob/
Blob支持SA1100的LART主板,但用户也可以自行修改移植。
Blob也提供两种工作模式,在启动时处于正常的启动加载模式,但是它会延时 10 秒等待终端用户按下任意键而将 Blob 切换到下载模式。如果在 10 秒内没有用户按键,则 Blob 继续启动 Linux 内核。其基本功能为:
初始化硬件(CPU速度,存储器,中断,RS232串口)
-引导Linux内核并提供ramdisk
-给LART下载一个内核或者ramdisk
-给FLASH片更新内核或者ramdisk
- 测定存储配置并通知内核
- 给内核提供一个命令行
Blob功能比较齐全,代码较少,比较适合做修改移植,用来引导Liunx,目前大部分S3C44B0板都用Blob修改移植后来加载uClinux。
Bios-lt
Bios-lt是专门支持三星(Samsung)公司ARM构架处理器S3C4510B的Loader,可以设置CPU/ROM/SDRAM/EXTIO,管理并烧写FLASH,装载引导uClinux内核。这是国内工程师申请GNU通用公共许可发布的。
Bios-lt的最新版本是Bios-lt-0.74,另外还提供了S3C4510B的一些外围驱动,其发布网址为:http://sourceforge.net/projects/bios-lt
Bootldr
Bootldr是康柏(Compaq)公司发布的,类似于compaq iPAQ Pocket PC,支持SA1100芯片。它被推荐用来引导Llinux,支持串口Y-modem协议以及jffs文件系统。
Bootldr的最后版本为Bootldr-2.19,其发布网址为: http://www.wearablegroup.org/software/bootldr/
3. Bootloader的启动过程之stage1
Bootloader的启动过程可以是单阶段(Single Stage)或多阶段(Multi-Stage)的。一些只需要完成很简单的功能的Bootloader可以是单段的,而多阶段的Bootloader能够提供更为复杂的功能,以及更好的移植性,从固态存储设备上启动的Bootloader大多数是2阶段的启动过程,业绩启动过程可以分为stage1和stage2两个部分。
大多数的Bootloader都分为stage1和stage2两大部分。Stage1依赖于CPU的体系结构,如设备初始化代码,因此一般用汇编语言实现,短小精悍。Stage2一般用C于眼实现,可以实现复杂的功能,代码也具有较好的可读性和可移植性。
Stage1直接运行在固态存储设备上,通常包括以下5个步骤:
1.硬件设备初始化
这是Bootloader一开始就执行的操作,目的是为stage2及kernel的执行准备好基本硬件环境,通常包括:
① 屏蔽所有的中断。为中断提供服务的通常是OS或设备驱动程序的责任,在Bootloader阶段不必响应任何中断。中断屏蔽可以通过写CPU的中断屏蔽寄存器或状态寄存器实现,比如写ARM的CPRS寄存器;
② 设置CPU的速率和时钟频率;
③ RAM的初始化。包括正确设置系统中的内存控制器的功能寄存器以及各CPU的外存(Memory Bank)的控制寄存器等;
④ 初始化LED。典型的,通过GPIO来驱动LED,其目的就是表明系统的状态是OK的还是Error的,若板子没有LED,也可以通过初始化UART想串口打印Bootloader的Logo字符信息来完成这点。
⑤ 关闭CPU内部指令/数据Cache。
2.为加载Bootloader的stage2准备RAM空间。为获得更快的执行速度,通常stage2被加载到RAM中执行,因此必须为加载stage2准备好一段可用的RAM空间。
当然,空间大小也应该考虑,通常是dtage2可执行映像的大小+堆栈空间,因为stage2通常是C语言代码实现的,此外,最好对齐到memory page的大小(通常是4KB),一般说来,准备1MB已经足够了。
具体的地址范围可以任意安排,比如,Blob将它的stage2 可执行映像安排在系统RAM的0x02000000开始的1M空间内。值得推荐的是可以将stage2安排到整个RAM空间的最顶的1MB,也即(RamEnd-1MB)开始处。假设空间大小为stage2_size(字节),起始和终止地址分别为:stage2_start和stage2_end(均与4字节对其),则有:stage2_end=stage2_start + stage2_size。这一步必须进行有效性测试,即必须确保安排的地址范围的确为可读写的RAM空间,Blob的内存有效性测试称为test_mempage,方法是:以内存页为被测试的单位,测试每个页面头的两个字是否可读写。test_mempage过程如下:
① 保存被测试页面头的两个字的内容;
② 向着两个字中写入任意的数字,比如:向第1个字写入0x55,第2个字写入0xaa。
③ 立即将这两个字的内容读回。应当与写入的内容一致,否则此页面地址范围不是一段有效的RAM空间;
④ 再次向这两个字中写入任意的数字,向第1个字写入0xaa,第2个字写入0x55。
⑤ 立即将这两个字的内容读回,判断依据同3;
⑥ 恢复这两个字的原始内容。
测试结束后,为了得到一段干净的RAM空间范围,可以将多安排的RAM空间范围进行清零操作。
3.拷贝Bootloader的stage2到RAM空间中。
拷贝时要确定:
stage2的可执行映像在固态存储设备的存放起始地址和终止地址
RAM空间的起始地址
4.设置好堆栈指针sp
对C语言编写的程序应当准备运行堆栈,通常将堆栈设置在上述1MB RAM空间的最顶端,即sp = ( stage2_end - 4)(注意:堆栈是向下生长的)。此外在设置堆栈指针前,也可以关闭led灯,以提示用户即将跳转到stage2。
5.跳转stage2的C入口点
在上述一切都就绪后,就可以跳转到Bootloader的stage2去执行了。比如在ARM系统中,这可以通过修改PC寄存器为合适的地址来实现。
4. Bootloader的启动过程之stage2
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的工作结束,系统完全交给操作系统。
Bootloader 的架构和功能
最新推荐文章于 2024-01-31 18:59:11 发布