嵌入式Linux学习之u-boot引导操作系统启动过程的初窥探

很久之前最初学习Linux的时候,其实是直接跳到Linux系统层次去的,也就是直接从驱动开发开始学起,并没有关注过UBOOT是如何启动Linux系统的以及Linux系统启动后的流程,重点就是掌握linux下各种驱动框架以及驱动相关系统API的使用。
然而,最近有个数据网关采集的项目,并没有购买现成的核心板(TI的平台),板上的器件与原厂不一致,因此导致芯片原厂提供的u-boot虽然能够运行,但相关的驱动是有问题的,比如LCD屏幕驱动,网口PHY芯片等。最大的问题就是网口无法使用,这样导致后期的系统移植和根文件系统的构建无法使用NFS(网络文件系统),而烧写固件到EMMC/NAND是十分耗时间的,造成调试困难。之前对U-BOOT的了解基本是一头雾水,不过后来参考正点原子的UBOOT分析,大概流程是清楚了,因此写下来记录心得。

何为u-boot?

相信用过windows系统的同学对BIOS一定不陌生,没错,ARM+Linux下的U-BOOT作用就类似于BIOS, Linux系统要启动就必须需要一个bootloader程序,也就是说芯片上电以后,会在操作系统启动之前,先运行一段bootloader程序。

这段bootloader程序会先初始化DDR控制器(即内存控制器),网卡等外设,在拷贝内核之前保证内存可读可写,然后会将Linux内核(编译后叫做zimage或uimage)从FLASH存储介质(NAND,NOR FLASH,SD卡,EMMC等)拷贝到DDR中,并将设备树在DDR中的地址以及其它一些环境变量参数传递给Linux内核,最后uboot跳转至Linux内核所在的DDR地址处执行,从此以后Linux就这样启动了。从此以后内核负责接管整个系统,永远不会再返回到U-BOOT了。

u-boot启动流程窥探

1 链接脚本u-boot.lds(引用自正点原子相关资料)

PS:GCC 编译器的编译流程是:预处理、编译、汇编和链接。预处理就是展开所有的头文件、替换程序中的宏、解析条件编译并添加到文件中。编译是将经过预编译处理的代码编译成汇编代码,也就是我们常说的程序编译。汇编就是将汇编语言文件编译成二进制目标文件。链接就是将汇编出来的多个二进制目标问价链接在一起,形成最终的可执行文件。
猜测:根据上面描述,gcc编译器编译C系语言貌似也是先编译为汇编,然后使用汇编器编译为最终的二进制码

要分析uboot的启动流程,首先要找到“入口”,找到第一行程序在哪里。程序的链接是由链接脚本来决定的,所以通过链接脚本可以找到程序的入口。如果没有编译过uboot的话链接脚本为arch/arm/cpu/u-boot.lds。但是这个不是最终使用的链接脚本,最终的链接脚本是在这个链接脚本的基础上生成的。编译一下uboot,编译完成以后就会在uboot根目录下生成u-boot.lds 文件,如图32.1.1所示:
嵌入式Linux学习之u-boot引导操作系统启动过程的初窥探_第1张图片

打开 u-boot.lds,内容如下:


OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
OUTPUT_ARCH(arm)  
ENTRY(_start)  
SECTIONS
{  . = 0x00000000; 7 . = ALIGN(4);
 .text :  {
 *(.__image_copy_start)
 *(.vectors)
 arch/arm/cpu/armv7/start.o (.text*)
 *(.text*)
 }
 . = ALIGN(4);
 .rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) }
 . = ALIGN(4);
 .data : {
 *(.data*)
 }
 . = ALIGN(4);
 . = .;
 . = ALIGN(4);
 .u_boot_list : {
 KEEP(*(SORT(.u_boot_list*)));
 }
 . = ALIGN(4);
 
 .image_copy_end :
 {
 *(.__image_copy_end)
 }
 .rel_dyn_start :
 {
 
 //省略部分代码
 .
 .
 .
 
 .gnu : { *(.gnu*) }
 .ARM.exidx : { *(.ARM.exidx*) }
 .gnu.linkonce.armexidx : { *(.gnu.linkonce.armexidx.*) }
 }

第 3 行为代码当前入口点:_start,_start 是个汇编代码,在文件 arch/arm/lib/vectors.S 中有定义,如图 32.1.2
所示:

.globl _start
.section ".vectors","ax"
_start:
 #ifdef CONFIG_SYS_DV_NOR_BOOT_CFG
   .word  CONFIG_SYS_DV_NOR_BOOT_CFG
 #endif
 b   reset
 ldr pc, _undefined_instruction
 ldr pc, _software_interrupt
 ldr pc, _prefetch_abort
 ldr pc, _data_abot
 ldr pc, _not_used
 ldr pc, _irq
 ldr pc, _fiq

从图 32.1.1 可以看出,_start 后面就是中断向量表,从图中的“.section “.vectors”, "ax”可以
得到,此代码存放在.vectors 段里面。

打开 u-boot.map,找到__image_copy_start:嵌入式Linux学习之u-boot引导操作系统启动过程的初窥探_第2张图片
打开 u-boot.map,如下:
嵌入式Linux学习之u-boot引导操作系统启动过程的初窥探_第3张图片

u-boot.map 是 uboot 的映射文件,可以从此文件看到某个函数链接到了哪个地址,
从图 932 行可以看到__image_copy_start 为 0X87800000,而.text 的起始地址也是0X87800000。
从图 uboot的地址映射图 可以看出,vectors 段的起始地址也是 0X87800000,vectors 段保存中断向量表中断向量表在ARM架构下通常会放在代码段的开头,说明整个 uboot 的起始地址就是 0X87800000。
这里注意,ARM架构芯片有的SOC内部是有出厂内置的IROM的,比如正点原子官方使用的NXP I.MX6ULL 芯片,内部的出厂代码会将UBOOT的二进制可执行代码从SD卡,EMMC或者NAND FLASH搬运进DDR内存,当然,这些芯片使用DCD数据来初始化DDR内存,因此在UBOOT中就不需要初始化DDR内存了如果没有的话,就需要在启动文件中自己用汇编代码实现DDR控制器的初始化,使内存可读可写,这样才能为C语言的运行准备好环境。

后面的具体流程比较复杂,因此简述如下:

上电以后执行第一条指令,就是从复位中断服务函数(汇编函数)开始,内容如下:


.globl reset
.globl save_boot_params_ret

 reset:
   b save_boot_params
 /* 

——————————————————————————————————————————
PS小知识:汇编 B 指令介绍
B 指令,可用小写b,ARM处理器的跳转指令。
这是最简单的跳转指令,B 指令会将 PC 寄存器的值设置为跳转目标地址, 一旦执行 B 指令,ARM 处理器就会立即跳转到指定的目标地址。如果要调用的函数不会再返回到原来的执行处,那就可以用 B 指令,如下示例:

_start: 
 ldr sp,=0X80200000   //设置栈指针
 b main              //跳转到 main 函数

上述代码就是典型的在汇编中初始化 C 运行环境,然后跳转到 C 文件的 main 函数中运行。上述代码只是初始化了 SP 指针,有些处理器还需要做其他的初始化,比如初始化 DDR 等等。因为跳转到 C 文件以后再也不会回到汇编了,所以使用了 B 指令来完成跳转。
——————————————————————————————————————

回到正题,b save_boot_params,意思就是跳转到save_boot_params这个函数,废话不多说,找到save_boot_params的定义,看它干了啥:

ENTRY(save_boot_params)
 b save_boot_params_ret   //back to my caller

save_boot_params这个函数,又跳转到了save_boot_params_ret函数,继续找到
save_boot_params_ret函数的定义:

save_boot_params_ret:
/*
 * disable interrupts (FIQ and IRQ), also set the cpu to SVC32 
 * mode, except if in HYP mode already
 */
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
50
51 /*
 * Setup vector:
 * (OMAP4 spl TEXT_BASE is not 32 byte aligned.
 * Continue to use ROM code vector only in OMAP4 spl)
 */
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

67 /* the mask ROM code should have PLL and others stable */
68 #ifndef CONFIG_SKIP_LOWLEVEL_INIT
69 bl cpu_init_cp15
70 bl cpu_init_crit
71 #endif
72
73 bl _main

第 43 行,读取寄存器 cpsr 中的值,并保存到 r0 寄存器中。

第 44 行,将寄存器 r0 中的值与 0X1F 进行与运算,结果保存到 r1 寄存器中,目的就是提取 cpsr 的 bit0~bit4 这 5 位,这 5 位为 M4 M3 M2 M1 M0,M[4:0]这五位用来设置处理器的工作模式,如表 32.2.1.1 所示:
嵌入式Linux学习之u-boot引导操作系统启动过程的初窥探_第4张图片

第 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,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 行就是设置向量表重定位的。

第69行,调用函数 bl cpu_init_cp15,cpu_init_crit 和_main。cpu_init_cp15 用来设置 CP15 相关的内容,比如关闭 MMU 啥的。比较长和繁琐,就不贴出代码了,感兴趣的可以搜一下ARM的CP15协处理器内容。

cpu_init_crit函数里面啥也没有,就是调用lowlevel_init 函数。内容如下:

14 #include <asm-offsets.h>
15 #include <config.h>
16 #include <linux/linkage.h>
17
18 ENTRY(lowlevel_init)
19 /*
20 * Setup a temporary stack. Global data is not available yet.
21 */
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
27 /*
28 * Set up global data for boards that still need it. This will be
29 * removed soon.
30 */
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
39 push {ip, lr}
//省略掉英文注释
57 bl s_init
58 pop {ip, pc}
59 ENDPROC(lowlevel_init) 

第 22 行设置 sp 指向 CONFIG_SYS_INIT_SP_ADDR,其实就是 芯片 内 部 ocram (这个小内存一般用来运行IROM)的首地址和大小。经过各种计算偏移量最终CONFIG_SYS_INIT_SP_ADDR的值为:
CONFIG_SYS_INIT_SP_ADDR = 0X0091FF00。

第 23 行对 sp 指针做 8 字节对齐处理!

第 34 行,sp 指针减去 GD_SIZE,GD_SIZE是个宏定义,值为248。

第 35 行,对 sp 做 8 字节对齐,此时 sp 的地址为 0X0091FF00-248=0X0091FE08

第 36 行将 sp 地址保存在 r9 寄存器中。
第 42 行将 ip 和 lr 压栈
第 57 行调用函数 s_init,又来了一个函数。此s_init函数主要用来判断当前处理器类型。
第 58 行将第 36 行入栈的 ip 和 lr 进行出栈,并将 lr 赋给 pc。

大概以上的调用路径如下:
嵌入式Linux学习之u-boot引导操作系统启动过程的初窥探_第5张图片
最后,就是调用_main了,同样,由于_main内容很多,并且是个汇编代码,因此不贴出,大致做了以下事情(忽略一些细节):
1, 调用board_init_f 做了初始化 DDR,定时器,完成代码拷贝等等工作。然后初始化 gd的各个成员变量,uboot 会将自己重定位到 DRAM 最后面的地址区域,也就是将自己拷贝到 DRAM 最后面的内存区域中。这么做的目的是给 Linux 腾出空间,防止 Linux
kernel 覆盖掉 uboot,将 DRAM 前面的区域完整的空出来。在拷贝之前肯定要给 uboot 各部分分配好内存位置和大小,比如 gd 应该存放到哪个位置,malloc 内存池应该存放到哪个位置等等。这些信息都保存在 gd 的成员变量中,因此要对 gd 的这些成员变量做初始化。最终形成一个完整的内存“分配图”,在后面重定位 uboot 的时候就会用到这个内存“分配图”。

2,调用函数 relocate_code进行uboot 重定位,也就是把代码拷贝到新的地方去(现在的 uboot 存放的起始地址为 0X87800000,下面要将 uboot 拷贝到 DDR 最后面的地址空间出,将 0X87800000 开始的内存空出来。

3,调用函数 relocate_vectors,对中断向量表做重定位。
4,调用函数 board_init_r ,前面的board_init_f并没有完成全部的外设初始化,由此函数来完成后续工作,比如初始化网口,串口,USB,QSPI,FEC,EMMC,NAND FLASH等等。

这个就是_main 函数的运行流程,主要在_main 函数里面调用了 board_init_f、relocate_code、relocate_vectors 和 board_init_r 这 4 个函数。

board_init_r内部有个初始化序列函数集合,里面每一个函数都会执行,函数集内容如下:

1 init_fnc_t init_sequence_r[] = { 
2 initr_trace, 
3 initr_reloc, 
4 initr_caches, 
5 initr_reloc_global_data, 
6 initr_barrier, 
7 initr_malloc, 
8 initr_console_record, 
9 bootstage_relocate, 
10 initr_bootstage, 
11 board_init, /* Setup chipselects */ 
12 stdio_init_tables, 
13 initr_serial, 
14 initr_announce, 
15 INIT_FUNC_WATCHDOG_RESET
16 INIT_FUNC_WATCHDOG_RESET
17 INIT_FUNC_WATCHDOG_RESET
18 power_init_board, 
19 initr_flash, 
20 INIT_FUNC_WATCHDOG_RESET
21 initr_nand, 
22 initr_mmc, 
23 initr_env, 
24 INIT_FUNC_WATCHDOG_RESET
25 initr_secondary_cpu, 
26 INIT_FUNC_WATCHDOG_RESET
27 stdio_add_devices, 
28 initr_jumptable, 
29 console_init_r, /* fully init console as a device */ 
30 INIT_FUNC_WATCHDOG_RESET 
31 interrupt_init, 
32 initr_enable_interrupts, 
33 initr_ethaddr, 
34 board_late_init, 
35 INIT_FUNC_WATCHDOG_RESET 
36 INIT_FUNC_WATCHDOG_RESET
37 INIT_FUNC_WATCHDOG_RESET
38 initr_net, 
39 INIT_FUNC_WATCHDOG_RESET
40 run_main_loop, 
41 };

可以看到,里面充满了init开头的函数,多达几十个,这个就是整个系统板子上的几乎所有硬件设备的初始化函数,使这些设备参数正常,为接下来进入Linux系统做好准备。
最后,执行 run_main_loop函数,是个死循环,内容如下


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()函数,注意这个main_loop()函数里的这个地方:


44 void main_loop(void)
{
 .....//省略掉前面的代码

autoboot_command(s);
cli_loop();
}

autoboot_command(s)是用来倒计时自动启动linux内核的(如果没有在引导界面按下ENTER键的话,此函数自动通过bootz命令启动Linux内核,不会出现命令行界面)

大家注意到cli_loop()函数了吗,什么是CLI?命令行呀,这可不就是按下ENTER键后u-boot里的命令行交互界面。我们在 uboot 中输入各种命令,进行各种操作就
是 cli_loop 来处理的。

OK,到此,我们从按下电源键,如果倒计时到按下ENTER键出现U-BOOT的命令行界面的话,就是以上过程了,现在,我们可以启动Linux系统了,我们已经拥有命令行了,可以执行bootz来启动操作系统了。bootz命令格式如下:

bootz 80800000 - 83000000:
80800000为Linux内核在DRAM中的存储位置,这个位置就是系统镜像的入口点。 83000000则是设备树文件在在DRAM中的存储位置,Linux系统启动的时候会用到并
解析设备树文件。在此之前uboot也会向其中的设备树节点添加一些信息

bootz命令虽然用起来简单, bootz 80800000 - 83000000 这样的一个命令就可以启动Linux系统,但是其实现也是很复杂的,bootz命令执行过程如下:
嵌入式Linux学习之u-boot引导操作系统启动过程的初窥探_第6张图片
可以看到,又是一个大量的函数调用,我们只需要关注其中的boot_jump_linux()函数,贴出其中一句关键代码的地方:


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 }

重点关注 void (*kernel_entry)(int zero, int arch, uint params);这个函数指针类型, 有个函数指针类型的变量kernel_entry,在299行,被赋予了一个值,这个值是images->ep,这个images->ep是什么东西?那么,kernel_entry它指向了谁?

函数 kernel_entry 并不是 uboot 定义的,而是 Linux 内核定义的,Linux 内核镜像文件的第一行代码就是函数 kernel_entry,而 images->ep 保存着 Linux内核镜像的起始地址,即 bootz 80800000 - 83000000这个命令的第二个参数80800000,该起始地址保存的正是 Linux 内核第一行代码在DDR内存中的地址!

通过 kernel_entry 函数指针跳转进入 Linux 内核,这一下调用将一去不复返,此后永远不会再返回到uboot的代码区了,uboot 的使命也就完成了,它可以安息了!

后面,你所看到的机器,屏幕或者是串口终端输出的东西,就是Linux系统的杰作了,从现在起,它接管整个系统!系统就从这里,启动了!

以上汇编代码解释出处来自正点原子linux资料

你可能感兴趣的:(嵌入式Linux学习之u-boot引导操作系统启动过程的初窥探)