原文:https://blog.csdn.net/HiWangWenBing/article/details/127062057
目录
第1章 uboot概述
1.1 概述
1.2 内存映射(案例)
1.3 uboot在嵌入式系统启动中的位置
第2章 uboot启动流程(源码分析)
2.1 入口函数:_start
2.3 执行流程(文字描述)
2.4 初始化过程
第3章 uboot如何加载内核
3.1 vmlinuz/vmlinux、Image、zImage与uImage的区别
3.2 uboot启动内核的大致步骤
3.3 内核文件的三种状态
3.4 内核的加载与重定位
3.5 启动内核的相关命令
3.6 uboot如何给内核传递参数?bootargs
3.6 内核是如何拿到这些参数的?
第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指针,就跳转到内核代码执行程序了。
————————————————
版权声明:本文为CSDN博主「文火冰糖的硅基工坊」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/HiWangWenBing/article/details/127062057