u-boot分析与使用

一、Makefile结构分析

1、make 100ask24x0_config配置芯片类型

打开u-boot-1.1.6/Makefile文件,发现:

1
2
3
4
5
6
SRCTREE    := $(CURDIR) *CURDIR是make的内嵌变量, 为当前目录
......
MKCONFIG := $(SRCTREE)/mkconfig
......
100ask24x0_config : unconfig
@$(MKCONFIG) $(@:_config=) arm arm920t 100ask24x0 NULL s3c24x0

上述$(SRCTREE)等于​$(CURDIR),也就是当前目录u-boot-1.1.6,所以MKCONFIG=./mkconfig
上述$(@:_config=)的结果就是将 “100ask24x0_config” 中的 “_config” 去掉, 结果为 “100ask24x0” 。
实际执行:

1
2
mkconfig 100ask24x0 arm arm920t 100ask24x0 NULL s3c24x0
$0 $1 $2 $3 $4 $5 $6

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#!/bin/sh -e
# Parameters: Target Architecture CPU Board [VENDOR] [SOC]
# mkconfig 100ask24x0 arm arm920t 100ask24x0 NULL s3c24x0

APPEND=no # Default: Create new config file
BOARD_NAME="" # Name to print in make output

while [ $# -gt 0 ] ; do
#不运行
done

[ "${BOARD_NAME}" ] || BOARD_NAME="$1"#BOARD_NAME=100ask24x0
# $#:参数的个数
[ $# -lt 4 ] && exit 1#不运行
[ $# -gt 6 ] && exit 1#不运行

echo "Configuring for ${BOARD_NAME} board..."

#
# Create link to architecture specific headers
#
if [ "$SRCTREE" != "$OBJTREE" ] ; then#通过Makefile发现相等
#不运行
else
cd ./include#进入include目录
rm -f asm
ln -s asm-$2 asm#ln -s asm-arm asm:建立链接文件asm,指向asm-arm
fi

rm -f asm-$2/arch#rm -f asm-arm/arch:删除文件

if [ -z "$6" -o "$6" = "NULL" ] ; then#第6个参数为空或等于NULL
ln -s ${LNPREFIX}arch-$3 asm-$2/arch#不运行
else
ln -s ${LNPREFIX}arch-$6 asm-$2/arch#ln -s arch-s3c24x0 asm-arm/arch:建立链接文件asm-arm/arch,指向arch-s3c24x0
fi

if [ "$2" = "arm" ] ; then
rm -f asm-$2/proc#删除文件
ln -s ${LNPREFIX}proc-armv asm-$2/proc#ln -s proc-armv asm-arm/proc:建立链接文件asm-arm/proc,指向proc-armv
fi

#
# Create include file for Make
#
echo "ARCH = $2" > config.mk#新建config.mk
echo "CPU = $3" >> config.mk#追加
echo "BOARD = $4" >> config.mk

[ "$5" ] && [ "$5" != "NULL" ] && echo "VENDOR = $5" >> config.mk#不会执行

[ "$6" ] && [ "$6" != "NULL" ] && echo "SOC = $6" >> config.mk

#
# Create board specific header file
#
if [ "$APPEND" = "yes" ] #第五行:APPEND=no
then
echo >> config.h
else
> config.h #新建config.h
fi
echo "/* Automatically generated - do not edit */" >>config.h
echo "#include <configs/$1.h>" >>config.h

exit 0

发现会创建include/config.mk文件:

1
2
3
4
ARCH = arm
CPU = arm920t
BOARD = 100ask24x0
SOC = s3c24x0

以及include/config.h

1
2
/* Automatically generated - do not edit */
#include <configs/100ask24x0.h>

mkconfig脚本总结

  1. 创建相关的头文件链接:

    1
    2
    3
    ln -s asm-arm asm
    ln -s arch-s3c24x0 asm-arm/arch
    ln -s proc-armv asm-arm/proc
  2. 创建顶层makefile包含的文件include/config.mk

    1
    2
    3
    4
    ARCH = arm
    CPU = arm920t
    BOARD = 100ask24x0
    SOC = s3c24x0
  3. 创建开发板相关的头文件include/config.h

    1
    2
    /* Automatically generated - do not edit */
    #include <configs/100ask24x0.h>

2、make 编译u-boot

u-boot-1.1.6/Makefile文件:

1
2
3
4
5
6
7
8
9
10
include $(OBJTREE)/include/config.mk//调用config.mk这个文件
...
ifeq ($(ARCH),ppc)//判断config.mk这个文件中ARCH是否等于ppc
CROSS_COMPILE = powerpc-linux-
endif
ifeq ($(ARCH),arm)//判断config.mk这个文件中ARCH是否等于arm
CROSS_COMPILE = arm-linux-
endif
...
include $(TOPDIR)/config.mk

最后的config.mk是属于顶层目录的config,主要通过arm,arm920t,100ask24x0,s3c24x0来确定编译器、编译选项等

顶层的config.mk部分为:

1
2
3
...
LDFLAGS += -Bstatic -T $(LDSCRIPT) -Ttext $(TEXT_BASE) $(PLATFORM_LDFLAGS)
...

这里的LDFLAGS就是下文中的$(LDFLAGS)

  • $(LDSCRIPT):链接脚本
  • $(TEXT_BASE):board/100ask24x0/config.mk中定义,为链接脚本的起始地址

继续看,发现all目标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
ALL = $(obj)u-boot.srec $(obj)u-boot.bin $(obj)System.map $(U_BOOT_NAND)//all的依赖文件

all: $(ALL)//使用make命令,相当于执行make all
...
$(obj)u-boot.bin: $(obj)u-boot//生成u-boot.bin需要elf格式的u-boot,elf也就是通过ld链接文件生成的。
$(OBJCOPY) ${OBJCFLAGS} -O binary $< $@//执行:arm-linux-objcopy -O binary u-boot u-boot.bin
...

$(obj)u-boot: depend version $(SUBDIRS) $(OBJS) $(LIBS) $(LDSCRIPT)//生成elf格式的u-boot所需要的依赖文件
UNDEF_SYM=`$(OBJDUMP) -x $(LIBS) |sed -n -e 's/.*\(__u_boot_cmd_.*\)/-u\1/p'|sort|uniq`;\
cd $(LNDIR) && $(LD) $(LDFLAGS) $$UNDEF_SYM $(__OBJS) \
--start-group $(__LIBS) --end-group $(PLATFORM_LIBS) \
-Map u-boot.map -o u-boot

上述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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
OUTPUT_ARCH(arm)//设置输出文件的体系架构。
ENTRY(_start)//将_start这个全局符号设置成入口地址,进行复位初始化
SECTIONS//输出文件的内容布局
{
. = 0x00000000;//指定地址0x00000000,最终运行地址在0x33F80000+0x00000000

. = ALIGN(4);//代码以4字节对齐
.text ://指定.text section段(位于0x33F80000)
{
cpu/arm920t/start.o (.text)//添加第一个目标文件cpu/arm920t/start.o里面的.text代码段
board/100ask24x0/boot_init.o (.text)//添加第二个目标文件board/100ask24x0/boot_init.o里面的.text代码段
*(.text)//*(.data) 表示添加剩下的全部文件的.text代码段
}

. = ALIGN(4);
.rodata : { *(.rodata) }//指定.rodata section段(位于0x33F80000+.text section),将所有的.rodata只读数据段合并成一个.rodata只读数据段

. = ALIGN(4);
.data : { *(.data) }//指定读写数据段

. = ALIGN(4);
.got : { *(.got) }//指定got段,got段是uboot自定义的一个段

. = .;
__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段结束位置

. = ALIGN(4);
__bss_start = .;//把__bss_start赋值为当前位置,即bss段的开始位置
.bss : { *(.bss) }//指定bss段,这里NOLOAD的意思是这段不需装载,仅在执行域中才会有这段
_end = .;//把_end赋值为当前位置,即bss段的结束位置
}

发现第一个文件是cpu/arm920t/start.S

二、start.S——硬件初始化

  1. 跳到reset处,执行复位函数

  2. 设为SVC(管理员)模式、f关闭看门狗、关闭中断、CPU的相关初始化(SDRAM初始化)

  3. 设置堆栈,为之后进入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

  4. 重定位

    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
  5. 清除BSS段,全部清为0

    _bss_start 和 _bss_end由链接脚本决定

    1
    2
    3
    4
    5
    6
    7
    8
    9
    clear_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
  6. 调用C函数start_armboot

    1
    2
    ldr	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函数

总结

重定位前的代码应该使用位置无关码来写,以确保不同位置都可运行

  1. 使用相对跳转指令:B、BL
  2. 不可使用绝对地址,不可访问全局变量、静态变量(根本方法是看反汇编)
  3. 不能访问有初始值的数组,这种数组(指局部数组,全局变量不能使用)本身会存在栈中,但是初始值会在rodata段中,访问这些数据的值会用绝对地址进行访问

重定位后的跳转到SDRAM代码应该用位置有关码(LDR)来写,以确保可以跳到外部SDRAM中的运行地址去运行

三、start_armboot

该函数会:

  1. 初始化堆栈,用于自定义的malloc和free

  2. 拥有读写flash的能力

    NOR:Flash_init

    NAND:nand_init

  3. 跳到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命令实现

核心机制

  1. 所有命令通过一个结构体进行封装(cmd_tb1_t)

  2. 所有命令通过结构体中的name成员进行查找

  3. 这些结构体存在一个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)可以找到相关的定义这些结构体的方法

  1. 通过宏U_BOOT_CMD定义这些命令结构体
1
2
3
4
5
U_BOOT_CMD(
名字,最大参数,是否可重复,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"
#ifdef CONFIG_OF_FLAT_TREE
"\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"
#endif
);

五、启动内核

uboot启动内核是依赖以下两条命令:

1
s = getenv ("bootcmd");
1
run_command (s, 0);

这里的s为环境变量bootcmd中的值,即

1
2
nand read.jffs2 0x30007EC0 kernel;//从kernel分区读出内核放到0x30007EC0位置
bootm 0x30007EC0//从0x30007EC0位置启动内核

这里的分区在源码中已经写死,位置为:include/configs/a00ask24x0.h中的MTDPARTS_DEFAULT宏:

1
2
3
4
#define MTDPARTS_DEFAULT "mtdparts=nandflash0:256k@0(bootloader)," \/*0开始的256k为bootloader*/
"128k(params)," \/*接下来的128k为环境变量*/
"2m(kernel)," \
"-(root)"

在uboot下可以通过mtd命令读取,kernel结果为0x0006 0000起始,大小为0x0020 0000,故原来的指令可以改写为:

1
2
nand read.jffs2 0x30007EC0 0x00060000 0x00200000;//从0x00060000读出0x00200000大小数据放到0x30007EC0位置
bootm 0x30007EC0//从0x30007EC0位置启动内核

nand命令

通过全局搜索nand可以搜索到其命令函数在common/cmd_nand.c下的do_nand函数,简化后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
int do_nand(cmd_tbl_t * cmdtp, int flag, int argc, char *argv[])
{
int i, dev, ret;
ulong addr, off, size;
char *cmd, *s;
nand_info_t *nand;
int quiet = 0;
const char *quiet_str = getenv("quiet");

/* at least two arguments please */
if (argc < 2)
goto usage;

if (quiet_str)
quiet = simple_strtoul(quiet_str, NULL, 0) != 0;

cmd = argv[1];

......
/* read write */
if (strncmp(cmd, "read", 4) == 0 || strncmp(cmd, "write", 5) == 0) {
int read;

if (argc < 4)
goto usage;

addr = (ulong)simple_strtoul(argv[2], NULL, 16);

read = strncmp(cmd, "read", 4) == 0; /* 1 = read, 0 = write */
printf("\nNAND %s: ", read ? "read" : "write");
if (arg_off_size(argc - 3, argv + 3, nand, &off, &size) != 0)
return 1;

s = strchr(cmd, '.');
if (s != NULL &&
(!strcmp(s, ".jffs2") || !strcmp(s, ".e") || !strcmp(s, ".i"))) {
if (read) {
/* read */
nand_read_options_t opts;
memset(&opts, 0, sizeof(opts));
opts.buffer = (u_char*) addr;
opts.length = size;
opts.offset = off;
opts.quiet = quiet;
ret = nand_read_opts(nand, &opts);
} else {
/* write */
......
}
}
......

printf(" %d bytes %s: %s\n", size,
read ? "read" : "written", ret ? "ERROR" : "OK");

return ret == 0 ? 0 : 1;
}
usage:
printf("Usage:\n%s\n", cmdtp->usage);
return 1;
}

最后是调用的nand_read_opts读出的内核

bootm命令

Flash上储存的是uImage=头部+zImage,其中头部为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct image_header {
uint32_t ih_magic; /* Image Header Magic Number */
uint32_t ih_hcrc; /* Image Header CRC Checksum */
uint32_t ih_time; /* Image Creation Timestamp */
uint32_t ih_size; /* Image Data Size */
uint32_t ih_load; /* Data Load Address */
uint32_t ih_ep; /* Entry Point Address */
uint32_t ih_dcrc; /* Image Data CRC Checksum */
uint8_t ih_os; /* Operating System */
uint8_t ih_arch; /* CPU architecture */
uint8_t ih_type; /* Image Type */
uint8_t ih_comp; /* Compression Type */
uint8_t ih_name[IH_NMLEN]; /* Image Name */
} image_header_t;

这里有以下两个成员值得关注:

  • ih_load:加载地址,即运行时放在那里(加载地址<—>存储地址)
  • ih_ep:入口地址,运行内核是跳转的地址(运行地址<—>链接地址)

bootm命令流程:

  1. 读取头部信息,并根据头部信息移动内核到ih_load

    uboot环境变量中的启动命令:

    nand read.jffs2 0x30007EC0 0x00060000 0x00200000;

    上述ih_load在本文所处的环境中为0x30008000 ,而头部信息刚好64字节,所以这里0x30007EC0=0x30008000 - 64,即提前将linux内核搬移到指定位置,避免bootm命令中重复搬移

  2. 通过do_bootm_linux启动内核

    1. 设置启动参数(各种tag:开始tag、命令行tag、结束tag…)

      这里的命令行tag就是uboot中的环境变量bootargs中的值

      本文所处的环境中:

      bootargs=noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0

    2. 跳转到入口地址ih_ep处,并将机器id和启动参数的所在地址一并传入

      linux内核会根据不同的机器id来决定是否支持该板卡