嵌入式系统
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源码,需要对以下几个概念做到有些了解;
- ELF;
- 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.elf
与u-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
- uboot.bin文件,通过objcpy工具将uboot.elf文件转换而来;
- 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
- 根据EL3/EL2/EL1的状态,配置MMU,i/dCache等;
- 配置ARM勘误表;
- 调用lowlevel_init:
- 目的:允许程序执行 board_init_f();
- 跳转执行_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;
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
参考资料
- SPI Memories - Linux Foundation Events
- U-boot Splashscreen through SPI
- U-Boot Drivers
- Driver Model In Uboot
uboot 参考资料
- UBoot 源码分析(1)——快刀斩乱麻认识UBoot
- uboot源码简要分析
- 嵌入式linux开发uboot启动过程源码分析(一)
- uboot学习前传
- UBOOT从零开始的学习
- UBOOT源码分析(详细)
- uboot启动流程
- u-boot
- uboot启动第一阶段
- uboot启动第二阶段
- uboot启动内核、命令体系、环境变量
- u-boot
- uboot代码解析