u-boot分析与使用
u-boot分析与使用
一、Makefile结构分析
1、make 100ask24x0_config配置芯片类型
打开u-boot-1.1.6/Makefile文件,发现:
1 | SRCTREE := $(CURDIR) *CURDIR是make的内嵌变量, 为当前目录 |
上述$(SRCTREE)等于$(CURDIR),也就是当前目录u-boot-1.1.6,所以MKCONFIG=./mkconfig
上述$(@:_config=)的结果就是将 “100ask24x0_config” 中的 “_config” 去掉, 结果为 “100ask24x0” 。
实际执行:
1 | mkconfig 100ask24x0 arm arm920t 100ask24x0 NULL s3c24x0 |
mkconfig脚本分析
显然这里是调用的mkconfig,打开当前目录u-boot-1.1.6下的该文件(用的linux_shell语法,可以参考《精通linux_shell编程教程pdf完整版》以及Linux应用开发手册第264页U-Boot配置过程),在第6行中给出了mkconfig的用法:
1 | # Parameters: Target Architecture CPU Board [VENDOR] [SOC] |
刚好对应mkconfig(参数) 100ask24x0(目标) arm(架构) arm920t(cpu)100ask24x0(开发板选型) NULL(供应商) s3c24x0(片上系统/芯片) 。
通过分析,精简后的mkconfig文件:
1 |
|
发现会创建include/config.mk文件:
1 | ARCH = arm |
以及include/config.h
1 | /* Automatically generated - do not edit */ |
mkconfig脚本总结
-
创建相关的头文件链接:
1
2
3ln -s asm-arm asm
ln -s arch-s3c24x0 asm-arm/arch
ln -s proc-armv asm-arm/proc -
创建顶层makefile包含的文件include/config.mk
1
2
3
4ARCH = arm
CPU = arm920t
BOARD = 100ask24x0
SOC = s3c24x0 -
创建开发板相关的头文件include/config.h
1
2/* Automatically generated - do not edit */
2、make 编译u-boot
u-boot-1.1.6/Makefile文件:
1 | include $(OBJTREE)/include/config.mk//调用config.mk这个文件 |
最后的config.mk是属于顶层目录的config,主要通过arm,arm920t,100ask24x0,s3c24x0来确定编译器、编译选项等
顶层的config.mk部分为:
1 | ... |
这里的LDFLAGS
就是下文中的$(LDFLAGS)
$(LDSCRIPT)
:链接脚本$(TEXT_BASE)
:board/100ask24x0/config.mk中定义,为链接脚本的起始地址
继续看,发现all目标:
1 | ... |
上述10行开始就是开始制作elf格式的u-boot,具体展开的命令可以看make后命令行最后输出的命令,值得注意的是这中间包含的链接脚本,他决定了各种.o文件是怎么组织的(注意$(LDFLAGS)
)
通过make后命令行输出的命令:
1 | ......&&arm-linux-ld -Bstatic -T /work/system/u-boot-1.1.6/board/100ask24x0/u-boot.lds -Ttext 0x33F80000 $UNDEF_SYM cpu/arm920t/start.o...... |
可以发现链接脚本为board/100ask24x0/u-boot.lds
,并且通过**-Ttext**参数确定链接脚本的起始地址为0x33F80000
开发板有64M的SDRAM,SDRAM从0x3000 0000开始,0x33F80000到SDRAM的结束地址刚好为512k
同时,第一个链接的文件为cpu/arm920t/start.o,若链接脚本中没有明确指定那个文件在代码段(text段)的开头,该文件就是第一个被执行的文件
链接脚本分析
1 | OUTPUT_ARCH(arm)//设置输出文件的体系架构。 |
发现第一个文件是cpu/arm920t/start.S
二、start.S——硬件初始化
-
跳到reset处,执行复位函数
-
设为SVC(管理员)模式、f关闭看门狗、关闭中断、CPU的相关初始化(SDRAM初始化)
-
设置堆栈,为之后进入C函数做准备
1
2
3
4
5
6
7
8
9
10
11
12
13
14/* Set up the stack */
stack_setup:
ldr r0, _TEXT_BASE/*_TEXT_BASE,代码段起始地址,就是之前的0x33F80000*/
sub r0, r0, #CFG_MALLOC_LEN /* malloc area */
sub r0, r0, #CFG_GBL_DATA_SIZE /* bdinfo */
#ifdef CONFIG_USE_IRQ
sub r0, r0, #(CONFIG_STACKSIZE_IRQ+CONFIG_STACKSIZE_FIQ)
#endif
sub sp, r0, #12 /* leave 3 words for abort-stack 最后sp指针的位置 */
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
bl clock_init/*跳到C函数初始化时钟(自己写的,一般为汇编写的)*/
#endif -
重定位
1
2
3
4
5
6
7
8
9
10
11
12
13
14//搬移代码,将完整的u-boot从flash(NOR、NAND)中搬移到SDRAM中的链接地址去
#ifndef CONFIG_SKIP_RELOCATE_UBOOT
relocate: /* relocate U-Boot to RAM */
adr r0, _start /* r0 <- current position of code */
ldr r1, _TEXT_BASE /* test if we run from flash or RAM */
cmp r0, r1 /* don't reloc during debug */
beq clear_bss
ldr r2, _armboot_start
ldr r3, _bss_start
sub r2, r3, r2 /* r2 <- size of armboot */
#if 1
bl CopyCode2Ram /* r0: source, r1: dest, r2: size */
#else -
清除BSS段,全部清为0
_bss_start 和 _bss_end由链接脚本决定
1
2
3
4
5
6
7
8
9clear_bss:
ldr r0, _bss_start /* find start of bss segment */
ldr r1, _bss_end /* stop here */
mov r2, #0x00000000 /* clear */
clbss_l:str r2, [r0] /* clear loop... */
add r0, r0, #4
cmp r0, r1
ble clbss_l -
调用C函数
start_armboot
1
2ldr pc, _start_armboot
_start_armboot: .word start_armboot
注意:
ldr是位置有关码,将_start_armboot这个标号的值作为地址,取出其中的值(即start_armboot的位置,该位置是通过链接脚本指定的)赋值给pc,所以无论该代码现在运行在哪里(芯片内部SRAM、外部NOR FLASH或者外部SDRAM)都会跳转到已经拷贝到的外部SDRAM中的start_armboot中去
实例:
若此时该汇编代码运行在芯片内部SRAM中,前期通过搬移已经将完整的u-boot搬移至片外SDRAM中,链接脚本指定的链接地址(运行地址)为
33f80000
,设此时汇编所对应的反汇编为:
1
2
3
4
5
6
7 #链接地址 机器码 反汇编指令
33f80070: e59ff04c ldr pc, [pc, #76] ; 33f800c4
......
33f800c4: 33f805a0 mvnccs r0, #671088640 ; 0x28000000
......
33f805a0 <start_armboot>:
33f805a0: e92d4030 stmdb sp!, {r4, r5, lr}此时第一行中的代码肯定是在片内SRAM中运行的,所以该行代码所处的实际物理地址应该是0x33f8 0070-0x33f8 0000=0x70,通过pc+8+76=pc+84=pc+0x54,得出pc应该赋值为地址0x70+0x54=0xc4中的值,即此时pc等于0xc4处所储存的数据33f805a0,正好对应start_armboot的链接地址。即下一刻跳转到片外的SDRAM中的start_armboot函数
总结:
重定位前的代码应该使用位置无关码来写,以确保不同位置都可运行
- 使用相对跳转指令:B、BL
- 不可使用绝对地址,不可访问全局变量、静态变量(根本方法是看反汇编)
- 不能访问有初始值的数组,这种数组(指局部数组,全局变量不能使用)本身会存在栈中,但是初始值会在rodata段中,访问这些数据的值会用绝对地址进行访问
重定位后的跳转到SDRAM代码应该用位置有关码(LDR)来写,以确保可以跳到外部SDRAM中的运行地址去运行
三、start_armboot
该函数会:
-
初始化堆栈,用于自定义的malloc和free
-
拥有读写flash的能力
NOR:Flash_init
NAND:nand_init
-
跳到
main_loop
函数1
2
3
4/* main_loop() can return to retry autoboot, if so just run it again. */
for (;;) {
main_loop ();
}main_loop函数可以等待用户输入相关的命令或者是通过uboot中的环境变量bootcmd中的命令启动linux内核
而main_loop函数的核心在于
getenv
函数和run_command
函数,前者负责从uboot中的环境变量中取出对应的数据,后者负责执行相关的命令
四、u-boot命令实现
核心机制
-
所有命令通过一个结构体进行封装(cmd_tb1_t)
-
所有命令通过结构体中的name成员进行查找
-
这些结构体存在一个uboot自定义的段内,在之前的链接脚本中已经进行声明了段的起始和结束地址:
1
2
3
4. = .;
__u_boot_cmd_start = .;//把__u_boot_cmd_start赋值为当前位置, 即起始位置
.u_boot_cmd : { *(.u_boot_cmd) }// u_boot_cmd段,所有的u-boot命令相关的定义都放在这个位置
__u_boot_cmd_end = .;// *u_boot_cmd段结束位置
通过搜索.u_boot_cmd可以找到怎样将这些结构体放入这个段中(利用__attribute__关键字)
通过搜索常用命令(如:bootm)可以找到相关的定义这些结构体的方法
- 通过宏U_BOOT_CMD定义这些命令结构体
1 | U_BOOT_CMD( |
例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 U_BOOT_CMD(
bootm, CFG_MAXARGS, 1, do_bootm,
"bootm - boot application image from memory\n",
"[addr [arg ...]]\n - boot application image stored in memory\n"
"\tpassing arguments 'arg ...'; when booting a Linux kernel,\n"
"\t'arg' can be the address of an initrd image\n"
"\tWhen booting a Linux kernel which requires a flat device-tree\n"
"\ta third argument is required which is the address of the of the\n"
"\tdevice-tree blob. To boot that kernel without an initrd image,\n"
"\tuse a '-' for the second argument. If you do not pass a third\n"
"\ta bd_info struct will be passed instead\n"
);
五、启动内核
uboot启动内核是依赖以下两条命令:
1 | s = getenv ("bootcmd"); |
1 | run_command (s, 0); |
这里的s
为环境变量bootcmd
中的值,即
1 | nand read.jffs2 0x30007EC0 kernel;//从kernel分区读出内核放到0x30007EC0位置 |
这里的分区在源码中已经写死,位置为:include/configs/a00ask24x0.h
中的MTDPARTS_DEFAULT
宏:
1 |
|
在uboot下可以通过mtd
命令读取,kernel结果为0x0006 0000起始,大小为0x0020 0000,故原来的指令可以改写为:
1 | nand read.jffs2 0x30007EC0 0x00060000 0x00200000;//从0x00060000读出0x00200000大小数据放到0x30007EC0位置 |
nand命令
通过全局搜索nand
可以搜索到其命令函数在common/cmd_nand.c
下的do_nand函数,简化后:
1 | int do_nand(cmd_tbl_t * cmdtp, int flag, int argc, char *argv[]) |
最后是调用的nand_read_opts
读出的内核
bootm命令
Flash上储存的是uImage=头部+zImage,其中头部为:
1 | typedef struct image_header { |
这里有以下两个成员值得关注:
- ih_load:加载地址,即运行时放在那里(加载地址<—>存储地址)
- ih_ep:入口地址,运行内核是跳转的地址(运行地址<—>链接地址)
bootm命令流程:
-
读取头部信息,并根据头部信息移动内核到
ih_load
处uboot环境变量中的启动命令:
nand read.jffs2 0x30007EC0 0x00060000 0x00200000;
上述ih_load在本文所处的环境中为0x30008000 ,而头部信息刚好64字节,所以这里0x30007EC0=0x30008000 - 64,即提前将linux内核搬移到指定位置,避免bootm命令中重复搬移
-
通过
do_bootm_linux
启动内核-
设置启动参数(各种tag:开始tag、命令行tag、结束tag…)
这里的命令行tag就是uboot中的环境变量bootargs中的值
本文所处的环境中:
bootargs=noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0
-
跳转到入口地址
ih_ep
处,并将机器id和启动参数的所在地址一并传入linux内核会根据不同的机器id来决定是否支持该板卡
-