目录
2. 分析启动流程
2.1 u-boot.lds链接脚本
2.2 vectors.S
2.3 start.S
2.3.1 设置SVC管理模式、关看门狗、关中断、设置时钟频率
2.3.2 禁用Cache和MMU、初始化存储控制器
2.4 crt0.S
2.4.1 设置栈
2.4.2 第一阶段C函数调用
2.4.2.1 调用board_init_f_alloc_reserve函数
2.4.2.2 调用board_init_f_init_reserve函数
2.4.2.3 调用board_init_f函数
2.4.2.4 重新设置栈
2.4.2.5 uboot重定位
2.4.2.5.1 拷贝uboot到SDRAM
2.4.2.5.2 修改动态链接地址数据
2.4.2.6 异常向量表重定位
2.4.2.7 清bss段、调用第二阶段C函数board_init_r
2. 分析启动流程
使用移植u-boot-2016.11到JZ2440(一:创建单板)生成u-boot.bin下载到开发板上,开发板无法启动。本节对uboot启动流程进行分析(可以认为是未经修改的u-boot-2016.11源码执行smdk2410_defconfig配置的情况),本节内容不进行修改源码。
2.1 u-boot.lds链接脚本
通过上一节编译,会在根目录生成一个u-boot.lds链接脚本文件,可以先通过u-boot.lds链接文件详解该文章对u-boot.lds进行基本了解,u-boot.lds代码如下:
/* 指定输出文件格式:32位efl格式、arm指令、小端格式 */
OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
OUTPUT_ARCH(arm) /* 设置输出文件的体系架构 */
ENTRY(_start) /* 指定可执行映像文件的起始段的段名是 _start,这里跳转到vector.S执行 */
SECTIONS /* SECTIONS 就是整个链接脚本的指定 */
{
/* 指定程序的链接地址有2种方法:一种是通过在include/configs/单板头文件.h(例如上一节的
jz2410.h)中定义CONFIG_SYS_TEXT_BASE,编译时Makefile会通过-Ttext来指定它;第二种是在链接脚
本也就是这里.=0x00000000来指定。两种都可以实现相同效果。这两种技巧是可以共同配合使用的,也就是说
既在链接脚本中指定也用CONFIG_SYS_TEXT_BASE指定。两个都指定的话以后者为准。这里的uboot的最终链
接起始地址就是在include/configs/jz2410.h里的CONFIG_SYS_TEXT_BASE中指定的。*/
. = 0x00000000; /* 链接地址 */
. = ALIGN(4); /* 4字节对齐 */
/* 代码段 */
.text :
{
/* 映像文件赋值起始地址,它在文件 arch/arm/lib/sections.c 中定义:* char
__image_copy_start[0] __attribute__((section(".__image_copy_start")));*/
*(.__image_copy_start)
/* 在arch/arm/lib/vectors.S有代码:.section ".vectors", "ax",所以链接时首先存放的是vectors.S的二进制文件 */
*(.vectors)
arch/arm/cpu/arm920t/start.o (.text*) /* 存放arch/arm/cpu/arm920t/start.o的代码段 */
*(.text*) /* 存放其他所有文件的代码段 */
}
. = ALIGN(4);
/* 只读数据段 */
.rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) }
. = ALIGN(4);
/* 普通数据段,即可读写数据段 */
.data : {
*(.data*)
}
. = ALIGN(4);
. = .;
. = ALIGN(4);
/* U-BOOT命令段 */
.u_boot_list : {
KEEP(*(SORT(.u_boot_list*)));
}
. = ALIGN(4);
/* UEFI段开始处 */
.__efi_runtime_start : {
*(.__efi_runtime_start)
}
.efi_runtime : {
*(efi_runtime_text)
*(efi_runtime_data)
}
.__efi_runtime_stop : {
*(.__efi_runtime_stop)
}
.efi_runtime_rel_start :
{
*(.__efi_runtime_rel_start)
}
.efi_runtime_rel : {
*(.relefi_runtime_text)
*(.relefi_runtime_data)
}
.efi_runtime_rel_stop :
{
*(.__efi_runtime_rel_stop)
}
. = ALIGN(4);
/* UEFI段结束 */
.image_copy_end :
{
*(.__image_copy_end) /* 映像文件赋值结束地址 */
}
/* __rel_dyn_start和__rel_dyn_end,这两个符号之间的区域存放着动态链接符号,只要给这里面的符号加上一定的偏移,
拷贝到内存中相应位置处,就可以在绝对跳转中找到正确的函数。 */
.rel_dyn_start :
{
*(.__rel_dyn_start)
}
.rel.dyn : {
*(.rel*)
}
.rel_dyn_end :
{
*(.__rel_dyn_end)
}
.end :
{
*(.__end)
}
_image_binary_end = .;
. = ALIGN(4096);
/* MMU 表项 */
.mmutable : {
*(.mmutable)
}
/* bss 段,.bss节包含了程序中所有未初始化的全局变量 */
/* 由链接指令(OVERLAY)可见,.bss_start与__rel_dyn_start,.bss与__bss_base,
.bss_end与__bss_limit是重叠的。*/
.bss_start __rel_dyn_start (OVERLAY) : {
KEEP(*(.__bss_start)); /* bss段起始地址 */
__bss_base = .;
}
.bss __bss_base (OVERLAY) : {
*(.bss*)
. = ALIGN(4);
__bss_limit = .;
}
.bss_end __bss_limit (OVERLAY) : {
KEEP(*(.__bss_end)); /* bss段结束地址 */
}
/* 其他段,这些节都是在编译链接时自动生成的,主要用于动态链接或调试使用 */
.dynsym _image_binary_end : { *(.dynsym) } /* 动态符号表`dynamic symbol`,但与`.symtab`不同,`.
dynsym`只保存动态链接相关的符号,而`.symtab`通常保存了所有的符号; */
.dynbss : { *(.dynbss) } /* 动态未初始化数据表`dynamic bss`; */
.dynstr : { *(.dynstr*) } /* 动态字符串表`dynamic string`,用于保存符号名的字符串表; */
.dynamic : { *(.dynamic*) } /* 保存了动态链接所需要的基本信息,例如依赖哪些共享对象,动态链接符号表的位置,
动态链接重定位表的位置,共享对象初始化代码的地址等; */
.plt : { *(.plt*) } /* 程序连接表`Procddure Linkage Table`,是实现动态链接的必要数据; */
.interp : { *(.interp*) } /* 解释器`interpreter`的缩写 */
/*.gnu.hash .gnu .ARM.exidx`和`.gnu.linkonce.armexidx`是针对`arm`体系专门生成的段,用于调试时函
数调用的`backtrace`,如果不需要调试,则可以不用这两段。 */
.gnu.hash : { *(.gnu.hash) }
.gnu : { *(.gnu*) }
.ARM.exidx : { *(.ARM.exidx*) }
.gnu.linkonce.armexidx : { *(.gnu.linkonce.armexidx.*) }
}
可以看到汇编的起始是_start标号,符号就类似于指针,用来表示一个地址,汇编中可以通过ldr得到符号表示的链接地址上的值(如: ldr r0, _start),通过adr得到符号代表的运行地址(如:adr r0, _start)。链接地址与运行地址在链接地址与运行地址中有介绍。
U-Boot编译之后会在其顶级目录中生成System.map和u-boot.map文件,System.map文件按链接地址由小到大的顺序列出了所有符号与其代表的链接地址,u-boot.map中包含了链接过程中涉及的目标文件将其所依赖的库文件(包含所有的text、data、bss等)的具体链接地址,System.map文件部分代码如下:
00000000 T __image_copy_start
00000000 T _start
00000020 T _undefined_instruction
00000024 T _software_interrupt
00000028 T _prefetch_abort
0000002c T _data_abort
00000030 T _not_used
00000034 T _irq
00000038 T _fiq
00000040 T IRQ_STACK_START_IN
00000060 t undefined_instruction
000000c0 t software_interrupt
00000120 t prefetch_abort
00000180 t data_abort
000001e0 t not_used
00000240 t irq
000002a0 t fiq
00000300 T reset
... ...
u-boot.map文件部分代码如下:
Linker script and memory map
Address of section .text set to 0x0
0x00000000 . = 0x0
0x00000000 . = ALIGN (0x4)
.text 0x00000000 0x62800
*(.__image_copy_start)
.__image_copy_start
0x00000000 0x0 arch/arm/lib/built-in.o
0x00000000 __image_copy_start
*(.vectors)
.vectors 0x00000000 0x300 arch/arm/lib/built-in.o
0x00000000 _start
0x00000020 _undefined_instruction
0x00000024 _software_interrupt
0x00000028 _prefetch_abort
0x0000002c _data_abort
0x00000030 _not_used
0x00000034 _irq
0x00000038 _fiq
0x00000040 IRQ_STACK_START_IN
arch/arm/cpu/arm920t/start.o(.text*)
.text 0x00000300 0x90 arch/arm/cpu/arm920t/start.o
0x00000300 reset
0x00000348 c_runtime_cpu_setup
这些链接地址就是通过u-boot.lds链接脚本与Makefile中用-Text指定共同决定的。下面从链接文件指定的程序入口_start开始分析,开发板一上电执行的就是这里的程序。
2.2 vectors.S
uboot的入口为_start符号代表的地址。_start这个全局符号在arch/arm/lib/vectors.S这个汇编文件中(汇编代码只要不遇到b或bl跳转指令,基本是按代码的从上到下的顺序执行的)。下面分几部分进行分析vectors.S。
.globl _start
.section ".vectors", "ax"
_start:
#ifdef CONFIG_SYS_DV_NOR_BOOT_CFG /* 未定义 */
.word CONFIG_SYS_DV_NOR_BOOT_CFG
#endif
/* LDR{条件} 目的寄存器 <存储器地址> */
/* 当异常发生的时候,由硬件机制处理器自动的跳到一个固定地址去执行相关异常处理程序,而这个固定地址就是所谓的异常向量。 */
b reset /* 跳转到reset执行 0x00000000 复位异常*/
ldr pc, _undefined_instruction /* 0x00000004 未定义指令异常 */
ldr pc, _software_interrupt /* 0x00000008 软中断异常 */
ldr pc, _prefetch_abort /* 0x0000000c 预取异常 */
ldr pc, _data_abort /* 0x00000010 数据异常 */
ldr pc, _not_used /* 0x00000014 未使用异常,多余的指令 */
ldr pc, _irq /* 0x00000018 外部中断异常 */
ldr pc, _fiq /* 0x0000001c 快速中断异常 */
.globl把_start这个标号全局化,是编译器的操作,并不是汇编指令。_start代表程序vectors.S的入口。这段代码的功能是设置异常向量表。b reset所处的位置是与异常向量表基地址偏移量为的0的地方,所以当复位异常发生时(开机也属于复位异常),CPU会自动跳转入异常表基址偏移量为0处执行复位异常程序,即跳转执行reset部分的代码(在start.S文件中)。
这部分代码只是构建了异常向量表,当每个异常向量指向的内容为空(除reset异常外)。因为uboot比较简单,只是作为引导程序,所以不需要很细致的处理各种异常。
2.3 start.S
reset这个全局符号在arch/arm/cpu/arm920t/start.S这个汇编文件中。下面分几部分进行分析start.S。
2.3.1 设置SVC管理模式、关看门狗、关中断、设置时钟频率
在上电或者重启后,处理器取得第一条指令就是b reset,所以会直接跳转到reset地址处。代码如下(arch/arm/cpu/arm920t/start.S文件中):
.globl reset
reset:
/*设置CPSR寄存器,让CPU进入SVC管理模式*/
mrs r0, cpsr //读出cpsr的值
bic r0, r0, #0x1f //清位
orr r0, r0, #0xd3 //位或
msr cpsr, r0 //写入cpsr
... ...
#ifdef CONFIG_S3C24X0
/* 关看门狗*/
# define pWTCON 0x53000000
# define INTMSK 0x4A000008 /* Interrupt-Controller base addresses */
# define INTSUBMSK 0x4A00001C
# define CLKDIVN 0x4C000014 /* clock divisor register */
ldr r0, =pWTCON
mov r1, #0x0
str r1, [r0] //关看门狗,使WTCON寄存器=0
/*关中断*/
mov r1, #0xffffffff
ldr r0, =INTMSK
str r1, [r0] //关闭所有中断
# if defined(CONFIG_S3C2410)
ldr r1, =0x3ff
ldr r0, =INTSUBMSK
str r1, [r0] //关闭次级所有中断
# endif
/* 设置时钟频率, FCLK:HCLK:PCLK = 1:2:4 ,而FCLK默认为120Mhz*/
ldr r0, =CLKDIVN
mov r1, #3
str r1, [r0]
#endif /* CONFIG_S3C24X0 */
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
bl cpu_init_crit //跳到cpu_init_crit里禁用Cache和MMU,初始化存储控制器
#endif
bl _main //跳到_main
... ...
该部分工作内容如下:
1. 设置SVC管理模式
2. 关看门狗
3. 关中断
4. 设置时钟频率
设置完时钟频率后执行bl cpu_init_crit跳转指令,cpu_init_crit也定义在start.S里。
2.3.2 禁用Cache和MMU、初始化存储控制器
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
bl cpu_init_crit
#endif
通过bl跳转指令跳到cpu_init_crit地址,代码如下(arch/arm/cpu/arm920t/start.S文件中):
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
cpu_init_crit:
/*
* flush v4 I/D caches
*/
mov r0, #0
mcr p15, 0, r0, c7, c7, 0 /* cache清零 */
mcr p15, 0, r0, c8, c7, 0 /* TLB清零 */
/*
* 禁用mmu和cashe
*/
mrc p15, 0, r0, c1, c0, 0
bic r0, r0, #0x00002300 @ clear bits 13, 9:8 (--V- --RS)
bic r0, r0, #0x00000087 @ clear bits 7, 2:0 (B--- -CAM)
orr r0, r0, #0x00000002 @ set bit 1 (A) Align
orr r0, r0, #0x00001000 @ set bit 12 (I) I-Cache
mcr p15, 0, r0, c1, c0, 0
#ifndef CONFIG_SKIP_LOWLEVEL_INIT_ONLY /* 未定义CONFIG_SKIP_LOWLEVEL_INIT_ONLY */
/*
* before relocating, we have to setup RAM timing
* because memory timing is board-dependend, you will
* find a lowlevel_init.S in your board directory.
*/
mov ip, lr /* 下面使用了双层bl跳转即双层函数调用,但是只有一个lr寄存器,所以先保存lr寄存器值 */
bl lowlevel_init /* 初始化存储控制器 */
mov lr, ip
#endif
mov pc, lr /* 返回 */
#endif /* CONFIG_SKIP_LOWLEVEL_INIT */
该部分主要工作内容如下:
1. 禁用MMU和cashe
2. 跳转到lowlevel_init初始化存储控制器
3. 使用mov pc, lr指令返回(lr是连接寄存器,保存了子程序返回地址)
cache是CPU的缓冲区,它的作用是存放常用的数据和指令,提高CPU与内存之间数据与指令的传输速率。MMU是CPU的内存管理单元,它的作用是转换虚拟地址与物理地址。
关闭cache的原因:上电初始,DDR未初始化,当CPU从cache中取数据时,可能导致数据预取异常。另一方面,当汇编指令读取缓存数据,而实际物理地址对应的数据发生变化,导致CPU不能获取最新的数据。在C语言中无需关闭caches,因为C语言中可以使用volatile关键字避免上述情况。
关闭MMU的原因:U-Boot的作用是硬件初始化和引导操作系统,纯粹的初始化阶段,开启MMU会导致这个过程更复杂。
接着便初始化存储控制器,通过使用bl跳转到lowlevel_init,lowlevel_init定义在具体单板目录下(如上一节创建的board/samsung/jz2440/lowlevel_init.S)的lowlevel_init.S文件,该文件部分代码如下:
.globl lowlevel_init
lowlevel_init:
/* memory control configuration */
/* make r0 relative the current location so that it */
/* reads SMRDATA out of FLASH rather than memory ! */
/* 初始化内存 */
ldr r0, =SMRDATA /* 将SMRDATA的首地址(第一个.word)内存单元数据放置到r0寄存器中 */
ldr r1, =CONFIG_SYS_TEXT_BASE /* CONFIG_SYS_TEXT_BASE=0x0(include/configs/jz2440中默认定义) */
sub r0, r0, r1
ldr r1, =BWSCON /* Bus Width Status Controller */
add r2, r0, #13*4
0:
ldr r3, [r0], #4 /* 将r0的值代表的内存单元放入r3中,之后r0的值偏移4位 */
str r3, [r1], #4 /* 将r3的值放入r1的值代表的地址中,r1的值代表的地址偏移4位 */
cmp r2, r0 /* 比较r2 和 r0 ,若不相等则执行下一句*/
bne 0b /* 向后跳转到标签0处*/
/* everything is fine now */
mov pc, lr /* 返回 */
.ltorg
SMRDATA:
.word (0+(B1_BWSCON<<4)+(B2_BWSCON<<8)+(B3_BWSCON<<12)+(B4_BWSCON<<16)+(B5_BWSCON<<20)+(B6_BWSCON<<24)+(B7_BWSCON<<28))
.word ((B0_Tacs<<13)+(B0_Tcos<<11)+(B0_Tacc<<8)+(B0_Tcoh<<6)+(B0_Tah<<4)+(B0_Tacp<<2)+(B0_PMC))
.word ((B1_Tacs<<13)+(B1_Tcos<<11)+(B1_Tacc<<8)+(B1_Tcoh<<6)+(B1_Tah<<4)+(B1_Tacp<<2)+(B1_PMC))
.word ((B2_Tacs<<13)+(B2_Tcos<<11)+(B2_Tacc<<8)+(B2_Tcoh<<6)+(B2_Tah<<4)+(B2_Tacp<<2)+(B2_PMC))
.word ((B3_Tacs<<13)+(B3_Tcos<<11)+(B3_Tacc<<8)+(B3_Tcoh<<6)+(B3_Tah<<4)+(B3_Tacp<<2)+(B3_PMC))
.word ((B4_Tacs<<13)+(B4_Tcos<<11)+(B4_Tacc<<8)+(B4_Tcoh<<6)+(B4_Tah<<4)+(B4_Tacp<<2)+(B4_PMC))
.word ((B5_Tacs<<13)+(B5_Tcos<<11)+(B5_Tacc<<8)+(B5_Tcoh<<6)+(B5_Tah<<4)+(B5_Tacp<<2)+(B5_PMC))
.word ((B6_MT<<15)+(B6_Trcd<<2)+(B6_SCAN))
.word ((B7_MT<<15)+(B7_Trcd<<2)+(B7_SCAN))
.word ((REFEN<<23)+(TREFMD<<22)+(Trp<<20)+(Trc<<18)+(Tchr<<16)+REFCNT)
.word 0x32
.word 0x30
.word 0x30
该部分内容是初始化存储控制器,经过此初始化之后,内存才可以使用。从lowlevel_init返回后再次取出之前保存的lr寄存器值从cpu_init_crit返回。返回后执行的是bl _main,跳转到_main处执行。
2.4 crt0.S
start.S中执行到了bl _main,跳转到_main,_main入口在arch/arm/lib/crt0.S文件中,下面分几部分进行分析crt0.S。
2.4.1 设置栈
_main被ENTRY宏定义在crt0.S文件中, start.S中执行bl _main跳转到该文件首先执行的就是设置栈,如下代码:
ENTRY(_main)
#if defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_STACK) /* 未定义,执行else语句 */
ldr sp, =(CONFIG_SPL_STACK)
#else
ldr sp, =(CONFIG_SYS_INIT_SP_ADDR) //sp = CONFIG_SYS_INIT_SP_ADDR = 0x30000f50
#endif
#if defined(CONFIG_CPU_V7M) /* 未定义 */
mov r3, sp
bic r3, r3, #7
mov sp, r3
#else
bic sp, sp, #7 /* SP的8字节对齐 */
#endif
首先将CONFIG_SYS_INIT_SP_ADDR定义的值加载到栈指针sp中,这个宏定义在单板相关头文件(上一节创建的include/configs/smdk2440.h)中默认指定,通过反汇编u-boot文件(根目录执行arm-linux-objdump -d u-boot > u-boot.dis,得到的u-boot.dis就是反汇编文件)可以查到该宏等于0x30000f50。这段代码是为后面调用个C函数提供环境,也就是栈指针sp初始化。
2.4.2 第一阶段C函数调用
设置完栈SP后继续往下执行代码:
mov r0, sp
bl board_init_f_alloc_reserve /* 以栈SP值作为参数调用board_init_f_alloc_reserve函数 */
mov sp, r0 /* 重新设置栈SP */
/* set up gd here, outside any C code */
mov r9, r0 /* 设置R9寄存器等于栈SP值 */
bl board_init_f_init_reserve /* 以新的栈SP值作为参数调用board_init_f_alloc_reserve函数 */
mov r0, #0
bl board_init_f /* 执行board_init_f函数 */
首先以栈SP值作为参数调用board_init_f_alloc_reserve函数。
2.4.2.1 调用board_init_f_alloc_reserve函数
board_init_f_alloc_reserve函数代码如下(conmmon/init/board_init.c文件中):
/* include/linux/kernei.h中有如下定义:
#define rounddown(x, y) ( \
{ \
typeof(x) __x = (x); \ /* 定义x类型的__x变量等于x */
__x - (__x % (y)); \ /* 栈向下增长,所以向下16字节对齐 */
} \
)
*/
ulong board_init_f_alloc_reserve(ulong top)
{
#if defined(CONFIG_SYS_MALLOC_F) /* 未定义 */
top -= CONFIG_SYS_MALLOC_F_LEN;
#endif
/* 由传入的栈SP减去global_data结构体大小(反汇编查到该大小为168字节)并16字节对齐后返回 */
top = rounddown(top-sizeof(struct global_data), 16);
return top;
}
该函数将传入的栈SP减去global_data结构体大小(反汇编查到该大小为168字节)并16字节对齐后返回(其中调用的rounddown宏定义里面调用的typeof类似于C++里面的auto类型说明符,能自动推导表达式类型),所以此时的栈SP=0x30000f50-0xa8=0x30000ea8,16字节对齐后也就是0x30000ea0。
从board_init_f_alloc_reserve函数返回后设置新的栈SP后调用board_init_f_init_reserve函数。
2.4.2.2 调用board_init_f_init_reserve函数
board_init_f_init_reserve函数代码如下(conmmon/init/board_init.c文件中):
void board_init_f_init_reserve(ulong base)
{
struct global_data *gd_ptr; /* 定义global_data结构体类型指针 */
#ifndef _USE_MEMCPY
int *ptr;
#endif
/*
* clear GD entirely and set it up.
* Use gd_ptr, as gd may not be properly set yet.
*/
gd_ptr = (struct global_data *)base; /* global_data结构体指针指向base即栈SP地址处 */
/* 清0 global_data结构体 */
memset(gd_ptr, '\0', sizeof(*gd));
... ... /* 无关代码 */
}
可以看到该函数只是将栈SP往上的global_data结构体大小空间清0。继续往下看代码,执行bl board_init_f跳到board_init_f函数。
2.4.2.3 调用board_init_f函数
board_init_f函数代码去掉无关代码后如下(common/board_f.c文件中):
void board_init_f(ulong boot_flags)
{
gd->flags = boot_flags; /* 设置gd->flags=0 */
gd->have_console = 0;
if (initcall_run_list(init_sequence_f)) /* 依次执行init_sequence_f数组里的初始化函数 */
hang(); /* 初始化失败则打印错误并死循环 */
}
首先设置gd指向的结构体成员,gd在中定义如下(arch/arm/include/asm/global_data.h文件中):
#define gd get_gd() /* gd等于get_gd()函数返回值 */
static inline gd_t *get_gd(void)
{
gd_t *gd_ptr;
__asm__ volatile("mov %0, r9\n" : "=r" (gd_ptr)); /* C语言嵌入汇编语法,设置gd_ptr=R9寄存器值 */
return gd_ptr; /* 返回R9寄存器值 */
}
由于在调用board_init_f函数之前设置了R9寄存器等于栈SP值,所以board_init_f函数里设置gd(gd是一个global_data结构体类型的全局变量)指向的结构体成员,设置的就是栈地址往上的global_data结构体,接着使用initcall_run_list函数循环调用init_sequence_f数组里的初始化函数,init_sequence_f数组去掉无用宏代码后如下(判断是否定义了宏方法:1、查看根目录的.config是否定义,2、Makefile是否-D指定):
static init_fnc_t init_sequence_f[] = {
/* setup_mon_len函数:
* 设置(ulong)&__bss_end - (ulong)_start;
* 这里gd->mon_len等于uboot.bin大小加上bss段的大小,_start为0,
* 通过反汇编文件定位到该处查到__bss_end=0x000c78cc,也就是gd->mon_len=0x000c78cc
*/
setup_mon_len,
initf_malloc, /* 空函数 */
initf_console_record, /* 空函数 */
arch_cpu_init, /* 空函数 */
mach_cpu_init, /* 空函数 */
initf_dm, /* 空函数 */
arch_cpu_init_dm, /* 空函数 */
mark_bootstage, /* 标记名字 */
board_early_init_f, /* 设置系统时钟,设置各个GPIO引脚 */
timer_init, /* 初始化定时器 */
env_init, /* 设置gd的成员,初始化环境变量 */
init_baud_rate, /* 设置波特率 */
serial_init, /* 初始化串口 */
console_init_f, /* 完成第一阶段的控制台初始化 */
display_options, /* 打印uboot版本等信息 */
display_text_info, /* 打印uboot代码信息 */
print_cpuinfo, /* 打印uboot时钟频率信息 */
announce_dram_init, /* 打印“ DRAM: ” */
dram_init, /* 设置gd->ram_size= 0x04000000(64MB) */
setup_dest_addr, /* 将gd->relocaddr、gd->ram_top指向SDRAM最顶端 */
reserve_round_4k, /* gd->relocaddr 4KB对齐 */
reserve_mmu, /* 预留16KB的MMU页表并且64KB对齐 */
reserve_trace, /* 空函数 */
/* reserve_uboot函数:
* gd->relocaddr -= gd->mon_len;
* gd->relocaddr &= ~(4096 - 1);
* gd->start_addr_sp = gd->relocaddr;
* 预留uboot空间(u-boot.bin大小加上bss段大小)
*/
reserve_uboot,
/* reserve_malloc函数:
* gd->start_addr_sp = gd->start_addr_sp - TOTAL_MALLOC_LEN;
* 因为jz2440.h默认定义了CONFIG_ENV_ADDR,所以此时在include/common.h中
* 执行#define TOTAL_MALLOC_LEN (CONFIG_SYS_MALLOC_LEN + CONFIG_ENV_SIZE)
* 也就是TOTAL_MALLOC_LEN=4*1024*1024+0x10000=4MB+64KB
* 预留4MB+64KB MALLOC内存池
*/
reserve_malloc,
/* reserve_board函数:
* gd->start_addr_sp -= sizeof(bd_t); 预留bd_t结构体空间,查看反汇编可知为80字节
* gd->bd = (bd_t *)gd->start_addr_sp; 指定重定位bd地址
* memset(gd->bd, '\0', sizeof(bd_t)); 清零
*/
reserve_board,
setup_machine, /* gd->bd->bi_arch_number = CONFIG_MACH_TYPE */
reserve_global_data, /* 预留gd结构体空间,查看反汇编可知为168字节。并设置gd->new_gd */
reserve_fdt, /* 如果设置了gd->fdt_blob则预留fdt设备树空间,这里没有设置,忽略 */
reserve_arch, /* 空函数 */
/* reserve_stacks函数:
* gd->start_addr_sp -= 16;
* gd->start_addr_sp &= ~0xf;
* return arch_reserve_stacks();这里调用的不是board_f.c里的arch_reserve_stacks函数
* 因为该函数被__weak修饰符声明,调用的是arch/arm/lib/stack.c里的arch_reserve_stacks函数
* gd->irq_sp = gd->start_addr_sp;
* gd->start_addr_sp -= 16;
*/
reserve_stacks, /* 设置栈SP16字节对齐 */
setup_dram_config, /* 设置gd结构体的SDRAM地址与大小 */
show_dram_config, /* 打印SDRAM信息 */
display_new_sp, /* 打印新的栈地址 */
reloc_fdt, /* 没有设置gd->new_fdt,忽略 */
/* setup_reloc函数:
* gd->reloc_off = gd->relocaddr - CONFIG_SYS_TEXT_BASE; 计算重定位地址与链接地址偏移值
* 由于jz2440.h中CONFIG_SYS_TEXT_BASE默认为0,所以gd->reloc_off=gd->relocaddr
* memcpy(gd->new_gd, (char *)gd, sizeof(gd_t));
* 将原来的gd复制到gd->new_gd上去
*/
setup_reloc,
NULL,
};
board_init_f函数的主要工作内容就是调用init_sequence_f数组的初始化函数并设置其他gd结构体成员,gd结构体是用来描述划分SDRAM的使用空间的结构。在上一节创建的单板配置文件/include/configs/jz2440.h中,定义了SDRAM基地址(定义宏CONFIG_SYS_SDRAM_BASE等于0X30000000)、SDMRAM大小(定义宏PHYS_SDRAM_1_SIZE等于0x04000000)等其他宏,board_init_f函数就通过这些宏还有__bss_end等其他相关信息来划分SDRAM的使用空间,即通过描述gd结构体来划分SDRAM的空间分配信息,为后面拷贝u-boot到SDRAM中(uboot重定位)做准备。
在执行完board_init_f函数后SDRAM内存划分如下图所示(未经修改的u-boot-2016.11源码执行smdk2410_defconfig配置的情况):
此时uboot还在flash中运行,执行完board_init_f函数后接着往下分析代码。
2.4.2.4 重新设置栈
从board_init_f函数执行完后,接着返回汇编文件crt0.S执行,紧接着2.4.2 第一阶段C函数调用的代码往下分析,部分代码如下:
ldr sp, [r9, #GD_START_ADDR_SP] /* 设置栈sp = gd->start_addr_sp */
#if defined(CONFIG_CPU_V7M) /* 未定义,执行#else分支 */
mov r3, sp
bic r3, r3, #7
mov sp, r3
#else
bic sp, sp, #7 /* 8字节对齐 */
#endif
ldr r9, [r9, #GD_BD] /* r9 = gd->bd */
sub r9, r9, #GD_SIZE /* new GD is below bd */
上面的GD_START_ADDR_SP、GD_BD、GD_SIZE定义如下(arch/arm/lib/asm-offsets.c文件中):
/*定义GD_SIZE为global_data结构体大小 */
DEFINE(GD_SIZE, sizeof(struct global_data));
/* 定义GD_BD为bd成员存放在global_data结构体中的偏移值 */
DEFINE(GD_BD, offsetof(struct global_data, bd));
/* 定义GD_START_ADDR_SP为start_addr_sp成员存放在global_data结构体中的偏移值 */
DEFINE(GD_START_ADDR_SP, offsetof(struct global_data, start_addr_sp));
其中GD_START_ADDR_SP表示start_addr_sp成员存放在global_data结构体中的偏移值,GD_BD为bd成员存放在global_data结构体中的偏移值,GD_SIZE为global_data结构体大小。在2.4.2 第一阶段C函数调用中r9就是global_data结构体首地址(也就是上面SDRAM划分图中的SP位置30000EA0),所以该部分的内容如下:
1. 设置栈sp = gd->start_addr_sp=0x33B17EE0,再8字节对齐;
2. 先令r9=gd->bd=0x33B17FB0;
3. 然后r9=r9-gd结构体大小=0x33B17F08(也就是r9=gd->new_gd)。
2.4.2.5 uboot重定位
继续往下分析代码,如下:
adr lr, here /* 链接寄存器lr指向here,此时指向地址还是在flash中 */
ldr r0, [r9, #GD_RELOC_OFF] /* r0 = gd->reloc_off=gd->relocaddr - CONFIG_SYS_TEXT_BASE */
add lr, lr, r0 /* 链接指向地址指向重定位后的here,此时指向地址在SDRAM中 */
ldr r0, [r9, #GD_RELOCADDR] /* r0 = gd->relocaddr */
b relocate_code /* uboot重定位,注意这里用的是b跳转,lr值不变 */
here:
bl relocate_vectors /* 这里使用的是bl,lr指向下一条指令,后面再详细分析 */
上面的GD_RELOC_OFF、GD_RELOCADDR定义如下(arch/arm/lib/asm-offsets.c文件中):
/* 定义GD_RELOCADDR为relocaddr成员存放在global_data结构体中的偏移值 */
DEFINE(GD_RELOCADDR, offsetof(struct global_data, relocaddr));
/* 定义GD_RELOC_OFF为reloc_off成员存放在global_data结构体中的偏移值 */
DEFINE(GD_RELOC_OFF, offsetof(struct global_data, reloc_off));
在2.4.2.3 调用board_init_f函数分析init_sequence_f数组时,调用的setup_reloc函数有如下代码:
/* 由于jz2440.h中CONFIG_SYS_TEXT_BASE默认为0,所以gd->reloc_off=gd->relocaddr */
gd->reloc_off = gd->relocaddr - CONFIG_SYS_TEXT_BASE;
也就是说操作lr链接寄存器的这3行汇编代码就相当于令lr等于here标号处的地址(在flash中)。然后r0=gd->relocaddr(重定位地址0x33F28000),跳到relocate_code执行uboot重定位。可以猜到重定位后PC会返回lr处也就是here标号处的地址(在flash中)去运行。
relocate_code重定位又可以分为两部分内容:
1. 拷贝uboot到SDRAM
2. 修改动态链接地址数据
2.4.2.5.1 拷贝uboot到SDRAM
relocate_code第一部分代码如下(arch/arm/lib/relocate.S文件中):
ENTRY(relocate_code)
/* 第一部分:拷贝uboot到SDRAM */
ldr r1, =__image_copy_start /* r1 =__image_copy_start的链接地址,这里为0 */
/* r4 =重定位地址-__image_copy_start的链接地址=重定位地址-0=0x33F28000 */
/* 通过r4的值是否等于0判断uboot是否已经在重定位地址上了,如果r4=0表示uboot已经在重定位地址上了,
不需要再重定位了直接跳到lr指向的地方去(也就是here标号处) */
subs r4, r0, r1
beq relocate_done /* 重定位完成,跳到lr指向的地方去(也就是here标号处) */
ldr r2, =__image_copy_end /* 令r2=__image_copy_end的链接地址 */
/* 循环uboot到重定位地址0x33F28000 */
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
/* 第二部分:修改动态链接地址数据 */
... ...
ENDPROC(relocate_code)
第一部分通过将flash中的uboot拷贝到SDRAM中,此时只是把代码复制到SDRAM上,如果指定的链接地址(指定链接地址方法在2.1 u-boot.lds链接脚本中有说明,jz2440.h中CONFIG_SYS_TEXT_BASE默认为0,所以这里链接地址为0)与重定位地址(0x33F28000)不一样时,直接跳到SDRAM上执行u-boot代码就会造成一个问题,下面进行说明。
2.4.2.5.2 修改动态链接地址数据
用两种情况说明为什么要修改动态链接地址数据。第一种情况:假设PC在flash上执行clbss_l部分的指令,情况如下:
查看u-boot的反汇编文件可以知道eea4为board_init_r函数的链接地址。
第二种情况:之前已经拷贝uboot代码到SDRAM中了,如果此时跳到SDRAM也执行重定位到0x33F28000的uboot的clbss_l部分指令,情况如下:
可以看到这样的话相对于uboot偏移地址为7d0也就是0x33f287d0的地方保存的数据没有变,这样运行该地址上的指令时PC又会跳到0000eea4地址也就是跳回flash上的board_init_r函数运行了,显然不对。
如果指定链接地址与重定位地址相同,即指定链接地址=重定位地址=0x33F28000,编译uboot时会将例如0000eea4改为board_init_r函数的链接地址,这样就只需要拷贝uboot到SDRAM上的重定位地址0x33F28000,重定位部分内容就完成了(这里指的是不考虑位置相关码的情况下!!!不然直接修改链接地址为0x33F28000,程序运行到board_init_f函数就会出错,因为里面使用了全局变量,是位置相关码,链接地址必须与运行地址一致)。
源码的重定位地址是程序计算得到的,而默认链接地址指定为0(jz2440.h中CONFIG_SYS_TEXT_BASE默认为0),明显不同,所以就要使用uboot采用的动态链接地址方法,在链接脚本uboot.lds中,可以看到.rel_dyn段,如下:
.rel_dyn_start :
{
*(.__rel_dyn_start)
}
.rel.dyn : {
*(.rel*)
}
.rel_dyn_end :
{
*(.__rel_dyn_end)
}
这个段里保存了各个文件的相对动态信息,修改动态链接地址数据就是例如将0000eea4改为board_init_r函数的链接地址加上重定位与链接地址偏移值,也就是改为了重定位到SDRAM中的board_init_r函数的地址值。循环修改所有这样的动态信息。下面看看详细代码,relocate_code第二部分代码如下(arch/arm/lib/relocate.S文件中):
ENTRY(relocate_code)
/* 第一部分:拷贝uboot到SDRAM */
... ...
/* 第二部分:修改动态链接地址数据 */
ldr r2, =__rel_dyn_start /* r2 <- SRC &__rel_dyn_start */
ldr r3, =__rel_dyn_end /* r3 <- SRC &__rel_dyn_end */
fixloop:
ldmia r2!, {r0-r1} /* (r0,r1) <- (SRC location,fixup) */
and r1, r1, #0xff
cmp r1, #23 /* relative fixup? */
bne fixnext
/* relative fix: increase location by offset */
add r0, r0, r4
ldr r1, [r0]
add r1, r1, r4
str r1, [r0]
fixnext:
cmp r2, r3
blo fixloop
relocate_done:
/* 去掉无关代码后 */
bx lr
ENDPROC(relocate_code)
当执行完第二部分修改动态链接地址数据后,例如与__bss_start、__bss_end这些标号还有所有函数地址相关数据等等这些数据都被修改加上了重定位基地址与链接基地址的偏移量。然后执行bx lr也就是PC跳到2.4.2.5 uboot重定位中的here标号处(在flash中)接着运行。
2.4.2.6 异常向量表重定位
在relocate_code里执行bx lr后程序就从flash中的here标号处继续往下运行,代码如下:
here:
bl relocate_vectors
relocate_vectors去掉无关代码如下(arch/arm/lib/relocate.S文件中):
ENTRY(relocate_vectors)
ldr r0, [r9, #GD_RELOCADDR] /* r0 = gd->relocaddr */
mrc p15, 0, r2, c1, c0, 0 /* V bit (bit[13]) in CP15 c1 */
ands r2, r2, #(1 << 13)
ldreq r1, =0x00000000 /* If V=0 */
ldrne r1, =0xFFFF0000 /* If V=1 */
ldmia r0!, {r2-r8,r10}
stmia r1!, {r2-r8,r10}
ldmia r0!, {r2-r8,r10}
stmia r1!, {r2-r8,r10}
bx lr
ENDPROC(relocate_vectors)
relocate_vectors主要是进行异常向量表的重定位,将异常向量表拷贝到SDRAM的正确的地址中去,然后执行bx lr(因为执行bl relocate_vectors使用的是bl指令,此时lr链接寄存器指向下一条指令地址,所以执行bx lr是执行bl relocate_vectors的下一条指令)。
2.4.2.7 清bss段、调用第二阶段C函数board_init_r
bl relocate_vectors之后(去掉无关代码)的代码如下(arch/arm/lib/crt0.S文件中):
bl c_runtime_cpu_setup /* mov pc, lr,跳到下一条指令 */
ldr r0, =__bss_start /* 得到bss段起始地址 */
ldr r1, =__bss_end /* 得到bss段结束地址 */
mov r2, #0x00000000 /* r2设为0 */
/* 循环将SDRAM中的bss段清0 */
clbss_l:cmp r0, r1 /* while not at end of BSS */
strlo r2, [r0] /* clear 32-bit BSS word */
addlo r0, r0, #4 /* move to next */
blo clbss_l
/* 点灯 */
bl coloured_LED_init
bl red_led_on
/* call board_init_r(gd_t *id, ulong dest_addr) */
mov r0, r9 /* r0等于新的gd结构体 */
ldr r1, [r9, #GD_RELOCADDR] /* r1等于gd->relocaddr也就是重定位地址 */
/* pc跳到board_init_r函数的链接地址处(已经动态修改,加上了重定位偏移值,所以在SDRAM中) */
ldr pc, =board_init_r
总该部分主要内容:
1. 清bss段
2. 调用点灯函数
3. 调用board_init_r函数
前两个在代码段里说明了,调用board_init_r函数时传入的参数r0等于r9也就是2.4.2.4 重新设置栈里分析的新的gd结构体(SDRAM划分图中的0x33B17F08),传入的参数r1等于gd->relocaddr,该成员在2.4.2.3 调用board_init_f函数调用init_sequence_f数组中的reserve_uboot函数里设置,主要代码如下(common/board_f.c文件中):
static int reserve_uboot(void)
{
gd->relocaddr -= gd->mon_len;
gd->relocaddr &= ~(4096 - 1);
debug("Reserving %ldk for U-Boot at: %08lx\n", gd->mon_len >> 10,
gd->relocaddr);
gd->start_addr_sp = gd->relocaddr;
return 0;
}
所以r1也就是2.4.2.3 调用board_init_f函数分析的SDRAM分布图中的uboot重定位空间首地址0x33F28000。接着使用ldr pc, =board_init_r让PC跳到board_init_r函数的链接地址处,该函数的链接地址在2.4.2.5.2 修改动态链接地址数据中加上了重定位偏移值,所以此时board_init_r函数的链接地址在SDRAM中了。这样就完成了从flash中跳到SDRAM中运行程序。
board_init_r函数去掉无关代码后如下(common/board_f.c文件中):
void board_init_r(gd_t *new_gd, ulong dest_addr)
{
if (initcall_run_list(init_sequence_r))
hang();
hang();
}
与board_init_f函数类似,使用initcall_run_list函数循环调用init_sequence_r数组里的初始化函数,init_sequence_r数组去掉无用宏代码后如下(判断是否定义了宏方法:1、查看根目录的.config是否定义,2、Makefile是否-D指定):
init_fnc_t init_sequence_r[] = {
initr_trace, /* 空函数 */
initr_reloc, /* gd->flags |= GD_FLG_RELOC | GD_FLG_FULL_MALLOC_INIT; */
#ifdef CONFIG_ARM
initr_caches, /* 打印"WARNING: Caches not enabled\n" */
#endif
initr_reloc_global_data, /* monitor_flash_len = _end - __image_copy_start; */
initr_barrier, /* 空函数 */
/* initr_malloc函数:
* malloc_start = gd->relocaddr - TOTAL_MALLOC_LEN;
* mem_malloc_init((ulong)map_sysmem(malloc_start, TOTAL_MALLOC_LEN),
* TOTAL_MALLOC_LEN);
*/
initr_malloc,
initr_console_record, /* 空函数 */
bootstage_relocate,
initr_bootstage, /* 标记名字 */
#if defined(CONFIG_ARM) || defined(CONFIG_NDS32)
/* board_init函数:
* gd->bd->bi_arch_number = MACH_TYPE_SMDK2410;
* gd->bd->bi_boot_params = 0x30000100;
* icache_enable();
* dcache_enable();
*/
board_init,
#endif
stdio_init_tables,
initr_serial, /* 调用serial_initialize() */
initr_announce, /* debug "Now running in RAM - U-Boot at: %08lx\n" */
power_init_board, /* 空函数 */
#ifndef CONFIG_SYS_NO_FLASH
initr_flash, /* 初始化NOR Flash */
#endif
#ifdef CONFIG_CMD_NAND
initr_nand, /* 初始化NAND Flash */
#endif
initr_env, /* 初始化环境变量 */
initr_secondary_cpu, /* 空函数 */
stdio_add_devices, /* 空函数 */
/* initr_jumptable函数:
* gd->jt = malloc(sizeof(struct jt_funcs));
* #include <_exports.h>
*/
initr_jumptable,
console_init_r, /* 控制台初始化 */
interrupt_init, /* 中断初始化 */
#if defined(CONFIG_ARM) || defined(CONFIG_AVR32)
initr_enable_interrupts, /* 使能中断 */
#endif
#ifdef CONFIG_CMD_NET
initr_ethaddr, /* eth_getenv_enetaddr("ethaddr", bd->bi_enetaddr); */
#endif
#ifdef CONFIG_CMD_NET
INIT_FUNC_WATCHDOG_RESET
initr_net, /* 网卡初始化 */
#endif
run_main_loop, /* 进入main_loop */
};
到这里整个U-Boot启动完成,进入main_loop循环。若控制台有任意输入,则进入控制台命令解析-执行的循环。若无,则U-Boot将启动内核。