u-boot启动流程,启动内核的关键点do_bootm分析

来源:互联网 发布:访问者模式 php 编辑:程序博客网 时间:2024/06/05 00:12

         在讲加载地址和入口地址之前先简单说下u-boot的启动过程。执行make可以找到链接脚本,位于board/$(BOARDNAME)/U-boot.lds,从连接脚本中知道第一个源文件为cpu\arm920t\start.s中,从start.s开顺藤摸瓜了解u-boot的启动过程,其主要分为两个阶段,start.s为第一个阶段,主要进行的工作有:关闭看门狗,屏蔽中断,设置时钟,初始化cpu,初始化堆栈(为调用c语言程序做准备),初始化SDRAM(lowlevel函数),然后进行代码重定位(relocate函数,将u-boot从flash拷贝到SDRAM中)。

        进行完上述一系列工作后为第二阶段准备好了RAM空间,程序进入第二阶段,跳转到start_armboot函数(lib_arm\Board.c),该函数通过调用全局的函数指针数组init_sequence进行初始化,包括cpu,board(在全局的gd->bd中设置了arch_number,以及boot_params的位置0x30000100),中断,环境变量,串口,以及ram(在全局的gd->bd中设置了内存起始地址和大小)初始化。然后初始化堆栈,初始化nand flash(为后续读写nand flash需要做准备),网卡,usb(为下载内核文件系统做准备)。然后程序进入倒数计时,如果没有被按键打断那么进入一个菜单,否则就启动内核。

        u-boot的终极目的是启动内核,那么是通过什么命令启动内核的呢,在u-boot的环境变量中有一个参数bootcmd= nand read.jffs2 0x30007fc0 kernel; bootm 0x30007fc0(此处根据具体的u-boot而定),意在将内核读取到0x30007fc0,然后在该地址处通过bootm命令启动内核,因此关键在于bootm命令,其具体的实现在do_bootm函数(common\cmd_bootm.c)中,这个函数的本质工作是:根据bootm命令传入的地址,以及uImage(uImage其实是包括一个头部和真正的内核,头部是包括内核的一些信息,真正的内核就是vmlinux)头部的加载地址和入口地址,来移动内核到入口地址,最后就通过函数指针的调用跳转到该地址,u-boot从此一去不复返,进入了内核的世界。

先通过编译内核后来看看加载地址和入口地址均,下图所示,两个地址均为0x30008000,下面截取bootm函数中的部分代码来分析如何进入内核的。

typedef struct image_header {        uint32_t        ih_magic;  /* Image Header Magic Number<span style="white-space:pre"></span>*/        uint32_t        ih_hcrc;   /* Image Header CRC Checksum<span style="white-space:pre"></span>*/        uint32_t        ih_time;   /* Image Creation Timestamp<span style="white-space:pre"></span>*/        uint32_t        ih_size;   /* Image Data Size           */        uint32_t        ih_load;   /* Data    Load  Address     */        uint32_t        ih_ep;     /* Entry Point Address */        uint32_t        ih_dcrc;   /* Image Data CRC Checksum   */        uint8_t         ih_os;     /* Operating System          */        uint8_t         ih_arch;   /* CPU architecture          */        uint8_t         ih_type;   /* Image Type                */        uint8_t         ih_comp;   /* Compression Type          */        uint8_t         ih_name[IH_NMLEN];     /*  Image Name   */} image_header_t;    //这就是uImage的头部信息结构体
    image_header_t *hdr = &header;  //局部变量指针hdr指向全局的header
    if (argc < 2) {        addr = load_addr;   //bootm命令的时候后面没有跟一个具体的地址,那么addr就等于默认的地址load_addr(0x33000000);    } else {        addr = simple_strtoul(argv[1], NULL, 16);  //否则addr就是紧跟在bootm命令后的地址,而在u-boot的环境变量bootcmd中该地址为0x30007fc0    }
    memmove (&header, (char *)addr, sizeof(image_header_t));   //将addr地址开始的内核头部信息读入到全局的header结构体变量中,
    if (ntohl(hdr->ih_magic) != IH_MAGIC) //如果hdr中的幻数跟IH_MAGIC不匹配则退出函数,启动失败,一般原因在于nand read后的地址跟bootm的地址不匹配导致。    {        puts ("Bad Magic Number\n");        SHOW_BOOT_PROGRESS (-1);        return 1;    }
    printf ("## Booting image at %08lx ...\n", addr);  //打印出内核启动地址
    data = (ulong)&header;  //data指向头部    len  = sizeof(image_header_t); //len为头部长度    checksum = ntohl(hdr->ih_hcrc);//    hdr->ih_hcrc = 0;    if (crc32 (0, (uchar *)data, len) != checksum) {   //采用CRC校验,如果校验和不与头部的校验值不当等则退出        puts ("Bad Header Checksum\n");        SHOW_BOOT_PROGRESS (-2);        return 1;    }
    print_image_hdr ((image_header_t *)addr);   //打印出头部信息,也就是上面图片中所展示的信息    data = addr + sizeof(image_header_t);         //data从此指向真正的内核地址入口地址了,而头部的大小为64个字节,addr=0x30007fc0,那么data刚好等于0x30008000
    switch (hdr->ih_comp) {       //此处开始进入高潮,决定是否移动内核。根据上面图片的信息可知是非压缩内核,执行下面的case       case IH_COMP_NONE:               if(ntohl(hdr->ih_load) == data) {         //如果加载地址等于data,那么不需要移动内核,否则移动内核到加载地址ih_load。可见从0x30007fc0启动的原因就是避免内核移动,从而节省时间。                      printf ("   XIP %s ... ", name);               } else {</span><span style="font-family: Arial, Helvetica, sans-serif;">                     memmove ((void *) ntohl(hdr->ih_load), (uchar *)data, len);                 }              break;</span>

      switch (hdr->ih_os) {               case IH_OS_LINUX:  //内核支持linux操作          do_bootm_linux (cmdtp, flag, argc, argv, addr, len_ptr, verify);  //在do_bootm_linux中进入内核的入口地址了

do_bootm_linux函数中,关键点在以下部分

        1、void (*theKernel)(int zero, int arch, uint params); 定义函数指针thekernel,带有三个参数

            theKernel = (void (*)(int, int, uint))ntohl(hdr->ih_ep);  thekernel指向了内核的入口地址,即0x30008000

        2、下面setup类的函数都是设定启动参数标记tag,如内存tag,命令行tag,这些tag都存在0x30000100开始的地址处(这个地址在board.c中board初始化中指定)

              setup_start_tag (bd);

              setup_memory_tags (bd);

             setup_commandline_tag (bd, commandline);

            setup_end_tag (bd);

       3、theKernel (0, bd->bi_arch_number, bd->bi_boot_params);  //跳入到内核的入口地址,并传入arch_number和boot_params地址(0x30000100),从此,u-boot完成使命,真正的进入内核了。


总结:

1、nand read后的地址跟bootm后的地址务必保持一致,否则无法读入正确的内核头部。

2、如果加载地址和入口地址相等,那么bootm后的地址可以任意,因为在任意的地址启动都会进行if(ntohl(hdr->ih_load) == data) 判断,可理解为判断头部指定的内核入口地址是否等于真实的内核入口地址,从而进行相应操作保证二者相等。注:本人所用u-boot是经过韦东山移植后的,原版的u-boot源码中是进行if(ntohl(hdr->ih_load) == addr) 判断,那么如果bootm后跟的地址是就是入口地址0x30008000,即addr=0x30008000,那么此处判断结果是相等,不进行内核移动,而实际的内核入口地址为0x30008040,不等于内核头部指定的0x30008000,那么后续内核启动必然出现问题,因此,对于原版的u-boot,如果内核加载地址等于入口地址,那么bootm后的地址不能为加载地址。

3、如果加载地址不等于入口地址(即入口地址=加载地址+0x40),那么对于韦东山这套u-boot,任意的启动地址都无法启动,因为if(ntohl(hdr->ih_load) == data)判断后的结果实际的内核入口地址=加载地址,而不等于入口地址,这显然不能启动。那么对于原版的u-boot,进行if(ntohl(hdr->ih_load) == addr) 判断必须结果为真才能启动内核,因为结果为假就会移动内核入口地址到加载地址去,所以对于原版u-boot,bootm后的地址必须为入口地址。


0 0