蒋迪的博客
| 关于 | 归档

#嵌入式 嵌入式系统

嵌入式系统

uboot/LinuxKernel/rootfs学习记录

修订时间 修订内容 修订人
2020-08-04 搭建文章框架;
补充uboot命令行模式;
JD
2020-09-01 完善uboot部分:
源码分析;
JD
2020-09-02 完善uboot部分:
uboot代码流程;
JD
2021-11-24 完善uboot部分:
uboot代码流程;
JD

uboot

U-boot(Universal Boot Loader) is an open-source, primary boot loader used in embedded devices to package the instructions to boot the device’s operating system kernel. –Wiki

基于ArmV8 学习 uboot。

我们常用的PC都是基于X86机器架构,通常PC的BIOS程序存放在Norflash中,OS存放在外部存储器磁盘中,RAM和CPU掉电后是不工作的。

上电后,Norflash中的BIOS程序先运行,BIOS主要是负责将RAM内存/GPIO/声卡/网卡等外设硬件的初始化并检查,然后将OS复制到RAM中,以完成对系统的启动。

在嵌入式设备中,uboot也是被用来解决PC下类似的问题。所以,uboot具备以下特点:

  • 上电后直接启动;
  • 管理SOC和板级外设(串口、LCD控制器等);
  • 能够引导内核并传参;
  • 具备完整系统部署的功能;
  • uboot是一个裸机程序,并且是单线程;
  • uboot的出口是启动内核,若没有启动内核,则uboot将会被一直执行;
  • uboot实现了一个shell界面;

uboot 源码文件框架

通过Linux 下 tree -L 1 -d 命令 ,我们可以获取uboot的文件框架通常如下所示:

uboot-1.0
├── api     与硬件无关的API函数
├── arch    与架构体系有关的代码
├── board   不同板子的定制代码
├── common  通用代码
├── disk    磁盘分区相关代码
├── doc
├── drivers 驱动代码
├── dts     设备树
├── examples示例代码
├── fs      文件系统
├── include 头文件
├── lib     库文件
├── nand_spl
├── net     网络相关代码
├── onenand_ipl
├── post    上电自检程序
├── spl
├── test    测试代码
└── tools

uboot 源码分析

由于整个uboot源码十分庞大,所以我们只需要分析部分重要部分的代码即可。

预备知识

学习uboot源码,需要对以下几个概念做到有些了解;

  1. ELF;
  2. uboot.lds;

1.ELF

通常,编译器编译源代码后生成的文件称作目标文件(.0),目标文件经过链接之后可以得到可执行文件。 目标文件和可执行文件缺省为ELF文件格式;

通常一个ELF文件由以下三部分构成:

  • ELF头(ELF header):描述文件主要特性(类型,CPU架构,入口地址,先右部分的大小和偏移等);
  • 程序头表(Program header table):有效的段(segments)和他们的属性;
  • 节表头(Section header table):包含对节(sections)的描述;

u-boot.elf 在编译完成后也会输出一个ELF文件,通过使用readelf工具可以查看其ELF文件信息;

查看ELF头部

readelf -h u-boot.elf 
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           AArch64
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          64 (bytes into file)
  Start of section headers:          484072 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         1
  Size of section headers:           64 (bytes)
  Number of section headers:         5
  Section header string table index: 2

查看程序头表

readelf -l u-boot.elf  

Elf file type is EXEC (Executable file)
Entry point 0x0
There are 1 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  LOAD           0x0000000000010000 0x0000000000000000 0x0000000000000000
                 0x00000000000662c0 0x00000000000662c0  RW     10000

 Section to Segment mapping:
  Segment Sections...
   00     .data 

参考:

2.uboot.lds文件

链接器脚本文件,用来描述链接器如何链接生成一个目标执行文件。

在uboot中,我们将汇编文件(start.s)生成目标文件(PS:汇编指令转为机器指令)后,需要再将目标文件链接生成可执行文件(.elf),uboot的链接过程需要lds文件传递给ld链接器用来生成可执行文件。

这里我们以u-boot.lds为例学习一下uboot的链接过程:

OUTPUT_FORMAT("elf64-littleaarch64", "elf64-littleaarch64", "elf64-littleaarch64")
OUTPUT_ARCH(aarch64)
ENTRY(_start)
SECTIONS
{
 . = 0x00000000;
 . = ALIGN(8);
 .text :
 {
  *(.__image_copy_start)
  arch/arm/cpu/armv8/start.o (.text*)
  *(.text*)
 }
 . = ALIGN(8);
 .rodata : { *(SORT_BY_ALIGNMENT(SORT_BY_NAME(.rodata*))) }
 . = ALIGN(8);
 .data : {
  *(.data*)
 }
 . = ALIGN(8);
 . = .;
 . = ALIGN(8);
 .u_boot_list : {
  KEEP(*(SORT(.u_boot_list*)));
 }
 . = ALIGN(8);
 .image_copy_end :
 {
  *(.__image_copy_end)
 }
 . = ALIGN(8);
 .rel_dyn_start :
 {
  *(.__rel_dyn_start)
 }
 .rela.dyn : {
  *(.rela*)
 }
 .rel_dyn_end :
 {
  *(.__rel_dyn_end)
 }
 _end = .;
 . = ALIGN(8);
 .bss_start : {
  KEEP(*(.__bss_start));
 }
 .bss : {
  *(.bss*)
   . = ALIGN(8);
 }
 .bss_end : {
  KEEP(*(.__bss_end));
 }
 /DISCARD/ : { *(.dynsym) }
 /DISCARD/ : { *(.dynstr*) }
 /DISCARD/ : { *(.dynamic*) }
 /DISCARD/ : { *(.plt*) }
 /DISCARD/ : { *(.interp*) }
 /DISCARD/ : { *(.gnu*) }
}

参考:

ELF与BIN文件的区别

uboot通常编译输出u-boot.elfu-boot.bin文件;那么他们彼此有什么区别呢?

#查看u-booe.bin 文件格式
file u-boot.bin 
u-boot.bin: PCX ver. 2.5 image data bounding box [8223, 54531] - [0, 0], 16 planes each of 20-bit uncompressed

#查看u-boot.elf 文件格式
file u-boot.elf 
u-boot.elf: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, not stripped
  1. uboot.bin文件,通过objcpy工具将uboot.elf文件转换而来;
  2. uboot.elf文件,通过GCC工具编译uboot源码而来;

因为uboot 和 linux kernel 启动时没有ELF loader,所以只能是raw binary格式的,即镜像文件image。直接烧录进CPU就可以直接执行,而ELF文件需要依赖操作系统的解读;

参考:

uboot启动源码分析

依据u-boot.lds文件可知,u-boot启动从Start.S文件开始,现在我们结合源码(基于ARMv8结构)分析启动流程,源码如下所示:

/* arch\arm\cpu\armv8\start.S */
#include <asm-offsets.h>
#include <config.h>
#include <linux/linkage.h>
#include <asm/macro.h>
#include <asm/armv8/mmu.h>

#include <config.h>(位于include\config.h)中,涵盖了我们目标板需要载入相关参数;

/* arch\arm\cpu\armv8\start.S */
.globl	_start
_start:
	b	reset

上电后执行的第一句代码跳转执行reset

/* arch\arm\cpu\armv8\start.S */
reset:
	/*
	 * Could be EL3/EL2/EL1, Initial State:
	 * Little Endian, MMU Disabled, i/dCache Disabled
	 */
	adr	x0, vectors
	switch_el x1, 3f, 2f, 1f
3:	msr	vbar_el3, x0
	mrs	x0, scr_el3
	orr	x0, x0, #0xf			/* SCR_EL3.NS|IRQ|FIQ|EA */
	msr	scr_el3, x0
	msr	cptr_el3, xzr			/* Enable FP/SIMD */
	ldr	x0, =COUNTER_FREQUENCY
	msr	cntfrq_el0, x0			/* Initialize CNTFRQ */
	b	0f
2:	msr	vbar_el2, x0
	mov	x0, #0x33ff
	msr	cptr_el2, x0			/* Enable FP/SIMD */
	b	0f
1:	msr	vbar_el1, x0
	mov	x0, #3 << 20
	msr	cpacr_el1, x0			/* Enable FP/SIMD */
0:

	/* Apply ARM core specific erratas */
	bl	apply_core_errata

	/*
	 * Cache/BPB/TLB Invalidate
	 * i-cache is invalidated before enabled in icache_enable()
	 * tlb is invalidated before mmu is enabled in dcache_enable()
	 * d-cache is invalidated before enabled in dcache_enable()
	 */

	/* Processor specific initialization */
	bl	lowlevel_init

#ifdef CONFIG_ARMV8_MULTIENTRY
	branch_if_master x0, x1, master_cpu

	/*
	 * Slave CPUs
	 */
slave_cpu:
	wfe
	ldr	x1, =CPU_RELEASE_ADDR
	ldr	x0, [x1]
	cbz	x0, slave_cpu
	br	x0			/* branch to the given address */
master_cpu:
	/* On the master CPU */
#endif /* CONFIG_ARMV8_MULTIENTRY */

	bl	_main
  1. 根据EL3/EL2/EL1的状态,配置MMU,i/dCache等;
  2. 配置ARM勘误表;
  3. 调用lowlevel_init:
    • 目的:允许程序执行 board_init_f();
  4. 跳转执行_main(arch\arm\lib\crt0_64.S);
/*arch\arm\lib\crt0_64.S*/
.global gd_chip_id
ENTRY(_main)

/*
 * Set up initial C runtime environment and call board_init_f(0).
 */
	ldr	x0, =(CONFIG_SYS_INIT_SP_ADDR)
	sub	x18, x0, #GD_SIZE	/* allocate one GD above SP */
	bic	x18, x18, #0x7		/* 8-byte alignment for GD */
zero_gd:
	sub	x0, x0, #0x8
	str	xzr, [x0]
	cmp	x0, x18
	b.gt	zero_gd
#if defined(CONFIG_SYS_MALLOC_F_LEN)
	sub	x0, x18, #CONFIG_SYS_MALLOC_F_LEN
	str	x0, [x18, #GD_MALLOC_BASE]
#endif
#if defined(CONFIG_TOP_RAM_CALLOC)
	sub	x0, x18, #CONFIG_TOP_RAM_CALLOC_LEN
	str	x0, [x18, #GD_TOP_RAM_BASE]
#endif
	bic	sp, x0, #0xf	/* 16-byte alignment for ABI compliance */

	mov     x0, x5
	mov     x1, x6
	mov     x2, x7
	bl      gd_chip_id
	mov	x0, #0
#if !defined(CONFIG_COMIP_TARGETLOADER)
	bl	board_init_f
#endif

设置C的运行环境并且调用board_init_f()函数。

#if !defined(CONFIG_COMIP_TARGETLOADER)
	ldr	x0, [x18, #GD_START_ADDR_SP]	/* x0 <- gd->start_addr_sp */
	bic	sp, x0, #0xf	/* 16-byte alignment for ABI compliance */
	ldr	x18, [x18, #GD_BD]		/* x18 <- gd->bd */
	sub	x18, x18, #GD_SIZE		/* new GD is below bd */
	adr	lr, relocation_return
	ldr	x9, [x18, #GD_RELOC_OFF]	/* x9 <- gd->reloc_off */
	add	lr, lr, x9	/* new return address after relocation */
	ldr	x0, [x18, #GD_RELOCADDR]	/* x0 <- gd->relocaddr */
	b	relocate_code
#endif
relocation_return:

/*
 * Set up final (full) environment
 */
	bl	c_runtime_cpu_setup		/* still call old routine */

/*
 * Clear BSS section
 */
	ldr	x0, =__bss_start		/* this is auto-relocated! */
	ldr	x1, =__bss_end			/* this is auto-relocated! */
	mov	x2, #0
clear_loop:
	str	x2, [x0]
	add	x0, x0, #8
	cmp	x0, x1
	b.lo	clear_loop

#if defined(CONFIG_COMIP_TARGETLOADER)
	bic	sp, x0, #0xf	/* 16-byte alignment for ABI compliance */
	mov	x0, #0
	b	board_init_tl
#else
	/* call board_init_r(gd_t *id, ulong dest_addr) */
	mov	x0, x18				/* gd_t */
	ldr	x1, [x18, #GD_RELOCADDR]	/* dest_addr */
	b	board_init_r			/* PC relative jump */

设置最终的C运行环境,调用board_init_r;

参考:u-boot启动流程分析(1)_平台相关部分

uboot到kernel的过程

在board_init_r的初始化函数中,最后执行到run_main_loop(common\board_r.c)函数中,即初始化的最后一个过程。

static int run_main_loop(void)
{
#if defined(CONFIG_LCD)
	drv_lcd_init();
#endif
#if defined(CONFIG_COMIP_FASTBOOT) || defined(CONFIG_USB_LC1881_MISC)
	if (gd->arch.fastboot) {
		/* FIX ME, framebuffer is overlapped with uboot,
		 * it's safe for now, for framebuffer occupies 12M of dram from the top,
		 * but only use the lowest 4M.
		 */

		extern int fastboot_init(void);
		fastboot_init();
		while(1);
	}
#endif

#if defined(CONFIG_COMIP)
	do_boot_linux();
#endif
#ifdef CONFIG_SANDBOX
	sandbox_main_loop_init();
#endif
	/* main_loop() can return to retry autoboot, if so just run it again */
	for (;;)
		main_loop();
	return 0;
}

然后跳入main_loop()(common\main.c)中

/* We come here after U-Boot is initialised and ready to process commands */
void main_loop(void)
{
	const char *s;

	bootstage_mark_name(BOOTSTAGE_ID_MAIN_LOOP, "main_loop");

#ifndef CONFIG_SYS_GENERIC_BOARD
	puts("Warning: Your board does not use generic board. Please read\n");
	puts("doc/README.generic-board and take action. Boards not\n");
	puts("upgraded by the late 2014 may break or be removed.\n");
#endif

	modem_init();
#ifdef CONFIG_VERSION_VARIABLE
	setenv("ver", version_string);  /* set version variable */
#endif /* CONFIG_VERSION_VARIABLE */

	cli_init();

	run_preboot_environment_command();

#if defined(CONFIG_UPDATE_TFTP)
	update_tftp(0UL);
#endif /* CONFIG_UPDATE_TFTP */

	s = bootdelay_process();
	if (cli_process_fdt(&s))
		cli_secure_boot_cmd(s);

	autoboot_command(s);

	cli_loop();
}

main_loop(void)中,s=bootdelay_process()函数判断在延时(BOOT_DALAY)阶段是否有按键按下,如果有按键按下,则陷入死循环等待执行敲入的命令;

如果没有按键按下,则返回s=”bootcmd”,具体如下代码所示,

const char *bootdelay_process(void)
{
	char *s;
	int bootdelay;
#ifdef CONFIG_BOOTCOUNT_LIMIT
	unsigned long bootcount = 0;
	unsigned long bootlimit = 0;
#endif /* CONFIG_BOOTCOUNT_LIMIT */

#ifdef CONFIG_BOOTCOUNT_LIMIT
	bootcount = bootcount_load();
	bootcount++;
	bootcount_store(bootcount);
	setenv_ulong("bootcount", bootcount);
	bootlimit = getenv_ulong("bootlimit", 10, 0);
#endif /* CONFIG_BOOTCOUNT_LIMIT */

	s = getenv("bootdelay");
	bootdelay = s ? (int)simple_strtol(s, NULL, 10) : CONFIG_BOOTDELAY;

#ifdef CONFIG_OF_CONTROL
	bootdelay = fdtdec_get_config_int(gd->fdt_blob, "bootdelay",
			bootdelay);
#endif

	debug("### main_loop entered: bootdelay=%d\n\n", bootdelay);

#if defined(CONFIG_MENU_SHOW)
	bootdelay = menu_show(bootdelay);
#endif
	bootretry_init_cmd_timeout();

#ifdef CONFIG_POST
	if (gd->flags & GD_FLG_POSTFAIL) {
		s = getenv("failbootcmd");
	} else
#endif /* CONFIG_POST */
#ifdef CONFIG_BOOTCOUNT_LIMIT
	if (bootlimit && (bootcount > bootlimit)) {
		printf("Warning: Bootlimit (%u) exceeded. Using altbootcmd.\n",
		       (unsigned)bootlimit);
		s = getenv("altbootcmd");
	} else
#endif /* CONFIG_BOOTCOUNT_LIMIT */
		s = getenv("bootcmd");

	process_fdt_options(gd->fdt_blob);
	stored_bootdelay = bootdelay;

	return s;
}

得到s=bootcmd后,传参给autoboot_command(s),linux内核就启动了;

void autoboot_command(const char *s)
{
	debug("### main_loop: bootcmd=\"%s\"\n", s ? s : "<UNDEFINED>");

	if (stored_bootdelay != -1 && s && !abortboot(stored_bootdelay)) {
#if defined(CONFIG_AUTOBOOT_KEYED) && !defined(CONFIG_AUTOBOOT_KEYED_CTRLC)
		int prev = disable_ctrlc(1);	/* disable Control C checking */
#endif

		run_command_list(s, -1, 0);

#if defined(CONFIG_AUTOBOOT_KEYED) && !defined(CONFIG_AUTOBOOT_KEYED_CTRLC)
		disable_ctrlc(prev);	/* restore Control C checking */
#endif
	}

#ifdef CONFIG_MENUKEY
	if (menukey == CONFIG_MENUKEY) {
		s = getenv("menucmd");
		if (s)
			run_command_list(s, -1, 0);
	}
#endif /* CONFIG_MENUKEY */
}

bootcmd 需要内核的具体参数(CONFIG_BOOTCOMMAND),通常我们在config文件中定义。

缺省CONFIG_BOOTCMD(include\env_default.h)为

#ifdef	CONFIG_BOOTCOMMAND
	"bootcmd="	CONFIG_BOOTCOMMAND		"\0"
#endif

uboot 命令行模式

进入uboot的命令行模式后,一般常用的命令如下所示:

#查看当前uboot所支持的命令
help
#查看某个命令的用法
helo bootz
#常用的信息查询命令(板子信息,输出环境变量,输出uboot版本号)
bdinfo
printenv
version
#修改环境变量
setenv
setenv bootdelay 5
setenv bootargs 'console = ttymxc0,115200 root = /dev/mmcblklp2 rootwait rw'
#删除环境变量
setenv bootargs
#保存修稿后的环境变量
saveenv
# 网络操作命令
ping
# BOOT 操作命令
# bootz命令用于自动zIamge文件
bootz
# bootm用于启动uImage镜像文件
bootm
# boot命令读取环境变量bootcmd来启动Linux系统
boot
# 其他常用命令
# 重启命令
reset
# 用于运行我们自定义的环境变量
run
# 内存读写测试命令
mtest

参考资料

uboot 参考资料

Linux Kernel

参考资料