嵌入式Linux系统分为三部分:引导程序BootLoader、根文件系统和Linux内核,针对不同的开发板需要不同Boot Loader来引导内核,本文开篇着重对U-Boot如何引导内核启动做详细分析,然后步步深入对内核的详解。该系统的硬件环境是基于S3C2440芯片的ARM9开发板,uboot的版本是u-boot-1.1.6,Linux源码版本是linux-3.4.2,交叉编译链为arm-linux-gcc-4.3.2。
1 U-Boot启动过程分析
Boot是linux内核启动程序前的一小段裸机程序,其功能类似PC机的BIOS程序,引导Widows操作系统启动,然后识别C、D盘,最后运行应用程序,所以嵌入式linux中的U-Boot的终极目的也是引导linux内核启动。本节对于Boot Loader的分析采用逆向思维寻找分析U-Boot的关键,反向推出分析U-Boot的关键文件。对于U-Boot最终是以映像文件移植到嵌入式平台的,执行make all命令,生成U-Boot镜像文件,在命令终端执从执行过程中可以得到链接命令部分代码:
arm-linux-ld -Bstatic -T /u-boot-1.1.6/board/my2440/u-boot.lds -Ttext 0x33F80000 $UNDEF_SYM cpu/arm920t/start.o \
--start-group lib_generic/libgeneric.a
该段代码中0x33F80000为代码运行起始段地址,其后面的所有*.a和*.o文件都是编译U-Boot所用的“原材料”,其中的文件start.o是由start.s文件汇编生成的,也是编译U-Boot所需要的第一个文件,所以下面将从Start.S文件着手对U-Boot进行分析。
1.1 U-Boot的stage1
U-Boot的第一阶段(stage1)不仅跟上述的文件cpu/arm920t/star.S有关,而且跟文件board/smdk2410/lowlevel.S也相关,前者与ARM平台相关,后者与开发板(CPU时钟)信息有关。该阶段主要完成平台的硬件初始化,代码主要由是汇编组成,以达到程序短小精悍的目的[2]。
(1)硬件初始化
设CPU的工作模式设置为管理模式(svc),该模式有额外的特权去进一步控制寄存器和CPU工作频率;关闭看门狗(WATCHDOG)防止系统资源浪费;根据芯片手册设置 CPU时钟(PCLK),总线时钟(HCLK),串口时钟(FCLK)比例,保证系统能正常运行;屏蔽所有中断和MMU以及清CACHE使能等。
(2)初始化外接SDRAM
Boot和Linux内核的程序最终都将在内存中运行,但是片上内存RAM只有4K,且RAM的价格昂贵,所以片内外设一般接SDRAM。此时的代码、数据还都保存在flash中,通过在start.S中调用lowlevel_init函数来设置存储控制器的值达到对SDRAM初始化的目的。其部分函数代码如下所示:
ldr r0, =SMRDATA /*13各寄存器值存放地址*/
ldr r1, _TEXT_BASE /*代码段地址*/
sub r0, r0, r1
ldr r1, =BWSCON /* 总线位宽控制寄存器 */
add r2, r0, #13*4 /*循环读取设置*/
(3)堆栈的设置
分析U-Boot的第二阶段主要是从文件lib_arm/board.c中的函数start_armboot开始,涉及到C函数必须设置堆栈,用于临时保存函数的传递参数和临时变量。栈的功能决定栈的灵活度很高,只需让sp寄存器指向SDRAM内存中未使用的空间即可,其代码如下所示:
stack_setup:
ldr r0, _TEXT_BASE /*重定位代码段起始地址 0x33F80000*/
sub r0, r0, #CFG_MALLOC_LEN /* 为实现malloc 预留内存空间*/
sub r0, r0, #CFG_GBL_DATA_SIZE /*为全局参数预留内存空间*/
#ifdef CONFIG_USE_IRQ
sub r0, r0, #(CONFIG_STACKSIZE_IRQ+CONFIG_STACKSIZE_FIQ) /*IRQ、FIQ模式栈*/
#endif
sub sp, r0, #12 /* 为abort异常留12字节的内存空间,往下全部内存设为栈了 */
经过上述分析,U-Boot各分区占用内存情况如图1-1所示
图1-1 U-Boot内存占用分配
SDRAM的起始地址是0x30000000,在系统上电后,U-Boot的镜像文件从Flash处的0x00000000地址被搬移到SDRAM内存中的_TEXT_BASE处运行,即0x33F80000地址处,这也就是下面将要解析的代码重定位。
(4)代码重定位
所谓代码重定位就是将U-Boot的第二段代码复制到RAM空间去,即将U-Boot的前4K以后的代码复制到SDRAM内存中(U-Boot的前4K代码通过片内内存自动复制到了SDRAM中),该功能代码如下所示:
relocate: /*重定位U-Boot剩余代码到SDRAM*/
adr r0, _start /* 当前位置代码地址赋给寄存器r0 */
ldr r1, _TEXT_BASE /* 将代码链接地址赋给寄存器r1*/
cmp r0, r1 /* 测试程序在flash还是SDRAM中运行*/
beq stack_setup /*如果在RAM中运行,则不用复制*/
ldr r2, _armboot_start
ldr r3, _bss_start
sub r2, r3, r2 /* 代码段长度赋值给寄存器r2 */
add r2, r0, r2 /*flash上的代码段结束地址赋值给寄存器r3 */
copy_loop:
ldmia r0!, {r3-r10} /* 从r0获得数据 */
stmia r1!, {r3-r10} /*复制到地址r1 */
cmp r0, r2 /* 判断是否复制完毕*/
ble copy_loop /*知道复制完*/
(5)清除BSS段
为了减少存储空间的使用,在进入U-Boot的第二阶段之前需要对BSS段进行清除操作,也就是将初始值为0、无初始的全局变量和静态变量放在BSS段。清除完毕后,C函数的运行环境也已经完全准备好,此时需调用lib_arm/board.c文件中的_start_armboot函数进入U-Boot的第二阶段。
1.2 U-Boot的stage2
第二阶段的最终目的是从Flash读取Linux内核然后启动。此时涉及到U-Boot下载、启动Linux内核等情况,至少需要初始化串口和时钟来作为程序员与Bootloader的交互工具。然后检测内存映射,确定开发板的内存占用情况,根据Bootloader的定制本身的特点,可以直接对开发板进行设置,免去对适应性的复杂算法的理解。最后将内核从Flash读到内存中,直接跳到C函数入口点启动内核。
2 内核的启动过程分析
常见的Linux内核镜像文件是uImage、zImage和bzImage,这三种内核都是由头部header和真正的内核vmlinux组成,所以Linux内核启动也可以分为两个阶段:机器架构引导阶段和vmlinux启动阶段。
在机器架构引导阶段Linux内核首先会检测是否支持当前开发板架构的处理器,然后再检测内核是否支持当前的开发板,比如设置页表、使能MMU和禁止ICache等操作,该阶段主要是通过汇编来完成的。vmlinux启动阶段的关键代码主要是用C语言编写完成,架构引导阶段完成后,内核调用start_kernel函数做启动工作,进行内核初始化的全部工作,最后调用rest_init函数创建启动第一个init进程,除以上功能,该阶段还需要通过函数setup_arch进行重新设置页表和初始化时钟等与开发板/架构相关的设置。ARM架构上vmlinux的启动过程如图2-1所示。
图2-1 ARM处理器Linux启动过程
2.1内核引导阶段分析
与U-Boot的启动一样,Linux内核启动的第一个代码文件也是汇编文件arch/arm/kernel/head.S。在调用内核时,U-Boot会把存在文件board/smdk2410/smdk2410.c中机器ID传送给内核,其代码如下:
gd->bd->bi_arch_number = MACH_TYPE_SMDK2410
其中ID值存储在head.S文件中的r1的寄存器中,每个开发板的ID值不通,所以主要根据该ID值去判断内核是否支持当前架构,其中汇编文件head.S中的部分代码如下所示:
ENTRY(stext)
msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE /*进去SVC模式且禁止中断*/
mrc p15, 0, r9, c0, c0 /*把CPU ID读入r9寄存器*/
bl __lookup_processor_type/*调用函数返回值 r5=procinfo,输入参数 r9=cpuid*/
movs r10, r5 /* 判断r5是否为0*/
beq __error_p /*若r5=0,打印错误*/
bl __lookup_machine_type /*调用machine_type函数,r5=machinfo*/
movs r8, r5 /*如果不支持当前开发板,返回值r5=0 */
beq __error_a /* r5=0,则打印错误*/
执行函数__lookup_processor_type,如果内核不支持当前的CPU,则返回值r5=0;反之r5返回一个具体描述当前CPU的结构体地址。执行函数__lookup_machine_type,若内核不支持当前开发板,则返回值r5=0;反之r5返回一个具体描述当前架构的结构体地址,若以上两者返回值有其中一个为0,则内核不能正常启动。
2.2内核与U-Boot参数交互
启动内核除了需要正确的机器ID,还需要设置启动参数和程序跳到启动入口地址。内核的加载地址和入口地址设置在文件include/iamge.h中,如代码所示:
uint32_t ih_load; /* 数据加载地址 */
uint32_t ih_ep; /*入口地址 */
在执行启动命令后,执行函数theKernel = (void (*)(int, int, uint))ntohl(hdr->ih_ep),函数theKernel 指向ih_ep,即指向内核启动入口地址,然后执行代码theKernel (0, bd->bi_arch_number, bd->bi_boot_params)开始执行判断机器ID和内核交互命令。
U-Boot启动内核后,为了保持内核的正常启动,内核还需要一些启动参数供其读取和处理,比如内存的大小、串口、videolfb、内存的起始地址和一些命令行参数等。U-Boot和Linux内核通过结构体struct tag来传递参数,Linux内核启动时,把结构体struct tag的地址传给内核。其主要的四个TAG如下所示:
setup_start_tag (¶ms); /*tag 开始*/
setup_memory_tags (bd); /*内存tag*/
setup_commandline_tag (bd, commandline); /*命令行tag*/
setup_end_tag (bd); /*tag结束*/
不同的开发板其启动参数的物理地址不同,当前所用的开发板的参数放在地址0x30000100处,位于文件board/my2440/my2440.c中,其代码为:gd->bd->bi_boot_params = 0x30000100。结构体struct tag在内存中的分布如图2-2所示。
图2-2 启动参数保存格式
2.3 内核初始化阶段
引导阶段完成后,从文件init/Main.c的start_kernel函数开始对内核进行初始化,首先是读取和处理U-Boot传给内核的参数,两者的交互参数分为保存在某个物理地址的TAG列表和调用内核时寄存器r1指定的机器ID两类,机器ID在内核启动引导阶段函数__lookup_processor_type和函数__lookup_machine_type中已经处理完毕,TAG列表中的参数在setup_arch函数中进行处理。
setup_arch函数定义在arch/arm/kernel/setup.c文件中,该函数主要进行处理器相关的一些设置、获取开发板的machine_desc结构、处理TAG列表、预处理命令行和二次初始化页表等操作,其部分代码如下所示:
setup_processor(); /*处理器相关的一些设置*/
mdesc = setup_machine(machine_arch_type);/*获取开发板的machine_desc的结构*/
tags = phys_to_virt(mdesc->boot_params); /*确定TAG首地址*/
parse_tags(tags); /*解释每个TAG*/
parse_cmdline(cmdline_p, from); /*命令预处理*/
paging_init(&meminfo, mdesc); /*二次初始化页表*/
由以上代码tags = phys_to_virt(mdesc->boot_params)可知,开发板中的machine_desc结构指定了TAG列表的首地址,并且先将物理地址转化成虚拟地址,对于当前使用的开发板,在文件mach-smdk2440.c中有代码所示:.boot_params= S3C2410_SDRAM_PA + 0x100,内存的起始地址加上偏移地址就是启动参数的首地址,为0x30000000+0x100=0x30000100。
2.4 根文件系统的简单介绍
尽管内核是系统的核心,但是文件是用户与操作系统的交互工具,在Linux系统中,它主要使用文件管理I/O机制操控硬件设备和数据。根文件系统是Linux内核启动时所挂载的第一个文件系统,包含着引导内核程序所必需的初始化脚本(如rcS、inittab)、内核镜像文件、内核启动的第一个init程序、shell命令和库文件等,构建根文件系统至少需要的五个目录如表2-3所示。
表2-3 根文件系统最小目录
/bin/ | 存储二进制可执行命令文件 |
/sbin/ | 系统命令的存储目录 |
/etc/ | 存储配置文件 |
/lib/ | 存储可执行文件的链接库 |
/dev/ | 存储设备文件 |
2.4 启动命令bootcmd
Flash存储器一般作为嵌入式系统的固态存储设备存在,是一种非易失性内存,支持断电保存数据,存储特性相当于硬盘。MTD内存技术用于访问ROM、Nor flash和Nand Flash等存储器,是屏蔽了底层硬件和各类设备的差别,向上统一提供读、写、擦除等功能抽象出来的一个设备层。得益于MTD,Nand Flash存储器划分区非常简单,一般划分为四个区,分别存储Kernel、Bootloader、Boot parameters和Root filesystem。
在U-Boot的控制界面下,输入命令打印命令print输出U-Boot的环境变量,其中有变量:bootcmd=nand read.jffs2 0x30007FC0 kernel; bootm 0x30007FC0,在U-Boot的控制界面下,使用该bootcmd可以直接启动内核,该命令分为两个步骤执行,首先是将内核kernel从Nand Flash的kernel分区通过nand read命令到内存的地址0x30007FC0处,即nand read.jffs2 0x30007FC0 kernel,其实现函数s = getenv ("bootcmd")位于文件common/Main.c中;然后执行bootm命令从内存地址0x30007FC0处启动内核,即bootm 0x30007FC0,其实现函数为run_command (s, 0),启动的起始地址0x30007FC0必须处于图1-1的用户堆栈中。