bootloader uboot内存映射与启动流程

第1章 uboot 概述

1.1 概述

uboot是bootloader的一种,是Linux内核的引导启动程序。

它会初始化嵌入式平台上的一些外设(比如:ddr等),把Linux内核镜像从flash中加载到内存,在完成一些初始化工作后,最后启动Linux内核,类似于windows的BIOS程序。

uboot相当于是一段功能复杂的裸机程序,单片机的裸机程序没有本质区别。

下面将是对uboot启动流程的源码分析,此处使用的嵌入式平台芯片是NXP的 i.mx6ull 芯片(Cortex-A7内核,arm v7架构),uboot源码是NXP官方提供的4.1.15版本uboot。

下载地址:http://git.freescale.com/git/cgit.cgi/imx/uboot-imx.git/tag/?h=imx_v2016.03_4.1.15_2.0.0_ga&id=rel_imx_4.1.15_2.1.0_ga

1.2 内存映射(案例)

1)有两个地方存放uboot的镜像:

ROM/FLASH/SD卡
SDRAM
2)用户栈区(栈)

用于存放函数调用的上下文
局部变量
3)IRQ堆栈区

用于执行中断服务程序的上下文(中断服务程序的嵌套)
4)全局变量数据区

初始为0的全局数据区
5)动态内存区(堆)

malloc内存区
free内存区
BSS段通常是指用来存放程序中未初始化的或者初始化为0的全局变量和静态变量的一块内存区域。特点是可读写的,在程序执行之前BSS段会自动清0。
 

1.3 uboot在嵌入式系统启动中的位置

 1)bootstrap =》 在Soc芯片内部,由soc芯片厂家提供

2)bootloader =》 uboot

3)OS内核 =》 Linux内核 

4)rootf根文件系统 =》存放各种工具、库、脚本、应用程序、数据等等。

5)根文件夹系统中的应用程序 =》特定的应用程序

第2章 uboot启动流程(源码分析)

2.1 入口函数:_start

2.2 两阶段流程(流程图描述)

2.3 执行流程(文字描述)

(1)设置CPU为管理模式

(2)关看门狗

(3)关中断

(4)设置时钟频率

(5)关mmu,初始化各个bank

(6)进入board_init_f () 函数 =》能够访问内存RAM和串口相关的外设

  • 初始化DDR
  • 定时器
  • 初始化波特率串口
  • 打印前面暂存在缓冲区的数据
  • 此时堆栈指针sp和gd指向DDR上了,而不是内部的RAM。

(7)重定位

uboot会将自己重定位到 DRAM最后面的地址区域,也就是将自己拷贝到 DRAM最后面的内存区域中。这么做的目的是给Linux腾出空间,防止 Linux kernel覆盖掉 uboot,将 DRAM前面(低位地址空间)的区域完整的空出来。

在拷贝之前肯定要给uboot各部分分配好内存位置和大小,比如 gd应该存放到哪个位置,malloc内存池应该存放到哪个位置等等。这些信息都保存在gd的成员变量中,因此要对 gd的这些成员变量做初始化。

最终形成 一个完整的内存“分配图”,在后面重定位uboot的时候就会用到这个内存“分配图”。

(8)copy

relocate_code代码重定位函数,负责将uboot从ROM拷贝到RAM新的地方,完成代码拷贝。

(9)清bss

(10)跳转到board_init_r() 函数 =》 能否访问Flash和网络相关外设

前面board_init_f函数里面会调用一系列的函数来初始化部分关键性外设和gd的成员变量。

但是board_init_f并没有初始化所有的外设,还需要做一些后续工作,这些后续工作就是由函数
board_init_r (这里的外设更多包括EMMC,NANDFLASH)来完成的, 存放于common/board_r.c。

(11)启动流程结束

最后运行的是run_main_loop,主循环,处理命令输入。

2.4 初始化过程

 2.5 代码树(代码描述)

Uboot启动流程:
   _start  <arch/arm/lib/vectors.S>
       |--->reset  <arch/arm/cpu/armv7/start.S>
       |       |--->save_boot_params   <arch/arm/cpu/armv7/start.S>
       |                      |--->save_boot_params_ret    <arch/arm/cpu/armv7/start.S>
       |                                   |               // 禁止中断,设置cpu的模式为SVC
       |                                   |               // 清楚SCTLR的bit13,允许向量重定位,同时
                                           |               // 重定位向量表,把CP15的c12(VBAR)寄存器设置为
                                           |               // 0x87800000(uboot的起始地址,也是向量表的起始地址)
                                           |
                                           |--->cpu_init_cp15      <arch/arm/cpu/armv7/start.S>
                                           |                       // 设置cp15相关内容,比如关闭mmu,cache
                                           |
                                           |--->cpu_init_crit      <arch/arm/cpu/armv7/start.S>
                                           |           |
                                           |           |--->lowlevel_init  <arch/arm/cpu/armv7/lowlevel_init.S>
                                           |           |                   // 设置栈指针sp = 0x0091FF00,属于MX6ULL
                                           |           |                   // 的内部ram,同时(sp - GD_SIZE(248))-->sp
                                           |           |                   // 留出global_data数据结构的位置sp = 0x0091FE08
                                           |           |                   // 设置sp-->r9, sp==r9
                                           |           |
                                           |           |--->s_init     <arch/arm/cpu/armv7/mx6/soc.c>
                                           |           |               // 空函数,直接返回
                                           |
                                           |--->_main      <arch/arm/lib/crt0.S>
                                                   |       // 设置sp为0x0091ff00,调用函数
                                                   |       // board_init_f_alloc_reserve(arg:0x0091FF00)后,把sp设为
                                                   |       // 此函数的返回值:0x0091FA00, r9(gd)设为0x0091FA00;
                                                   |       // 调用board_init_f_init_reserve(arg:0x0091FA00)后
                                                   |       // 把gb的成员malloc_base设为0x0091FB00(early_malloc的起始地址)
                                                   |       // 调用board_init_f函数:会初始化gd,返回之后重新设置环境(sp和gd)
                                                   |       // 把gd的成员start_addr(0x9EF44E90)赋值给sp, 此时sp == 0x9EF44E90
                                                   |       // 是外部DDR的地址,gd->bd赋给r9(gd),新的gd结构在bd结构下面,
                                                   |       // 重新设置gd = r9 - sizeof(*gd); lr = here. gd指向新的区域(DDR内)时
                                                   |       // lr = here + 68,这是为什么?uboot的拷贝目的地址:0x9FF47000
                                                   |--->board_init_f_alloc_reserve(arg:0x0091FF)    <common/init/board_init.c>
                                                   |                       // 在包含此函数的文件中有:DECLARE_GLOBAL_DATA_PTR;
                                                   |                       // 是个宏定义:#define  DECLARE_GLOBAL_DATA_PTR \
                                                   |                       //                 register volatile gd_t *gd asm("r9")
                                                   |                       // 此函数设置留出早期malloc和global_data内存区域,
                                                   |                       // 返回值:0x0091FA00
                                                   |
                                                   |--->board_init_f_init_reserve(arg:0x0091FA00)     <common/init/board_init.c>
                                                   |                       // 此函数用于初始化gd所指向的结构(清零处理)
                                                   |                       // 设置gd的成员malloc_base为0x91FB00
                                                   |                       // 就是early_malloc的起始地
                                                   |
                                                   |--->board_init_f       <common/board_f.c>
                                                   |                       // 主要做两个工作:初始化一系列外设(串口、定时器等)
                                                   |                       // 初始化gd的各个成员变量(此时gd还保存在内部ocram中)。上面的工作都是通过在函数内运行
                                                   |                       // initcall_sequence_f函数表中的一些函数来实现的,此函数表与
                                                   |                       // board_init_f函数定义在相同的文件,是static属性的静态表
                                                   |                       // 表中的函数执行完后会把gd->mon_len设为0xA8E74(__bss_end-_start),
                                                   |                       // 也就是代码长度。gd->malloc_init设为0x400(malloc内存池的大小)
                                                   |                       // gd->ram_size:0x20000000  gd->ram_top:0xA0000000  gd->relocaddr:0x9FF47000
                                                   |                       // gd->arch.tlb_size:0x4000  gd->arch.tlb_addr:0x9FFF0000
                                                   |
                                                   |--->relocate_code(arg:0x9FF47000)      <arch/arm/lib/relocate.S>
                                                   |                       // 代码拷贝。0x9FF47000是uboot拷贝目标首地址,offset=0x9FF47000-0x8780000,offset:0x18747000
                                                   |                       // 拷贝源地址:__image_copy_start=0x87800000,结束地址:__image_copy_end =0x8785dd54
                                                   |                       // 裸机程序运行需要链接地址与运行地址相同,uboot解决拷贝后的重定位问题是采用ld链接器
                                                   |                       // 链接时使用选项'-pie'生成位置无关的可执行文件,使用此选项时会生成一个.rel.dyn段,
                                                   |                       // uboot就是靠这个.rel.dyn来解决重定位问题的(.rel.dyn 段是存放.text 段中需要重定位地址的集合)
                                                   |                       // 修改.rel.dyn中的label来重定位
                                                   |
                                                   |--->relocate_vectors       <arch/arm/lib/relocate.S>
                                                   |                       // 重定位向量表,将CP15的VBAR寄存器的值设为0x9FF47000,uboot拷贝后的目标首地址
                                                   |
                                                   |--->c_runtime_cpu_setup      <arch/arm/cpu/armv7/start.S>
                                                   |
                                                   |--->board_init_r       <common/board_r.c>
                                                   |           |           // 初始化一些在board_init_f函数中未初始化的一些外设,做些后续工作。
                                                   |           |           // 是通过运行init_sequence_r函数集合中的函数来实现的,init_sequence_r与board_init_f函数
                                                   |           |           // 在同一个文件。在函数集合中initr_reloc_global_data函数初始化重定位后的gd的一些成员变量
                                                   |           |           // 集合中的其他函数:初始化了malloc、串口、电源芯片、emmc、环境变量、LCD、初始化跳转表、中断,使能中断
                                                   |           |           // 初始化网络地址(获取MAC地址,通过读取环境变量ethaddr的值,环境变量保存在emmc中)、
                                                   |           |           // 初始化网络设备,最后执行run_main_loop函数,主循环(处理命令)
                                                   |           |
                                                   |           |--->run_main_loop      <common/board_r.c>
                                                                       |            // uboot启动以后会进入3秒倒计时,如果在3秒倒计时结束之前按下按下回车键,那么就
                                                                       |            // 会进入uboot的命令模式,如果倒计时结束以后都没有按下回车键,那么就会自动启动Linux内
                                                                       |            // 核,这个功能就是由run_main_loop函数来完成的.
                                                                       |
                                                                       |--->main_loop(void)    <common/main.c>
                                                                       |           |        // 如果如果倒计时结束之前按下按键,那么就会执行cli_loop函数,这个就是
                                                                       |           |        // 命令处理函数,负责接收好处理输入的命令。
                                                                       |           |
                                                                       |           |--->bootstage_mark_name
                                                                       |           |                   // 打印处启动进度
                                                                                   |
                                                                                   |--->autoboot_command
                                                                                   |                   // 此函数就是检查倒计时是否结束?倒计时结束之前有没有被打断?
                                                                                   |
                                                                                   |--->cli_loop       <common/cli.c>
                                                                                   |       |     // cli_loop函数是uboot的命令行处理函数,我们在uboot中输入
                                                                                   |       |     // 各种命令,进行各种操作就是由cli_loop来处理的
                                                                                   |       |
                                                                                   |       |--->parse_file_outer
                                                                                                       |
                                                                                                       |--->setup_file_in_str
                                                                                                       |
                                                                                                       |--->parse_stream_outer
                                                                                                       |                   // 这个函数就是 hush shell 的命令解释器,
                                                                                                       |                   // 负责接收命令行输入,然后解析并执行相应的命令

第3章 uboot如何加载内核

3.1 vmlinuz/vmlinux、Image、zImage与uImage的区别

内核镜像和其他的镜像并没有本质上的区别,都是用同一套交叉编译工具链来生成的。

为了满足各种启动方式,编译后的内核提供多种不同类型的镜像。本质上和其他镜像都是一样的,只是在此基础上做了修改。

生成镜像的过程:

(1)编译生成vmlinuz/vmlinux

就是普通的elf可执行文件,嵌入式设备一般部署时不会用这种格式的镜像,因为体积太大,并且elf格式也不能直接烧录使用;//elf格式的文件可以在操作系统下执行,但不能在裸机上运行。

(2)将elf格式的vmlinuz/vmlinux变成bin格式的可烧录文件

用交叉编译工具链里的objcopy,将elf格式的vmlinuz/vmlinux变成bin格式的可烧录文件,名字为Image。bin文件是可以直接在裸机上运行的程序。

objcopy把几十M大的vmlinuz/vmlinux精简成了几M大小的Image,因此这个制作烧录镜像主要目的就是缩减大小,节省磁盘;

(3)zip压缩Image文件=》zImage

实际上Image已经可以直接烧录到flash中进行执行,但是人们觉得内核还是太大了,于是对Image进行压缩,再在压缩得到的文件前端加一段解压缩代码,这样就得到了zImage;(zImage= 解压缩代码 + Image压缩得到的文件)

(4)uboot专用的uImage (uboot Image)

uImage是用uboot中的mkimage工具根据zImage制作而来。

uImage是专门给uboot使用的,在zImage头部添加64个字节的头,说明这个内核的版本、加载位置、生成时间、大小等信息,其0x40之后与zImage没区别。

其中最重要的信息就是加载的内存地址信息,即把Linux kernel加载到内存什么地方。

    uint32_t    ih_load;    /* Data     Load  Address        */
    uint32_t    ih_ep;        /* Entry Point Address        */

// 如下展示uImage的头部信息
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        */
    uint8_t        ih_type;    /* Image Type            */
    uint8_t        ih_comp;    /* Compression Type        */
    uint8_t        ih_name[IH_NMLEN];    /* Image Name        */
} image_header_t;

备注:

  • uboot都支持uImage,不一定支持zImage;
  • 现在uImage的方式被逐渐设备树dtb的方式替代;

3.2 uboot启动内核的大致步骤

(1)编译、制作内核镜像

(2)把内核加载或存放到指定的位置(具体位置与启动方式相关)

(3)uboot加载内核镜像

uboot要通过如下的几种方式将特定位置的内核加载到内存的链接地址处;

  • 读取SD卡上的内核镜像
  • 读取flash上的内核镜像
  • 通过tftp下载内核镜像
  • 通过nfs下载内核镜像

(4)uboot 解析内核镜像,得到image_header_t的内容。

uboot区分出当前启动方式是zImage、uImage还是设备树方式。

然后,构建出描述该内核的image_header_t结构体;

(5)启动内核

将上一步得到的image_header_t结构体和内核所在地址,传入do_bootm_linux函数,启动Linux内核;
(6)内核接管CPU的控制权,uboot结束

boot->bootm->do_bootm_linux->内核启动,uboot结束。

3.3 内核文件的三种状态

(1)静态文件:以文件的方式存在于ROM中。

(2)静态内核:以可执行代码的方式存在于RAM中。

(3)动态执行:以正在运行状态存在与RAM中。

3.4 内核的加载与重定位

内核从外存加载到DDR RAM中,但还没有执行之前的这个过程就叫做重定位。

必须加载到DDR的特定地址(即编译链接时指定的物理地址),因为启动的时候就是去链接地址启动。

内核的重定位是uboot完成的,根据启动方式的不同,uboot可能从flash等外存去读取内核,也可能通过tftp、nfs等网络下载方式读取内核,但不管何种读取内核的方式,最终内核都是被加载到链接地址。

链接地址在编译脚本、环境变量bootcmd、配置文件的CONFIG_BOOTCOMMAND宏定义可以查到。

3.5 启动内核的相关命令

(1)boot

该命令会先将内核重定位,把内核下载到指定的内存空间中。

然后调用bootm命令从内存空间中启动内核;

(2)bootm

这是直接启动内核的命令,只能启动已经加载到DDR的内核,在调用时传入内核在DDR中的地址(一般是内核的链接地址)即可启动内核。

在bootm命令的实现代码里,其实主要完成的是启动方式的判断,判断出启动的操作系统类型后,完成初始化就会去调用相关操作系统的启动函数。

(3)do_bootm_linux函数:

这是启动linux系统的函数,功能包括:准备给内核的传参、找到内核程序入口、启动内核。

3.6 uboot如何给内核传递参数?bootargs

uboot 往内核携带的参数就是bootargs携带的,bootargs并不是一个固定长度、固定结构的数据结构,而是可变长度,可变内容的结构,称为“tag”。

bootargs=earlyprintk console=ttyS0,115200 rootwait nprofile_irq_duration=on root=/dev/ram0 rootfstype=ramfs rdinit=/linuxrc 
bootcmd=run tftploadk
bootdelay=3
setenv bootargs 'mem=512M console=ttyAMA0,115200 clk_ignore_unused rw rootwait root=/dev/mmcblk0p5 rootfstype=ext4  
blkdevparts=mmcblk0:1M(u-boot.bin),5M(kernel),512K(logo.bin),512K(logo.jpg),1000M(rootfs.ext4)'

(1)mem:设置操作系统内存大小。

以上设置mem=512M,表示分配给操作系统内存为512M。

(2)console:设置控制台设备。

格式为console=ttyAMA0,115200表示控制台为串口0,波特率115200。

(3)root:设置根文件系统rootfs挂载设备, 用来指定rootfs的位置

格式为root=/dev/mmcblk0p5表示从Flash第5个分区挂载(Flash分区编号从0开始)。

常见的rootfs的位置有:

    root=/dev/ram rw 

    root=/dev/mtdx rw

    root=/dev/mtdblockx rw

    root=/dev/mtdblock/x rw

    root=31:0x

    root=/dev/nfs    # rootfs在网络上。

(4)rootfstype:设置挂载文件系统类型

此处用的是ext4文件系统格式,或是ramfs。

(5)Linux内核初始化后的初始线程的位置

init指定的是内核启起来后,进入系统中运行的第一个脚本.

一般init=/linuxrc, 或者init=/etc/preinit,

preinit的内容一般是创建console,null设备节点。

运行init程序,挂载一些文件系统等等操作。

很多初学者以为,init=/linuxrc是固定写法,其实不然,/linuxrc指的是/目录下面的linuxrc脚本,一般是一个连接罢了。

通过该脚本,Linux内核可以自动创建应用程序环境和启动应用程序。

3.6 内核是如何拿到这些参数的?

由于uboot和内核其实是两个独立的程序,并不能通过函数调用传递参数。

另外,uboot传递给内核的参数的内容并不是固定的结构体。那么内核是如何获取从uboot获取到可变个数的参数呢?

Linux内核发明了一种称为“Tag”的参数传递的方式:

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

(2)如setup_serial_tag -> tag,在uboot\include\asm-arm里面

(3)tag_header和tag_xxx。tag_header中有这个tag的size和类型编码,kernel拿到耦合tag后先分析tag_header得到tag的类型和大小,然后将tag中剩下部分当做一个tag_xxx来处理。类似TLV编码的数据。

(4)tag_start与tag_end。

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

所有要传递的tag类型的参数,都会被封装在【tag_start】和【tag_end】之间。

(5)tag传参的方式是由linux kernel发明的,kernel定义了这种向我传参的方式,uboot只是遵循了这种传参的方式给kernel传参罢了。

tag 是一个数据结构:stract tag  这种数据格式在uboot和kernel是一样的,也就是说uboot在启动的时候将需要传给kernel的参数放在了DDR的某个地址处,而存放格式就是tag格式,然后kernel就会到这个地址去读取这些参数,读取方式也是按tag格式去读取的。

简单来说tag就像是一个数组一样,是一块连续的内存,里面存放了uboot需要传递给kernel的参数信息,我们通过定义一个指针params(static struct tag *params;),先使其指向tag的存放地址(30000100,uboot也就是说在30000100的地方存放了一条信息,告诉kernel去0x54410001这个地方去读取tag(也就是uboot传递给kernel的参数))

uboot最终通过调用theKernel (0, machid,  bd->bi_boot_params); 函数来执行linux内核的,uboot调用这个函数(其实就是linux内核)时传递了三个参数。

theKernel = (void (*)(int, int, uint))ntohl(hdr->ih_ep);

这三个参数就是uboot直接传递给linux内核,这3个参数是通过寄存器来实现的传参的:

  • 第一个参数0就放在r0中,
  • 第二个参数(机器码)放在r1中,
  • 第三个参数放在r2中(第三个参数传递的就是tag的首地址这里是30000100)
  • 最后,把内核的地址传递给pc指针,就跳转到内核代码执行程序了。
  • 0
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值