通过对 uboot 启动流程的梳理,我们就可以掌握一些外设是在哪里被初始化的,这样当我们需要修改这些外设驱动的时候就会心里有数。另外,通过分析 uboot 的启动流程可以了解 Linux 内核是如何被启动的,uboot的研究只限于了解阶段,看得懂、会ctrl+C就行了,学习一下一些外设的启动即可。
链接脚本 u-boot.lds 详解
编译之前:只有arch/arm/cpu/u-boot.lds链接脚本;
编译之后:在arch/arm/cpu/u-boot.lds基础上生成了根目录下的 u-boot.lds链接脚本。
u-boot.lds文件:
1 OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
2 OUTPUT_ARCH(arm)
3 ENTRY(_start)
4 SECTIONS
5 {
6 . = 0x00000000;
7 . = ALIGN(4);
8 .text :
9 {
10 *(.__image_copy_start)
11 *(.vectors)
12 arch/arm/cpu/armv7/start.o (.text*)
13 *(.text*)
14 }
15 . = ALIGN(4);
16 .rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) }
17 . = ALIGN(4);
18 .data : {
19 *(.data*)
20 }
21 . = ALIGN(4);
22 . = .;
23 . = ALIGN(4);
24 .u_boot_list : {
25 KEEP(*(SORT(.u_boot_list*)));
26 }
27 . = ALIGN(4);
28 .image_copy_end :
29 {
30 *(.__image_copy_end)
31 }
32 .rel_dyn_start :
33 {
34 *(.__rel_dyn_start)
35 }
36 .rel.dyn : {
37 *(.rel*)
38 }
39 .rel_dyn_end :
40 {
41 *(.__rel_dyn_end)
42 }
43 .end :
44 {
45 *(.__end)
46 }
47 _image_binary_end = .;
48 . = ALIGN(4096);
49 .mmutable : {
50 *(.mmutable)
51 }
52 .bss_start __rel_dyn_start (OVERLAY) : {
53 KEEP(*(.__bss_start));
54 __bss_base = .;
55 }
56 .bss __bss_base (OVERLAY) : {
57 *(.bss*)
58 . = ALIGN(4);
59 __bss_limit = .;
60 }
61 .bss_end __bss_limit (OVERLAY) : {
62 KEEP(*(.__bss_end));
63 }
64 .dynsym _image_binary_end : { *(.dynsym) }
65 .dynbss : { *(.dynbss) }
66 .dynstr : { *(.dynstr*) }
67 .dynamic : { *(.dynamic*) }
68 .plt : { *(.plt*) }
69 .interp : { *(.interp*) }
70 .gnu.hash : { *(.gnu.hash) }
71 .gnu : { *(.gnu*) }
72 .ARM.exidx : { *(.ARM.exidx*) }
73 .gnu.linkonce.armexidx : { *(.gnu.linkonce.armexidx.*) }
74 }
第 3 行为代码当前入口点: _start, _start 在文件 arch/arm/lib/vectors.S 中有定义,(和汇编向量文件类似),_start 后面就是中断向量表。__image_copy_start 为 0X87800000,而.text 的起始地址也是0X87800000(和裸机开发时候的链接地址类似)。
第 12 行将 arch/arm/cpu/armv7/start.s 编译出来的代码放到中断向量表后面(汇编启动文件)。
第 13 行为 text 段,其他的代码段就放到这里(和裸机开发中的main.o文件类似)。
在 u-boot.lds 中有一些跟地址有关的“变量”需要学习一下下,此地址应该就是DRAM中的地址,“变量”值可以在 u-boot.map 文件中查找,中除了__image_copy_start以外,其他的变量值每次编译的时候可能会变化,如果修改了 uboot 代码、修改了 uboot 配置、选用不同的优化等级等等都会影响到这些值。所以,一切以实际值为准!:
U-Boot 启动流程详解
从 u-boot.lds 中我们已经知道了入口点是 arch/arm/lib/vectors.S 文件中的_start,
vectors.S 代码段,_start 开始的是中断向量表,(和裸机开发相比,中断向量表自立门户,存在一个自己的文件中):
48 _start:
49
50 #ifdef CONFIG_SYS_DV_NOR_BOOT_CFG
51 .word CONFIG_SYS_DV_NOR_BOOT_CFG
52 #endif
53
54 b reset
55 ldr pc, _undefined_instruction
56 ldr pc, _software_interrupt
57 ldr pc, _prefetch_abort
58 ldr pc, _data_abort
59 ldr pc, _not_used
60 ldr pc, _irq
61 ldr pc, _fiq
reset 函数源码详解
跳转到 reset 函数里面, reset 函数在 arch/arm/cpu/armv7/start.S 里面:
1、完成设置 CPU 处于 SVC 模式,并且关闭FIQ 和 IRQ 这两个中断。
32 .globl reset
33 .globl save_boot_params_ret
35 reset:
37 b save_boot_params
100 ENTRY(save_boot_params)
101 b save_boot_params_ret @ back to my caller
38 save_boot_params_ret:
39 /*
40 * disable interrupts (FIQ and IRQ), also set the cpu to SVC32
41 * mode, except if in HYP mode already
42 */
43 mrs r0, cpsr
44 and r1, r0, #0x1f @ mask mode bits
45 teq r1, #0x1a @ test for HYP mode
46 bicne r0, r0, #0x1f @ clear all mode bits
47 orrne r0, r0, #0x13 @ set SVC mode
48 orr r0, r0, #0xc0 @ disable FIQ and IRQ
49 msr cpsr,r0
关于汇编比较命令的理解:
teq 判断是否相等
bicne 不相等则执行
biceq 相等则执行
ENTRY是汇编中的伪指令,表示程序入口地址,和C语言中的main()函数类似。
程序应该能看懂,其中cpsr 的 bit0~bit4 这 5 位,这 5 位为 M4 M3 M2 M1 M0, M[4:0]这五位用来设置处理器的工作模式,前面的笔记已经介绍过了:
第 45 行,teq,判断 r1 寄存器的值是否等于 0X1A(0b11010),也就是判断当前处理器模式是否处于 Hyp 模式。
第 46 行,bicne,如果 r1 和 0X1A 不相等,也就是 CPU 不处于 Hyp 模式的话就将 r0 寄存器的bit0~5 进行清零,其实就是清除模式位
第 47 行,orrne,如果处理器不处于 Hyp 模式的话就将 r0 的寄存器的值与 0x13 进行或运算,0x13=0b10011,也就是设置处理器进入 SVC 模式。
2、向VBAR 寄存器向量表重定位地址。
56 #if !(defined(CONFIG_OMAP44XX) && defined(CONFIG_SPL_BUILD))
57 /* Set V=0 in CP15 SCTLR register - for VBAR to point to vector */
58 mrc p15, 0, r0, c1, c0, 0 @ Read CP15 SCTLR Register
59 bic r0, #CR_V @ V = 0
60 mcr p15, 0, r0, c1, c0, 0 @ Write CP15 SCTLR Register
61
62 /* Set vector address in CP15 VBAR register */
63 ldr r0, =_start
64 mcr p15, 0, r0, c12, c0, 0 @Set VBAR
65 #endif
第 58 行读取 CP15 中 c1 寄存器的值到 r0 寄存器中,这里是读取SCTLR 寄存器的值。
第 59 行, 清除 SCTLR 寄存器中的 bit13。
CR_V 在 arch/arm/include/asm/system.h 中有如下所示定义:#define CR_V (1 << 13)
SCTLR 寄存器的bit13 为 V 位,此位是向量表控制位,
当为 0 的时候向量表基地址为 0X00000000,软件可以重定位向量表。
为 1 的时候向量表基地址为 0XFFFF0000,软件不能重定位向量表。
这里将 V 清零,目的就是为了接下来的向量表重定位
第 63行设置 r0寄存器的值为_start, _start就是整个 uboot的入口地址,其值为 0X87800000,相当于 uboot 的起始地址,因此 0x87800000 也是向量表的起始地址。
第 64 行将 r0 寄存器的值(向量表值)写入到 CP15 的 c12 寄存器中,也就是VBAR 寄存器,设置向量表重定位的 。
3、判断是否调用函数 cpu_init_cp15、 cpu_init_crit 和_main。
68 #ifndef CONFIG_SKIP_LOWLEVEL_INIT
69 bl cpu_init_cp15
70 bl cpu_init_crit
71 #endif
72
73 bl _main
函数 cpu_init_cp15 用来设置 CP15 相关的内容,比如关闭 MMU 啥的。
函数 cpu_init_crit 内部仅仅是调用了函数 lowlevel_init。
以上1、2、3步都是在save_boot_params_ret函数中。
lowlevel_init 函数详解
cpu_init_crit 函数中调用了 lowlevel_init 函数,函数 lowlevel_init 在文件 arch/arm/cpu/armv7/lowlevel_init.S 中定义:
14 #include <asm-offsets.h>
15 #include <config.h>
16 #include <linux/linkage.h>
17
18 ENTRY(lowlevel_init)
22 ldr sp, =CONFIG_SYS_INIT_SP_ADDR
23 bic sp, sp, #7 /* 8-byte alignment for ABI compliance */
24 #ifdef CONFIG_SPL_DM
25 mov r9, #0
26 #else
31 #ifdef CONFIG_SPL_BUILD
32 ldr r9, =gdata
33 #else
34 sub sp, sp, #GD_SIZE
35 bic sp, sp, #7
36 mov r9, sp
37 #endif
38 #endif
42 push {ip, lr}
57 bl s_init
58 pop {ip, pc}
59 ENDPROC(lowlevel_init)
CONFIG_SPL_DM和CONFIG_SPL_BUILD都没有定义,左中凯老师是这个意思。
第 22 行设置 sp 指向 CONFIG_SYS_INIT_SP_ADDR,此时 sp 指向 0X91FF00,这属于 IMX6UL/IMX6ULL 的内部 ram。
对于CONFIG_SYS_INIT_SP_ADDR的值,就是一些基地址和偏移地址计算得到,以后用到再深究:
CONFIG_SYS_INIT_SP_OFFSET = 0x00020000 – 256 = 0x1FF00。
CONFIG_SYS_INIT_SP_ADDR = 0x00900000 + 0X1FF00 = 0X0091FF00
第 34 行, sp 指针减去 GD_SIZE, GD_SIZE 同样在 generic-asm-offsets.h 中定义了大小为248。
第 35 行对 sp 做 8 字节对齐 (将后3位清零== 8的倍数 ==8字节对齐) ,此时 sp 的地址为 0X0091FF00-248=0X0091FE08。
第 36 行将 sp 地址保存在 r9 寄存器中。
第 42 行将 ip 和 lr 压栈
第 57 行调用函数 s_init,得,又来了一个函数。
第 58 行将第 36 行入栈的 ip 和 lr 进行出栈,并将 lr 赋给 pc。
最终SP指向的地址如图所示:
s_init 函数详解
lowlevel_init 函数后面会调用 s_init 函数, s_init 函数定义在文件
arch/arm/cpu/armv7/mx6/soc.c 中:
808 void s_init(void)
809 {
810 struct anatop_regs *anatop = (struct anatop_regs *)ANATOP_BASE_ADDR;
811 struct mxc_ccm_reg *ccm = (struct mxc_ccm_reg *)CCM_BASE_ADDR;
812 u32 mask480;
813 u32 mask528;
814 u32 reg, periph1, periph2;
815
816 if (is_cpu_type(MXC_CPU_MX6SX) || is_cpu_type(MXC_CPU_MX6UL) ||
817 is_cpu_type(MXC_CPU_MX6ULL) || is_cpu_type(MXC_CPU_MX6SLL))
818 return;
819
820 /* Due to hardware limitation, on MX6Q we need to gate/ungate
821 * all PFDs to make sure PFD is working right, otherwise, PFDs
822 * may not output clock after reset, MX6DL and MX6SL have added
823 * 396M pfd workaround in ROM code, as bus clock need it
824 */
825
826 mask480 = ANATOP_PFD_CLKGATE_MASK(0) |
827 ANATOP_PFD_CLKGATE_MASK(1) |
828 ANATOP_PFD_CLKGATE_MASK(2) |
829 ANATOP_PFD_CLKGATE_MASK(3);
830 mask528 = ANATOP_PFD_CLKGATE_MASK(1) |
831 ANATOP_PFD_CLKGATE_MASK(3);
832
833 reg = readl(&ccm->cbcmr);
834 periph2 = ((reg & MXC_CCM_CBCMR_PRE_PERIPH2_CLK_SEL_MASK)
835 >> MXC_CCM_CBCMR_PRE_PERIPH2_CLK_SEL_OFFSET);
836 periph1 = ((reg & MXC_CCM_CBCMR_PRE_PERIPH_CLK_SEL_MASK)
837 >> MXC_CCM_CBCMR_PRE_PERIPH_CLK_SEL_OFFSET);
838
839 /* Checking if PLL2 PFD0 or PLL2 PFD2 is using for periph clock */
840 if ((periph2 != 0x2) && (periph1 != 0x2))
841 mask528 |= ANATOP_PFD_CLKGATE_MASK(0);
842
843 if ((periph2 != 0x1) && (periph1 != 0x1) &&
844 (periph2 != 0x3) && (periph1 != 0x3))
845 mask528 |= ANATOP_PFD_CLKGATE_MASK(2);
846
847 writel(mask480, &anatop->pfd_480_set);
848 writel(mask528, &anatop->pfd_528_set);
849 writel(mask480, &anatop->pfd_480_clr);
850 writel(mask528, &anatop->pfd_528_clr);
851 }
第 816 行会判断当前 CPU 类型,如果 CPU 为 MX6SX、 MX6UL、 MX6ULL 或 MX6SLL中 的 任 意 一 种 , 那 么 就 会 直 接 返 回 , 相 当 于 s_init 函 数 什 么 都 没 做 。 所 以 对 于I.MX6UL/I.MX6ULL 来说, s_init 就是个空函数。
reset函数总结
最终结束了整个save_boot_params_ret函数(reset函数),貌似还是初始化时候的模式切换、中断向量表地址的设置、MMU设置、中断禁止等操作,如图所示:
_main 函数详解
main 函数定义在文件 arch/arm/lib/crt0.S 中:
63 /*
64 * entry point of crt0 sequence
65 */
66
67 ENTRY(_main)
68
69 /*
70 * Set up initial C runtime environment and call board_init_f(0).
71 */
72
73 #if defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_STACK)
74 ldr sp, =(CONFIG_SPL_STACK)
75 #else
76 ldr sp, =(CONFIG_SYS_INIT_SP_ADDR)
77 #endif
78 #if defined(CONFIG_CPU_V7M) /* v7M forbids using SP as BICdestination */
79 mov r3, sp
80 bic r3, r3, #7
81 mov sp, r3
82 #else
83 bic sp, sp, #7 /* 8-byte alignment for ABI compliance */
84 #endif
85 mov r0, sp
86 bl board_init_f_alloc_reserve
87 mov sp, r0
88 /* set up gd here, outside any C code */
89 mov r9, r0
90 bl board_init_f_init_reserve
91
92 mov r0, #0
93 bl board_init_f
94
95 #if ! defined(CONFIG_SPL_BUILD)
96
97 /*
98 * Set up intermediate environment (new sp and gd) and call
99 * relocate_code(addr_moni). Trick here is that we'll return
100 * 'here' but relocated.
101 */
102
103 ldr sp, [r9, #GD_START_ADDR_SP] /* sp = gd->start_addr_sp */
104 #if defined(CONFIG_CPU_V7M) /* v7M forbids using SP as BICdestination */
105 mov r3, sp
106 bic r3, r3, #7
107 mov sp, r3
108 #else
109 bic sp, sp, #7 /* 8-byte alignment for ABI compliance */
110 #endif
111 ldr r9, [r9, #GD_BD] /* r9 = gd->bd */
112 sub r9, r9, #GD_SIZE /* new GD is below bd */
113
114 adr lr, here
115 ldr r0, [r9, #GD_RELOC_OFF] /* r0 = gd->reloc_off */
116 add lr, lr, r0
117 #if defined(CONFIG_CPU_V7M)
118 orr lr, #1 /* As required by Thumb-only */
119 #endif
120 ldr r0, [r9, #GD_RELOCADDR] /* r0 = gd->relocaddr */
121 b relocate_code
122 here:
123 /*
124 * now relocate vectors
125 */
126
127 bl relocate_vectors
128
129 /* Set up final (full) environment */
130
131 bl c_runtime_cpu_setup /* we still call old routine here */
132 #endif
133 #if !defined(CONFIG_SPL_BUILD) || defined(CONFIG_SPL_FRAMEWORK)
134 # ifdef CONFIG_SPL_BUILD
135 /* Use a DRAM stack for the rest of SPL, if requested */
136 bl spl_relocate_stack_gd
137 cmp r0, #0
138 movne sp, r0
139 movne r9, r0
140 #endif
141 ldr r0, =__bss_start /* this is auto-relocated! */
142
143 #ifdef CONFIG_USE_ARCH_MEMSET
144 ldr r3, =__bss_end /* this is auto-relocated! */
145 mov r1, #0x00000000 /* prepare zero to clear BSS */
146
147 subs r2, r3, r0 /* r2 = memset len */
148 bl memset
149 #else
150 ldr r1, =__bss_end /* this is auto-relocated! */
151 mov r2, #0x00000000 /* prepare zero to clear BSS */
152
153 clbss_l:cmp r0, r1 /* while not at end of BSS */
154 #if defined(CONFIG_CPU_V7M)
155 itt lo
156 #endif
157 strlo r2, [r0] /* clear 32-bit BSS word */
158 addlo r0, r0, #4 /* move to next */
159 blo clbss_l
160 #endif
161
162 #if ! defined(CONFIG_SPL_BUILD)
163 bl coloured_LED_init
164 bl red_led_on
165 #endif
166 /* call board_init_r(gd_t *id, ulong dest_addr) */
167 mov r0, r9 /* gd_t */
168 ldr r1, [r9, #GD_RELOCADDR] /* dest_addr */
169 /* call board_init_r */
170 #if defined(CONFIG_SYS_THUMB_BUILD)
171 ldr lr, =board_init_r /* this is auto-relocated! */
172 bx lr
173 #else
174 ldr pc, =board_init_r /* this is auto-relocated! */
175 #endif
176 /* we should not return here. */
177 #endif
178
179 ENDPROC(_main)
遇到定义的条件语句,一般都是没定义。
第 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 中。
这一部分作用就是,传入当前SP指针,函数将其留出早期的 malloc 内存区域和 gd 内存区域,进行内存管理分配,最后返回新的SP指针,如图所示:
As known to us,汇编中的函数参数传入和传出都是使用R0、R1、R2等寄存器传递的。
第 89 行,将 r0 寄存器的值写到寄存器 r9 里面,因为 r9 寄存器存放着全局变量 gd 的地址。
第 90 行调用函数 board_init_f_init_reserve,此函数在文件
common/init/board_init.c 中有定义,
此函数用于初始化 gd,就是清零处理;
还设置了gd->malloc_base,也就是 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。
第 109 行, sp 做 8 字节对齐。
第 111 行,获取 gd->bd 的地址赋给 r9,此时 r9 存放的是老的 gd,这里通过获取 gd->bd 的地址来计算出新的 gd 的位置。 GD_BD=0。
第 112 行,新的 gd 在 bd 下面,所以 r9 减去 gd 的大小就是新的 gd 的位置,获取到新的 gd的位置以后赋值给 r9。
第 114 行,设置 lr 寄存器为 here,这样后面执行其他函数返回的时候就返回到了第 122 行的 here 位置处。
第 115,读取 gd->reloc_off 的值复制给 r0 寄存器,GD_RELOC_OFF=68。
第 116 行, lr 寄存器的值加上 r0 寄存器的值,重新赋值给 lr 寄存器。因为接下来要重定位代码,也就是把代码拷贝到新的地方去(现在的 uboot 存放的起始地址为 0X87800000,下面要将 uboot 拷贝到 DDR 最后面的地址空间出,将 0X87800000 开始的内存空出来),其中就包括here,因此 lr 中的 here 要使用重定位后的位置。
第 120 行,读取 gd->relocaddr 的值赋给 r0 寄存器,此时 r0 寄存器就保存着 uboot 要拷贝的目的地址,为 0X9FF47000,GD_RELOCADDR=48。
第 121 行,调用函数 relocate_code ,也就是代码重定位函数,此函数负责将 uboot 拷贝到新的地方去,此函数定义在文件 arch/arm/lib/relocate.S 中稍后会详细分析此函数。
第 127 行,调用函数 relocate_vectors,对中断向量表做重定位,此函数定义在文件arch/arm/lib/relocate.S 中,稍后会详细分析此函数。
第 131 行,调用函数 c_runtime_cpu_setup,此函数定义在文件 arch/arm/cpu/armv7/start.S 中,CP15寄存器中的一些操作。
第 141~159 行,清除 BSS 段。
第 167 行,设置函数 board_init_r 的两个参数。
第 168 行,设置函数 board_init_r 的第二个参数是目的地址,因此r1= gd->relocaddr。
第 174 行,调用函数 board_init_r,此函数定义在文件common/board_r.c 中,稍后会详细的分析此函数。
在_main 函数里面调用了 board_init_f、 relocate_code、relocate_vectors 和 board_init_r 这 4 个函数,接下来依次看一下这 4个函数都是干啥的。
board_init_f 函数详解
board_init_f 函数主要有两个工作:
①、初始化一系列外设,比如串口、定时器,或者打印一些消息等。
②、初始化 gd 的各个成员变量, uboot 会将自己重定位到 DRAM 最后面的地址区域,也就是将自己拷贝到 DRAM 最后面的内存区域中并自己分好区。这么做的目的是给 Linux 腾出空间,防止 Linux kernel 覆盖掉 uboot,将 DRAM 前面的区域完整的空出来。
此函数定义在文件 common/board_f.c 中定义:
1035 void board_init_f(ulong boot_flags)
1036 {
1037 #ifdef CONFIG_SYS_GENERIC_GLOBAL_DATA
1038 /*
1039 * For some archtectures, global data is initialized and used
1040 * before calling this function. The data should be preserved.
1041 * For others, CONFIG_SYS_GENERIC_GLOBAL_DATA should be defined
1042 * and use the stack here to host global data until relocation.
1043 */
1044 gd_t data;
1045
1046 gd = &data;
1047
1048 /*
1049 * Clear global data before it is accessed at debug print
1050 * in initcall_run_list. Otherwise the debug print probably
1051 * get the wrong vaule of gd->have_console.
1052 */
1053 zero_global_data();
1054 #endif
1055
1056 gd->flags = boot_flags;
1057 gd->have_console = 0;
1058
1059 if (initcall_run_list(init_sequence_f))
1060 hang();
1061
1062 #if !defined(CONFIG_ARM) && !defined(CONFIG_SANDBOX) && \
1063 !defined(CONFIG_EFI_APP)
1064 /* NOTREACHED - jump_to_copy() does not return */
1065 hang();
1066 #endif
1067 }
没有定义 CONFIG_SYS_GENERIC_GLOBAL_DATA,
第 1056 行,初始化 gd->flags=boot_flags=0。
第 1057 行,设置 gd->have_console=0。
第 1059 行! 通过函数 initcall_run_list 来运行初始化数组 init_sequence_f 里面的一系列函数(个人猜测init_fnc_t是函数指针类型)。
init_sequence_f 里面包含了一系列的初始化函数:
这些函数就是关于一些驱动初始化、“搬家”时内存分配以及一些硬件信息的相关函数,函数中基本上就是操作结构体类型gd获取一些信息。
board_init_f 函数就执行完成后,最终的内存分配如图所示:
relocate_code 函数详解
relocate_code 函数是用于代码拷贝的,此函数定义在文件 arch/arm/lib/relocate.S 中:
79 ENTRY(relocate_code)
80 ldr r1, =__image_copy_start /* r1 <- SRC &__image_copy_start */
81 subs r4, r0, r1 /* r4 <- relocation offset */
82 beq relocate_done /* skip relocation */
83 ldr r2, =__image_copy_end /* r2 <- SRC &__image_copy_end */
84
85 copy_loop:
86 ldmia r1!, {r10-r11} /* copy from source address [r1] */
87 stmia r0!, {r10-r11} /* copy to target address [r0] */
88 cmp r1, r2 /* until source end address [r2] */
89 blo copy_loop
90
91 /*
92 * fix .rel.dyn relocations
93 */
94 ldr r2, =__rel_dyn_start /* r2 <- SRC &__rel_dyn_start */
95 ldr r3, =__rel_dyn_end /* r3 <- SRC &__rel_dyn_end */
96 fixloop:
97 ldmia r2!, {r0-r1} /* (r0,r1) <- (SRC location,fixup) */
98 and r1, r1, #0xff
99 cmp r1, #23 /* relative fixup? */
100 bne fixnext
101
102 /* relative fix: increase location by offset */
103 add r0, r0, r4
104 ldr r1, [r0]
105 add r1, r1, r4
106 str r1, [r0]
107 fixnext:
108 cmp r2, r3
109 blo fixloop
110
111 relocate_done:
112
113 #ifdef __XSCALE__
114 /*
115 * On xscale, icache must be invalidated and write buffers
116 * drained, even with cache disabled - 4.2.7 of xscale core
117 developer's manual */
118 mcr p15, 0, r0, c7, c7, 0 /* invalidate icache */
119 mcr p15, 0, r0, c7, c10, 4 /* drain write buffer */
120 #endif
121
122 /* ARMv4- don't know bx lr but the assembler fails to see that */
123
124 #ifdef __ARM_ARCH_4__
125 mov pc, lr
126 #else
127 bx lr
128 #endif
129
130 ENDPROC(relocate_code)
第一部分,第80~89行,将uboot代码拷贝到DRAM高地址,腾地儿。
第 80 行, r1=__image_copy_start,r1 寄存器保存拷贝之前的代码源地址,__image_copy_start=0X87800000。
第 81 行, r0=0X9FF47000,这个地址就是 uboot 拷贝的目标首地址。 r4=r0-r1=0X9FF47000-0X87800000=0X18747000,因此 r4 保存偏移量。
第 82 行,如果在第 81 中, r0-r1 等于 0,说明 r0 和 r1 相等,也就是源地址和目的地址是一样的,那肯定就不需要拷贝了!执行 relocate_done 函数。
beq 表示如果subs操作中相减为0(即某个标志位为0)则跳到对应的函数。
第 83 行, r2=__image_copy_end, r2 中保存拷贝之前的代码结束地址,__image_copy_end =0x8785dd54。
第 84 行,函数 copy_loop 完成代码拷贝工作! 从 r1,也就是__image_copy_start 开始,读取 uboot 代码保存到 r10 和 r11 中,一次就只拷贝这 2 个 32 位的数据。拷贝完成以后 r1 的值会
更新,保存下一个要拷贝的数据地址。
第 87 行,将 r10 和 r11 的数据写到 r0 开始的地方,也就是目的地址。写完以后 r0 的值会更新,更新为下一个要写入的数据地址。
第 88 行,比较 r1 是否和 r2 相等,也就是检查是否拷贝完成,如果不相等的话说明没有拷贝完成, 没有拷贝完成的话就跳转到 copy_loop 接着拷贝,直至拷贝完成。
第二部分,第 94 行~109 行是重定位.rel.dyn 段,除了拷贝代码,还要保证uboot继续运行。 .rel.dyn 段是存放.text 段中需要重定位地址的集合。重定位就是 uboot 将自身拷贝到 DRAM 的另一个地放去继续运行(DRAM 的高地址处)。
第 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 值,相当于 0X87804198+0X18747000=0X9FF4B198。
第 104,读取重定位后 Label 所保存的变量地址,此时这个变量地址还是重定位前的(相当于 rel_a 重定位前的地址 0X8785DA50),将得到的值放到 r1 寄存器中。
第 105 行 , r1+r4 即 可 得 到 重 定 位 后 的 变 量 地 址 , 相 当 于 rel_a 重 定 位 后 的0X8785DA50+0X18747000=0X9FFA4A50。
第 106 行,重定位后的变量地址写入到重定位后的 Label 中,相等于设置地址 0X9FF4B198处的值为 0X9FFA4A50。
第 108 行,比较 r2 和 r3,查看.rel.dyn 段重定位是否完成。
第 109 行,如果 r2 和 r3 不相等,说明.rel.dyn 重定位还未完成,因此跳到 fixloop 继续重定位.rel.dyn 段。
我们知道,一个可执行的 bin 文件,其链接地址和运行地址要相等,也就是链接到哪个地址,在运行之前就要拷贝到哪个地址去。uboot 对于重定位后链接地址和运行地址不一致的解决方法就是采用位置无关码。
relocate_vectors 函数详解
函数 relocate_vectors 用于重定位向量表,此函数定义在文件 relocate.S 中:
27 ENTRY(relocate_vectors)
28
29 #ifdef CONFIG_CPU_V7M
30 /*
31 * On ARMv7-M we only have to write the new vector address
32 * to VTOR register.
33 */
34 ldr r0, [r9, #GD_RELOCADDR] /* r0 = gd->relocaddr */
35 ldr r1, =V7M_SCB_BASE
36 str r0, [r1, V7M_SCB_VTOR]
37 #else
38 #ifdef CONFIG_HAS_VBAR
39 /*
40 * If the ARM processor has the security extensions,
41 * use VBAR to relocate the exception vectors.
42 */
43 ldr r0, [r9, #GD_RELOCADDR] /* r0 = gd->relocaddr */
44 mcr p15, 0, r0, c12, c0, 0 /* Set VBAR */
45 #else
46 /*
47 * Copy the relocated exception vectors to the
48 * correct address
49 * CP15 c1 V bit gives us the location of the vectors:
50 * 0x00000000 or 0xFFFF0000.
51 */
52 ldr r0, [r9, #GD_RELOCADDR] /* r0 = gd->relocaddr */
53 mrc p15, 0, r2, c1, c0, 0 /* V bit (bit[13]) in CP15 c1 */
54 ands r2, r2, #(1 << 13)
55 ldreq r1, =0x00000000 /* If V=0 */
56 ldrne r1, =0xFFFF0000 /* If V=1 */
57 ldmia r0!, {r2-r8,r10}
58 stmia r1!, {r2-r8,r10}
59 ldmia r0!, {r2-r8,r10}
60 stmia r1!, {r2-r8,r10}
61 #endif
62 #endif
63 bx lr
64
65 ENDPROC(relocate_vectors)
第 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 中,设置向量表偏移。
就是将重定位后 uboot 的首地址作为新的向量表首地址提交给寄存器 VBAR。
board_init_r 函数详解
board_init_r 函数也会调用函数来初始化一些外设和 gd 的成员变量。 因为 board_init_f 函数并没有初始化所有的外设,还需要board_init_r 函数做一些后续工作。
991 void board_init_r(gd_t *new_gd, ulong dest_addr)
992 {
993 #ifdef CONFIG_NEEDS_MANUAL_RELOC
994 int i;
995 #endif
996
997 #ifdef CONFIG_AVR32
998 mmu_init_r(dest_addr);
999 #endif
1000
1001 #if !defined(CONFIG_X86) && !defined(CONFIG_ARM) && !defined(CONFIG_ARM64)
1002 gd = new_gd;
1003 #endif
1004
1005 #ifdef CONFIG_NEEDS_MANUAL_RELOC
1006 for (i = 0; i < ARRAY_SIZE(init_sequence_r); i++)
1007 init_sequence_r[i] += gd->reloc_off;
1008 #endif
1009
1010 if (initcall_run_list(init_sequence_r))
1011 hang();
1012
1013 /* NOTREACHED - run_main_loop() does not return */
1014 hang();
1015 }
第 1010 行调用 initcall_run_list 函数来执行初始化序列数组init_sequence_r, init_sequence_r 是一个函数集合(猜测和init_sequence_f一样,init_fnc_t是一种函数指针), init_sequence_r 也定义在文件 common/board_r.c 中:
以上函数执行board_init_f 函数未完成的一些外设和gd成员变量的初始化工作。
run_main_loop 函数详解
run_main_loop 函 数 来 完 成 自动启动 Linux 内核。run_main_loop 函 数 定 义 在 文 件common/board_r.c 中,函数内容如下:
753 static int run_main_loop(void)
754 {
755 #ifdef CONFIG_SANDBOX
756 sandbox_main_loop_init();
757 #endif
758 /* main_loop() can return to retry autoboot, if so just run it again */
759 for (;;)
760 main_loop();
761 return 0;
762 }
main_loop 函数定义在文件 common/main.c 里面:
43 /* We come here after U-Boot is initialised and ready to process commands */
44 void main_loop(void)
45 {
46 const char *s;
47
48 bootstage_mark_name(BOOTSTAGE_ID_MAIN_LOOP, "main_loop");
49
50 #ifndef CONFIG_SYS_GENERIC_BOARD
51 puts("Warning: Your board does not use generic board. Please read\n");
52 puts("doc/README.generic-board and take action. Boards not\n");
53 puts("upgraded by the late 2014 may break or be removed.\n");
54 #endif
55
56 #ifdef CONFIG_VERSION_VARIABLE
57 setenv("ver", version_string); /* set version variable */
58 #endif /* CONFIG_VERSION_VARIABLE */
59
60 cli_init();
61
62 run_preboot_environment_command();
63
64 #if defined(CONFIG_UPDATE_TFTP)
65 update_tftp(0UL, NULL, NULL);
66 #endif /* CONFIG_UPDATE_TFTP */
67
68 s = bootdelay_process();
69 if (cli_process_fdt(&s))
70 cli_secure_boot_cmd(s);
71
72 autoboot_command(s);
73
74 cli_loop();
75 }
第 48 行,调用 bootstage_mark_name 函数,打印出启动进度。
第 57 行,如果定义了宏 CONFIG_VERSION_VARIABLE 的话就会执行函数 setenv,设置换将变量 ver 的值为 version_string,也就是设置版本号环境变量。
第 60 行, cli_init 函数,跟命令初始化有关,初始化 hush shell 相关的变量。
第 62 行, run_preboot_environment_command 函数,获取环境变量 perboot 的内容, perboot是一些预启动命令,一般不使用这个环境变量。
第 68 行, bootdelay_process 函数,此函数会读取环境变量 bootdelay 和 bootcmd 的内容,然后将 bootdelay 的值赋值给全局变量 stored_bootdelay,返回值为环境变量 bootcmd 的值。
第 69 行,如果定义了 CONFIG_OF_CONTROL 的话函数cli_process_fdt 就会实现,如果没有定义 CONFIG_OF_CONTROL 的话函数 cli_process_fdt 直接返回一个 false。在本 uboot 中没有定义 CONFIG_OF_CONTROL,因此 cli_process_fdt 函数返回值为 false。
第 72 行, autoboot_command 函数,此函数就是检查倒计时是否结束?倒计时结束之前有没有被打断?
1 void autoboot_command(const char *s)
2 {
3 if (stored_bootdelay != -1 && s && !abortboot(stored_bootdelay)) {
4 run_command_list(s, -1, 0);
5 }
6 }
重点!!! 倒计时时候是否被打断:
abort=0,没被打断,执行函数run_command_list,执行参数 s (环境变量 bootcmd)指定的一系列命令,启动内核;
abort=1,被打断了, run_command_list函数就不会执行,相当于 autoboot_command 是个空函数,回去执行cli_loop 函数。
cli_loop 函数详解
cli_loop 函数是 uboot 的命令行处理函数,此函数定义在文件common/cli.c 中:
202 void cli_loop(void)
203 {
204 #ifdef CONFIG_SYS_HUSH_PARSER
205 parse_file_outer();
206 /* This point is never reached */
207 for (;;);
208 #else
209 cli_simple_loop();
210 #endif /*CONFIG_SYS_HUSH_PARSER*/
211 }
宏 CONFIG_SYS_HUSH_PARSER 有定义。
第 205 行调用函数 parse_file_outer。
第 207 行是个死循环,永远不会执行到这里。
函数 parse_file_outer 定义在文件 common/cli_hush.c 中
1 int parse_file_outer(void)
2 {
3 int rcode;
4 struct in_str input;
5
6 setup_file_in_str(&input);
7 rcode = parse_stream_outer(&input, FLAG_PARSE_SEMICOLON);
8 return rcode;
9 }
1 static int parse_stream_outer(struct in_str *inp, int flag)
2 {
3 struct p_context ctx;
4 o_string temp=NULL_O_STRING;
5 int rcode;
6 int code = 1;
7 do {
8 ......
9 rcode = parse_stream(&temp, &ctx, inp,
10 flag & FLAG_CONT_ON_NEWLINE ? -1 : '\n');
11 ......
12 if (rcode != 1 && ctx.old_flag == 0) {
13 ......
14 run_list(ctx.list_head);
15 ......
16 } else {
17 ......
18 }
19 b_free(&temp);
20 /* loop on syntax errors, return on EOF */
21 } while (rcode != -1 && !(flag & FLAG_EXIT_FROM_LOOP) &&
22 (inp->peek != static_peek || b_peek(inp)));
23 return 0;
24 }
第一部分:
第 3 行调用函数 setup_file_in_str 初始化变量 input 的成员变量。
第 4 行调用函数 parse_stream_outer,这个函数就是 hush shell 的命令解释器,负责接收命令行输入,然后解析并执行相应的命令。
第二部分:
第 7~21 行中的 do-while 循环就是处理输入命令的。
第 9 行调用函数 parse_stream 进行命令解析。
第 14 行调用 run_list 函数来执行解析出来的命令。
函数 run_list 会经过一系列的函数调用,最终通过调用 cmd_process 函数来处理命令。
cmd_process 函数详解
C 语言中的 “##”连接符 ,
"##"表示用 _name 传递过来的值来替换。“##_name”就是用_name 的值来替换,和宏定义类似。
“ # ” 表示将 _name 传递过来的值字符串化。
uboot使用宏 U_BOOT_CMD来定义命令,宏 U_BOOT_CMD 定义在文件 include/command.h 中,宏函数定义的比较复杂。
#define U_BOOT_CMD(_name, _maxargs, _rep, _cmd, _usage, _help) \
U_BOOT_CMD_COMPLETE(_name, _maxargs, _rep, _cmd, _usage, _help,NULL)
#define U_BOOT_CMD_COMPLETE(_name, _maxargs, _rep, _cmd, _usage, _help,_comp) \
ll_entry_declare(cmd_tbl_t, _name, cmd) = \
U_BOOT_CMD_MKENT_COMPLETE(_name, _maxargs, _rep, _cmd, \
_usage, _help, _comp);
#define ll_entry_declare(_type, _name, _list) \
_type _u_boot_list_2_##_list##_2_##_name __aligned(4) \
__attribute__((unused, \
section(".u_boot_list_2_"#_list"_2_"#_name)))
#define U_BOOT_CMD_MKENT_COMPLETE(_name, _maxargs, _rep, _cmd, _usage, _help, _comp) \
{ #_name, _maxargs, _rep, _cmd, _usage, \
_CMD_HELP(_help) _CMD_COMPLETE(_comp) }
........(每个宏函数都扩展了很多,不列举了)
以命令 dhcp 为例:
U_BOOT_CMD(
dhcp, 3, 1, do_dhcp,
"boot image via network using DHCP/TFTP protocol",
"[loadAddress] [[hostIPaddr:]bootfilename]"
);
宏函数一步步展开后:
1 cmd_tbl_t _u_boot_list_2_cmd_2_dhcp __aligned(4) \
2 __attribute__((unused,section(.u_boot_list_2_cmd_2_dhcp))) \
3 { "dhcp", 3, 1, do_dhcp, \
4 "boot image via network using DHCP/TFTP protocol", \
5 "[loadAddress] [[hostIPaddr:]bootfilename]",\
6 NULL}
第 1 行定义了一个 cmd_tbl_t 类型的变量,变量名为_u_boot_list_2_cmd_2_dhcp,此变量 4字节对齐。
第 2 行 , 使 用 __ attribute __ 关 键 字 设 置 变 量_u_boot_list_2_cmd_2_dhcp 存储在.u_boot_list_2_cmd_2_dhcp 段中,设置变量_u_boot_list_2_cmd_2_dhcp 的存储位置。
第 3~6 行, cmd_tbl_t 是个结构体,因此第 3-6 行是初始化 cmd_tbl_t 这个结构体的各个成员变量。
定义了变量_u_boot_list_2_cmd_2_dhcp 的各个成员的值,最终执行的是 do_dhcp 这个函数。
总结一下,
uboot 中使用 U_BOOT_CMD 来定义一个命令,最终的目的就是为了定义一个cmd_tbl_t 类型的变量,并初始化这个变量的各个成员。 uboot 中的每个命令都存储在.u_boot_list段中,每个命令都有一个名为 do_xxx(xxx 为具体的命令名)的函数,这个 do_xxx 函数就是具体的命令处理函数。
cmd_process函数定义在文件 common/command.c 中:
cmd_process函数中调用了函数 find_cmd,返回值是cmd_tbl_t 结构体类型的指针,就是返回命令表中的命令。
118 cmd_tbl_t *find_cmd(const char *cmd)
119 {
120 cmd_tbl_t *start = ll_entry_start(cmd_tbl_t, cmd);
121 const int len = ll_entry_count(cmd_tbl_t, cmd);
122 return find_cmd_tbl(cmd, start, len);
123 }
参数 cmd 就是所查找的命令名字, uboot 中的命令表其实就是 cmd_tbl_t 结构体数组,通过函数 ll_entry_start 得到数组的第一个元素,也就是命令表起始地址。通过函数 ll_entry_count得到数组长度,也就是命令表的长度。最终通过函数 find_cmd_tbl 在命令表中找到所需的命令,每个命令都有一个 name 成员,所以将参数 cmd 与命令表中每个成员的 name 字段都对比一下,如果相等的话就说明找到了这个命令,找到以后就返回这个命令。
bootz 启动 Linux 内核过程
images 全局变量
不管是 bootz 还是 bootm 命令,在启动 Linux 内核的时候都会用到一个重要的全局变量:images, images 在文件 cmd/bootm.c 中有如下定义:
43 bootm_headers_t images; /* pointers to os/initrd/fdt images */
bootm_headers_t 是个 boot 头结构体,在文件include/image.h 中有定义。
304 typedef struct bootm_headers {
305 /*
306 * Legacy os image header, if it is a multi component image
307 * then boot_get_ramdisk() and get_fdt() will attempt to get
308 * data from second and third component accordingly.
309 */
310 image_header_t *legacy_hdr_os; /* image header pointer */
311 image_header_t legacy_hdr_os_copy; /* header copy */
312 ulong legacy_hdr_valid;
313
......
333
334 #ifndef USE_HOSTCC
335 image_info_t os; /* OS 镜像信息 */
336 ulong ep; /* OS 入口点 */
337
338 ulong rd_start, rd_end; /* ramdisk 开始和结束位置 */
339
340 char *ft_addr; /* 设备树地址 */
341 ulong ft_len; /* 设备树长度 */
342
343 ulong initrd_start; /* initrd 开始位置 */
344 ulong initrd_end; /* initrd 结束位置 */
345 ulong cmdline_start; /* cmdline 开始位置 */
346 ulong cmdline_end; /* cmdline 结束位置 */
347 bd_t *kbd;
348 #endif
349
350 int verify; /* getenv("verify")[0] != 'n' */
351
352 #define BOOTM_STATE_START (0x00000001)
353 #define BOOTM_STATE_FINDOS (0x00000002)
354 #define BOOTM_STATE_FINDOTHER (0x00000004)
355 #define BOOTM_STATE_LOADOS (0x00000008)
356 #define BOOTM_STATE_RAMDISK (0x00000010)
357 #define BOOTM_STATE_FDT (0x00000020)
358 #define BOOTM_STATE_OS_CMDLINE (0x00000040)
359 #define BOOTM_STATE_OS_BD_T (0x00000080)
360 #define BOOTM_STATE_OS_PREP (0x00000100)
361 #define BOOTM_STATE_OS_FAKE_GO (0x00000200)/*'Almost' run the OS*/
362 #define BOOTM_STATE_OS_GO (0x00000400)
363 int state;
364
365 #ifdef CONFIG_LMB
366 struct lmb lmb; /* 内存管理相关,不深入研究 */
367 #endif
368 } bootm_headers_t;
第 335 行的 os 成员变量是 image_info_t 类型的,为系统镜像信息。
结构体 image_info_t,也就是系统镜像信息结构体,此结构体在文件
include/image.h 中的定义如下:
292 typedef struct image_info {
293 ulong start, end; /* blob 开始和结束位置*/
294 ulong image_start, image_len; /* 镜像起始地址(包括 blob)和长度 */
295 ulong load; /* 系统镜像加载地址*/
296 uint8_t comp, type, os; /* 镜像压缩、类型, OS 类型 */
297 uint8_t arch; /* CPU 架构 */
298 } image_info_t;
第 352~362 行这 11 个宏定义表示 BOOT 的不同阶段。
全局变量 images 会在 bootz 命令的执行中频繁使用到,相当于 Linux 内核启动的“灵魂”。
do_bootz 函数
bootz 命令的执行函数为 do_bootz,在文件 cmd/bootm.c 中有如下定义:
622 int do_bootz(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[])
623 {
624 int ret;
625
626 /* Consume 'bootz' */
627 argc--; argv++;
628
629 if (bootz_start(cmdtp, flag, argc, argv, &images))
630 return 1;
631
632 /*
633 * We are doing the BOOTM_STATE_LOADOS state ourselves, so must
634 * disable interrupts ourselves
635 */
636 bootm_disable_interrupts();
637
638 images.os.os = IH_OS_LINUX;
639 ret = do_bootm_states(cmdtp, flag, argc, argv,
640 BOOTM_STATE_OS_PREP | BOOTM_STATE_OS_FAKE_GO |
641 BOOTM_STATE_OS_GO,
642 &images, 1);
643
644 return ret;
645 }
第 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_start 函数
bootz_start 主要用于初始化 images 的相关成员变量,定义在文件 cmd/bootm.c 中:
578 static int bootz_start(cmd_tbl_t *cmdtp, int flag, int argc,
579 char * const argv[], bootm_headers_t *images)
580 {
581 int ret;
582 ulong zi_start, zi_end;
583
584 ret = do_bootm_states(cmdtp, flag, argc, argv,
585 BOOTM_STATE_START, images, 1);
586
587 /* Setup Linux kernel zImage entry point */
588 if (!argc) {
589 images->ep = load_addr;
590 debug("* kernel: default image load address = 0x%08lx\n",
591 load_addr);
592 } else {
593 images->ep = simple_strtoul(argv[0], NULL, 16);
594 debug("* kernel: cmdline image address = 0x%08lx\n",
595 images->ep);
596 }
597
598 ret = bootz_setup(images->ep, &zi_start, &zi_end);
599 if (ret != 0)
600 return 1;
601
602 lmb_reserve(&images->lmb, images->ep, zi_end - zi_start);
603
604 /*
605 * Handle the BOOTM_STATE_FINDOTHER state ourselves as we do not
606 * have a header that provide this informaiton.
607 */
608 if (bootm_find_images(flag, argc, argv))
609 return 1;
610
......
619 return 0;
620 }
第 584 行,调用函数 do_bootm_states,执行 BOOTM_STATE_START 阶段,do_bootm_states 会调用 bootm_os_get_boot_func 来查找对应系统的启动函数。
第 593 行,设置 images 的 ep 成员变量,也就是系统镜像的入口点,使用 bootz 命令启动系统的时候就会设置系统在 DRAM 中的存储位置,这个存储位置就是系统镜像的入口点,因此 images->ep=0X80800000。
第 598 行,调用 bootz_setup 函数,此函数会判断当前的系统镜像文件是否为 Linux 的像文件,并且会打印出镜像相关信息。
第 608 行,调用函数 bootm_find_images 查找 ramdisk 和设备树(dtb)文件,但是我们没有用到 ramdisk,因此此函数在这里仅仅用于查找设备树(dtb)文件。
bootz_setup 函数
会判断当前的系统镜像文件是否为 Linux 的像文件,并且会打印出镜像相关信息。,此函数定义在文件 arch/arm/lib/bootm.c 中:
370 #define LINUX_ARM_ZIMAGE_MAGIC 0x016f2818
371
372 int bootz_setup(ulong image, ulong *start, ulong *end)
373 {
374 struct zimage_header *zi;
375
376 zi = (struct zimage_header *)map_sysmem(image, 0);
377 if (zi->zi_magic != LINUX_ARM_ZIMAGE_MAGIC) {
378 puts("Bad Linux ARM zImage magic!\n");
379 return 1;
380 }
381
382 *start = zi->zi_start;
383 *end = zi->zi_end;
384
385 printf("Kernel image @ %#08lx [ %#08lx - %#08lx ]\n", image,
386 *start, *end);
387
388 return 0;
389 }
第 370 行,宏 LINUX_ARM_ZIMAGE_MAGIC 就是 ARM Linux 系统魔术数。
第 376 行,从传递进来的参数 image(也就是系统镜像首地址)中获取 zimage 头。 zImage 头结构体为 zimage_header。
第 377~380 行,判断 image 是否为 ARM 的 Linux 系统镜像,如果不是的话就直接返回,并且打印出“Bad Linux ARM zImage magic!”
第 382、 383 行初始化函数 bootz_setup 的参数 start 和 end。
第 385 行,打印启动信息。
函数 bootm_find_images
查找 ramdisk 和设备树(dtb)文件,此函数定义在文件 common/bootm.c 中:
225 int bootm_find_images(int flag, int argc, char * const argv[])
226 {
227 int ret;
228
229 /* find ramdisk */
230 ret = boot_get_ramdisk(argc, argv, &images, IH_INITRD_ARCH,
231 &images.rd_start, &images.rd_end);
232 if (ret) {
233 puts("Ramdisk image is corrupt or invalid\n");
234 return 1;
235 }
236
237 #if defined(CONFIG_OF_LIBFDT)
238 /* find flattened device tree */
239 ret = boot_get_fdt(flag, argc, argv, IH_ARCH_DEFAULT, &images,
240 &images.ft_addr, &images.ft_len);
241 if (ret) {
242 puts("Could not find a valid device tree\n");
243 return 1;
244 }
245 set_working_fdt_addr((ulong)images.ft_addr);
246 #endif
......
258 return 0;
259 }
第 230~235 行是跟查找 ramdisk,但是我们没有用到 ramdisk,因此这部分代码不用管。
第 237~244 行是查找设备树(dtb)文件,找到以后就将设备树的起始地址和长度分别写到images 的 ft_addr 和 ft_len 成员变量中。我们使用 bootz 启动 Linux 的时候已经指明了设备树在DRAM 中的存储地址,因此 images.ft_addr=0X83000000,长度根据具体的设备树文件而定,比如我现在使用的设备树文件长度为 0X8C81,因此 images.ft_len=0X8C81。
do_bootm_states 函数
函数 do_bootm_states 根据不同的 BOOT 状态执行不同的代码段,函 数 定 义 在 文件common/bootm.c 中,精简后:
do_bootz 函数中会用到 BOOTM_STATE_OS_PREP 、BOOTM_STATE_OS_FAKE_GO 和BOOTM_STATE_OS_GO 这三个 BOOT 状态;
bootz_start 函数中会用到 BOOTM_STATE_START这个 BOOT 状态。
do_bootm_linux 函数
经过前面的分析,我们知道了 do_bootm_linux 就是最终启动 Linux 内核的函数,此函数定义在文件 arch/arm/lib/bootm.c
339 int do_bootm_linux(int flag, int argc, char * const argv[],
340 bootm_headers_t *images)
341 {
342 /* No need for those on ARM */
343 if (flag & BOOTM_STATE_OS_BD_T || flag & BOOTM_STATE_OS_CMDLINE)
344 return -1;
345
346 if (flag & BOOTM_STATE_OS_PREP) {
347 boot_prep_linux(images);
348 return 0;
349 }
350
351 if (flag & (BOOTM_STATE_OS_GO | BOOTM_STATE_OS_FAKE_GO)) {
352 boot_jump_linux(images, flag);
353 return 0;
354 }
355
356 boot_prep_linux(images);
357 boot_jump_linux(images, flag);
358 return 0;
359 }
第 351行,如果参数 flag等于 BOOTM_STATE_OS_GO或者 BOOTM_STATE_OS_FAKE_GO的话就执行 boot_jump_linux 函数。boot_selected_os 函数在调用 do_bootm_linux 的时候会将 flag设置为 BOOTM_STATE_OS_GO。
第 352 行,执行函数 boot_jump_linux,此函数定义在文件 arch/arm/lib/bootm.c 中:
272 static void boot_jump_linux(bootm_headers_t *images, int flag)
273 {
274 #ifdef CONFIG_ARM64
......
292 #else
293 unsigned long machid = gd->bd->bi_arch_number;
294 char *s;
295 void (*kernel_entry)(int zero, int arch, uint params);
296 unsigned long r2;
297 int fake = (flag & BOOTM_STATE_OS_FAKE_GO);
298
299 kernel_entry = (void (*)(int, int, uint))images->ep;
300
301 s = getenv("machid");
302 if (s) {
303 if (strict_strtoul(s, 16, &machid) < 0) {
304 debug("strict_strtoul failed!\n");
305 return;
306 }
307 printf("Using machid 0x%lx from environment\n", machid);
308 }
309
310 debug("## Transferring control to Linux (at address %08lx)" \
311 "...\n", (ulong) kernel_entry);
312 bootstage_mark(BOOTSTAGE_ID_RUN_OS);
313 announce_and_cleanup(fake);
314
315 if (IMAGE_ENABLE_OF_LIBFDT && images->ft_len)
316 r2 = (unsigned long)images->ft_addr;
317 else
318 r2 = gd->bd->bi_boot_params;
319
......
328 kernel_entry(0, machid, r2);
329 }
330 #endif
331 }
第 274~292 行是 64 位 ARM 芯片对应的代码, Cortex-A7 是 32 位芯片,因此用不到。
第 293 行,变量 machid 保存机器 ID,如果不使用设备树的话这个机器 ID 会被传递给 Linux内核, Linux 内核会在自己的机器 ID 列表里面查找是否存在与 uboot 传递进来的 machid 匹配的项目,如果存在就说明 Linux 内核支持这个机器,那么 Linux 就会启动!如果使用设备树的话这个 machid 就无效了,设备树存有一个“兼容性”这个属性, Linux 内核会比较“兼容性”属性的值(字符串)来查看是否支持这个机器。
第 295 行,函数 kernel_entry,看名字“内核_进入”,说明此函数是进入 Linux 内核的,也就是最终的大 boos!!此函数有三个参数: zero, arch, params,第一个参数 zero 同样为 0;第二个参数为机器 ID; 第三个参数 ATAGS 或者设备树(DTB)首地址, ATAGS 是传统的方法,用于传递一些命令行信息啥的,如果使用设备树的话就要传递设备树(DTB)。
第 299 行,获取 kernel_entry 函数,函数 kernel_entry 并不是 uboot 定义的,而是 Linux 内核定义的, Linux 内核镜像文件的第一行代码就是函数 kernel_entry,而 images->ep 保存着 Linux内核镜像的起始地址,起始地址保存的正是 Linux 内核第一行代码!
第 313 行,调用函数 announce_and_cleanup 来打印一些信息并做一些清理工作,包括在启动 Linux 之前输出“Starting kernel …”信息。
第 315~318 行是设置寄存器 r2 的值?为什么要设置 r2 的值呢? Linux 内核一开始是汇编代码,因此函数 kernel_entry 就是个汇编函数。向汇编函数传递参数要使用 r0、 r1 和 r2(参数数量不超过 3 个的时候),所以 r2 寄存器就是函数 kernel_entry 的第三个参数。
第 316 行,如果使用设备树的话, r2 应该是设备树的起始地址,而设备树地址保存在 images的 ftd_addr 成员变量中。
第 317 行,如果不使用设备树的话, r2 应该是 uboot 传递给 Linux 的参数起始地址,也就是环境变量 bootargs 的值。
第 328 行,调用 kernel_entry 函数进入 Linux 内核,此行将一去不复返, uboot 的使命也就完成了,它可以安息了!
哦,天哪!!看完这一章,我也快安息了!!!
总结 bootz 命令
总结一下 bootz 命令的执行过程:
左忠凯安慰:在工作中我们基本不需要这么详细的去了解 uboot,半导体厂商提供给我们的 uboot 一般是可以直接用的,只要能跑起来,可以使用就可以了。但是作为学习,我们是必须去详细的了解一下 uboot 的启动流程,否则如果在工作中遇到问题我们连解决的方法都没有,都不知道该从哪里看起。但是呢,如果第一次就想弄懂 uboot 的整个启动流程还是有点困难的,所以如果没有看懂的话,不要紧!不要气馁,大多数人第一次看 uboot 启动流程基本都有各种各样的问题。
不要气馁!千里之行始于足下,所有你羡慕的人都曾经痛苦过,挫败过。脚踏实地,一步一个脚印,一点一滴的积累,最终你也会成为你所羡慕的人。在嵌入式 Linux 这条道路上,有众多的学习者陪着你,大家相互搀扶,终能踏出一条康庄大道,祝所有的同学终有所获!