拷贝时要确定两点:
(1) stage2 的可执行映象在固态存储设备的存放起始地址和终止地址;
(2) RAM 空间的起始地址。
3.1.4 设置堆栈指针 sp
堆栈指针的设置是为了执行 C 语言代码作好准备。通常我们可以把 sp 的值设置为(stage2_end-4),也即在 3.1.2 节所安排的那个 1MB 的 RAM 空间的最顶端(堆栈向下生长)。
此外,在设置堆栈指针 sp 之前,也可以关闭 led 灯,以提示用户我们准备跳转到 stage2。
经过上述这些执行步骤后,系统的物理内存布局应该如下图2所示。
3.1.5 跳转到 stage2 的 C 入口点
在上述一切都就绪后,就可以跳转到 Boot Loader 的 stage2 去执行了。
比如,在 ARM 系统中,这可以通过修改 PC 寄存器为合适的地址来实现。
3.2 Boot Loader 的stage2
正如前面所说,stage2 的代码通常用 C 语言来实现,以便于实现更复杂的功能和取得更好的代码可读性和可移植性。
但是与普通 C 语言应用程序不同的是,在编译和链接boot loader 这样的程序时,我们不能使用 glibc 库中的任何支持函数。其原因是显而易见的。这就给我们带来一个问题,那就是从那里跳转进 main() 函数呢?直接把 main() 函数的起始地址作为整个 stage2 执行映像的入口点或许是最直接的想法。但是这样做有两个缺点:
1)无法通过main() 函数传递函数参数;
2)无法处理 main() 函数返回的情况。
一种更为巧妙的方法是利用 trampoline(弹簧床)的概念。也即,用汇编语言写一段trampoline 小程序,并将这段 trampoline 小程序来作为 stage2 可执行映象的执行入口点。然后我们可以在 trampoline 汇编小程序中用 CPU 跳转指令跳入 main() 函数中去执行;而当 main() 函数返回时,CPU 执行路径显然再次回到我们的 trampoline 程序。简而言之,这种方法的思想就是:用这段 trampoline 小程序来作为main() 函数的外部包裹(external 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 S3C44B0X CPU 中,从 0x0c00,0000 到 0x1000,0000 之间的64M地址空间被用作系统的 RAM 地址空间。虽然 CPU 通常预留出一大段足够的地址空间给系统 RAM,但是在搭建具体的嵌入式系统时却不一定会实现 CPU 预留的全部 RAM 地址空间。也就是说,具体的嵌入式系统往往只把 CPU 预留的全部 RAM 地址空间中的一部分映射到 RAM 单元上,而让剩下的那部分预留 RAM 地址空间处于未使用状态。
由于上述这个事实,因此 Boot Loader 的 stage2 必须在它想干点什么 (比如,将存储在 flash 上的内核映像读到 RAM 空间中) 之前检测整个系统的内存映射情况,也即它必须知道CPU 预留的全部 RAM 地址空间中的哪些被真正映射到 RAM 地址单元,哪些是处于 “unused” 状态的。
(1) 内存映射的描述
可以用如下数据结构来描述 RAM 地址空间中的一段连续(continuous)的地址范围:
typedef struct memory_area_struct {
u32 start; /* the base address of the memory region */
u32 size; /* the byte number of the memory region */
int used;
} memory_area_t;
这段 RAM 地址空间中的连续地址范围可以处于两种状态之一:
(1)used=1,则说明这段连续的地址范围已被实现,也即真正地被映射到 RAM 单元上。
(2)used=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,
.used = 0
},
};
(2) 内存映射的检测
下面我们给出一个可用来检测整个 RAM 地址空间内存映射情况的简单而有效的算法:
/* 数组初始化 */
for(i = 0; i < NUM_MEM_AREAS; i++)
memory_map[i].used = 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 */
if(memory_map[i].used )
i++;
continue;
}
/*
* 当前页已经是一个被映射到 RAM 的有效地址范围
* 但是还要看看当前页是否只是 4GB 地址空间中某个地址页的别名?
*/
if(* (u32 *)addr != 0) { /* alias? */
/* 这个内存页是 4GB 地址空间中某个地址页的别名 */
if ( memory_map[i].used )
i++;
continue;
}
/*
* 当前页已经是一个被映射到 RAM 的有效地址范围
* 而且它也不是4GB 地址空间中某个地址页的别名。
*/
if (memory_map[i].used == 0) {
memory_map[i].start = addr;
memory_map[i].size = PAGE_SIZE;
memory_map[i].used = 1;
} else {
memory_map[i].size += PAGE_SIZE;
}
} /* end of for (…) */
在用上述算法检测完系统的内存映射情况后,Boot Loader 也可以将内存映射的详细信息打印到串口。
评论
查看更多