title: u-boot分析与使用
date: 2019-08-12 19:16:50
categories:
打开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,打开当前目录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>
创建相关的头文件链接:
ln -s asm-arm asm
ln -s arch-s3c24x0 asm-arm/arch
ln -s proc-armv asm-arm/proc
创建顶层makefile包含的文件include/config.mk
ARCH = arm
CPU = arm920t
BOARD = 100ask24x0
SOC = s3c24x0
创建开发板相关的头文件include/config.h
/* Automatically generated - do not edit */
#include <configs/100ask24x0.h>
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)
$(LDSCRIPT)
:链接脚本$(TEXT_BASE)
:board/100ask24x0/config.mk中定义,为链接脚本的起始地址继续看,发现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
跳到reset处,执行复位函数
设为SVC(管理员)模式、f关闭看门狗、关闭中断、CPU的相关初始化(SDRAM初始化)
设置堆栈,为之后进入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
重定位
//搬移代码,将完整的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由链接脚本决定
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
调用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函数
总结:
重定位前的代码应该使用位置无关码来写,以确保不同位置都可运行
- 使用相对跳转指令:B、BL
- 不可使用绝对地址,不可访问全局变量、静态变量(根本方法是看反汇编)
- 不能访问有初始值的数组,这种数组(指局部数组,全局变量不能使用)本身会存在栈中,但是初始值会在rodata段中,访问这些数据的值会用绝对地址进行访问
重定位后的跳转到SDRAM代码应该用位置有关码(LDR)来写,以确保可以跳到外部SDRAM中的运行地址去运行
该函数会:
初始化堆栈,用于自定义的malloc和free
拥有读写flash的能力
NOR:Flash_init
NAND:nand_init
跳到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中的环境变量中取出对应的数据,后者负责执行相关的命令
所有命令通过一个结构体进行封装(cmd_tb1_t)
所有命令通过结构体中的name成员进行查找
这些结构体存在一个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)可以找到相关的定义这些结构体的方法
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
可以搜索到其命令函数在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
读出的内核
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命令流程:
读取头部信息,并根据头部信息移动内核到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来决定是否支持该板卡