uboot如何启动内核

注:本文是学习朱老师课程整理的笔记,基于uboot-1.3.4和s5pc11x分析。

操作系统内核本身就是一个裸机程序,和uboot、和其他裸机程序并没有本质区别,区别就是操作系统运行起来后在软件上分为内核层和应用层,分层后两层的权限不同,内存访问和设备操作的管理上更加精细(内核可以随便访问各种硬件,而应用程序只能被限制的访问硬件和内存地址)。

uboot的镜像是u-boot.bin,linux系统的镜像是zImage,这两个东西其实都是两个裸机程序镜像。从系统的启动角度来讲,内核其实就是一个大的复杂点裸机程序。

  • 部署在SD卡中特定分区内

一个完整的软件+硬件的嵌入式系统,静止时(未上电时)bootloader、kernel、rootfs等必须的软件都以镜像的形式存储在启动介质中(X210中是iNand/SD卡);运行时都是在DDR内存中运行的,与存储介质无关。上面2个状态都是稳定状态,第3个状态是动态过程,即从静止态到运行态的过程,也就是启动过程。

动态启动过程就是一个从SD卡逐步搬移到DDR内存,并且运行启动代码进行相关的硬件初始化和软件架构的建立,最终达到运行时稳定状态。

静止时u-boot.bin, zImage, rootfs都在SD卡中,他们不可能随意存在SD卡的任意位置,因此需要对SD卡进行一个分区,然后将各种镜像各自存在各自的分区中,这样在启动过程中uboot、内核等就知道到哪里去找谁。(uboot和kernel中的分区表必须一致,可以分别建立相同的分区表,也可以通过uboot传参的方式传给kernel,这样更方便)。

  • 加载内核到DDR中

uboot在第一阶段中进行重定位时将整个uboot镜像加载到DDR的0xc3e00000地址处,这个地址就是uboot的链接地址。

内核也有类似要求,uboot启动内核时将内核从SD卡读取放到DDR中,不能随意放置,必须放在内核的链接地址处,否则启动不起来。譬如我们使用的内核链接地址是0x30008000(zImage)。

内核镜像的加载有2种方式

  1. uboot从SD卡的kernel分区去读取内核镜像到DDR中。
    读取要使用uboot的命令来读取(譬如X210的iNand版本是movi命令,X210的Nand版本就是Nand命令)。iNand版本使用命令:movi read kernel 30008000。就是从SD中的kernel分区读取内核镜像,然后加载到DDR的30008000地址处。

  2. uboot通过网络从服务器中下载镜像到DDR中。uboot支持远程启动,也就是内核镜像不烧录到开发板的SD卡中,而是放在主机的服务器中。

分析:产品出厂时会设置为从SD卡中启动;开发者一般用tftp、nfs下载远程启动这种方式。

  • do_bootm(镜像的头部信息校验)

命令名前加do_即可构成这个命令对应的函数,因此当我们bootm命令执行时,uboot实际执行的函数叫do_bootm函数,在common/cmd_bootm.c中。

do_bootm函数大体框架如下:

int do_bootm (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[])
{
    image_header_t  *hdr; /* 镜像头文件信息结构体 */

#ifdef CONFIG_ZIMAGE_BOOT
#define LINUX_ZIMAGE_MAGIC  0x016f2818
    /* 1.find out kernel image address */
    if (argc < 2) {  /* 如果不带参数直接bootm,则会从CFG_LOAD_ADDR地址去执行 */
        addr = load_addr;
    } else {
        addr = simple_strtoul(argv[1], NULL, 16);
    }
    /* 第37-40字节处存放着zImage标志魔数,从这个位置取出然后对比LINUX_ZIMAGE_MAGIC */
    if (*(ulong *)(addr + 9*4) == LINUX_ZIMAGE_MAGIC) {
        printf("Boot with zImage\n");
        /* 2.对zImage头信息进行修改 */
        addr = virt_to_phys(addr);
        hdr = (image_header_t *)addr;  /* zImage头信息的开始地址 */
        hdr->ih_os = IH_OS_LINUX;
        hdr->ih_ep = ntohl(addr);

        memmove (&images.legacy_hdr_os_copy, hdr, sizeof(image_header_t));

        /* save pointer to image header */
        images.legacy_hdr_os = hdr;
        images.legacy_hdr_valid = 1;

        goto after_header_check;    /* 跳过下面的uImage和设备树的校验 */
    }
#endif

    /* get kernel image header, start address and length */
    /* uImage头信息的校验,和zImage的校验相似 */
    os_hdr = boot_get_kernel (cmdtp, flag, argc, argv,
            &images, &os_data, &os_len);    

    /* get image parameters */
    switch (genimg_get_format (os_hdr)) {
    case IMAGE_FORMAT_LEGACY:   /* uImage启动 */       
        type = image_get_type (os_hdr);  /* 镜像类型 */   
        comp = image_get_comp (os_hdr);   /* 镜像压缩类型 */   
        os = image_get_os (os_hdr);      /* 操作系统类型 */ 

        image_end = image_get_image_end (os_hdr);
        load_start = image_get_load (os_hdr);
        break;

    case IMAGE_FORMAT_FIT:  /* 设备树启动 */ 
        ……      
        break;
        return 1;
    }

    image_start = (ulong)os_hdr;
    load_end = 0;
    type_name = genimg_get_type_name (type);

    switch (comp) { 
    case IH_COMP_NONE:
        if (load_start == (ulong)os_hdr) {
            printf ("   XIP %s ... ", type_name);
        } else {
            printf ("   Loading %s ... ", type_name);
            memmove_wd ((void *)load_start,
                   (void *)os_data, os_len, CHUNKSZ);
        }
        load_end = load_start + os_len;
        puts("OK\n");
        break;
    case IH_COMP_GZIP:
        ……
        break;
    case IH_COMP_BZIP2:
        ……
        break;
        return 1;
    }
    puts ("OK\n");

after_header_check:
    os = hdr->ih_os;   
    switch (os) {
    case IH_OS_LINUX:
        do_bootm_linux (cmdtp, flag, argc, argv, &images);/* 3.启动内核 */
        break;

    /* 其它操作系统 */
    ……

    }

    return 1;
}

do_bootm函数进行镜像的头部信息校验。校验时就要根据不同种类的image类型进行不同的校验,然后通过则进入下一步准备启动内核;如果校验失败则认为镜像有问题,所以不能启动。

zImage启动:
CONFIG_ZIMAGE_BOOT,用这个宏来控制进行条件编译一段代码,这段代码是用来支持zImage格式的内核启动的。

#define LINUX_ZIMAGE_MAGIC 0x016f2818定义一个魔数,这个数等于0x016f2818,表示这个镜像是一个zImage。也就是说zImage格式的镜像中在头部的一个固定位置存放了这个数作为格式标记。如果我们拿到了一个image,去他的那个位置去取4字节判断它是否等于LINUX_ZIMAGE_MAGIC,则可以知道这个镜像是不是一个zImage。

启动命令: bootm 0x30008000,传给do_boom后,argc=2,argv[0]=bootm , argv[1]=0x30008000。但是实际bootm命令还可以不带参数执行。如果不带参数直接bootm,则会从CFG_LOAD_ADDR地址去执行(定义在x210_sd.h中)。

zImage头部开始的第37-40字节处存放着zImage标志魔数,从这个位置取出然后对比LINUX_ZIMAGE_MAGIC。可以用二进制阅读软件(如winhex、UltraEditor)来打开zImage查看,就可以证明。

image_header_t结构体的定义如下:

/*
 * Legacy format image header,
 * all data in network byte order (aka natural aka bigendian).
 */
typedef struct image_header {  
 uint32_t ih_magic; /* Image Header Magic Number */  //镜像头号  
 uint32_t ih_hcrc; /* Image Header CRC Checksum */   //镜像头部检验和  
 uint32_t ih_time; /* Image Creation Timestamp */    //镜像创建时间  
 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  */           //cpu架构  
 uint8_t  ih_type; /* Image Type   */                //镜像类型  
 uint8_t  ih_comp; /* Compression Type  */           //压缩类型  
 uint8_t  ih_name[IH_NMLEN]; /* Image Name  */       //镜像名  
} image_header_t;

这个数据结构是我们uboot启动内核使用的一个标准启动数据结构,zImage头信息也是一个image_header_t,但是在实际启动之前需要进行一些改造。如:

hdr->ih_os = IH_OS_LINUX;
hdr->ih_ep = ntohl(addr);

images全局变量是在do_bootm函数中使用,用来完成启动过程的。

static bootm_headers_t images;

zImage的校验过程其实就是先确认是不是zImage,确认后再修改zImage的头信息到合适,修改后用头信息去初始化images这个全局变量,然后就完成了校验。

uImage启动:

LEGACY(遗留的),在do_bootm函数中,这种方式指的就是uImage的方式。

uImage方式是uboot本身发明的支持linux启动的镜像格式,但是后来这种方式被一种新的方式替代,这个新的方式就是设备树方式(在do_bootm方式中叫FIT),本文不讲设备树的启动。

uImage的启动校验主要在boot_get_kernel函数中,主要任务就是校验uImage的头信息,并且得到真正的kernel的起始位置去启动。

第二阶段校验头信息结束,下面进入第三阶段,第三阶段主要任务是启动linux内核,调用do_bootm_linux函数来完成。do_bootm_linux函数在lib_arm/bootm.c中。

  • do_bootm_linux

do_bootm_linux函数大体框架如下:

void do_bootm_linux (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[],
             bootm_headers_t *images)
{   
    char *commandline = getenv ("bootargs");

    /* find kernel entry point */   
    ep = image_get_ep (&images->legacy_hdr_os_copy);
    theKernel = (void (*)(int, int, uint))ep;  /*镜像的执行入口*/

    /* 机器码传给内核 */
    s = getenv ("machid");
    machid = simple_strtoul (s, NULL, 16);
    printf ("Using machid 0x%x from environment\n", machid);

    /* 传参内核 */
    setup_start_tag (bd);   /* 传参开始的地方 */
    setup_memory_tags (bd);
    setup_commandline_tag (bd, commandline);
    setup_initrd_tag (bd, initrd_start, initrd_end);
    setup_mtdpartition_tag();   
    setup_end_tag (bd);   /* 传参结束的地方 */

    /* we assume that the kernel is in place */
    printf ("\nStarting kernel ...\n\n");

    theKernel (0, machid, bd->bi_boot_params);
    /* does not return */
    return;
}

ep就是entrypoint的缩写,就是程序入口。一个镜像文件的起始执行部分不是在镜像的开头(镜像开头有n个字节的头信息),真正的镜像文件执行时第一句代码在镜像的中部某个字节处,相当于头是有一定的偏移量的。这个偏移量记录在头信息中。

将ep赋值给theKernel,则这个函数指向就指向了内存中加载的OS镜像的真正入口地址(就是操作系统的第一句执行的代码)。

总结:一般执行一个镜像都是:

  1. 先读取头信息,在头信息的特定地址找MAGIC_NUM,由此来确定镜像种类;
  2. 对镜像进行校验;
  3. 再次读取头信息,由特定地址知道这个镜像的各种信息(镜像长度、镜像种类、入口地址);
  4. 去entrypoint处开始执行镜像。

uboot传给内核的机器码是怎么确定的?第一顺序备选是环境变量machid,第二顺序备选是gd->bd->bi_arch_num(x210_sd.h中硬编码配置的)。

  • tag方式传参
struct tag {
        struct tag_header hdr;
        union { 
                struct tag_core         core;
                struct tag_mem32        mem;
                struct tag_initrd       initrd;
                struct tag_cmdline      cmdline;     
                 ……
                struct tag_mtdpart      mtdpart_info;
        } u;
};

/*其中tag_header的结构体如下:*/
/* The list ends with an ATAG_NONE node. */
#define ATAG_NONE   0x00000000
struct tag_header {
    u32 size;
    u32 tag;
};

部分tag_XXX如下:
/* The list must start with an ATAG_CORE node */
#define ATAG_CORE   0x54410001
struct tag_core {
    u32 flags;      /* bit 0 = read-only */
    u32 pagesize;
    u32 rootdev;
};

/* it is allowed to have multiple ATAG_MEM nodes */
#define ATAG_MEM    0x54410002
struct tag_mem32 {
    u32 size;
    u32 start;  /* physical start address */
};

……

tag是一个数据结构,在uboot和linux kernel中都有定义tag数据结构,而且定义是一样的。

tag_header中有这个tag的size和类型编码,kernel拿到一个tag后先分析tag_header得到tag的类型和大小,然后将tag中剩余部分当作一个tag_xxx来处理。

  • x210_sd.h中配置传参宏
#define CONFIG_SETUP_MEMORY_TAGS
#define CONFIG_CMDLINE_TAG
#define CONFIG_INITRD_TAG
#define CONFIG_MTDPARTITION "80000 400000 3000000"

CONFIG_SETUP_MEMORY_TAGS:传参内容是内存配置信息。

CONFIG_CMDLINE_TAG:传参内容是启动命令行参数,也就是uboot环境变量的bootargs。

CONFIG_MTDPARTITION:传参内容是iNand/SD卡的分区表。

static void setup_start_tag (bd_t *bd)
{
    params = (struct tag *) bd->bi_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);
}

/* 其他的ATAG_XXX */
……

static void setup_end_tag (bd_t *bd)
{
    params->hdr.tag = ATAG_NONE;
    params->hdr.size = 0;
}

起始tag是ATAG_CORE、结束tag是ATAG_NONE,其他的ATAG_XXX都是有效信息tag。

kernel接收到的传参是若干个tag构成的,这些tag由setup_end_tag起始,到setup_end_tag结束。

tag传参的方式是由linux kernel发明的,kernel定义了这种向我传参的方式,uboot只是实现了这种传参方式从而可以支持给kernel传参。

Starting kernel … 是uboot中最后一句打印出来的东西。这句如果能出现,说明uboot整个是成功的,也成功的加载了内核镜像,也校验通过了,也找到入口地址了,也试图去执行了。如果这句后串口就没输出了,说明内核并没有被成功执行。原因一般是:传参(80%)、内核在DDR中的加载地址·······

  • 跳转到内核

uboot最终是调用theKernel函数来执行linux内核的,uboot调用这个函数(其实就是linux内核)时传递了3个参数。这3个参数就是uboot直接传递给linux内核的3个参数,通过寄存器来实现传参的。(第1个参数就放在r0中,第二个参数放在r1中,第3个参数放在r2中)第1个参数固定为0,第2个参数是机器码,第3个参数传递的就是大片传参tag的首地址。

uboot移植时一般只需要配置相应的宏即可。kernel启动不成功,注意传参是否成功。传参不成功首先看uboot中bootargs设置是否正确,其次看uboot是否开启了相应宏以支持传参。

  • 总结

启动4步骤

第一步:将内核搬移到DDR中
第二步:校验内核格式、CRC等
第三步:准备传参
第四步:跳转执行内核

涉及到的主要函数
do_boom和do_bootm_linux

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值