Introduction to Computer Systems II @ Fudan University, spring 2020.
U-Boot for MIPS SoC 研究报告
Das U-Boot - the Universal Boot Loader 是一个开源的用于嵌入式系统的引导加载程序,支持多种计算机体系结构,包括 ARM, RISC-V, MIPS, x86 等等。本文将在 MIPS 架构下试分析 U-Boot 的启动流程,自知理解尚浅,也没能找到完备的参考资料,如有错误还望指正。
0. 简单启动
对于一个最精简的引导加载程序(bootloader)来说,至少需要完成以下几项工作:[^1]
- 初始化硬件
- 将内核(kernel)镜像文件加载到 RAM 中
- 将启动参数传给内核
- 跳转到内核函数入口
如此精简的引导加载程序通常可以直接以汇编的形式保存在 ROM / FLASH 中。(为什么选择 ROM / FLASH?因为 RAM 是易失性存储器,断电就会丢失数据,而硬盘又不能被 CPU 直接访问。)但对于像 U-Boot 这样比较复杂的引导加载程序来说,ROM 的空间大小限制(512 B)难以保存完整的启动代码,因此就需要分为几个阶段:[^1] [^2]
- 加载 ROM / FLASH 中保存的汇编形式的 Primary Program Loader,初始化硬件,其中包括 Secondary Program Loader (SPL) 所需的内部 SRAM;然后在 SRAM 中载入 SPL,并跳转到其入口
- 在 SRAM 中的 SPL 可以使用 C 语言实现更复杂的功能了,本阶段的工作主要是初始化所有硬件,其中包括内核所需的外部 DRAM(First stage bootloader)
- 最后在 DRAM 中载入内核镜像文件,将启动参数传给内核,跳转到内核函数入口(Second stage bootloader)
以下将详细介绍 U-Boot 在这几个阶段具体完成了哪些工作。
1. U-Boot 启动流程
1.1 Primary Program Loader
Primary Program Loader 的代码位于 arch/mips/cpu/start.S,从 ENTRY(_start)
开始执行,到 END(_start)
结束。目前代码在 ROM / FLASH 中执行,本阶段主要完成了以下工作:[^3]
1.1.1 初始化异常向量(exception vector)
.macro uhi_mips_exception
move k0, t9 # preserve t9 in k0
move k1, a0 # preserve a0 in k1
li t9, 15 # UHI exception operation
li a0, 0 # Use hard register context
sdbbp 1 # Invoke UHI operation
.endm
#if defined(CONFIG_ROM_EXCEPTION_VECTORS)
/*
* Exception vector entry points. When running from ROM, an exception
* cannot be handled. Halt execution and transfer control to debugger,
* if one is attached.
*/
.org 0x200
/* TLB refill, 32 bit task */
uhi_mips_exception
.org 0x280
/* XTLB refill, 64 bit task */
uhi_mips_exception
.org 0x300
/* Cache error exception */
uhi_mips_exception
.org 0x380
/* General exception */
uhi_mips_exception
.org 0x400
/* Catch interrupt exceptions */
uhi_mips_exception
.org 0x480
/* EJTAG debug exception */
1: b 1b
nop
.org 0x500
#endif
1.1.2 初始化协处理器 CP0 的寄存器
协处理器 CP0 用于处理异常和中断,这里初始化了以下寄存器:[^4]
CP0_COUNT
:处理器周期计数器
mtc0 zero, CP0_COUNT # clear cp0 count for most accurate boot timing
CP0_STATUS
:处理器状态及控制器
/* Init CP0 Status */
4: mfc0 t0, CP0_STATUS
and t0, ST0_IMPL
or t0, ST0_BEV | ST0_ERL | STATUS_SET
mtc0 t0, CP0_STATUS
CP0_WATCHLO
,CP0_WATCHHI
:(若CP0_CONFIG1
未实现则初始化)断点(watchpoint)地址及控制器
/*
* Check whether CP0 Config1 is implemented. If not continue
* with legacy Watch register initialization.
*/
mfc0 t0, CP0_CONFIG
bgez t0, wr_legacy
nop
wr_legacy:
MTC0 zero, CP0_WATCHLO
mtc0 zero, CP0_WATCHHI
CP0_CAUSE
:上次异常的原因CP0_COMPARE
:时钟中断控制器(与CP0_COUNT
一起使用)
wr_done:
/* Clear WP, IV and SW interrupts */
mtc0 zero, CP0_CAUSE
/* Clear timer interrupt (CP0_COUNT cleared on branch to 'reset') */
mtc0 zero, CP0_COMPARE
1.1.3 禁用缓存(如果需要)
跳转到 mips_cache_disable
禁用缓存,代码位于 arch/mips/lib/cache_init.S。
#ifdef CONFIG_MIPS_CACHE_DISABLE
/* Disable caches */
PTR_LA t9, mips_cache_disable
jalr t9
nop
#endif
1.1.4 初始化硬件
跳转到 lowlevel_init
初始化 SRAM, I-Cache, D-Cache, CPU, DDR 等 SPL 所需的硬件,更新 CP0_CONFIG
,代码位置取决于具体开发板,如 arch/mips/mach-mtmips/mt7628/lowlevel_init.S。
#ifndef CONFIG_SKIP_LOWLEVEL_INIT
/* Initialize any external memory */
PTR_LA t9, lowlevel_init
jalr t9
nop
#endif
1.1.5 初始化缓存
跳转到 mips_cache_reset
初始化缓存,代码位于 arch/mips/lib/cache_init.S。
#ifdef CONFIG_MIPS_CACHE_SETUP
/* Initialize caches... */
PTR_LA t9, mips_cache_reset
jalr t9
nop
#endif
1.1.6 初始化栈
执行 setup_stack_gd
宏初始化栈(可以通过 CONFIG_MIPS_INIT_STACK_IN_SRAM
设置在 SRAM 中初始化栈,这里就不展开讲了)。
.macro setup_stack_gd
li t0, -16
PTR_LI t1, CONFIG_SYS_INIT_SP_ADDR
and sp, t1, t0 # force 16 byte alignment
PTR_SUBU \
sp, sp, GD_SIZE # reserve space for gd
and sp, sp, t0 # force 16 byte alignment
move k0, sp # save gd pointer
#if CONFIG_VAL(SYS_MALLOC_F_LEN) && \
!CONFIG_IS_ENABLED(INIT_STACK_WITHOUT_MALLOC_F)
li t2, CONFIG_VAL(SYS_MALLOC_F_LEN)
PTR_SUBU \
sp, sp, t2 # reserve space for early malloc
and sp, sp, t0 # force 16 byte alignment
#endif
move fp, sp
/* Clear gd */
move t0, k0
1:
PTR_S zero, 0(t0)
PTR_ADDIU t0, PTRSIZE
blt t0, t1, 1b
nop
#if CONFIG_VAL(SYS_MALLOC_F_LEN) && \
!CONFIG_IS_ENABLED(INIT_STACK_WITHOUT_MALLOC_F)
PTR_S sp, GD_MALLOC_BASE(k0) # gd->malloc_base offset
#endif
.endm
/* Set up initial stack and global data */
setup_stack_gd
1.1.7 调用函数 board_init_f()
调用函数 board_init_f()
进入下一阶段,代码位置取决于具体开发板,如 arch/mips/mach-mtmips/spl.c(在以前的版本中,位于 arch/mips/lib/board.c,此文件于 2014/11/27 被移除)。
PTR_LA t9, board_init_f
jr t9
move ra, zero
1.2 Secondary Program Loader
1.2.1 First stage bootloader
目前代码在 SRAM 中执行,本阶段主要是为 Second stage bootloader 做准备工作:[^2] [^5]
- 调用函数
spl_init()
初始化 bootstage(用于记录启动阶段的相关信息)、日志及全部硬件(包括 DRAM),代码位于 common/spl/spl.c - 调用函数
mtmips_spl_serial_init()
初始化串口(serial),函数名及代码位置取决于具体开发板,如 arch/mips/mach-mtmips/mt7628/serial.c - 调用函数
preloader_console_init()
初始化控制台(console),代码位于 common/spl/spl.c - 调用函数
board_init_r()
进入下一阶段,代码位于 common/board_r.c
1.2.2 Second stage bootloader
到了这里,引导加载程序终于有足够的资源来完成所有应当完成的工作了。目前代码在 DRAM 中执行,结合反编译(objdump)最终的 u-boot 二进制文件(MIPS 架构)得到的代码可知,本阶段依次执行了以下这些函数(即 init_sequence_r[]
),进行了一系列初始化工作,最后调用 run_main_loop()
进入主循环 main_loop()
:[^6] [^7]
initr_trace()
initr_reloc()
initr_reloc_global_data()
initr_barrier()
initr_malloc()
log_init()
initr_bootstage()
initr_console_record()
initr_binman()
initr_dm_devices()
stdio_init_tables()
serial_initialize()
initr_announce()
initr_trap()
power_init_board()
initr_flash()
initr_env()
initr_malloc_bootparams()
initr_secondary_cpu()
stdio_add_devices()
initr_jumptable()
console_init_r()
misc_init_r()
interrupt_init()
timer_init()
initr_ethaddr()
initr_net()
initr_ide()
run_main_loop()
主循环 main_loop()
的代码位于 common/main.c,主要完成的工作是设置环境变量,初始化 CLI (Command Line Interface),最后启动操作系统内核,U-Boot 的启动流程到这里就结束了。
void main_loop(void)
{
const char *s;
bootstage_mark_name(BOOTSTAGE_ID_MAIN_LOOP, "main_loop");
if (IS_ENABLED(CONFIG_VERSION_VARIABLE))
env_set("ver", version_string); /* set version variable */
cli_init();
if (IS_ENABLED(CONFIG_USE_PREBOOT))
run_preboot_environment_command();
if (IS_ENABLED(CONFIG_UPDATE_TFTP))
update_tftp(0UL, NULL, NULL);
s = bootdelay_process();
if (cli_process_fdt(&s))
cli_secure_boot_cmd(s);
autoboot_command(s);
cli_loop();
panic("No CLI available");
}
2. 研究历程
2.1 获得交叉编译工具链
由于目标架构是 MIPS,而本机则是 x86_64,因此我们需要一个交叉编译工具链(toolchain)。当然我们可以从各种地方直接下载得到,不过为了熟悉整个流程,这里我们还是手动编译了一下。相关代码已经上传到 GitHub,目标架构为 mipsel-unknown-elf
。
2.2 编译 U-Boot
配置完交叉编译环境后,就可以编译 U-Boot 了。由于手边没有开发板,我们使用 QEMU 来模拟 MIPS 环境。U-Boot 的源码位于 git://www.denx.de/git/u-boot.git。
这里主要参考的是清华的教程,在此感谢清华前辈的工作。
2.3 研究 U-Boot 源码
主要就是以看源码为主,遇到理解不了的地方就上 Google 查。本来正常的思路应该是查阅官方文档,但是官方文档是 2008 年的版本,年久失修,其中提到的很多实现细节已经和当前代码有很大出入。此外,英文互联网似乎对 U-Boot 的研究分析并不太多,至少我好像搜不太到。而中文互联网又或者是在研究旧版本(然后互相抄来抄去),或者是在研究 ARM 架构,较新版本 MIPS 架构的相关资料也比较少见。所以整个研究过程还是比较煎熬的,资料匮乏,缺少指导,自己也没什么头绪。大概也只能研究到这种程度了(
研究期间遇到一处有趣的事情,就是 MIPS 架构是默认有一个 delay slot 的,但是我不知道。然后我就很困惑源码里 branch 以后,b 指令下面的那条指令什么时候执行。当然实际上它会在 branch 时就得到执行,虽然我们自己设计 MIPS CPU 时会把它 discard 掉,但按照规范它其实应该这样做。
2.4 心得体会
总体就是这样。其实我对这样的研究过程不太满意,我希望能有一个更完善的引导过程,像北航和清华一样(北航的 manual 也给了我一些启发),不然全程就会处于一种莫名其妙的状态,没有方向。毕竟我们目前还没有系统上过 OS 课程(只是学了个基础),这个期末项目说实在的,有点高估了学生的自学能力。
3. 参考资料
[^1]: Understanding Of U-Boot - So, today I thought...
[^2]: u-boot SPL 启动流程 - wowothink
[^3]: u-boot/u-boot/arch/mips/cpu/start.S - GitHub
[^4]: MIPS32™ Architecture For Programmers Volume III: The MIPS32™ Privileged Resource Architecture
[^5]: u-boot/u-boot/arch/mips/mach-mtmips/spl.c - GitHub
[^6]: u-boot/u-boot/common/board_r.c - GitHub
[^7]: u-boot 启动流程 - wowothink
版权声明:本文为原创文章,版权归 Hakula 所有。
本文链接:https://hakula.xyz/os/u-boot_mips.html
本文采用 CC BY-NC-SA 4.0 许可协议 进行许可。
5 条评论