购物网站建设目标客户分析论文,淘宝客不建立网站怎么做,家装设计需要学什么软件,杭州哪些做网站公司原文链接#xff1a;I.MX6ULL_系统篇(16) uboot分析-启动流程 – WSY Personal Blog (cpolar.cn) 前面我们详细的分析了 uboot 的顶层 Makefile#xff0c;了解了 uboot 的编译流程。本章我们来详细的分析一下 uboot 的启动流程#xff0c;理清 uboot 是如何启动的。通过对 …原文链接I.MX6ULL_系统篇(16) uboot分析-启动流程 – WSY Personal Blog (cpolar.cn) 前面我们详细的分析了 uboot 的顶层 Makefile了解了 uboot 的编译流程。本章我们来详细的分析一下 uboot 的启动流程理清 uboot 是如何启动的。通过对 uboot 启动流程的梳理我们就可以掌握一些外设是在哪里被初始化的这样当我们需要修改这些外设驱动的时候就会心里有数。另外通过分析 uboot 的启动流程可以了解 Linux 内核是如何被启动的。
链接脚本 u-boot.lds
要分析 uboot 的启动流程首先要找到“入口”找到第一行程序在哪里。程序的链接是由链接脚本来决定的所以通过链接脚本可以找到程序的入口。如果没有编译过 uboot 的话链接脚本为 arch/arm/cpu/u-boot.lds。但是这个不是最终使用的链接脚本最终的链接脚本是在这个链接脚本的基础上生成的。编译一下 uboot编译完成以后就会在 uboot 根目录下生成 u-boot.lds文件如图所示 OUTPUT_FORMAT(elf32-littlearm, elf32-littlearm, elf32-littlearm)OUTPUT_ARCH(arm)ENTRY(_start) #代码入口SECTIONS{. 0x00000000;. 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 :{*(.__rel_dyn_start)}.rel.dyn : {*(.rel*)}.rel_dyn_end :{*(.__rel_dyn_end)}.end :{*(.__end)}_image_binary_end .;. ALIGN(4096);.mmutable : {*(.mmutable)}.bss_start __rel_dyn_start (OVERLAY) : {KEEP(*(.__bss_start));__bss_base .;}.bss __bss_base (OVERLAY) : {*(.bss*). ALIGN(4);__bss_limit .;}.bss_end __bss_limit (OVERLAY) : {KEEP(*(.__bss_end));}.dynsym _image_binary_end : { *(.dynsym) }.dynbss : { *(.dynbss) }.dynstr : { *(.dynstr*) }.dynamic : { *(.dynamic*) }.plt : { *(.plt*) }.interp : { *(.interp*) }.gnu.hash : { *(.gnu.hash) }.gnu : { *(.gnu*) }.ARM.exidx : { *(.ARM.exidx*) }.gnu.linkonce.armexidx : { *(.gnu.linkonce.armexidx.*) }}
第 3 行为代码当前入口点 _start _start 在文件 arch/arm/lib/vectors.S 中有定义如图所示 从图中的代码可以看出 _start 后面就是中断向量表从图中的“.section “.vectors”,”ax”可以得到此代码存放在.vectors 段里面。使用如下命令在 uboot 中查找“__image_copy_start”
grep -nR __image_copy_start 打开 u-boot.map找到如上图所示位置 u-boot.map 是 uboot 的映射文件可以从此文件看到某个文件或者函数链接到了哪个地址从上图的 932 行可以看到__image_copy_start 为 0X87800000而.text 的起始地址也是0X87800000。
继续回到链接脚本中 第 11 行是 vectors 段 vectors 段保存中断向量表我们知道了 vectors.S 的代码是存在 vectors 段中的。从上图中可以看出 vectors 段的起始地址也是 0X87800000说明整个 uboot 的起始地址就是 0X87800000。第 12 行将 arch/arm/cpu/armv7/start.s 编译出来的代码放到中断向量表后面。第 13 行为 text 段其他的代码段就放到这里在 u-boot.lds 中有一些跟地址有关的“变量”需要我们注意一下后面分析 u-boot 源码的时候会用到这些变量要最终编译完成才能确定的比如我编译完成以后这些“变量”的值如表所示 变量 数值 描述 __image_copy_start 0x87800000 uboot 拷贝的首地址 __image_copy_end 0x8785dd54 uboot 拷贝的结束地址 __rel_dyn_start 0x8785dd54 .rel.dyn 段起始地址 __rel_dyn_end 0x878668f4 .rel.dyn 段结束地址 _image_binary_end 0x878668f4 镜像结束地址 __bss_start 0x8785dd54 .bss 段起始地址 __bss_end 0x878a8e74 .bss 段结束地址
表中的“变量”值可以在 u-boot.map 文件中查找其中除了__image_copy_start以外其他的变量值每次编译的时候可能会变化如果修改了 uboot 代码、修改了 uboot 配置、选用不同的优化等级等等都会影响到这些值。所以一切以实际值为准
启动流程详解
reset 函数
从 u-boot.lds 中我们已经知道了入口点是 arch/arm/lib/vectors.S 文件中的_start代码如下 第 48 行_start 开始的是中断向量表其中 54~61 行就是中断向量表和我们裸机例程里面一样。54 行跳转到 reset 函数里面 reset 函数在 arch/arm/cpu/armv7/start.S 里面代码如下 第 35 行就是 reset 函数。第 37 行从 reset 函数跳转到了 save_boot_params 函数而 save_boot_params 函数同样定义在 start.S 里面定义如下 save_boot_params 函数也是只有一句跳转语句跳转到 save_boot_params_ret 函数save_boot_params_ret 函数代码如下 第 43 行读取寄存器 cpsr 中的值并保存到 r0 寄存器中。
第 44 行将寄存器 r0 中的值与 0X1F 进行与运算结果保存到 r1 寄存器中目的就是提取 cpsr 的bit0~bit4 这 5 位这 5 位为 M4 M3 M2 M1 M0 M[4:0]这五位用来设置处理器的工作模式如表所示 M[4:0] 模式 10000 User(usr) 10001 FIQ(fiq) 10010 IRQ(irq) 10011 Supervisor(svc) 10110 Monitor(mon) 10111 Abort(abt) 11010 Hyp(hyp) 11011 Undefined(und) 11111 System(sys)
第 45 行判断 r1 寄存器的值是否等于 0X1A(0b11010)也就是判断当前处理器模式是否处于 Hyp 模式。第 46 行如果 r1 和 0X1A 不相等也就是 CPU 不处于 Hyp 模式的话就将 r0 寄存器的bit0~5 进行清零其实就是清除模式位第 47 行如果处理器不处于 Hyp 模式的话就将 r0 的寄存器的值与 0x13 进行或运算0x130b10011也就是设置处理器进入 SVC 模式。第 48 行 r0 寄存器的值再与 0xC0 进行或运算那么 r0 寄存器此时的值就是 0xD3 cpsr的 I 位和 F 位分别控制 IRQ 和 FIQ 这两个中断的开关设置为 1 就关闭了 FIQ 和 IRQ第 49 行将 r0 寄存器写回到 cpsr 寄存器中。完成设置 CPU 处于 SVC 模式并且关闭FIQ 和 IRQ 这两个中断。继续执行执行下面的代码第 56 行如果没有定义 CONFIG_OMAP44XX 和 CONFIG_SPL_BUILD 的话条件成立此处条件成立。第 58 行读取 CP15 中 c1 寄存器的值到 r0 寄存器中这里是读取SCTLR 寄存器的值。第 59 行 CR_V 在 arch/arm/include/asm/system.h 中有如下所示定义
#define CR_V (1 13) /* Vectors relocated to 0xffff0000 */
因此这一行的目的就是清除 SCTLR 寄存器中的 bit13 SCTLR 寄存器结构如图所示 从图中可以看出 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 行就是设置向量表重定位的。代码继续往下执行第 68 行如果没有定义 CONFIG_SKIP_LOWLEVEL_INIT 的话条件成立。我们没有定义CONFIG_SKIP_LOWLEVEL_INIT因此条件成立执行下面的语句。就是分别调用函数 cpu_init_cp15、 cpu_init_crit 和_main。函数 cpu_init_cp15 用来设置 CP15 相关的内容比如关闭 MMU 啥的此函数同样在 start.S文件中定义的代码如下 函数 cpu_init_crit 也在是定义在 start.S 文件中函数内容如下 可以看出函数 cpu_init_crit 内部仅仅是调用了函数 lowlevel_init接下来就是详细的分析一下 lowlevel_init 和_main 这两个函数。
lowlevel_init 函数
函数 lowlevel_init 在文件 arch/arm/cpu/armv7/lowlevel_init.S 中定义内容如下 第 22 行设置 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 的首地址和大小。
71 #define IRAM_BASE_ADDR 0x00900000......408 #if !(defined(CONFIG_MX6SX) || defined(CONFIG_MX6UL) || \409 defined(CONFIG_MX6SLL) || defined(CONFIG_MX6SL))410 #define IRAM_SIZE 0x00040000411 #else412 #define IRAM_SIZE 0x00020000413 #endif
如果 408 行的条件成立的话 IRAM_SIZE0X40000当定义了 CONFIG_MX6SX、CONFIG_MX6U、 CONFIG_MX6SLL 和 CONFIG_MX6SL 中的任意一个的话条件就不成立在.config 中定义了 CONFIG_MX6UL所以条件不成立因此 IRAM_SIZE0X20000128KB。可以得到如下值
CONFIG_SYS_INIT_RAM_ADDR IRAM_BASE_ADDR 0x00900000。CONFIG_SYS_INIT_RAM_SIZE 0x00020000 128KB。
还需要知道GENERATED_GBL_DATA_SIZE的值在文件include/generated/generic-asm-offsets.h中有定义如下
1 #ifndef __GENERIC_ASM_OFFSETS_H__2 #define __GENERIC_ASM_OFFSETS_H__3 /*4 * DO NOT MODIFY.5 *6 * This file was generated by Kbuild7 */8 9#define GENERATED_GBL_DATA_SIZE 25610 #define GENERATED_BD_INFO_SIZE 8011 #define GD_SIZE 24812 #define GD_BD 013 #define GD_MALLOC_BASE 19214 #define GD_RELOCADDR 4815 #define GD_RELOC_OFF 6816 #define GD_START_ADDR_SP 641718 #endif
GENERATED_GBL_DATA_SIZE256 GENERATED_GBL_DATA_SIZE 的含义为(sizeof(struct global_data) 15) ~15 。综上所述 CONFIG_SYS_INIT_SP_ADDR 值如下
CONFIG_SYS_INIT_SP_OFFSET 0x00020000 – 256 0x1FF00。
CONFIG_SYS_INIT_SP_ADDR 0x00900000 0X1FF00 0X0091FF00
结果如下图所示 此时 sp 指向 0X91FF00这属于 IMX6UL/IMX6ULL 的内部 ram。继续回到文件 lowlevel_init.S第 23 行对 sp 指针做 8 字节对齐处理第 34 行 sp 指针减去 GD_SIZE GD_SIZE 同样在 generic-asm-offsets.h 中定了大小为248。第 35 行对 sp 做 8 字节对齐此时 sp 的地址为 0X0091FF00-2480X0091FE08此时 sp 位置如图所示 第 36 行将 sp 地址保存在 r9 寄存器中。第 42 行将 ip 和 lr 压栈第 57 行调用函数 s_init得又来了一个函数。第 58 行将第 36 行入栈的 ip 和 lr 进行出栈并将 lr 赋给 pc。
s_init 函数详解
在上一小节中我们知道 lowlevel_init 函数后面会调用 s_init 函数 s_init 函数定义在文件arch/arm/cpu/armv7/mx6/soc.c 中如下所示 在第 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函数调用路径如图所示 从图中可知接下来要执行的是 save_boot_params_ret 中的_main 函数接下来分析_main 函数。
_main 函数
_main 函数定义在文件 arch/arm/lib/crt0.S 中函数内容如下 第 76 行设置 sp 指针为 CONFIG_SYS_INIT_SP_ADDR也就是 sp 指向 0X0091FF00。第 83 行 sp 做 8 字节对齐。第 85 行读取 sp 到寄存器 r0 里面此时 r00X0091FF00。第 86 行调用函数 board_init_f_alloc_reserve此函数有一个参数参数为 r0 中的值也就是 0X0091FF00此函数定义在文件 common/init/board_init.c 中内容如下 函数 board_init_f_alloc_reserve 主要是留出早期的 malloc 内存区域和 gd 内存区域其中CONFIG_SYS_MALLOC_F_LEN0X400( 在 文 件 include/generated/autoconf.h 中 定 义 ) sizeof(struct global_data)248(GD_SIZE 值)完成以后的内存分布如图所示 函数 board_init_f_alloc_reserve 是有返回值的返回值为新的 top 值此时 top0X0091FA00。继续回到代码中第 87 行将 r0 写入到 sp 里面 r0 保存着函数board_init_f_alloc_reserve 的返回值所以这一句也就是设置 sp0X0091FA00。第 89 行将 r0 寄存器的值写到寄存器 r9 里面因为 r9 寄存器存放着全局变量 gd 的地址在文件 arch/arm/include/asm/global_data.h 中有如图所示宏定义 uboot 中定义了一个指向 gd_t 的指针 gd gd 存放在寄存器 r9 里面因此 gd 是个全局变量。 gd_t 是个结构体在 include/asm-generic/global_data.h 里面有定义。因此这一行代码就是设置 gd 所指向的位置也就是 gd 指向 0X0091FA00。继续回到代码中第 90 行调用函数 board_init_f_init_reserve此函数在文件common/init/board_init.c 中有定义函数内容如下 可以看出此函数用于初始化 gd其实就是清零处理。另外此函数还设置了gd-malloc_base 为 gd 基地址gd 大小0X0091FA002480X0091FAF8在做 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_sp0X9EF44E90 所以这里相当于设置spgd-start_addr_sp0X9EF44E90。 0X9EF44E90 是 DDR 中的地址说明新的 sp 和 gd 将会存放到 DDR 中而不是内部的 RAM 了。 GD_START_ADDR_SP64。第 109 行 sp 做 8 字节对齐。第 111 行获取 gd-bd 的地址赋给 r9此时 r9 存放的是老的 gd这里通过获取 gd-bd 的地址来计算出新的 gd 的位置。 GD_BD0。第 112 行新的 gd 在 bd 下面所以 r9 减去 gd 的大小就是新的 gd 的位置获取到新的 gd的位置以后赋值给 r9。第 114 行设置 lr 寄存器为 here这样后面执行其他函数返回的时候就返回到了第 122 行的 here 位置处。第 115读取 gd-reloc_off 的值复制给 r0 寄存器 GD_RELOC_OFF68。第 116 行 lr 寄存器的值加上 r0 寄存器的值重新赋值给 lr 寄存器。因为接下来要重定位代码也就是把代码拷贝到新的地方去(现在的 uboot 存放的起始地址为 0X87800000下面要将 uboot 拷贝到 DDR 最后面的地址空间将 0X87800000 开始的内存空出来)其中就包括here因此 lr 中的 here 要使用重定位后的位置。第 120 行读取 gd-relocaddr 的值赋给 r0 寄存器此时 r0 寄存器就保存着 uboot 要拷贝的目的地址为 0X9FF47000。 GD_RELOCADDR48。第 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 中函数内容如下 关闭I-cache。 第 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 个函数接下来依次看一下这 4 个函数都是干啥的。
board_init_f 函数
main 中会调用 board_init_f 函数 board_init_f 函数主要有两个工作①、初始化一系列外设比如串口、定时器或者打印一些消息等。②、初始化 gd 的各个成员变量 uboot 会将自己重定位到 DRAM 最后面的地址区域也就是将自己拷贝到 DRAM 最后面的内存区域中。这么做的目的是给 Linux 腾出空间防止 Linux kernel 覆盖掉 uboot将 DRAM 前面的区域完整的空出来。在拷贝之前肯定要给 uboot 各部分分配好内存位置和大小比如 gd 应该存放到哪个位置 malloc 内存池应该存放到哪个位置等等。这些信息都保存在 gd 的成员变量中因此要对 gd 的这些成员变量做初始化。最终形成一个完整的内存“分配图”在后面重定位 uboot 的时候就会用到这个内存“分配图”。此函数定义在文件 common/board_f.c 中定义代码如下 因为没有定义CONFIG_SYS_GENERIC_GLOBAL_DATA所以第1037~1054行代码无效。第 1056 行初始化 gd-flagsboot_flags0。第 1057 行设置 gd-have_console0。重点在第 1059 行通过函数 initcall_run_list 来运行初始化序列 init_sequence_f 里面的一些列函数 init_sequence_f 里面包含了一系列的初始化函数 init_sequence_f 也是定义在文件common/board_f.c 中 init_sequence_f 定义如下 第 833行 setup_mon_len 函数设置 gd 的 mon_len 成员变量此处为__bss_end -_start也就是整个代码的长度。 0X878A8E74-0x878000000XA8E74这个就是代码长度。第 840 行 initf_malloc 函数初始化 gd 中跟 malloc 有关的成员变量比如 malloc_limit此函数会设置 gd-malloc_limit CONFIG_SYS_MALLOC_F_LEN0X400。 malloc_limit 表示 malloc内存池大小。第 841 行 initf_console_record 如 果 定 义 了 宏 CONFIG_CONSOLE_RECORD 和 宏CONFIG_SYS_MALLOC_F_LEN 的话此函数就会调用函数 console_record_init但是 IMX6ULL的 uboot 没有定义宏 CONFIG_CONSOLE_RECORD所以此函数直接返回 0。第 849 行 arch_cpu_init 函数。第 850 行 initf_dm 函数驱动模型的一些初始化。第 851 行 arch_cpu_init_dm 函数未实现。第 852 行 mark_bootstage 函数应该是和啥标记有关的第 854 行 board_early_init_f 函数板子相关的早期的一些初始化设置 I.MX6ULL 用来初始化串口的 IO 配置。 第 869 行 timer_init初始化定时器 Cortex-A7 内核有一个定时器这里初始化的就是 CortexA 内核的那个定时器。通过这个定时器来为 uboot 提供时间。就跟 Cortex-M 内核 Systick 定时器一样。关于 Cortex-A 内部定时器的详细内容请参考文档《ARM ArchitectureReference Manual ARMv7-A and ARMv7-R edition.pdf》 的“Chapter B8 The Generic Timer”章节。第 877 行 board_postclk_init对于 I.MX6ULL 来说是设置 VDDSOC 电压。第 880 行 get_clocks 函数用于获取一些时钟值 I.MX6ULL 获取的是 sdhc_clk 时钟也就是 SD 卡外设的时钟。第 882 行 env_init 函数是和环境变量有关的设置 gd 的成员变量 env_addr也就是环境变量的保存地址。第 890 行 init_baud_rate 函数用于初始化波特率根据环境变量 baudrate 来初始化 gd-baudrate。第 891 行 serial_init初始化串口。第 892 行 console_init_f设置 gd-have_console 为 1表示有个控制台此函数也将前面暂存在缓冲区中的数据通过控制台打印出来。第 899 行、 display_options通过串口输出一些信息如图所示 第 900 行 display_text_info打印一些文本信息如果开启 UBOOT 的 DEBUG 功能的话就会输出 text_base、 bss_start、 bss_end形式如下
debug(U-Boot code: %08lX - %08lX BSS: - %08lX\n,text_base, bss_start, bss_end); 继续初始化序列 第 911 行 print_cpuinfo 函数用于打印 CPU 信息结果如图所示 第 916 行 show_board_info 函数用于打印板子信息会调用 checkboard 函数结果如图所示 第 918 行 INIT_FUNC_WATCHDOG_INIT初始化看门狗对于 I.MX6ULL 来说是空函数第 922 行 INIT_FUNC_WATCHDOG_RESET复位看门狗对于 I.MX6ULL 来说是空函数第 924 行 init_func_i2c 函数用于初始化 I2C初始化完成以后会输出如图所示信息 第 929 行 announce_dram_init此函数很简单就是输出字符串“DRAM:”第 933 行 dram_init并非真正的初始化 DDR只是设置 gd-ram_size 的值对于正点原子 I.MX6ULL 开发板 EMMC 版本核心板来说就是 512MB。
继续初始化序列 第 939 行 post_init_f此函数用来完成一些测试初始化 gd-post_init_f_time第 943 行 testdram测试 DRAM空函数。第 963 行 setup_dest_addr函数设置目的地址设置gd-ram_size gd-ram_top gd-relocaddr这三个的值。接下来我们会遇到很多跟数值有关的设置如果直接看代码分析的话就太费时间了我可以修改 uboot 代码直接将这些值通过串口打印出来比如这里我们修改文件common/board_f.c因为 setup_dest_addr 函数定义在文件 common/board_f.c 中在setup_dest_addr函数输入如图所示内容 设置好以后重新编译 uboot然后烧写到 SD 卡中选择 SD 卡启动重启开发板打开SecureCRT uboot 会输出如图所示信息 继续 第 977 行 reserve_round_4k 函 数 用 于 对 gd-relocaddr 做 4KB 对 齐 因 为gd-relocaddr0XA0000000已经是 4K 对齐了所以调整后不变。第 980 行 reserve_mmu留出 MMU 的 TLB 表的位置分配 MMU 的 TLB 表内存以后会对 gd-relocaddr 做 64K 字节对齐。完成以后 gd-arch.tlb_size、 gd-arch.tlb_addr 和 gd-relocaddr如图所示 第 995 行 reserve_trace 函数留出跟踪调试的内存 I.MX6ULL 没有用到第 997 行 reserve_uboot 留出重定位后的 uboot 所占用的内存区域 uboot 所占用大小由gd-mon_len 所指定留出 uboot 的空间以后还要对 gd-relocaddr 做 4K 字节对齐并且重新设置 gd-start_addr_sp结果如图所示 第 1000 行 reserve_malloc留出 malloc 区域调整 gd-start_addr_sp 位置 malloc 区域由宏TOTAL_MALLOC_LEN 定义宏定义如下
#define TOTAL_MALLOC_LEN (CONFIG_SYS_MALLOC_LEN CONFIG_ENV_SIZE)
mx6ull_alientek_emmc.h 文件中定义宏 CONFIG_SYS_MALLOC_LEN 为 16MB0X1000000宏 CONFIG_ENV_SIZE8KB0X2000因此 TOTAL_MALLOC_LEN0X1002000。调整以后gd-start_addr_sp 如图所示 第 1001 行 reserve_board 函数留出板子 bd 所占的内存区 bd 是结构体 bd_t bd_t 大小为80 字节结果如图所示 第 1003 行 setup_machine设置机器 ID linux 启动的时候会和这个机器 ID 匹配如果匹配的话 linux 就会启动正常。但是 I.MX6ULL 不用这种方式了这是以前老版本的 uboot 和linux 使用的新版本使用设备树了因此此函数无效。第 1004 行 reserve_global_data 函数保留出 gd_t 的内存区域 gd_t 结构体大小为 248B结果如图所示 第 1005 行 reserve_fdt留出设备树相关的内存区域 I.MX6ULL 的 uboot 没有用到因此此函数无效。第 1006 行 reserve_arch 是个空函数。第 1007 行 reserve_stacks留出栈空间先对 gd-start_addr_sp 减去 16然后做 16 字节对齐。如果使能 IRQ 的话还要留出 IRQ 相应的内存具体工作是由 arch/arm/lib/stack.c 文件中的函数 arch_reserve_stacks 完成。结果如图所示 在本 uboot 中并没有使用到 IRQ所以不会留出 IRQ 相应的内存区域。第 1008 行 setup_dram_config 函数设置 dram 信息就是设置 gd-bd-bi_dram[0].start 和gd-bd-bi_dram[0].size后面会传递给 linux 内核告诉 linux DRAM 的起始地址和大小。结果如图所示 第 1009 行 show_dram_config 函数用于显示 DRAM 的配置如图所示 继续 第 1017 行 display_new_sp 函数显示新的 sp 位置也就是 gd-start_addr_sp不过要定义宏 DEBUG结果如图所示 图中的 gd-start_addr_sp 值和我们前面分析的最后一次修改的值一致。第 1022 行 reloc_fdt 函数用于重定位 fdt没有用到。第 1023 行 setup_reloc设置 gd 的其他一些成员变量供后面重定位的时候使用并且将以前的 gd 拷贝到 gd-new_gd 处。需要使能 DEBUG 才能看到相应的信息输出如图所示 从图中可以看出 uboot 重定位后的偏移为 0X18747000重定位后的新地址为0X9FF4700新的 gd 首地址为 0X9EF44EB8最终的 sp 为 0X9EF44E90。至此 board_init_f 函数就执行完成了最终的内存分配如图所示 relocate_code 函数
relocate_code 函数是用于代码拷贝的此函数定义在文件 arch/arm/lib/relocate.S 中代码如下 第 80 行 r1__image_copy_start也就是 r1 寄存器保存源地址__image_copy_start0X87800000。第 81 行 r00X9FF47000这个地址就是 uboot 拷贝的目标首地址。 r4r0-r10X9FF47000-0X878000000X18747000因此 r4 保存偏移量。第 82 行如果在第 81 中 r0-r1 等于 0说明 r0 和 r1 相等也就是源地址和目的地址是一样的那肯定就不需要拷贝了执行 relocate_done 函数第 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 段 .rel.dyn 段是存放.text 段中需要重定位地址的集合。重定位就是 uboot 将自身拷贝到 DRAM 的另一个地放去继续运行(DRAM 的高地址处)。我们知道一个可执行的 bin 文件其链接地址和运行地址要相等也就是链接到哪个地址在运行之前就要拷贝到哪个地址去。现在我们重定位以后运行地址就和链接地址不同了这样寻址的时候不会出问题吗
uboot 对于重定位后链接地址和运行地址不一致的解决方法就是采用位置无关码在使用 ld 进行链接的时候使用选项“ -pie”生成位置无关的可执行文件。在文件arch/arm/config.mk 下有如下代码
# needed for relocationLDFLAGS_u-boot -pie
使用“-pie”选项以后会生成一个.rel.dyn 段 uboot 就是靠这个.rel.dyn 来解决重定位问题的在 u-bot.dis 的.rel.dyn 段中有如下所示内容
Disassembly of section .rel.dyn8785da44 __rel_dyn_end-0x8ba0:8785da44: 87800020 strhi r0, [r0, r0, lsr #32]8785da48: 00000017 andeq r0, r0, r7, lsl r0......8785dfb4: 87804198 ; UNDEFINED instruction: 0x878041988785dfb8: 00000017 andeq r0, r0, r7, lsl r0
先来看一下.rel.dyn 段的格式类似第 7 行和第 8 行这样的是一组也就是两个 4 字节数据为一组。高 4 字节是 Label 地址标识 0X17低 4 字节就是 Label 的地址首先判断 Label 地址标识是否正确也就是判断高 4 字节是否为 0X17如果是的话低 4 字节就是 Label 地址值。第 7 行值为 0X87804198第 8 行为 0X00000017说明第 7 行的 0X87804198 是个 Label根据前面的分析只要将地址0X87804198offset 处的值改为重定位后的地址即可。我们猜测的是否正确看一下uboot 对.rel.dyn 段的重定位即可 .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 保存着重定位后的地址偏移 r0r4 就得到了重定位后的Label 值。此时 r0 保存着重定位后的 Label 值相当于 0X878041980X187470000X9FF4B198。第 104读取重定位后 Label 所保存的变量地址此时这个变量地址还是重定位前的(相当于变量重定位前的地址 0X8785DA50)将得到的值放到 r1 寄存器中。第 105 行 r1r4 即 可 得 到 重 定 位 后 的 变 量 地 址 相 当 于 rel_a 重 定 位 后 的0X8785DA500X187470000X9FFA4A50。第 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_vectors 用于重定位向量表此函数定义在文件 relocate.S 中 函数源码如下 第 29 行如果定义了 CONFIG_CPU_V7M 的话就执行第 30~36 行的代码这是 Cortex-M内核单片机执行的语句因此对于 I.MX6ULL 来说是无效的。第 38 行如果定义了 CONFIG_HAS_VBAR 的话就执行此语句这个是向量表偏移 CortexA7 是支持向量表偏移的。而且在.config 里面定义了 CONFIG_HAS_VBAR因此会执行这个分支。第 43 行 r0gd-relocaddr也就是重定位后 uboot 的首地址向量表肯定是从这个地址开始存放的。第 44 行将 r0 的值写入到 CP15 的 VBAR 寄存器中也就是将新的向量表首地址写入到寄存器 VBAR 中设置向量表偏移。
board_init_r 函数
前面讲解了 board_init_f 函数在此函数里面会调用一系列的函数来初始化一些外设和 gd 的成员变量。但是 board_init_f 并没有初始化所有的外设还需要做一些后续工作这些后续工作就是由函数 board_init_r 来完成的 board_init_r 函数定义在文件 common/board_r.c中代码如下 第 1010 行调用 initcall_run_list 函数来执行初始化序列 init_sequence_r init_sequence_r 是一个函数集合 init_sequence_r 也定义在文件 common/board_r.c 中init_sequence_r 定义如下 第 774 行 initr_trace 函数如果定义了宏 CONFIG_TRACE 的话就会调用函数 trace_init初始化和调试跟踪有关的内容。第 775 行 initr_reloc 函数用于设置 gd-flags标记重定位完成。第 778 行 initr_caches 函数用于初始化 cache使能 cache。第 786 行 initr_reloc_global_data 函数初始化重定位后 gd 的一些成员变量。第 790 行 initr_barrier 函数 PPC架构使用。第 791 行 initr_malloc 函数初始化 malloc。第 792 行 initr_console_record 函数初始化控制台相关的内容 需要定义CONSOLE_RECORD。第 796 行 bootstage_relocate 函数启动状态重定位。第 800 行 initr_bootstage 函数初始化 bootstage 什么的。第 802 行 board_init 函数板级初始化该函数一般由soc原厂或用户编写。这里执行的是 mx6ull_alientek_emmc.c 文件中的 board_init 函数。 第 813 行 stdio_init_tables 函数 stdio 相关初始化。第 814 行 initr_serial 函数初始化串口。第 815 行 initr_announce 函数与调试有关通知已经在 RAM 中运行。 第 853 行 power_init_board 函数初始化电源芯片即PMIC。第 855 行 initr_flash 函数初始化flash第 867 行 initr_nand 函数初始化 NAND如果使用 NAND 版本核心板的话就会初始化NAND。第 873 行 initr_mmc 函数初始化 EMMC如果使用 EMMC 版本核心板的话就会初始化EMMC串口输出如图所示信息 从图中可以看出此时有两个 EMCM 设备 FSL_SDHC:0 和 FSL_SDHC:1。第 878 行 initr_env 函数初始化环境变量。第 883 行 initr_secondary_cpu 函数初始化其他 CPU 核。 第 894 行 stdio_add_devices 函数各种输入输出设备的初始化如 LCD driver I.MX6ULL使用 drv_video_init 函数初始化 LCD。会输出如图所示信息 第 895 行 initr_jumptable 函数初始化跳转表。第 899 行 console_init_r 函 数 控 制 台 初 始 化 初 始 化 完 成 以 后 此 函 数 会 调 用stdio_print_current_devices 函数来打印出当前的控制台设备如图所示 第 913 行 interrupt_init 函数初始化中断。第 915 行 initr_enable_interrupts 函数使能中断。第 925 行 initr_ethaddr 函数初始化网络地址也就是获取 MAC 地址。读取环境变量“ethaddr”的值。 第 928 行 board_late_init 函数板子后续初始化此函数定义在文件 mx6ull_alientek_emmc.c中如果环境变量存储在 EMMC 或者 SD 卡中的话此函数会调用 board_late_mmc_env_init 函数初始化 EMMC/SD。会切换到正在时候用的 emmc 设备代码如图所示 图中的第 46 行和第 47 行就是运行“mmc dev xx”命令用于切换到正在使用的EMMC 设备串口输出信息如图所示 第 952 行initr_net函数初始化网络函数调用顺序为 initr_net-eth_initialize-board_eth_init() 串口输出如图所示信息 第 988 行 run_main_loop 行主循环处理命令。
run_main_loop 函数
uboot 启动以后会进入 3 秒倒计时如果在 3 秒倒计时结束之前按下按下回车键那么就会进入 uboot 的命令模式如果倒计时结束以后都没有按下回车键那么就会自动启动 Linux 内核 这个功能就是由 run_main_loop 函 数 来 完 成 的 。 run_main_loop 函 数 定 义 在 文 件common/board_r.c 中函数内容如下 第 759 行和第 760 行是个死循环“for(;;)”和“while(1)”功能一样死循环里面就一个main_loop 函数 main_loop 函数定义在文件 common/main.c 里面代码如下 第 48 行调用 bootstage_mark_name 函数打印出启动进度。第 57 行如果定义了宏 CONFIG_VERSION_VARIABLE 的话就会执行函数 setenv设置换将变量 ver 的值为 version_string也就是设置版本号环境变量。 version_string 定义在文件cmd/version.c 中定义如下
const char __weak version_string[] U_BOOT_VERSION_STRING;
U_BOOT_VERSION_STRING 是个宏 定义在文件 include/version.h如下 U_BOOT_VERSION 定 义 在 文 件 include/generated/version_autogenerated.h 中 文 件version_autogenerated.h 内如如下
#define PLAIN_VERSION 2016.03
#define U_BOOT_VERSION U-Boot PLAIN_VERSION
#define CC_VERSION_STRING arm-linux-gnueabihf-gcc (Linaro GCC 4.9-2017.01) 4.9.4
#define LD_VERSION_STRING GNU ld (Linaro_Binutils-2017.01)2.24.0.20141017 Linaro 2014_11-3-git
可以看出 U_BOOT_VERSION 为“U-boot 2016.03”U_BOOT_DATE 、 U_BOOT_TIME 和 U_BOOT_TZ 这 定 义 在 文 件include/generated/timestamp_autogenerated.h 中如下所示
#define U_BOOT_DATE Apr 25 2019
#define U_BOOT_TIME 21:10:53
#define U_BOOT_TZ 0800
#define U_BOOT_DMI_DATE 04/25/2019
宏 CONFIG_IDENT_STRING 为空所以 U_BOOT_VERSION_STRING 为“U-Boot 2016.03(Apr 25 2019 – 21:10:53 0800)”进入 uboot 命令模式输入命令“version”查看版本号如图所示 上图中的第一行就是 uboot 版本号和我们分析的一致。接着回到函数main_loop中第 60 行 cli_init 函数跟命令初始化有关初始化 hush shell 相关的变量。第 62 行 run_preboot_environment_command 函数获取环境变量 perboot 的内容 preboot是一些预启动命令一般不使用这个环境变量。第 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 函数此函数就是检查倒计时是否结束,倒计时结束之前有没有被打断,此函数定义在文件 common/autoboot.c 中内容如下 当以下三条全部成立的话就会执行函数 run_command_list。①、 stored_bootdelay 不等于-1。②、 s 不为空。③、函数 abortboot 返回值为 0。stored_bootdelay 等于环境变量 bootdelay 的值 s 是环境变量 bootcmd 的值一般不为空因此前两个成立就剩下了函数abortboot 的返回值abortboot函数也定义在文件common/autoboot.c 内容如下 因为宏 CONFIG_AUTOBOOT_KEYE 未定义因此执行函数 abortboot_normal接着来看函数abortboot_normal此函数也定义在文件 common/autoboot.c 中内容如下 第 227 行的变量 abort 是函数 abortboot_normal 的返回值默认值为 0。第 234 行通过串口输出“Hit any key to stop autoboot”字样如图所示 第 251-267 行就是倒计时的具体实现。
第 256 行判断键盘是否有按下也就是是否打断了倒计时如果键盘按下的话就执行相应的分支。比如设置 abort 为 1设置 bootdelay 为 0 等最后跳出倒计时循环。第 279 行返回abort的值如果倒计时自然结束没有被打断 abort 就为 0否则的话 abort的值就为1。回到autoboot_command 函数中如果倒计时自然结束那么就执行函数run_command_list此函数会执行参数 s 指定的一系列命令也就是环境变量 bootcmd 的命令bootcmd 里面保存着默认的启动命令因此 linux 内核启动这个就是 uboot 中倒计时结束以后自动启动 linux 内核的原理。如果倒计时结束之前按下了键盘上的按键那么 run_command_list函数就不会执行相当于 autoboot_command 是个空函数。回到“遥远”的 main_loop 函数中如果倒计时结束之前按下按键那么就会执行第 74 行的 cli_loop 函数这个就是命令处理函数负责接收处理输入的命令。
cli_loop 函数
cli_loop 函数是 uboot 的命令行处理函数我们在 uboot 中输入各种命令进行各种操作就是有 cli_loop 来处理的此函数定义在文件 common/cli.c 中函数内容如下 在文件 include/configs/mx6_common.h 中有定义宏 CONFIG_SYS_HUSH_PARSER而开发板配置头文件 mx6ullevk.h 里面会引用 mx_common.h 这个头文件因此宏 CONFIG_SYS_HUSH_PARSER 有定义。第 205 行调用函数 parse_file_outer。第 207 行是个死循环永远不会执行到这里。函数 parse_file_outer 定义在文件 common/cli_hush.c 中函数内容如下 第 3296 行调用函数 setup_file_in_str 初始化变量 input 的成员变量。第 3300 行调用函数 parse_stream_outer这个函数就是 hush shell 的命令解释器负责接收命令行输入然后解析并执行相应的命令函数parse_stream_outer 定义在文件 common/cli_hush.c中函数内容如下
1 static int parse_stream_outer(struct in_str *inp, int flag) 2 { 3 struct p_context ctx; 4 o_string tempNULL_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 }
第 7~21 行中的 do-while 循环就是处理输入命令的。第 9 行调用函数 parse_stream 进行命令解析。第 14 行调用调用 run_list 函数来执行解析出来的命令。函数 run_list 会经过一系列的函数调用最终通过调用 cmd_process 函数来处理命令过程如下 …… …… run_list 函数调用 run_list_real 函数。run_list_real 函数调用 run_pipe_real 函数。run_pipe_real 函数调用 cmd_process 函数。最终通过函数 cmd_process 来处理命令接下来就是分析 cmd_process 函数。
cmd_process 函数
在学习cmd_process 之前先看一下uboot中命令是如何定义的。uboot使用宏U_BOOT_CMD来定义命令宏 U_BOOT_CMD 定义在文件 include/command.h 中定义如下 可 以 看 出 U_BOOT_CMD 是 U_BOOT_CMD_COMPLETE 的 特 例 将U_BOOT_CMD_COMPLETE 的 最 后 一 个 参 数 设 置 成 NULL 就 是 U_BOOT_CMD 。 宏U_BOOT_CMD_COMPLETE 如下 宏 U_BOOT_CMD_COMPLETE 又 用 到 了 ll_entry_declare 和U_BOOT_CMD_MKENT_COMPLETE。 ll_entry_declar 定义在文件 include/linker_lists.h 中定义如下 _type 为 cmd_tbl_t因此 ll_entry_declare 就是定义了一个 cmd_tbl_t 变量这里用到了 C 语言中的“##”连接符。其中的“##_list”表示用_list 的值来替换“##_name”就是用_name 的值来替换。宏 U_BOOT_CMD_MKENT_COMPLETE 定义在文件 include/command.h 中内容如下 上 述 代 码 中 的 “ # ” 表 示 将 _name 传 递 过 来 的 值 字 符 串 化 U_BOOT_CMD_MKENT_COMPLETE 又用到了宏_CMD_HELP 和_CMD_COMPLETE这两个宏的定义如下 可以看出如果定义了宏 CONFIG_AUTO_COMPLETE 和 CONFIG_SYS_LONGHELP 的话 _CMD_COMPLETE 和 _CMD_HELP 就 是 取 自 身 的 值 然 后 在 加 上 一 个 ‘ , ’。CONFIG_AUTO_COMPLETE 和 CONFIG_SYS_LONGHELP 这 两 个 宏 有 定 义 在 文 件mx6_common.h 中。U_BOOT_CMD宏的流程我们已经清楚了我们就以一个具体的命令为例来看一下 U_BOOT_CMD 经过展开以后究竟是个什么模样的。以命令 dhcp 为例 dhcp 命令定义如下
U_BOOT_CMD( dhcp, 3, 1, do_dhcp, boot image via network using DHCP/TFTP protocol, [loadAddress] [[hostIPaddr:]bootfilename]
);
将其展开结果如下
U_BOOT_CMD( dhcp, 3, 1, do_dhcp, boot image via network using DHCP/TFTP protocol, [loadAddress] [[hostIPaddr:]bootfilename]
); 1、将 U_BOOT_CMD 展开后为
U_BOOT_CMD_COMPLETE(dhcp, 3, 1, do_dhcp, boot image via network using DHCP/TFTP protocol, [loadAddress] [[hostIPaddr:]bootfilename], NULL) 2、将 U_BOOT_CMD_COMPLETE 展开后为
ll_entry_declare(cmd_tbl_t, dhcp, cmd) \
U_BOOT_CMD_MKENT_COMPLETE(dhcp, 3, 1, do_dhcp, \ boot image via network using DHCP/TFTP protocol, \ [loadAddress] [[hostIPaddr:]bootfilename], \ NULL); 3、将 ll_entry_declare 和 U_BOOT_CMD_MKENT_COMPLETE 展开后为
cmd_tbl_t _u_boot_list_2_cmd_2_dhcp aligned(4) \ __attribute((unused,section(.u_boot_list_2_cmd_2_dhcp))) \
{ dhcp, 3, 1, do_dhcp, \ boot image via network using DHCP/TFTP protocol, \ [loadAddress] [[hostIPaddr:]bootfilename],\
NULL}
dhcp 命令最终展开结果为
cmd_tbl_t _u_boot_list_2_cmd_2_dhcp __aligned(4) \ __attribute__((unused,section(.u_boot_list_2_cmd_2_dhcp))) \ { dhcp, 3, 1, do_dhcp, \ boot image via network using DHCP/TFTP protocol, \ [loadAddress] [[hostIPaddr:]bootfilename],\ 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.lds 链接脚本中有一个名为“.u_boot_list”的段所有.u_boot_list 开头的段都存放到.u_boot.list 中如图所示 因此第 2 行就是设置变量_u_boot_list_2_cmd_2_dhcp 的存储位置。第 3~6 行 cmd_tbl_t 是个结构体因此第 3-6 行是初始化 cmd_tbl_t 这个结构体的各个成员变量。 cmd_tbl_t 结构体定义在文件 include/command.h 中内容如下 可以得出变量_u_boot_list_2_cmd_2_dhcp 的各个成员的值如下所示
_u_boot_list_2_cmd_2_dhcp.name dhcp
_u_boot_list_2_cmd_2_dhcp.maxargs 3
_u_boot_list_2_cmd_2_dhcp.repeatable 1
_u_boot_list_2_cmd_2_dhcp.cmd do_dhcp
_u_boot_list_2_cmd_2_dhcp.usage boot image via network using DHCP/TFTP protocol
_u_boot_list_2_cmd_2_dhcp.help [loadAddress] [[hostIPaddr:]bootfilename]
_u_boot_list_2_cmd_2_dhcp.complete NULL
当我们在 uboot 的命令行中输入“dhcp”这个命令的时候最终执行的是 do_dhcp 这个函数。总结一下 uboot 中使用 U_BOOT_CMD 来定义一个命令最终的目的就是为了定义一个cmd_tbl_t 类型的变量并初始化这个变量的各个成员。 uboot 中的每个命令都存储在.u_boot_list段中每个命令都有一个名为 do_xxx(xxx 为具体的命令名)的函数这个 do_xxx 函数就是具体的命令处理函数。了解了 uboot 中命令的组成以后再来看一下 cmd_process 函数的处理过程 cmd_process函数定义在文件 common/command.c 中函数内容如下 第 507 行调用函数 find_cmd 在命令表中找到指定的命令 find_cmd 函数内容如下 参数 cmd 就是所查找的命令名字 uboot 中的命令表其实就是 cmd_tbl_t 结构体数组通过函数 ll_entry_start 得到数组的第一个元素也就是命令表起始地址。通过函数 ll_entry_count得到数组长度也就是命令表的长度。最终通过函数 find_cmd_tbl 在命令表中找到所需的命令每个命令都有一个 name 成员所以将参数 cmd 与命令表中每个成员的 name 字段都对比一下如果相等的话就说明找到了这个命令找到以后就返回这个命令。回到 cmd_process 函数中找到命令以后肯定就要执行这个命令了第 533 行调用函数 cmd_call 来执行具体的命令 cmd_call 函数内容如下 在前面的分析中我们知道 cmd_tbl_t 的 cmd 成员就是具体的命令处理函数所以第 494 行调用 cmdtp 的 cmd 成员来处理具体的命令返回值为命令的执行结果。cmd_process 中会检测 cmd_tbl 的返回值如果返回值为 CMD_RET_USAGE 的话就会调用cmd_usage 函数输出命令的用法其实就是输出 cmd_tbl_t 的 usage 成员变量。