BootLoader的设计与实现Boot Loader 的设计与实现
3.2 Boot Loader 的 stage2
正如前⾯所说,stage2 的代码通常⽤ C 语⾔来实现,以便于实现更复杂的功能和取得更
好的代码可读性和可移植性。但是与普通 C 语⾔应⽤程序不同的是,在编译和链接 boot
loader 这样的程序时,我们不能使⽤ glibc 库中的任何⽀持函数。其原因是显⽽易见的
。这就给我们带来⼀个问题,那就是从那⾥跳转进 main() 函数呢?直接把main() 函数
的起始地址作为整个 stage2 执⾏映像的⼊⼝点或许是最直接的想法。但是这样做有两个
缺点:1)⽆法通过main() 函数传递函数参数;2)⽆法处理 main() 函数返回的情况。⼀种
更为巧妙的⽅法是利⽤ trampoline(弹簧床)的概念。也即,⽤汇编语⾔写⼀段trampolin
e ⼩程序,并将这段 trampoline ⼩程序来作为 stage2 可执⾏映象的执⾏⼊⼝点。然后
我们可以在 trampoline 汇编⼩程序中⽤ CPU 跳转指令跳⼊ main() 函数中去执⾏;⽽当
main() 函数返回时,CPU 执⾏路径显然再次回到我们的 trampoline 程序。简⽽⾔之,
这种⽅法的思想就是:⽤这段 trampoline ⼩程序来作为 main() 函数的外部包裹(exter
nal wrapper)。
下⾯给出⼀个简单的 trampoline 程序⽰例(来⾃blob):
.text
.globl _trampoline
_trampoline:
bl main
/* if main ever returns we just call it again */
b _trampoline
顽皮的杜鹃歌曲
可以看出,当 main() 函数返回后,我们⼜⽤⼀条跳转指令重新执⾏ trampoline 程序―
―当然也就重新执⾏ main() 函数,这也就是 trampoline(弹簧床)⼀词的意思所在。
3.2.1 初始化本阶段要使⽤到的硬件设备
这通常包括:(1)初始化⾄少⼀个串⼝,以便和终端⽤户进⾏ I/O 输出信息;(2)初始
化计时器等。在初始化这些设备之前,也可以重新把 LED 灯点亮,以表明我们已经进⼊
main() 函数执⾏。
苏步青学校
设备初始化完成后,可以输出⼀些打印信息,程序名字字符串、版本号等。
3.2.2 检测系统的内存映射(memory map)
所谓内存映射就是指在整个 4GB 物理地址空间中有哪些地址范围被分配⽤来寻址系统的
RAM 单元。⽐如,在 SA-1100 CPU 中,从 0xC000,0000 开始的 512M 地址空间被⽤作系
统的 RAM 地址空间,⽽在 Samsung S 3C 44B0X CPU 中,从 0x 0c 00,0000 到 0x1000,0000
之间的 64M 地址空间被⽤作系统的 RAM 地址空间。虽然 CPU 通常预留出⼀⼤段⾜够的
地址空间给系统 RAM,但是在搭建具体的嵌⼊式系统时却不⼀定会实现 CPU 预留的全部
RAM 地址空间。也就是说,具体的嵌⼊式系统往往只把 CPU 预留的全部 RAM 地址空间中
的⼀部分映射到 RAM 单元上,⽽让剩下的那部分预留 RAM 地址空间处于未使⽤状态。由
flash 上的内核映像读到 RAM 空间中) 之前检测整个系统的内存映射情况,也即它必须知道 CPU 预留的全部 RAM 地址空间中的哪些被真正映射到 RAM 地址单元,哪些是处于 "u nud" 状态的。
(1) 内存映射的描述
可以⽤如下数据结构来描述 RAM 地址空间中的⼀段连续(continuous)的地址范围:
typedef struct memory_area_struct {
u32 start; /* the ba address of the memory region */
u32 size; /* the byte number of the memory region */
int ud;
} memory_area_t;
这段 RAM 地址空间中的连续地址范围可以处于两种状态之⼀:(1)ud=1,则说明这段连续的地址范围已被实现,也即真正地被映射到 RAM 单元上。(2)ud=0,则说明这段连续的地址范围并未被系统所实现,⽽是处于未使⽤状态。
基于上述 memory_area_t 数据结构,整个 CPU 预留的 RAM 地址空间可以⽤⼀个 memory _area_t 类型的数组来表⽰,如下所⽰:
memory_area_t memory_map[NUM_MEM_AREAS] = {
[0 ... (NUM_MEM_AREAS - 1)] = {
.start = 0,
.size = 0,
.ud = 0
},
};
(2) 内存映射的检测
下⾯我们给出⼀个可⽤来检测整个 RAM 地址空间内存映射情况的简单⽽有效的算法:
/* 数组初始化 */
for(i = 0; i < NUM_MEM_AREAS; i++)
memory_map.ud = 0;
/* first write a 0 to all memory locations */
for(addr = MEM_START; addr < MEM_END; addr += PAGE_SIZE)
* (u32 *)addr = 0;
for(i = 0, addr = MEM_START; addr < MEM_END; addr += PAGE_SIZE) {
/*
* 检测从基地址 MEM_START+i*PAGE_SIZE 开始,⼤⼩为
* PAGE_SIZE 的地址空间是否是有效的RAM地址空间。
*/
调⽤3.1.2节中的算法test_mempage();
if ( current memory page isnot a valid ram page) {
/* no RAM here */
i++;
continue;
}
/*
* 当前页已经是⼀个被映射到 RAM 的有效地址范围
* 但是还要看看当前页是否只是 4GB 地址空间中某个地址页的别名?
*/
if(* (u32 *)addr != 0) { /* alias? */
/* 这个内存页是 4GB 地址空间中某个地址页的别名 */
if ( memory_map.ud )
i++;
我与公司共成长continue;
}
/*
* 当前页已经是⼀个被映射到 RAM 的有效地址范围
* ⽽且它也不是 4GB 地址空间中某个地址页的别名。
*/
if (memory_map.ud == 0) {
memory_map.start = addr;
memory_map.size = PAGE_SIZE;
memory_map.ud = 1;
} el {
memory_map.size += PAGE_SIZE;
}
} /* end of for (…) */
在⽤上述算法检测完系统的内存映射情况后,Boot Loader 也可以将内存映射的详细信息
打印到串⼝。
3.2.3 加载内核映像和根⽂件系统映像
(1) 规划内存占⽤的布局
这⾥包括两个⽅⾯:(1)内核映像所占⽤的内存范围;(2)根⽂件系统所占⽤的内存范围
。在规划内存占⽤的布局时,主要考虑基地址和映像的⼤⼩两个⽅⾯。
对于内核映像,⼀般将其拷贝到从(MEM_START+0x8000) 这个基地址开始的⼤约1MB⼤⼩的内存范围内(嵌⼊式 Linux 的内核⼀般都不操过 1MB)。为什么要把从 MEM_START 到 MEM _START+0x8000 这段 32KB ⼤⼩的内存空出来呢?这是因为 Linux 内核要在这段内存中
放置⼀些全局数据结构,如:启动参数和内核页表等信息。
⽽对于根⽂件系统映像,则⼀般将其拷贝到 MEM_START+0x0010,0000 开始的地⽅。如果⽤Ramdisk 作为根⽂件系统映像,则其解压后的⼤⼩⼀般是1MB。
(2)从 Flash 上拷贝
由于像 ARM 这样的嵌⼊式 CPU 通常都是在统⼀的内存地址空间中寻址 Flash 等固态存储
设备的,因此从 Flash 上读取数据与从 RAM 单元中读取数据并没有什么不同。⽤⼀个简
微信有邮箱吗
单的循环就可以完成从 Flash 设备上拷贝映像的⼯作:
*dest++ = *src++; /* they are all aligned with word boundary */
count -= 4; /* byte number */
};
3.2.4 设置内核的启动参数
应该说,在将内核映像和根⽂件系统映像拷贝到 RAM 空间中后,就可以准备启动 Linux
内核了。但是在调⽤内核之前,应该作⼀步准备⼯作,即:设置 Linux 内核的启动参数。
Linux 2.4.x 以后的内核都期望以标记列表(tagged list)的形式来传递启动参数。启动参
数标记列表以标记 ATAG_CORE 开始,以标记 ATAG_NONE 结束。每个标记由标识被传递参
数的 tag_header 结构以及随后的参数值数据结构来组成。数据结构 tag 和 tag_header
定义在 Linux 内核源码的include/asm/tup.h 头⽂件中:
/
* The list ends with an ATAG_NONE node. */
#define ATAG_NONE 0x00000000
struct tag_header {
u32 size; /* 注意,这⾥size是字数为单位的 */
u32 tag;
};
……
struct tag {
struct tag_header hdr;
union {
struct tag_core core;
struct tag_mem32 mem;
struct tag_videotext videotext;
struct tag_ramdisk ramdisk;
struct tag_initrd initrd;
struct tag_rialnr rialnr;
struct tag_revision revision;
struct tag_videolfb videolfb;
struct tag_cmdline cmdline;
/*
* Acorn specific
*/
struct tag_acorn acorn;
购买香水
/*
* DC21285 specific
*/
struct tag_memclk memclk;
} u;
};
在嵌⼊式 Linux 系统中,通常需要由 Boot Loader 设置的常见启动参数有:ATAG_CORE、
ATAG_MEM、ATAG_CMDLINE、ATAG_RAMDISK、ATAG_INITRD等。⽐如,设置 ATAG_CORE 的代
params = (struct tag *)BOOT_PARAMS;
params->hdr.tag = ATAG_CORE;
params->hdr.size = tag_size(tag_core);
params-&flags = 0;
params-&pagesize = 0;
params-&ootdev = 0;
params = tag_next(params);
鸡排怎么煎
其中,BOOT_PARAMS 表⽰内核启动参数在内存中的起始基地址,指针 params 是⼀个 str uct tag 类型的指针。宏 tag_next() 将以指向当前标记的指针为参数,计算紧临当前标
记的下⼀个标记的起始地址。注意,内核的根⽂件系统所在的设备ID就是在这⾥设置的。
下⾯是设置内存映射情况的⽰例代码:
for(i = 0; i < NUM_MEM_AREAS; i++) {
if(memory_map.ud) {什么东西辟邪最好
params->hdr.tag = ATAG_MEM;
params->hdr.size = tag_size(tag_mem32);
params-&start = memory_map.start;
params-&size = memory_map.size;
params = tag_next(params);
}
}
可以看出,在 memory_map[]数组中,每⼀个有效的内存段都对应⼀个 ATAG_MEM 参数标记。
Linux 内核在启动时可以以命令⾏参数的形式来接收信息,利⽤这⼀点我们可以向内核提
供那些内核不能⾃⼰检测的硬件参数信息,或者重载(override)内核⾃⼰检测到的信息。
⽐如,我们⽤这样⼀个命令⾏参数字符串"console=ttyS0,115200n8"来通知内核以 ttyS0
中文系大学排名作为控制台,且串⼝采⽤ "115200bps、⽆奇偶校验、8位数据位"这样的设置。下⾯是⼀
段设置调⽤内核命令⾏参数字符串的⽰例代码:
char *p;
/* eat leading white space */
for(p = commandline; *p == ' '; p++)
;
/* skip non-existent command lines so the kernel will still
* u its default command line.
*/
if(*p == '/0')