参考:U-Boot顶层目录链接脚本文件(u-boot.lds)介绍
作者:一只青木呀
发布时间: 2020-10-23 13:52:23
网址:https://blog.csdn.net/weixin_45309916/article/details/109240625
目录
- 链接脚本 u-boot.lds 详解
- 1、u-boot.lds文件
- 2、arch/arm/lib/vectors.S 文件
- 3、u-boot.map(地址映射文件)
- 4、链接文件分析
- 总结
- U-Boot 启动流程详解
- reset 函数源码详解
- lowlevel_init 函数详解
- s_init 函数详解
- _main 函数详解
- board_init_f 函数详解(初始化外设、uboot重定位给Linux腾空间)
- relocate_code 函数详解
- relocate_vectors 函数详解
- board_init_r 函数详解
- run_main_loop 函数详解
- cli_loop 函数详解
- cmd_process 函数详解
- bootz 启动 Linux 内核过程
- images 全局变量
- do_bootz 函数(bootz启动内核命令的执行函数)
- bootz_start 函数
- do_bootm_states 函数
- bootm_os_get_boot_func 函数
- do_bootm_linux 函数
上一章我们详细的分析了 uboot 的顶层 Makefile,理清了 uboot 的编译流程。本章我们来详细的分析一下 uboot 的启动流程,理清 uboot 是如何启动的。通过对 uboot 启动流程的梳理,我们就可以掌握 ①外设是在哪里被初始化的,这样当我们需要修改这些外设驱动的时候就会心里有数。另外,通过分析 uboot 的启动流程可以 ②了解 Linux 内核是如何被启动的。
链接脚本 u-boot.lds 详解
要分析 uboot 的启动流程,首先要找到“入口”,找到第一行程序在哪里。程序的链接是由链接脚本(链接脚本是编译生成的)来决定的,所以通过链接脚本可以找到程序的入口。如果没有编译过 uboot 的话链接脚本为 arch/arm/cpu/u-boot.lds。但是这个不是最终使用的链接脚本,最终的链接脚本是在这个链接脚本的基础上生成的。编译一下 uboot,编译完成以后就会在 uboot 根目录下生成 u-boot.lds
文件,如下图所示:
只有编译 u-boot 以后才会在根目录下出现 u-boot.lds 文件!
1、u-boot.lds文件
打开 u-boot.lds,内容如下:
第 3 行为代码当前入口点: _start, _start 在文件 arch/arm/lib/vectors.S 中有定义,如图下所示:
2、arch/arm/lib/vectors.S 文件
从上图可以看出, _start 后面就是中断向量表,从图中的“.section “.vectors”, "ax”可以得到,此代码存放在.vectors 段里面。
3、u-boot.map(地址映射文件)
使用如下命令在uboot 中查找“__image_copy_start”:(上图第一节第十行代码处)
grep -nR "__image_copy_start"
搜索结果如图32.1.3 所示:
打开 u-boot.map(地址映射文件):
u-boot.map 是 uboot 的映射文件,可以从此文件看到某个文件或者函数链接到了哪个地址,从上图932 行可以看到 __image_copy_start 为 0X87800000,而.text 的起始地址也是0X87800000。
4、链接文件分析
.text(代码段) | 描述 |
---|---|
*(.__image_copy_start) | uboot 拷贝的首地址 |
在链接文件中第 10 行*(._image_copystart) 在映射文件中可以看到地址为 0X87800000,而.text 的起始地址也是0X87800000。
在链接文件中第 11 行是 vectors 段, vectors 段保存中断向量表(裸机部分讲过),从u-boot.lds文件我们知道了 vectors.S 的代码是存在 vectors 段中的。从地址映射文件中, vectors 段的起始地址也是 0X87800000,说明整个 uboot 的起始地址就是 0X87800000,这也是为什么我们裸机例程的链接起始地址选择0X87800000 了,目的就是为了和uboot 一致。
在链接文件中第 12 行将 arch/arm/cpu/armv7/start.s 编译出来的.o代码放到中断向量表后面(参照上图u-boot.map的代码)。
在链接文件中第 13 行为 text 段,其他的代码段就放到这里
在链接文件中第 16 行 .rodata只读数据段(一般存放常量)
在链接文件中第 18 行,数据段 (一般存放已初始化的全局和静态变量)
在链接文件中第 24 行 ,.u_boot_list段
在链接文件中第 28 行, .image_copy_end:uboot 拷贝的结束地址
在链接文件中第 32 行,.rel_dyn_start:.rel.dyn 段起始地址
在链接文件中第 39 行,.rel_dyn_end:.rel_dyn段结束地址
在链接文件中第 52 行,.bss_start:.bss 段起始地址(静态数据区,一般存放未初始化的全局和静态变量)
在链接文件中第 61 行,.bss_end:.bss段结束
总结
在u-boot.lds 中有一些跟地址有关的“变量”需要我们注意一下,后面分析u-boot 源码的时候会用到,这些变量要最终编译完成才能确定的!!!比如我编译完成以后这些“变量”的值如表所示:
变量 | 数值 | 描述 |
---|---|---|
*(.vectors) | 0x87800000 | 中断向量表 |
arch/arm/cpu/armv7/start.o | 0x87800300 | strrt.c |
__image_copy_start | 0x87800000 | uboot 拷贝的首地址 |
__image_copy_end | 0x8785dd54 | uboot 拷贝的结束地址 |
__rel_dyn_start | 0x8785dd54 | .rel.dyn 段起始地址 |
__rel_dyn_end | 0x878668f4 | .rel.dyn 段结束地址 |
_image_binary_end | 0x878668f4 | 镜像结束地址 |
__bss_start | 0x8785dd54 | .bss 段起始地址 |
__bss_end | 0x878a8e74 | .bss 段结束地址 |
上表中的“变量”值可以在 u-boot.map 文件中查找,上表中除了__image_copy_start以外,其他的变量值每次编译的时候可能会变化,如果修改了 uboot 代码、修改了 uboot 配置、选用不同的优化等级等等都会影响到这些值。所以,一切以实际值为准。
U-Boot 启动流程详解
reset 函数源码详解
从u-boot.lds 中我们已经知道了入口点是arch/arm/lib/vectors.S 文件中的_start,代码如下:
第48 行_start 开始的是中断向量表,其中54~61 行就是中断向量表,和我们裸机例程里面一样。54 行跳转到reset 函数里面,reset 函数在arch/arm/cpu/armv7/start.S 里面,代码如下:
第35 行就是reset 函数。
第37 行从reset 函数跳转到了save_boot_params 函数,而save_boot_params 函数同样定义在start.S 里面,定义如下:
save_boot_params 函数也是只有一句跳转语句,跳转到save_boot_params_ret 函数(为啥不直接跳转到呢,搞得这么麻烦),
save_boot_params_ret 函数代码如下:
第43 行,读取寄存器cpsr(程序状态寄存器,前面架构部分讲过) 中的值,并保存到r0 寄存器中。
第44 行,将寄存器r0 中的值与0X1F 进行与运算,结果保存到r1 寄存器中,目的就是提取cpsr 的bit0~bit4 这5 位,这5 位为M4 M3 M2 M1 M0,M[4:0]这五位用来设置处理器的工作模式,如表32.2.1.1 所示:
第45 行,判断r1 寄存器的值是否等于0X1A(0b11010),也就是判断当前处理器模式是否处于Hyp 模式。
第46 行,如果r1 和0X1A 不相等,也就是CPU 不处于Hyp 模式的话就将r0 寄存器的bit0~5 进行清零,其实就是清除模式位。
第47 行,如果处理器不处于Hyp 模式的话就将r0 的寄存器的值与0x13 进行或运算,0x13=0b10011,也就是设置处理器进入SVC 模式。
第48 行,r0 寄存器的值再与0xC0 进行或运算,那么r0 寄存器此时的值就是0xD3,cpsr的I 为和F 位分别控制IRQ 和FIQ 这两个中断的开关,设置为1 就关闭了FIQ 和IRQ!
第49 行,将r0 寄存器写回到cpsr 寄存器中。完成设置CPU 处于SVC32 模式,并且关闭FIQ 和IRQ 这两个中断。
继续执行执行下面的代码:
第56 行,如果没有定义CONFIG_OMAP44XX 和CONFIG_SPL_BUILD 的话条件成立,此处条件成立。
第58 行读取CP15 中c1 寄存器的值到r0 寄存器中,根据17.1.4 小节可知,这里是读取SCTLR 寄存器的值。
第59 行,CR_V 在arch/arm/include/asm/system.h 中有如下所示定义:
#define CR_V (1 << 13) /* Vectors relocated to 0xffff0000 */
因此这一行的目的就是清除SCTLR 寄存器中的bit13,SCTLR 寄存器结构如图32.2.1.1 所示:
从图32.2.1.1 可以看出,bit13 为V 位,此位是向量表控制位,当为0 的时候向量表基地址为0X00000000,软件可以重定位向量表。为1 的时候向量表基地址为0XFFFF0000,软件不能重定位向量表。这里将V 清零,目的就是为了接下来的向量表重定位,这个我们在第十七章有过详细的介绍了。
第60 行将r0 寄存器的值重写写入到寄存器SCTLR 中。
第63 行设置r0 寄存器的值为_start,_start 就是整个uboot 的入口地址,其值为0X87800000,相当于uboot 的起始地址,因此0x87800000 也是向量表的起始地址。
第64 行将r0 寄存器的值(向量表值)写入到CP15 的c12 寄存器中,也就是VBAR 寄存器。因此第58~64 行就是设置向量表重定位的。
代码继续往下执行:
第68 行如果没有定义CONFIG_SKIP_LOWLEVEL_INIT 的话条件成立。我们没有定义CONFIG_SKIP_LOWLEVEL_INIT,因此条件成立,执行下面的语句。
示例代码32.2.1.6 中的内容比较简单,就是分别调用函数cpu_init_cp15、cpu_init_crit 和_main。
函数cpu_init_cp15 用来设置CP15 相关的内容,比如关闭MMU 啥的,此函数同样在start.S文件中定义的,代码如下:
函数cpu_init_cp15 都是一些和CP15 有关的内容,我们不用关心,有兴趣的可以详细的看一下。
函数cpu_init_crit 也在是定义在start.S 文件中,函数内容如下:
可以看出函数cpu_init_crit 内部仅仅是调用了函数lowlevel_init,接下来就是详细的分析一下lowlevel_init 和_main 这两个函数。
lowlevel_init 函数详解
函数lowlevel_init 在文件arch/arm/cpu/armv7/lowlevel_init.S 中定义,内容如下:
第22 行设置sp 指向CONFIG_SYS_INIT_SP_ADDR,CONFIG_SYS_INIT_SP_ADDR 在include/configs/mx6ullevk.h 文件中,在mx6ullevk.h 中有如下所示定义:
示例代码32.2.2.2 中的IRAM_BASE_ADDR 和IRAM_SIZE 在文件
arch/arm/include/asm/arch-mx6/imx-regs.h 中有定义,如下所示,其实就是IMX6UL/IM6ULL 内部ocram 的首地址和大小。
s_init 函数详解
在上一小节中,我们知道lowlevel_init 函数后面会调用s_init 函数,s_init 函数定义在文件arch/arm/cpu/armv7/mx6/soc.c 中,如下所示:
在第816 行会判断当前CPU 类型,如果CPU 为MX6SX、MX6UL、MX6ULL 或MX6SLL中的任意一种,那么就会直接返回,相当于s_init 函数什么都没做。所以对于I.MX6UL/I.MX6ULL 来说,s_init 就是个空函数。从s_init 函数退出以后进入函数lowlevel_init,但是lowlevel_init 函数也执行完成了,返回到了函数cpu_init_crit,函数cpu_init_crit 也执行完成了,最终返回到save_boot_params_ret,函数调用路径如图32.2.3.1 所示:
从图32.2.3.1 可知,接下来要执行的是save_boot_params_ret 中的_main 函数,接下来分析_main 函数。
_main 函数详解
第93 行,调用board_init_f 函数,此函数定义在文件common/board_f.c 中!主要用来初始化DDR,定时器,完成代码拷贝等等,此函数我们后面在详细的分析。
第103 行,重新设置环境(sp 和gd)、获取gd->start_addr_sp 的值赋给sp,在函数board_init_f中会初始化gd 的所有成员变量,其中gd->start_addr_sp=0X9EF44E90,所以这里相当于设置
sp=gd->start_addr_sp=0X9EF44E90。0X9EF44E90 是DDR 中的地址,说明新的sp 和gd 将会存放到DDR 中,而不是内部的RAM 了。GD_START_ADDR_SP=64,参考示例代码32.2.2.4。
这个就是_main 函数的运行流程,在_main 函数里面调用了board_init_f、relocate_code、relocate_vectors 和board_init_r 这4 个函数,接下来依次看一下这4 个函数都是干啥的。
board_init_f 函数详解(初始化外设、uboot重定位给Linux腾空间)
_main 中会调用board_init_f 函数,board_init_f 函数主要有两个工作:
- ①、初始化一系列外设,比如串口、定时器,或者打印一些消息等。
- ②、初始化gd 的各个成员变量,uboot 会将自己重定位到DRAM 最后面的地址区域,也就是将自己拷贝到DRAM 最后面的内存区域中。这么做的目的是给Linux 腾出空间,防止Linux kernel 覆盖掉uboot,将DRAM 前面的区域完整的空出来。在拷贝之前肯定要给uboot 各部分分配好内存位置和大小,比如gd 应该存放到哪个位置,malloc 内存池应该存放到哪个位置等等。这些信息都保存在gd 的成员变量中,因此要对gd 的这些成员变量做初始化。最终形成一个完整的内存“分配图”,在后面重定位uboot 的时候就会用到这个内存“分配图”。
。。代码未贴出:
第9 行,board_early_init_f 函数,板子相关的早期的一些初始化设置,I.MX6ULL 用来初始化串口的IO 配置
第10 行,timer_init,初始化定时器,Cortex-A7 内核有一个定时器,这里初始化的就是Cortex-A 内核的那个定时器。通过这个定时器来为uboot 提供时间。就跟Cortex-M 内核Systick 定时器一样。关于Cortex-A 内部定时器的详细内容,请参考文档《ARM ArchitectureReference Manual ARMv7-A and ARMv7-R edition.pdf》的“Chapter B8 The Generic Timer”章节。
第11 行,board_postclk_init,对于I.MX6ULL 来说是设置VDDSOC 电压。
第12 行,get_clocks 函数用于获取一些时钟值,I.MX6ULL 获取的是sdhc_clk 时钟,也就是SD 卡外设的时钟。
第13 行,env_init 函数是和环境变量有关的,设置gd 的成员变量env_addr,也就是环境变量的保存地址。
第14 行,init_baud_rate 函数用于初始化波特率,根据环境变量baudrate 来初始化gd->baudrate。
第15 行,serial_init,初始化串口。
第16 行,console_init_f,设置gd->have_console 为1,表示有个控制台,此函数也将前面暂存在缓冲区中的数据通过控制台打印出来。
第17 行、display_options,通过串口输出一些信息,如图32.2.5.1 所示:
第19 行,print_cpuinfo 函数用于打印CPU 信息,结果如图32.2.5.3 所示:
第20 行,show_board_info 函数用于打印板子信息,会调用checkboard 函数,结果如图32.2.5.4 所示:
第23 行,init_func_i2c 函数用于初始化I2C,初始化完成以后会输出如图32.2.5.5 所示信息:
第44 行,setup_dest_addr 函数,设置目的地址,设置gd->ram_size,gd->ram_top,gd->relocaddr这三个的值。接下来我们会遇到很多跟数值有关的设置,如果直接看代码分析的话就太费时间了,我可以修改uboot 代码,直接将这些值通过串口打印出来,比如这里我们修改文件common/board_f.c,因为setup_dest_addr 函数定义在文件common/board_f.c 中,在setup_dest_addr函数输入如图32.2.5.6 所示内容:
设置好以后重新编译uboot,然后烧写到SD 卡中,选择SD 卡启动,重启开发板,打开SecureCRT,uboot 会输出如图32.2.5.7 所示信息:
从图32.2.5.7 可以看出:
gd->ram_size = 0X20000000 //ram 大小为0X20000000=512MB
gd->ram_top = 0XA0000000 //ram 最高地址为0X80000000+0X20000000=0XA0000000
gd->relocaddr = 0XA0000000 //重定位后最高地址为0XA0000000
第45 行,reserve_round_4k 函数用于对gd->relocaddr 做4KB 对齐,因为
gd->relocaddr=0XA0000000,已经是4K 对齐了,所以调整后不变。
第46 行,reserve_mmu,留出MMU 的TLB 表的位置,分配MMU 的TLB 表内存以后会对gd->relocaddr 做64K 字节对齐。完成以后gd->arch.tlb_size、gd->arch.tlb_addr 和gd->relocaddr如图32.2.5.8 所示:
从图32.2.5.8 可以看出:
gd->arch.tlb_size= 0X4000 //MMU 的TLB 表大小
gd->arch.tlb_addr=0X9FFF0000 //MMU 的TLB 表起始地址,64KB 对齐以后
gd->relocaddr=0X9FFF0000 //relocaddr 地址
第47 行,reserve_trace 函数,留出跟踪调试的内存,I.MX6ULL 没有用到!
第48 行,reserve_uboot,留出重定位后的uboot 所占用的内存区域,uboot 所占用大小由gd->mon_len 所指定,留出uboot 的空间以后还要对gd->relocaddr 做4K 字节对齐,并且重新设置gd->start_addr_sp,结果如图32.2.5.9 所示:
。。。
从图32.2.5.16 可以看出,uboot 重定位后的偏移为0X18747000,重定位后的新地址为0X9FF4700,新的gd 首地址为0X9EF44EB8,最终的sp 为0X9EF44E90。
至此,board_init_f 函数就执行完成了,最终的内存分配如图32.2.5.16 所示:
relocate_code 函数详解
relocate_vectors 函数详解
board_init_r 函数详解
run_main_loop 函数详解
cli_loop 函数详解
cmd_process 函数详解
bootz 启动 Linux 内核过程
uboot启动Linux使用bootz命令,①bootz命令是如何启动Linux内核?②uboot的生命是怎么终止的?③Linux又是怎么启动的呢?
images 全局变量
不管是bootz命令 还是bootm 命令,在启动Linux 内核的时候都会用到一个重要的全局变量:images,images 在文件cmd/bootm.c 中有如下定义:
images 是bootm_headers_t 类型的全局变量,bootm_headers_t 是个boot 头结构体,在文件include/image.h 中的定义如下(删除了一些条件编译代码):
第335 行的os 成员变量是image_info_t 类型的,为系统镜像信息。
第352~362 行这11 个宏定义表示BOOT 的不同阶段。
接下来看一下结构体image_info_t,也就是系统镜像信息结构体,此结构体在文件include/image.h 中的定义如下:
全局变量images 会在bootz 命令的执行中频繁使用到,相当于Linux 内核启动的“灵魂”。
下面详细介绍各个函数:
do_bootz 函数(bootz启动内核命令的执行函数)
bootz 命令的执行函数为do_bootz,在文件cmd/bootm.c 中有如下定义: