Linux内核启动分析

嵌入式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的用户堆栈中。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值