首页 | 新闻 | 新品 | 文库 | 方案 | 视频 | 下载 | 商城 | 开发板 | 数据中心 | 座谈新版 | 培训 | 工具 | 博客 | 论坛 | 百科 | GEC | 活动 | 主题月 | 电子展
返回列表 回复 发帖

Boot Loader 的详细解释,STAGE2

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(弹簧床)一词的意思所在。

在vivi中采用如下的代码 实现 循环调用: main返回之后 就reset
@ get read to call C functions
 ldr sp, DW_STACK_START @ setup stack pointer
   // STACK_BASE+STACK_SIZE-4
// STACK_BASE = (VIVI_PRIV_RAM_BASE - STACK_SIZE)
//STACK从上往下用。  所以 STACK_START = STACK_BASE+STACK_SIZE-4
 mov fp, #0   @ no previous frame, so fp=0
 mov a2, #0   @ set argv to NULL

 bl main   @ call main //如果正常,一去不复返的了

mov pc, #FLASH_BASE @ otherwise, reboot, //FLASH_BASE=ROM_BASE0 = 0x0

1.2.1初始化本阶段要使用到的硬件设备

这通常包括:(1)初始化至少一个串口,以便和终端用户进行 I/O 输出信息;(2)初始化计时器等。  在初始化这些设备之前,也可以重新把 LED 灯点亮,以表明我们已经进入 main() 函数执行。  设备初始化完成后,可以输出一些打印信息,程序名字字符串、版本号等。

对VIVI而言,这些已经在stage 1中完成了。

1.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" 状态的。

VIVI中 用CONFIG_BOOTUP_MEMTEST 中的memtest完成基本的RAM检测

51 c8051f(f020,f040) msp430 arm(2410,2510) fpga(xc3s4000) dsp(5116 dm642) keilc vc++ matlab linux protel Ten_layerPCB mpegx h.26x Rscode Turbocode ofdm VideoBroadcasting ldpc_code(now!)

1.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 设备上拷贝映像的工作:


while(count) {
 *dest++ = *src++; /* they are all aligned with word boundary */
 count -= 4; /* byte number */
};

 


1.2.4 设置内核的启动参数

应该说,在将内核映像和根文件系统映像拷贝到 RAM 空间中后,就可以准备启动 Linux 内核了。但是在调用内核之前,应该作一步准备工作,即:设置 Linux 内核的启动参数。

Linux 2.4.x 以后的内核都期望以标记列表(tagged list)的形式来传递启动参数。启动参数标记列表以标记 ATAG_CORE 开始,以标记 ATAG_NONE 结束。每个标记由标识被传递参数的 tag_header 结构以及随后的参数值数据结构来组成。数据结构 tag 和 tag_header 定义在 Linux 内核源码的include/asm/setup.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_serialnr serialnr;
  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;
};

51 c8051f(f020,f040) msp430 arm(2410,2510) fpga(xc3s4000) dsp(5116 dm642) keilc vc++ matlab linux protel Ten_layerPCB mpegx h.26x Rscode Turbocode ofdm VideoBroadcasting ldpc_code(now!)

在嵌入式 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->u.core.flags = 0;
 params->u.core.pagesize = 0;
 params->u.core.rootdev = 0;

 params = tag_next(params);

 


其中,BOOT_PARAMS 表示内核启动参数在内存中的起始基地址,指针 params 是一个 struct tag 类型的指针。宏 tag_next() 将以指向当前标记的指针为参数,计算紧临当前标记的下一个标记的起始地址。注意,内核的根文件系统所在的设备ID就是在这里设置的。

下面是设置内存映射情况的示例代码:


for(i = 0; i < NUM_MEM_AREAS; i++) {
  if(memory_map.used) {
   params->hdr.tag = ATAG_MEM;
   params->hdr.size = tag_size(tag_mem32);

   params->u.mem.start = memory_map.start;
   params->u.mem.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
    * use its default command line.
  */
 if(*p == '\0')
  return;

 params->hdr.tag = ATAG_CMDLINE;
 params->hdr.size = (sizeof(struct tag_header) + strlen(p) + 1 + 4) >> 2;

 strcpy(params->u.cmdline.cmdline, p);

 params = tag_next(params);

 


请注意在上述代码中,设置 tag_header 的大小时,必须包括字符串的终止符'\0',此外还要将字节数向上圆整4个字节,因为 tag_header 结构中的size 成员表示的是字数。

下面是设置 ATAG_INITRD 的示例代码,它告诉内核在 RAM 中的什么地方可以找到 initrd 映象(压缩格式)以及它的大小:


 params->hdr.tag = ATAG_INITRD2;
 params->hdr.size = tag_size(tag_initrd);
 
 params->u.initrd.start = RAMDISK_RAM_BASE;
 params->u.initrd.size = INITRD_LEN;
 
 params = tag_next(params);

51 c8051f(f020,f040) msp430 arm(2410,2510) fpga(xc3s4000) dsp(5116 dm642) keilc vc++ matlab linux protel Ten_layerPCB mpegx h.26x Rscode Turbocode ofdm VideoBroadcasting ldpc_code(now!)
返回列表