1. 链接脚本u-boot.lds
指定链接的首地址在哪里,哪一行代码是第一行。所以需要先编译。
打开该源码,可知u-boot的入口地址是_start;
可以搜索_start. 在文件 arch/arm/lib/vectors.S 中有定义:
如代码中定义的,这里面包含复位和中断向量表的起始地址;继续回到u-boot.lds:
.text是代码段,中间有个__image_copy_start,可以通过全局搜索在
可以看到,是0x87800000,.vectors也存在这里,它是中断向量表,和裸机中断时一样的;继续回到u-boot.lds:
对应的map为(复位和初始化CPU,就类似裸机开发必须要存放的两个,剩下的代码段就可以随意存放了):继续回到u-boot.lds:
镜像拷贝结束,它和image_copy_start是成对的,因为需要重定位,所以要拷贝copy,搜索一下它的位置:
由图中可以看到它的镜像结束地址,不同的编译可能最后的低吗段结束不一样,这样就得到代码的大小了,差不多300多kb继续回到u-boot.lds:
rel_dyn_start在map中的地址为:
(end就不找了),差不多30多k
__end段
再找找bss段
自此,我们每个段的地址都找到了,不同编译产生的地址是不一样的,除了开始的87800000
2. u-boot启动流程
刚才说到了,入口地址是_start,所以汇编从这里分析:
2.1 reset函数
上一张图是vector.S主要定义了一些中断向量表,我们找rest函数,是在start.s
由图中可以看到,reset是跳转函数,不用往下搜索了,跳着跳着还是回到下面的函数里了:
注:HYP模式是超级监视者模式,比超级管理员模式第一点,主要用来做一些虚拟化扩展
继续往下看:
如果没有定义这两个变量,就执行下面语句,#if后面都是注释的颜色,那肯定没有定义
这个寄存器如图所示:
综上所述:就是设置偏移为0,重定位向量表,再把中断向量表迁移到87800000
接下来有两个函数,一个看名字就知道是初始化cp15,比如关闭MMU啥的,这些都比较固定,就不分析了,
一个是cpu_init_crit,最后一个是main
cpu_init_crit是跳到lowlevel_init
2.2 lowlevel_init函数
函数 lowlevel_init 在文件 arch/arm/cpu/armv7/lowlevel_init.S 中定义
GENERATED_GBL_DATA_SIZE 256
总结:这个函数就是设置sp指针和R9寄存器,保存栈指针
接下来我们来看s_init
2.3 s_init函数
我们知道 lowlevel_init 函数后面会调用 s_init 函数,s_init 函数定义在文件arch/arm/cpu/armv7/mx6/soc.c
由图可知,相当于空函数
2.4 __main函数
上面分析可知,该进入main函数了_main 函数定义在文件 arch/arm/lib/crt0.S 中
我们再看board_init_f_alloc_reserve这个函数,此函数定义在文件common/init/board_init.c 中
gd_t 是个结构体,在 include/asm-generic/global_data.h 里面有定义,gd_t里面还有一个bd_t也是非常重要的函数
我们再回到89行crt0.S,board_init_f_init_reserve函数,
此时和上面对起来了
我们再回到crt0.S,
这里借助bd计算出新的gd位置,然后将其保存在r9里面,bd可以直接获得,而gd紧挨着bd,所以减去gd大小就是gd的新地址这里面有个问题,就是bd的大小可以直接获得
接下来可以重定位代码
在整个ddr中,从87800000开始,是uboot的代码,要将这部分代码移植到高地址,也就是最下面,这样把其他的代码考到87800000上就还是会运行,因为uboot去高地址了
here函数中:
重定位中断向量表;
调用函数 c_runtime_cpu_setup,此函数定义在文件 arch/arm/cpu/armv7/start.S
关闭ICache和DCache
这段代码清除bss段
由图可知,board_init_r函数要传递两个参数r0,r1
2.5 board_init_f函数
- 初始化一系列外设,比如串口、定时器,或者打印一些消息等。
- 初始化 gd 的各个成员变量,uboot 会将自己重定位到 DRAM 最后面的地址区域,也就是将自己拷贝到 DRAM 最后面的内存区域中。这么做的目的是给 Linux 腾出空间,防止 Linux kernel 覆盖掉 uboot,将 DRAM 前面的区域完整的空出来。在拷贝之前肯定要给 uboot 各部分分配好内存位置和大小,比如 gd 应该存放到哪个位置,malloc 内存池应该存放到哪个位置等等。这些信息都保存在 gd 的成员变量中,因此要对 gd 的这些成员变量做初始化。最终形成一个完整的内存“分配图”,在后面重定位 uboot 的时候就会用到这个内存“分配图”。
再看一下init_sequence_f
整理一下
/*****************去掉条件编译语句后的 init_sequence_f***************/
static init_fnc_t init_sequence_f[] = {
setup_mon_len, //__bss_end-_start是整个代码的长度, 镜像大小,大约600-700k,也是要拷贝的的代码大小
initf_malloc, // 里面设置了malloc大小0x400
initf_console_record, //因为uboot没有定义,返回0
arch_cpu_init, //初始化架构有关的工作,CPU级别
initf_dm, // 驱动初始化
arch_cpu_init_dm, //该函数未实现
mark_bootstage, /* need timer, go after init dm */
board_early_init_f, //板子一些早期初始化配置,初始化串口IO等
timer_init, /* initialize timer */
board_postclk_init, //设置vddsoc电压
get_clocks, //获取时钟,对于I.MX获取SD卡外设时钟
env_init, /* initialize environment */
init_baud_rate, /* initialze baudrate settings */
serial_init, /* serial communications setup */
console_init_f, /*设置 gd->have_console 为 1,打开控制台,初始化之前是把环境放在缓冲区里,这个为1时正好打印 */
display_options, /* 通过串口输出一些信息 */
// 需要开启debug,在configs/mx6ull_evk里面define
display_text_info, /* ,打印一些文本信息,如果开启 UBOOT 的 DEBUG 功能的话就
会输出 text_base、bss_start、bss_end */
print_cpuinfo, /* CPU信息 */
show_board_info, // 打印板子信息
INIT_FUNC_WATCHDOG_INIT //初始化看门狗
INIT_FUNC_WATCHDOG_RESET //复位看门狗
init_func_i2c, //初始化i2c
announce_dram_init, //输出字符串“DRAM”
/* TODO: unify all these dram functions? */
dram_init, /* 并不是初始化DRAM只是设置这个值为512M ,是通过读取SD卡的配置信息初始化dram*/
post_init_f, //用来完成测试
INIT_FUNC_WATCHDOG_RESET
testdram, //测试ddr,空函数
INIT_FUNC_WATCHDOG_RESET
INIT_FUNC_WATCHDOG_RESET
/*
* Now that we have DRAM mapped and working, we can
* relocate the code and continue running from DRAM.
*
* Reserve memory at end of RAM for (top down in that order):
* - area that won't get touched by U-Boot and Linux (optional)
* - kernel log buffer
* - protected RAM
* - LCD framebuffer
* - monitor code
* - board info struct
*/
setup_dest_addr, //设置gd->ram_size,gd->ram_top,gd->relocaddr这三个的值
//gd->ram_size = 0X20000000 //ram 大小为 0X20000000=512MB
//gd->ram_top = 0XA0000000//ram最高地址为 0X80000000+0X20000000=0XA0000000
//gd->relocaddr = 0XA0000000 //重定位后最高地址为 0XA0000000,拷贝的地址
reserve_round_4k, //gd->relocaddr四字节对齐,本来就4k对齐,所以不需要了
reserve_mmu, //留出mmu区域(内存映射单元),0x4000,所以要减去
//gd->arch.tlb_size= 0X4000 //MMU 的 TLB 表大小
//gd->arch.tlb_addr=0X9FFF0000 //MMU 的 TLB 表起始地址,64KB 对齐以后
//gd->relocaddr=0X9FFF0000 //relocaddr 地址
reserve_trace, //留出调试跟踪的内存,没有用到
reserve_uboot, //reserve_uboot, 留出重定位后的 uboot 所占用的内存区域,uboot 所占用大小由gd->mon_len 所指定,留出 uboot 的空间以后还要对 gd->relocaddr 做 4K 字节对齐,并且重新设置 gd->start_addr_sp
//gd->mon_len = 0XA8EF4
//gd->start_addr_sp = 0X9FF47000
//gd->relocaddr = 0X9FF47000, 从这开始定死了,把8780000的代码拷贝到这里,拷贝大小为0xA8EF4,此时定义的是relocaddr,接下来变化的就是start_addr_sp
reserve_malloc, //留出 malloc 区域,调整 gd->start_addr_sp 位置,malloc 区域由宏TOTAL_MALLOC_LEN 定义
//TOTAL_MALLOC_LEN=0X1002000
//gd->start_addr_sp=0X9EF45000 //0X9FF47000-16MB-8KB=0X9EF45000
reserve_board, //留出板子 bd 所占的内存区,bd 是结构体 bd_t,bd_t 大小为80 字节
//gd->start_addr_sp=0X9EF44FB0
//gd->bd=0X9EF44FB0
setup_machine, //setup_machine,设置机器 ID,linux 启动的时候会和这个机器 ID 匹配,如果匹配的话 linux 就会启动正常。但是!!I.MX6ULL 不用这种方式了,这是以前老版本的 uboot 和linux 使用的,新版本使用设备树了,因此此函数无效。
reserve_global_data,// 保留出 gd_t 的内存区域,gd_t 结构体大小为 248B
//gd->start_addr_sp=0X9EF44EB8 //0X9EF44FB0-248=0X9EF44EB8
//gd->new_gd=0X9EF44EB8
reserve_fdt, //留出设备树相关的内存区域,I.MX6ULL 的 uboot 没有用到
reserve_arch, //空函数
reserve_stacks,//留出栈空间,先对 gd->start_addr_sp 减去 16,然后做 16 字节对其。如果使能 IRQ 的话还要留出 IRQ 相应的内存,在本 uboot 中并没有使用到 IRQ
//gd->start_addr_sp=0X9EF44E90
setup_dram_config, //函数设置 dram 信息,就是设置 gd->bd->bi_dram[0].start 和gd->bd->bi_dram[0].size,后面会传递给 linux 内核,告诉 linux DRAM 的起始地址和大小
show_dram_config, //显示dram信息
display_new_sp, //显示新的 sp 位置,也就是 gd->start_addr_sp,不过要定义宏 DEBUG
INIT_FUNC_WATCHDOG_RESET//
reloc_fdt, //函数用于重定位 fdt,没有用到
setup_reloc, //设置 gd 的其他一些成员变量,供后面重定位的时候使用,并且将以前的 gd(R9) 拷贝到 gd->new_gd 处。需要使能 DEBUG 才能看到相应的信息输出,这里是在new——gd里有,r9此时还是内部ram里面的地址呢
NULL,
};
最后的内存分布图:
2.6 relocate_code函数
目前分析的uboot代码还没有拷贝到gd->relocaddr中,这个函数是拷贝用的
调用这个函数之前,参数r0=0x9ff47000
此函数定义在文件 arch/arm/lib/relocate.S 中
这里面有一个问题:
我原来的函数的地址(没拷贝之前)在那,有另一个函数会调用它,但是你把它拷贝走了再调用,就没了,全局变量引用也会出问题。uboot对于这个处理方法,就是位置无关方法,借助.rel.dyn段
如上图可知:
borad_init 调用 rel_test 函数,用到了 bl 指令,而 bl 指令时位置无关指令,bl 指令是相对寻址的(pc+offset),因此 uboot 中函数调用是与绝对位置无关的。
再来看一下函数 rel_test 对于全局变量 rel_a 的调用,第 2 行设置 r3 的值为 pc+12 地址处的值,因为ARM流水线的原因,pc寄存器的值为当前地址+8,因此pc = 0X87804184 + 8 = 0X8780418C, r3=0X8780418C+12=0X87804198,第 7 行就是0X87804198 这个地址,0X87804198 处的值为0X8785DA50。根据第 17 行可知,0X8785DA50 正是变量 rel_a 的地址,最终 r3=0X8785DA50。
并没有直接读取变量的地址,而是借助了另一个地址,间接读取,+8+12偏移地址,也就是说这个过程也没有用到绝对地址;
继续往下:
这个段的作用是,给需要偏移的加上label,判断是label之后就要偏移了
2.7 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 中,设置向量表偏移。
2.8 board_init_r函数
第 32.2.5 小节讲解了 board_init_f 函数,在此函数里面会调用一系列的函数来初始化一些外设和 gd 的成员变量。但是 board_init_f 并没有初始化所有的外设,还需要做一些后续工作,这些后续工作就是由函数 board_init_r 来完成的,board_init_r 函数定义在文件common/board_r.c
init_fnc_t init_sequence_r[] = {
initr_trace, //如果定义了宏 CONFIG_TRACE 的话就会调用函数 trace_init,初始化和调试跟踪有关的内容
initr_reloc, // 标记重定位完成
initr_caches, // 初始化cache,并使能cache
initr_reloc_global_data, // 初始化重定位后的gd相关成员变量
initr_barrier, // I.MX没用到
initr_malloc, // 初始化malloc区域
initr_console_record, // 初始化控制台的,也没用到
bootstage_relocate, // 启动状态重定位
initr_bootstage, // 初始化启动状态
board_init, /* Setup chipselects */ //板级初始化,包括 74XX 芯片,I2C、FEC、USB 和 QSPI 等。这里执行的是 mx6ull_alientek_emmc.c 文件中的 board_init 函数
stdio_init_tables, //stdio相关初始化
initr_serial, //初始化串口
initr_announce, // 与调试有关,通知已在ram运行
INIT_FUNC_WATCHDOG_RESET// 看门狗
INIT_FUNC_WATCHDOG_RESET//
INIT_FUNC_WATCHDOG_RESET//
power_init_board, // 初始化电源,没有用到
initr_flash, // flash初始化,如果有宏定义了才要初始化,没有定义就不需要了
INIT_FUNC_WATCHDOG_RESET//
initr_nand, // 初始化nand
initr_mmc, // 初始化emmc
initr_env, // 初始化环境
INIT_FUNC_WATCHDOG_RESET//
initr_secondary_cpu, // 初始化其他cpu,单核不需要
INIT_FUNC_WATCHDOG_RESET//
stdio_add_devices, // 输入输出设备初始化,如lcd
initr_jumptable, // 初始化跳表
console_init_r, /* fully init console as a device *///控 制 台 初 始 化 , 初 始 化 完 成 以 后 此 函 数 会 调 用stdio_print_current_devices 函数来打印出当前的控制台设备
INIT_FUNC_WATCHDOG_RESET //
interrupt_init, // 初始化中断
initr_enable_interrupts, // 使能中断
initr_ethaddr, // 初始化网络MAC
board_late_init, //board_late_init 函数,板子后续初始化,此函数定义在文件 mx6ull_alientek_emmc.c中,如果环境变量存储在 EMMC 或者 SD 卡中的话此函数会调用 board_late_mmc_env_init 函数初始化 EMMC/SD
INIT_FUNC_WATCHDOG_RESET //
INIT_FUNC_WATCHDOG_RESET//
INIT_FUNC_WATCHDOG_RESET//
initr_net, // 初始化网络设备
INIT_FUNC_WATCHDOG_RESET//
run_main_loop, // 主循环,处理命令
};
2.9 run_main_loop函数
uboot 启动以后会进入 3 秒倒计时,如果在 3 秒倒计时结束之前按下按下回车键,那么就会进入 uboot 的命令模式,如果倒计时结束以后都没有按下回车键,那么就会自动启动 Linux 内核 ,这个功能就是由run_main_loop函数来完成的。run_main_loop函数定义在文件common/board_r.c 中
2.10 cli_loop函数
2.12 cmd_process函数
在学习cmd_process 之前先看一下uboot中命令是如何定义的。uboot使用宏U_BOOT_CMD来定义命令,宏 U_BOOT_CMD 定义在文件 include/command.h 中
当我们在 uboot 的命令行中输入“dhcp”这个命令的时候,最终执行的是 do_dhcp 这个函数。总结一下,uboot 中使用 U_BOOT_CMD 来定义一个命令,最终的目的就是为了定义一个cmd_tbl_t 类型的变量,并初始化这个变量的各个成员。uboot 中的每个命令都存在.u_boot_list段中,每个命令都有一个名为 do_xxx(xxx 为具体的命令名)的函数,这个do_xxx 函数就是具体的命令处理函数。
3. u-boot启动linux内核函数过程
3.1 images全局变量
typedef struct bootm_headers {
/*
* Legacy os image header, if it is a multi component image
* then boot_get_ramdisk() and get_fdt() will attempt to get
* data from second and third component accordingly.
*/
image_header_t *legacy_hdr_os; /* image header pointer */
image_header_t legacy_hdr_os_copy; /* header copy */
ulong legacy_hdr_valid;
......
#ifndef USE_HOSTCC
image_info_t os; /* OS 镜像信息 */
ulong ep; /* OS 入口点 */
ulong rd_start, rd_end; /* ramdisk 开始和结束位置 */
char *ft_addr; /* 设备树地址 */
ulong ft_len; /* 设备树长度 */
ulong initrd_start; /* initrd 开始位置 */
ulong initrd_end; /* initrd 结束位置 */
ulong cmdline_start; /* cmdline 开始位置 */
ulong cmdline_end; /* cmdline 结束位置 */
bd_t *kbd;
#endif
int verify; /* getenv("verify")[0] != 'n' */
#define BOOTM_STATE_START (0x00000001)
#define BOOTM_STATE_FINDOS (0x00000002)
#define BOOTM_STATE_FINDOTHER (0x00000004)
#define BOOTM_STATE_LOADOS (0x00000008)
#define BOOTM_STATE_RAMDISK (0x00000010)
#define BOOTM_STATE_FDT (0x00000020)
#define BOOTM_STATE_OS_CMDLINE (0x00000040)
#define BOOTM_STATE_OS_BD_T (0x00000080)
#define BOOTM_STATE_OS_PREP (0x00000100)
#define BOOTM_STATE_OS_FAKE_GO (0x00000200)/*'Almost' run the OS*/
#define BOOTM_STATE_OS_GO (0x00000400)
int state;
#ifdef CONFIG_LMB
struct lmb lmb; /* 内存管理相关,不深入研究 */
#endif
} bootm_headers_t;
3.2 do_bootz函数
bootz 命令的执行函数为 do_bootz,在文件 cmd/bootm.c
3.3 bootz_start函数
先看看bootz_setup:
bootm_find_images:
3.4 do_bootm_states函数
do_bootz最后调用的就是函数do_bootm_states,而且在bootz_start中也调用了do_bootm_states函数,看来do_bootm_states函数还是个香饽饽。此函数定义在文件common/bootm.c 中
3.5 bootm_os_get_boot_func函数
do_bootm_states 会调用 bootm_os_get_boot_func 来查找对应系统的启动函数
3.6 do_bootm_linux函数
经过前面的分析,我们知道了 do_bootm_linux 就是最终启动 Linux 内核的函数,此函数定义在文件 arch/arm/lib/bootm.c
第 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 内核第一行代码!
第 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 的使命也就完成了,它可以安息了!