title: u-boot分析与使用
date: 2019-08-12 19:16:50
categories:


u-boot分析与使用

一、Makefile结构分析

1、make 100ask24x0_config配置芯片类型

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

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” 。
实际执行:

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的用法:

# Parameters: Target Architecture CPU Board [VENDOR] [SOC]

刚好对应mkconfig(参数) 100ask24x0(目标) arm(架构) arm920t(cpu)100ask24x0(开发板选型) NULL(供应商) s3c24x0(片上系统/芯片) 。

通过分析,精简后的mkconfig文件:

#!/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文件:

  ARCH = arm
  CPU = arm920t
  BOARD = 100ask24x0
  SOC = s3c24x0

以及include/config.h

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

mkconfig脚本总结

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

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

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

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

2、make 编译u-boot

u-boot-1.1.6/Makefile文件:

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部分为:

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

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

继续看,发现all目标:

...
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后命令行输出的命令:

......&&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段)的开头,该文件就是第一个被执行的文件

链接脚本分析

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函数做准备

    /* 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. 重定位

    //搬移代码,将完整的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由链接脚本决定

    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

    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,设此时汇编所对应的反汇编为:

#链接地址     机器码          反汇编指令
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函数

    /* 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自定义的段内,在之前的链接脚本中已经进行声明了段的起始和结束地址:

. = .;
__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定义这些命令结构体
U_BOOT_CMD(
名字,最大参数,是否可重复,cmd函数,
短字符串,
长字符串
);

例子:

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启动内核是依赖以下两条命令:

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

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

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

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

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

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

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

nand命令

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

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,其中头部为:

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;

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

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来决定是否支持该板卡