要分析 uboot 的启动流程,首先要找到“入口”,找到第一行程序在哪里。程序的链接是由链接脚本来决定的,所以通过链接脚本可以找到程序的入口。如果没有编译过 uboot 的话链接脚本为 arch/arm/cpu/u-boot.lds。
打开u-boot.lds可以看到下图所示的内容。
指定输出文件的格式为 ,这是一个针对32位ARM架构的ELF文件格式。
指定输出文件的目标架构为ARM。
定义程序的入口点是 符号。这通常是执行开始的地方。
开始定义各个段的布局。
设置当前位置计数器(location counter,)的初始值为0x00000000。
确保下一个段地址是4字节对齐的。
定义 段,通常包含程序的执行代码:
- 包含所有 段的内容。
- 包含所有 段的内容,这通常用于存放中断向量等。
- 仅包含指定对象文件中的 段。
- 包含所有 段的内容。
再次确保地址对齐。
定义 段,存放只读数据。使用 和 来排序,以优化访问速度和存储效率。
定义 段,存放初始化的全局变量和静态变量。
连续的地址对齐,确保后续段的地址对齐。
定义 段,可能用于U-Boot引导程序。 指令确保链接器不会在优化时移除这些符号。
定义 段,包含所有 段的内容。
接下来的部分定义了动态重定位、结束符号、不初始化的数据段(BSS)、动态链接表等相关段。这些定义确保了程序在运行时能正确处理动态链接、异常表和其他运行时必需的信息。
定义 段,并使用 指令与其他段共享地址空间, 保证不被优化移除。
定义 段,存放未初始化的数据。使用 指令共享地址空间。
定义 段。
这行代码是一个汇编指令,用于告诉编译器和链接器将随后的代码段放入特定的内存区域(段)。段名为 .vectors,而 "ax" 指定了该段的属性,所以此代码存放在.vectors 段里面。
a: 表示该段是可分配的(Allocatable)。在链接时,链接器会为此段分配内存空间。
x: 表示该段是可执行的(Executable)。这意味着存放在这一段的代码可以被处理器执行。
使用如下命令在 uboot 中查找“__image_copy_start”:
搜索完成后可以得到下图。u-boot.map 是 uboot 的映射文件,可以从此文件看到某个文件或者函数链接到了哪个地址,我们打开u-boot.map进一步查看其中内容。
找到下图的位置:
vectors 段的起始地址也是 0X,说明整个 uboot 的起始地址就是 0X,这也是为什么我们裸机例程的链接起始地址选择 0X 了,目的就是为了和 uboot 一致,一致的原因如下:
1:U-Boot的起始地址为 0X:这个地址是U-Boot开始的地方,通常包括了其启动时的中断向量表。选择这个地址可能是基于硬件要求,或者为了优化系统性能和兼容性。
2:裸机程序使用相同的起始地址:当U-Boot将控制权转交给裸机程序时,如果两者的起始地址相同,这种过渡就会更加顺畅。这样做的好处包括:
3:无需重新配置内存映射:如果裸机程序使用和U-Boot相同的起始地址,它可以直接利用已经由U-Boot设置好的内存配置。
4:简化系统设计:保持一致的内存起始地址可以减少设计复杂性,避免在从U-Boot到裸机程序的过渡中出现错误。
5:提高系统启动效率:处理器不需要重新定位中断向量表,可以直接继续使用U-Boot时的配置
- 全局标签声明:
这一行声明了 符号为全局符号,使得它可以被链接器脚本中其他模块或者代码引用。在嵌入式系统中, 通常是程序的入口点。
- 节(Section)声明:
这一行指定接下来的代码放在名为 的段中,同时给这个段指定了“ax”属性,表示这个段是可执行的( 代表可分配, 代表可执行)。这是因为异常向量表需要被CPU执行。
- 异常向量表定义:
这里 标签标记了异常向量表的开始位置,它是程序的入口点。
- 可选的启动配置字:
这部分代码是条件编译的一部分,只有当 宏被定义时,才会在向量表中包含一个字(word)的配置信息。这通常用于配置启动时的某些硬件特性。
- 异常处理跳转指令:
这些指令设置了ARM处理器的主要异常处理向量:
- :无条件跳转到 标签处,通常用于处理系统复位。
- :加载指令,用于将程序计数器(PC)设置到各种异常处理程序的地址。这些处理程序包括未定义指令异常、软件中断、预取中止、数据中止、未使用、普通中断请求(IRQ)和快速中断请求(FIQ)。
- 可选的启动配置字:
上面跳转涉及到ret函数。ret函数在arch/arm/cpu/armv7/start.S 里面。
reset 函数跳转到了 save_boot_params 函数,而 save_boot_params 函数同样定义,在 start.S 里面。
save_boot_params 函数也是只有一句跳转语句,跳转到 save_boot_params_ret 函数,save_boot_params_ret 函数代码如下:
这段代码是用于在ARM架构的处理器上设置处理器状态的典型代码,主要目的是为了在启动或异常处理流程中禁用中断和设置正确的处理器模式。这里的代码主要通过操作CPSR(当前程序状态寄存器)来实现这些功能。下面是对代码的逐行解释:
- 读取当前程序状态寄存器(CPSR)到寄存器r0:
指令用于将CPSR的值移动到寄存器r0中。CPSR寄存器包含了当前处理器的状态信息,例如中断禁用位和当前处理器模式。
- 将r0中的模式位与0x1F进行AND操作,结果存入r1:
这行代码使用AND操作来屏蔽掉r0中除了最低5位之外的所有位。最低的5位定义了处理器的模式(如用户模式、系统模式等)。将寄存器 r0 中的值与 0X1F 进行与运算,结果保存到 r1 寄存器中,目的就是提取 cpsr 的 bit0~bit4 这 5 位,这 5 位为 M4 M3 M2 M1 M0, M[4:0]这五位用来设置处理器的工作模式,具体工作模式如下图所示:
- 比较r1与0x1A(HYP模式的模式值):
指令用于测试r1和0x1A是否相等,但不更新任何寄存器,只更新条件标志。0x1A是HYP模式(Hypervisor模式)的模式值。
- 如果不是HYP模式,清除r0中的模式位:
指令在条件“不等于”(NE)满足时执行。它将r0与0x1F的位取反后的值进行AND操作,实际上是清除了r0的最低5位。如果 r1 和 0X1A 不相等,也就是 CPU 不处于 Hyp 模式的话就将 r0 寄存器的bit0~5 进行清零,其实就是清除模式位。
- 如果不是HYP模式,将处理器设置为SVC(超级用户)模式:
指令在条件“不等于”满足时执行。这里它将r0与0x13进行OR操作,0x13是SVC模式的模式值。如果处理器不处于 Hyp 模式的话就将 r0 的寄存器的值与 0x13 进行或运算,0x13=0b10011,也就是设置处理器进入 SVC 模式。
- 无论当前模式如何,禁用FIQ和IRQ中断:
这行代码将r0与0xC0进行OR操作。0xC0的位模式是,其中第6位和第7位分别对应于禁用IRQ和FIQ中断。r0 寄存器的值再与 0xC0 进行或运算,那么 r0 寄存器此时的值就是 0xD3, cpsr的 I 为和 F 位分别控制 IRQ 和 FIQ 这两个中断的开关,设置为 1 就关闭了 FIQ 和 IRQ。
- 将修改后的值写回CPSR:
指令用于将r0的值移动回CPSR寄存器,更新处理器的状态。
接下来来看这部分代码
这段代码用于ARM处理器上配置系统控制寄存器(SCTLR)和向量基地址寄存器(VBAR),以便正确设置异常处理向量的位置。代码中使用了条件编译指令来控制特定配置下的代码编译。我们来逐行解释这段代码:
这行代码检查是否定义了和宏。如果两者都未同时定义,那么编译器将会编译接下来的代码块。这通常用于在特定的硬件配置或构建阶段中排除或包含特定的代码。
- 读取SCTLR寄存器到r0:
其中各部分的含义如下:
指定协处理器的编号。在ARM体系结构中,最常用的系统控制协处理器为p15。
操作码,用于指定协处理器内部的特定操作,这里为0。
目标寄存器,用于接收从协处理器读出的数据,这里是r0。
源寄存器(协处理器中的寄存器),这里为c1,通常关联到系统控制寄存器(SCTLR)。
另一个协处理器寄存器,用于进一步指定操作,这里为c0。
第二操作码,进一步细化指令的行为,这里为0。
指令功能
对于指令 mrc p15, 0, r0, c1, c0, 0:
p15 表明这是与系统控制相关的操作,使用的是系统控制协处理器。
c1 表示操作的是系统控制寄存器(SCTLR)。SCTLR寄存器用于控制处理器的多种功能,如缓存、分支预测、异常处理等。
r0 是目标寄存器,用于存储从SCTLR读取的值,这里使用(Move to Register from Coprocessor)指令从协处理器p15的c1寄存器(系统控制寄存器,SCTLR)读取值到通用寄存器r0中。这个寄存器主要用于控制处理器的一些基本功能,如缓存、分支预测等。
- 修改SCTLR寄存器中的V位(V=0): (Bit Clear)指令用于将r0寄存器中的特定位清零。
- 将修改后的值写回SCTLR寄存器:
使用(Move to Coprocessor from Register)指令将r0的值写回到协处理器p15的c1寄存器中。
- 加载_start地址到r0:
设置r0寄存器的值为_start, _start就是整个uboot的入口地址,其值为0X,相当于 uboot 的起始地址,因此 0x 也是向量表的起始地址。
- 写入VBAR寄存器: 这条指令将r0中的地址(即的地址)写入到协处理器p15的c12寄存器(向量基地址寄存器,VBAR)中。这样设置后,所有的异常(如中断、系统调用等)都会跳转到指定的地址去处理。
接下来再往底下看代码。
上面的代码就是分别调用函数 cpu_init_cp15、 cpu_init_crit 和_main。
函数 cpu_init_cp15 用来设置 CP15 相关的内容,比如关闭 MMU 啥的,此函数同样在 start.S文件中定义的,
可以看出函数 cpu_init_crit 内部仅仅是调用了函数 lowlevel_init,接下来就是详细的分析一下 lowlevel_init 和_main 这两个函数。
详细代码如上图所示。这段代码是嵌入式系统(特别是基于 ARM 的系统)启动过程中的低级初始化代码。代码来自于 U-Boot 的启动阶段,主要负责设置堆栈、全局数据以及调用最初的初始化函数。
设置 sp 指向 CONFIG_SYS_INIT_SP_ADDR, CONFIG_SYS_INIT_SP_ADDR 在include/configs/mx6ullevk.h 文件中,在 mx6ullevk.h 中有如下所示定义:
上面的的 IRAM_BASE_ADDR 和 IRAM_SIZE 在 文 件arch/arm/include/asm/arch-mx6/imx-regs.h 中有定义,如下所示,其实就是 IMX6UL/IM6ULL 内部 ocram 的首地址和大小。
示例代码 32.2.2.3 imx-regs.h 代码段
如果 408 行的条件成立的话 IRAM_SIZE=0X40000,当定义了 CONFIG_MX6SX、CONFIG_MX6UL、 CONFIG_MX6SLL 和 CONFIG_MX6SL 中的任意一个的话条件就不成立,在.config 中定义了 CONFIG_MX6UL,所以条件不成立,因此 IRAM_SIZE=0X20000=128KB。可以得到如下值:
还需要知道GENERATED_GBL_DATA_SIZE的值,在文件include/generated/generic-asm-offsets.h中有定义,如下:
CONFIG_SYS_INIT_SP_ADDR 值如下:
此时 sp 指向 0X91FF00,这属于 IMX6UL/IMX6ULL 的内部 ram。
上面这一句进行八字节对齐。堆栈指针 sp 对齐到 8 字节边界意味着它的地址在二进制表示中的最后三位应该是 000。这是因为 8 字节(即 2 的 3 次方)对齐的地址总是以 8 的倍数计算,而 8 的倍数的二进制表示总是以 000 结尾。当使用 bic sp, sp, #7 指令时,实际上是将 sp 的最低三位清零,因为 1111 1000(掩码取反后的结果)与任何数值进行“与”操作都会将这个数值的最低三位清零。这样,sp 就被强制对齐到最接近的低位 8 字节边界。
sp 指针减去 GD_SIZE, GD_SIZE 同样在 generic-asm-offsets.h 中定了,大小为248,此时 sp 的地址为 0X0091FF00-248=0X0091FE08,此时 sp 位置如图 32.2.2.2 所示:
第 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,函数调用路径如图所示:
_main 函数定义在文件 arch/arm/lib/crt0.S 。
第 76 行,设置 sp 指针为 CONFIG_SYS_INIT_SP_ADDR,也就是 sp 指向 0X0091FF00。
第 83 行, sp 做 8 字节对齐。
第 85 行,读取 sp 到寄存器 r0 里面,此时 r0=0X0091FF00。
第 86 行,调用函数 board_init_f_alloc_reserve,此函数有一个参数,参数为 r0 中的值,也
就是 0X0091FF00,此函数定义在文件 common/init/board_init.c 中,内容如下:
函数 board_init_f_alloc_reserve
函数 board_init_f_alloc_reserve 主要是留出早期的 malloc 内存区域和 gd 内存区域,其中
CONFIG_SYS_MALLOC_F_LEN=0X400( 在 文 件 include/generated/autoconf.h 中 定 义 ) ,
sizeof(struct global_data)=248(GD_SIZE 值),完成以后的内存分布如图 32.2.4.1 所示:
函数 board_init_f_alloc_reserve 是有返回值的,返回值为新的 top 值,从图 32.2.4.1 可知,此时 top=0X0091FA00。将 r0 写入到 sp 里面, r0 保存着函数board_init_f_alloc_reserve 的返回值,所以这一句也就是设置 sp=0X0091FA00。第 89 行,将 r0 寄存器的值写到寄存器 r9 里面,因为 r9 寄存器存放着全局变量 gd 的地址。在文件 arch/arm/include/asm/global_data.h 中有如图 32.2.4.2 所示宏定义:
uboot 中定义了一个指向 gd_t 的指针 gd, gd 存放在寄存器 r9 里面的,因此 gd 是个全局变量。 gd_t 是个结构体,在 include/asm-generic/global_data.h 里面有定义,gd_定义如下:
因此这一行代码就是设置 gd 所指向的位置,也就是 gd 指向 0X0091FA00。继续回到示例代码 32.2.4.1 中,第 90 行调用函数 board_init_f_init_reserve,此函数在文件common/init/board_init.c 中有定义,函数内容如下:
可以看出来上面的函数可以看出,此函数用于初始化 gd,其实就是清零处理。在你提供的代码段中,循环的停止条件是:
这个条件是基于指针算术的。让我们详细解释一下这个条件的意思和它如何工作:
- 指针初始化:循环首先将 初始化为 的地址,但被转换为 类型。这意味着 将按照 类型的大小(通常是4字节)来递增。
- 循环条件:循环的条件是 必须小于 的地址,同样转换为 类型。这里的 表示地址增加了整个 结构的大小。将这个地址转换为 意味着, 在递增时将按照 类型的大小逐步逼近 的地址。
- 递增操作:在每次循环迭代中, 递增,即 。由于 是 类型,这个递增是以 的大小(通常是4字节)为单位进行的。
- 循环执行:在每次迭代中, 被设置为 0,这样逐步将 结构中的每个 单位清零,直到 达到 的位置。
- 停止条件:当 达到或超过 的地址时,循环停止。这意味着整个 结构已被清零。
这个循环确保了 结构的每个部分都被正确初始化为0,从而为系统的后续操作提供了一个干净、预期的状态。此函数还设置了gd->malloc_base 为 gd 基地址+gd 大=0X0091FA00+248=0X0091FAF8,在做 16 字节对齐,最终 gd->malloc_base=0X0091FB00,这个也就是 early malloc 的起始地址。
第 92 行设置 R0 为 0。
第 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。
第 109 行, sp 做 8 字节对齐。
第 111 行,获取 gd->bd 的地址赋给 r9,此时 r9 存放的是老的 gd,这里通过获取 gd->bd 的地址来计算出新的 gd 的位置。 GD_BD=0,参考示例代码 32.2.2.4。在 U-Boot 中使用 这种形式的地址访问,是为了从全局数据结构 中取出特定的成员变量。这里的 是一个偏移量,它指向 结构中的 成员(即板级信息结构 的指针)。让我们详细解析这个过程的原因和背景。
全局数据结构 ()
U-Boot 使用一个全局数据结构 来存储整个启动过程中需要的状态信息和配置参数。这个结构包括了如下一些关键信息:
- 内存大小
- 时钟频率
- 环境变量
- 网络配置
- 板级信息(通过 结构)
在 ARM 架构中, 寄存器在某些情况下被用作全局数据指针(Global Data Pointer, GDP)。在 U-Boot 的启动代码中, 被初始化为指向 结构的起始地址。这样,任何时候需要访问全局数据时,可以直接通过 进行。
第 112 行,新的 gd 在 bd 下面,所以 r9 减去 gd 的大小就是新的 gd 的位置,获取到新的 gd的位置以后赋值给 r9。
第 114 行,设置 lr 寄存器为 here,这样后面执行其他函数返回的时候就返回到了第 122 行的 here 位置处。
第 115,读取 gd->reloc_off 的值复制给 r0 寄存器, GD_RELOC_OFF=68,参考示例代码32.2.2.4。
第 116 行, lr 寄存器的值加上 r0 寄存器的值,重新赋值给 lr 寄存器。因为接下来要重定位代码,也就是把代码拷贝到新的地方去(现在的 uboot 存放的起始地址为 0X,下面要将 uboot 拷贝到 DDR 最后面的地址空间出,将 0X 开始的内存空出来),其中就包括here,因此 lr 中的 here 要使用重定位后的位置。
第 120 行,读取 gd->relocaddr 的值赋给 r0 寄存器,此时 r0 寄存器就保存着 uboot 要拷贝的目的地址,为 0X9FF47000。 GD_RELOCADDR=48,参考示例代码 32.2.2.4。
第 121 行,调用函数 relocate_code,也就是代码重定位函数,此函数负责将 uboot 拷贝到新的地方去,此函数定义在文件 arch/arm/lib/relocate.S 中稍后会详细分析此函数。
继续回到示例代码 32.2.4.1 第 127 行,调用函数 relocate_vectors,对中断向量表做重定位,此函数定义在文件 arch/arm/lib/relocate.S 中,稍后会详细分析此函数。
继续回到示例代码 32.2.4.1 第 131 行,调用函数 c_runtime_cpu_setup,此函数定义在文件arch/arm/cpu/armv7/start.S 中。
第 141~159 行,清除 BSS 段。
第 167 行,设置函数 board_init_r 的两个参数,函数 board_init_r 声明如下:board_init_r(gd_t *id, ulong dest_addr)第一个参数是 gd,因此读取 r9 保存到 r0 里面。
第 168 行,设置函数 board_init_r 的第二个参数是目的地址,因此 r1= gd->relocaddr。
第 174 行、调用函数 board_init_r,此函数定义在文件 common/board_r.c 中,稍后会详细的
分析此函数。
这个就是_main 函数的运行流程,在_main 函数里面调用了 board_init_f、 relocate_code、relocate_vectors 和 board_init_r 这 4 个函数,
main 中会调用 board_init_f 函数, board_init_f 函数主要有两个工作:
①、初始化一系列外设,比如串口、定时器,或者打印一些消息等。
②、初始化 gd 的各个成员变量, uboot 会将自己重定位到 DRAM 最后面的地址区域,也就是将自己拷贝到 DRAM 最后面的内存区域中。这么做的目的是给 Linux 腾出空间,防止 Linuxkernel 覆盖掉 uboot,将 DRAM 前面的区域完整的空出来。在拷贝之前肯定要给 uboot 各部分分配好内存位置和大小,比如 gd 应该存放到哪个位置, malloc 内存池应该存放到哪个位置等等。这些信息都保存在 gd 的成员变量中,因此要对 gd 的这些成员变量做初始化。最终形成一个完整的内存“分配图”,在后面重定位 uboot 的时候就会用到这个内存“分配图”。
第 80 行, r1=__image_copy_start,也就是 r1 寄存器保存源地址,由表 31.4.1.1 可知,
__image_copy_start=0X。
第 81 行, r0=0X9FF47000,这个地址就是 uboot 拷贝的目标首地址。 r4=r0-r1=0X9FF47000-
0X=0X,因此 r4 保存偏移量。
第 82 行,如果在第 81 中, r0-r1 等于 0,说明 r0 和 r1 相等,也就是源地址和目的地址是一样的,那肯定就不需要拷贝了!执行 relocate_done 函数
第 83 行, r2=__image_copy_end, r2 中保存拷贝之前的代码结束地址,由表 31.4.1.1 可知_image_copy_end =0x8785dd54。第 83 行ldr r2, =__image_copy_end /* r2 <- SRC &__image_copy_end /
r2 寄存器加载了源代码的结束地址 __image_copy_end。
第 85-89 行 - copy_loop
copy_loop:
ldmia r1!, {r10-r11} / copy from source address [r1] /
stmia r0!, {r10-r11} / copy to target address [r0] /
cmp r1, r2 / until source end address [r2] */
blo copy_loop
这是一个循环,用于将数据从源地址 (r1) 复制到目标地址 (r0)。每次循环复制两个寄存器的数据(r10 和 r11),这相当于复制 8 个字节。
ldmia 和 stmia 是多重加载/存储指令,用于从内存地址读取/写入多个寄存器的数据。r1! 和 r0! 表示在数据传输后自动更新这些寄存器的值。
cmp 和 blo 指令用于检查是否已经复制到源代码的结束地址,如果没有,则继续循环。
在嵌入式系统中,尤其是在使用像 U-Boot 这样的引导加载器时,重定位是一个关键步骤,确保代码能在物理内存中的正确位置执行。这里的代码段(第 94 行到第 109 行)处理的是 段,这是动态重定位信息的一部分,用于调整程序运行时的内存引用,以确保它们指向正确的地址。
第 94-95 行
- 这两行加载 段的开始和结束地址到 和 寄存器。 段包含了需要进行动态重定位的所有项。
第 96-109 行 -
- : 从 指向的地址加载两个寄存器 和 ,其中 是需要修正的内存位置, 是修正类型和值。 表示加载后 的值自动增加,以指向下一个条目。
- 和 : 这两行代码检查修正的类型是否为相对地址修正(类型 23 是 ELF 格式中常见的相对地址修正类型)。
- : 如果不是相对地址修正,跳过此次修正。
- : 将偏移量 加到原始位置 ,更新位置到新的地址。
- : 加载 指向的地址的内容到 。
- : 将偏移量 加到 ,进行地址修正。
- : 将修正后的值存回 指向的地址。
- 和 : 检查是否已经处理完所有重定位项,如果没有,则继续循环。
uboot 对于重定位后链接地址和运行地址不一致的解决方法就是采用位置无关码,在使用 ld 进行链接的时候使用选项“ -pie”生成位置无关的可执行文件。在文件arch/arm/config.mk 下有如下代码:
使用“-pie”选项以后会生成一个.rel.dyn 段, uboot 就是靠这个.rel.dyn 来解决重定位问题的,在 u-bot.dis 的.rel.dyn 段中有如下所示内容:
第 7 行值为 0X,第 8 行为 0X00000017,说明第 7 行的 0X 是个 Label,
这个正是示例代码 32.2.6.3 中存放变量 rel_a 地址的那个 Label。根据前面的分析,只要将地址
0X+offset 处的值改为重定位后的变量 rel_a 地址即可。我们猜测的是否正确,看一下
uboot 对.rel.dyn 段的重定位即可(示例代码代码 32.2.6.1 中的第 94~109 行), .rel.dyn 段的重定位
代码如下:
第 7 行值为 0X,第 8 行为 0X00000017,说明第 7 行的 0X 是个 Label,这个正是示例代码 32.2.6.3 中存放变量 rel_a 地址的那个 Label。根据前面的分析,只要将地址0X+offset 处的值改为重定位后的变量 rel_a 地址即可。我们猜测的是否正确,看一下uboot 对.rel.dyn 段的重定位即可(示例代码代码 32.2.6.1 中的第 94~109 行), .rel.dyn 段的重定位代码如下:
第 94 行, r2=__rel_dyn_start,也就是.rel.dyn 段的起始地址。
第 95 行, r3=__rel_dyn_end,也就是.rel.dyn 段的终止地址。
第 97 行,从.rel.dyn 段起始地址开始,每次读取两个 4 字节的数据存放到 r0 和 r1 寄存器中, r0 存放低 4 字节的数据,也就是 Label 地址; r1 存放高 4 字节的数据,也就是 Label 标志。
第 98 行, r1 中给的值与 0xff 进行与运算,其实就是取 r1 的低 8 位。
第 99 行,判断 r1 中的值是否等于 23(0X17)。
第 100 行,如果 r1 不等于 23 的话就说明不是描述 Label 的,执行函数 fixnext,否则的话继续执行下面的代码。
第 103 行, r0 保存着 Label 值, r4 保存着重定位后的地址偏移, r0+r4 就得到了重定位后的Label 值。此时 r0 保存着重定位后的 Label 值,相当于 0X+0X=0X9FF4B198。
第 104,读取重定位后 Label 所保存的变量地址,此时这个变量地址还是重定位前的(相当于 rel_a 重定位前的地址 0X8785DA50),将得到的值放到 r1 寄存器中。
第 105 行 , r1+r4 即 可 得 到 重 定 位 后 的 变 量 地 址 , 相 当 于 rel_a 重 定 位 后 的0X8785DA50+0X=0X9FFA4A50。
第 106 行,重定位后的变量地址写入到重定位后的 Label 中,相等于设置地址 0X9FF4B198处的值为 0X9FFA4A50。
第 108 行,比较 r2 和 r3,查看.rel.dyn 段重定位是否完成。
第 109 行,如果 r2 和 r3 不相等,说明.rel.dyn 重定位还未完成,因此跳到 fixloop 继续重定位.rel.dyn 段。
可以看出, uboot 中对.rel.dyn 段的重定位方法和我们猜想的一致。 .rel.dyn 段的重定位比较复杂一点,有点绕,因为涉及到链接地址和运行地址的问题。
函数 relocate_vectors 用于重定位向量表,此函数定义在文件 relocate.S 中, 函数源码如下
第 29 行,如果定义了 CONFIG_CPU_V7M 的话就执行第 30~36 行的代码,这是 Cortex-M
内核单片机执行的语句,因此对于 I.MX6ULL 来说是无效的。
第 38 行,如果定义了 CONFIG_HAS_VBAR 的话就执行此语句,这个是向量表偏移, CortexA7 是支持向量表偏移的。而且,在.config 里面定义了 CONFIG_HAS_VBAR,因此会执行这个分支。
第 43 行, r0=gd->relocaddr,也就是重定位后 uboot 的首地址,向量表肯定是从这个地址开始存放的。
第 44 行,将 r0 的值写入到 CP15 的 VBAR 寄存器中,也就是将新的向量表首地址写入到寄存器 VBAR 中,设置向量表偏移。
- 低级硬件初始化:包括时钟、内存、CPU 核心配置等。
- 早期串行控制台的设置:以便可以输出启动过程中的调试信息。
- 栈的设置:在某些架构中,这一步非常关键,因为后续的代码需要一个工作的栈。
- 全局数据结构 (gd) 的初始化:设置全局数据区,这对后续的功能至关重要。
- 重定位到 RAM:将 U-Boot 的代码从非易失性存储器(如 NOR/NAND Flash)复制到 RAM 中。
- 完整的设备初始化:
- 初始化剩余的外设和驱动,例如网络接口、USB 控制器等。
- 在某些板子上,可能还包括图形显示接口的初始化。
- 环境变量的加载:
- 从存储介质(如 EEPROM、Flash)中加载环境变量。
- 设置网络配置,如 IP 地址、服务器地址等。
- 内存管理系统的配置:
- 设置动态内存分配器(如 malloc)。
- 启动脚本的执行:
- 执行在环境变量中定义的启动脚本,这些脚本可以包括加载操作系统映像、执行系统检查等操作。
- 操作系统映像的加载:
- 加载并准备启动操作系统,比如 Linux、VxWorks 等。
- 这可能包括从网络、硬盘、SD 卡或其他存储介质读取系统映像。
- 命令行界面的提供:
- 如果启动脚本没有配置为自动启动操作系统,U-Boot 会提供一个命令行界面供用户手动操作。
- 系统服务的启动:
- 启动可能需要的背景服务,如网络时间协议(NTP)客户端等。
uboot 启动以后会进入 3 秒倒计时,如果在 3 秒倒计时结束之前按下按下回车键,那么就会进入 uboot 的命令模式,如果倒计时结束以后都没有按下回车键,那么就会自动启动 Linux 内核 , 这 个功 能 就 是由 run_main_loop 函 数 来 完成 的 。
第 759 行和第 760 行是个死循环,“for(;;)”和“while(1)”功能一样,死循环里面就一个main_loop 函数, main_loop 函数定义在文件 common/main.c 里面,代码如下:
这段代码是 U-Boot 启动完成后执行的主循环(),它处理用户输入的命令和一些自动化的启动任务。下面是对代码中每个关键部分的详细解释:
第 43-75 行: 函数定义
第 48 行
这行代码标记了启动阶段的一个重要时刻,即主循环的开始。这有助于性能分析和调试,可以追踪 U-Boot 启动过程中的不同阶段。
第 50-54 行
这些行是一个预处理指令,检查是否定义了 。如果没有定义,会输出一条警告信息,提示开发者他们的板子不使用通用板架构。这是提醒开发者可能需要更新他们的板级支持包(BSP)以适应 U-Boot 的更新。
第 56-58 行
如果定义了 ,这段代码会在环境变量中设置版本信息。这允许在运行时通过环境变量获取 U-Boot 的版本。
第 60 行
初始化命令行界面(CLI)。这是为了准备接收和处理来自用户的命令。
第 62 行
执行在环境变量中设置的预启动命令。这些命令通常用于设置网络、测试硬件或其他启动前必要的配置。
第 64-66 行
如果定义了 ,这段代码会尝试通过 TFTP 更新固件或其他资源。这是一种远程更新系统的方法,常用于无需物理访问设备的场景。
第 68 行
处理启动延迟,通常用于给用户一定时间来中断自动启动过程。 变量可能会包含用户在启动延迟期间输入的命令。
第 69-70 行
这里首先处理可能来自启动延迟的命令(如修改设备树的命令)。如果有命令被处理,然后执行安全启动相关的命令。
第 72 行
尝试自动启动系统,通常这会加载和启动操作系统,除非用户已经通过启动延迟输入了其他命令。
第 74 行
进入持续的命令行界面循环,这允许用户输入和执行更多命令。
cli_loop 函数是 uboot 的命令行处理函数,我们在 uboot 中输入各种命令,进行各种操作就是有 cli_loop 来处理的,此函数定义在文件 common/cli.c 中,函数内容如下:
这段代码是 函数的实现,它是 U-Boot 命令行界面(CLI)的主循环部分。这个函数负责处理用户输入的命令,并根据配置决定使用哪种解析器来处理这些命令。这里涉及到两种可能的命令解析器:Hush shell 和一个简单的命令行解析器。下面是对代码的详细解释:
第 202-211 行: 函数定义
第 204 行
这是一个预处理指令,检查是否定义了 。这个宏定义决定了 U-Boot 是否使用 Hush shell 作为命令解析器。Hush shell 是一个相对复杂的 shell,支持更多的脚本功能和语法,如条件判断、循环等。
第 205 行
如果使用 Hush parser,这个函数被调用来解析和执行命令。 是处理多行输入和复杂脚本的函数,它能够处理来自不同输入源的命令,包括直接的用户输入、环境变量或者通过网络接收的脚本。
第 206-207 行
在 函数执行之后,理论上这部分代码是不会被执行的,因为 应当持续处理输入,直到系统重启或关闭。 是一个无限循环,确保如果出现任何异常导致 返回,系统不会继续向下执行并退出 函数,这是一种安全措施。
第 208-210 行
如果没有定义 ,则使用一个更简单的命令行解析器。 函数是一个更基础的命令处理循环,它处理用户输入但不支持 Hush shell 的高级特性。这种模式适用于资源受限或对 shell 功能要求不高的环境。
不管是 bootz 还是 bootm 命令,在启动 Linux 内核的时候都会用到一个重要的全局变量:images, images 在文件 cmd/bootm.c 中有如下定义:
第 335 行的 os 成员变量是 image_info_t 类型的,为系统镜像信息。
第 352~362 行这 11 个宏定义表示 BOOT 的不同阶段。
接下来看一下结构体 image_info_t,也就是系统镜像信息结构体,此结构体在文件include/image.h 中的定义如下:
全局变量 images 会在 bootz 命令的执行中频繁使用到,相当于 Linux 内核启动的“灵魂”。
第 629 行,调用 bootz_start 函数,
第 636 行,调用函数 bootm_disable_interrupts 关闭中断。
第 638 行,设置 images.os.os 为 IH_OS_LINUX,也就是设置系统镜像为 Linux,表示我们要启动的是 Linux 系统!后面会用到 images.os.os 来挑选具体的启动函数。
第 639 行,调用函数 do_bootm_states 来执行不同的 BOOT 阶段,这里要执行的 BOOT 阶
段有: BOOTM_STATE_OS_PREP 、BOOTM_STATE_OS_FAKE_GO 和BOOTM_STATE_OS_GO。
bootz_srart 函数也定义在文件 cmd/bootm.c 中,函数内容如下:
第 584 行,调用函数 do_bootm_states,执行 BOOTM_STATE_START 阶段。
第 593 行,设置 images 的 ep 成员变量,也就是系统镜像的入口点,使用 bootz 命令启动系统的时候就会设置系统在 DRAM 中的存储位置,这个存储位置就是系统镜像的入口点,因此 images->ep=0X。
第 598 行,调用 bootz_setup 函数,此函数会判断当前的系统镜像文件是否为 Linux 的镜像文件,并且会打印出镜像相关信息, bootz_setup 函数稍后会讲解。
第 608 行,调用函数 bootm_find_images 查找 ramdisk 和设备树(dtb)文件,但是我们没有用到 ramdisk,因此此函数在这里仅仅用于查找设备树(dtb)文件。
bootz_setup 函数,此函数定义在文件 arch/arm/lib/bootm.c 中,函数内容如下:
这段代码是 函数的实现,它用于设置并验证 ARM 架构下的 Linux 内核 zImage。这个函数确保加载的 zImage 是有效的,并提取出内核映像的起始和结束地址。下面是对这段代码的详细解释:
第 370 行
这行定义了一个宏 ,它是一个魔数(magic number),用于验证 zImage 的有效性。这个特定的值是 Linux ARM zImage 文件的标准标识符,用来确认文件确实是一个有效的 ARM zImage。
第 372 行
这是 函数的定义。函数接收三个参数:
- :这是 zImage 的内存起始地址。
- 和 :这两个是指针,用于存放解析出的内核映像的起始和结束地址。
第 374-375 行
定义了一个指向 结构体的指针 。这个结构体通常包含了 zImage 的元数据,如魔数、起始地址和结束地址等。
第 376 行
调用 函数将物理地址 映射为虚拟地址,并将结果转换为 结构体指针。这允许代码直接通过结构体访问 zImage 的头部信息。 的第二个参数 表示没有特定的大小限制。
第 377-380 行
这里检查 zImage 的魔数是否与预定义的 相匹配。如果不匹配,输出错误信息并返回 ,表示错误。
第 382-383 行
如果魔数检查通过,则从 zImage 头部读取内核映像的起始和结束地址,并通过指针参数返回这些值。
第 385-386 行
输出内核映像的地址信息,包括映射的起始地址和解析出的起始和结束地址。这对于调试和验证加载过程非常有用。
第 388 行
do_bootz 最 后 调 用 的 就 是 函 数 do_bootm_states , 而 且 在 bootz_start 中 也 调 用 了do_bootm_states 函数 ,看来 do_bootm_states 函数 还是 个香饽 饽。 此函数 定义 在文 件common/bootm.c 中,函数代码如下:
版权声明:
本文来源网络,所有图片文章版权属于原作者,如有侵权,联系删除。
本文网址:https://www.mushiming.com/mjsbk/11511.html