24、Bootloader开发入门:Android Bootloader源码结构、链接脚本与启动汇编

说实话,Bootloader 这玩意儿,很多做上层应用开发的兄弟可能一辈子都碰不到。但你要是想深入 Android 底层,搞系统移植、做安全启动,那这块就是绕不开的坎儿。

我记得我刚入行那会儿,第一次看 Bootloader 源码,整个人是懵的。一堆汇编,一堆看不懂的宏定义,链接脚本更是天书。后来硬着头皮啃了几个月,才慢慢理清头绪。今天我就把这点经验分享给你。

Bootloader 源码结构:别被目录吓到

我们先看看 Android 常用的 Bootloader——Little Kernel(LK)的源码结构。说白了,它就是个微内核,负责初始化硬件、加载内核。

bootable/bootloader/lk/
├── arch/              # 架构相关代码(ARM、ARM64、x86)
│   ├── arm/
│   │   ├── start.S    # 启动汇编入口
│   │   └── crt0.S     # C 运行时初始化
│   └── arm64/
│       ├── start.S
│       └── crt0.S
├── target/            # 板级支持包(BSP)
│   └── msm8953/       # 以高通 MSM8953 为例
│       ├── include/   # 板级头文件
│       ├── platform.c # 平台初始化
│       └── rules.mk   # 编译规则
├── lib/               # 通用库
│   ├── libc/          # 迷你 C 库
│   └── heap/          # 内存管理
├── app/               # 应用层(如 fastboot)
│   └── fastboot/
│       ├── fastboot.c
│       └── protocol.c
├── include/           # 全局头文件
├── makefile           # 顶层 Makefile
└── kernel/            # 内核加载逻辑

你可能会问:「这么多目录,我该从哪看起?」

我个人习惯,先看 arch/arm/start.S。因为这是 Bootloader 的第一行代码,是一切的开端。

核心要点: Bootloader 源码分三层——架构层(arch)、板级层(target)、应用层(app)。架构层管 CPU 初始化,板级层管外设,应用层管用户交互。

链接脚本:告诉编译器怎么排兵布阵

链接脚本(Linker Script)是 Bootloader 开发里最容易忽略、但又最关键的部分。它决定了你的代码和数据在内存里怎么摆放。

我见过一个坑:有人改了代码,Bootloader 就是跑不起来。查了两天,最后发现是链接脚本里某个段的地址写错了。嗯,从那以后我再也不敢随便改 .lds 文件了。

我们看一个典型的 LK 链接脚本片段:

/* arch/arm/system-onesegment.ld */
SECTIONS
{
    . = 0x8F000000;  /* 起始地址,由 SoC 决定 */

    .text : {
        *(.text.start)   /* 启动代码最先放 */
        *(.text*)        /* 其他代码 */
    }

    .rodata : {
        *(.rodata*)
    }

    .data : {
        *(.data*)
    }

    .bss : {
        *(.bss*)
    }

    . = ALIGN(4);
    _end = .;
}

这里有几个关键点:

  • 起始地址:0x8F000000 是 SoC 规定的 Bootloader 加载地址。不同芯片不一样,高通、MTK、展讯各有各的规矩。
  • .text.start:这个段必须放在最前面。因为 CPU 上电后,PC 指针直接跳到这里。
  • ALIGN(4):4 字节对齐。ARM 指令要求对齐,不对齐会触发异常。
我的经验: 调试链接脚本时,可以用 readelf -S 查看生成的 ELF 文件,确认每个段的地址对不对。我每次改完 .lds 都会跑一遍这个命令。

启动汇编:从 CPU 上电到 C 世界

好,现在到了最硬核的部分——启动汇编。说白了,就是用汇编语言把 CPU 从「裸机状态」带到「能跑 C 代码」的状态。

我们以 ARM64 的 start.S 为例,一步步拆解:

/* arch/arm64/start.S */
.section .text.start
.globl _start

_start:
    /* 1. 设置异常向量表 */
    ldr x0, =vector_table
    msr vbar_el1, x0

    /* 2. 设置栈指针 */
    ldr x0, =stack_top
    mov sp, x0

    /* 3. 清零 BSS 段 */
    ldr x0, =__bss_start
    ldr x1, =__bss_end
    mov x2, #0
1:  str x2, [x0], #8
    cmp x0, x1
    b.lo 1b

    /* 4. 跳转到 C 代码 */
    bl main
    b .

这段代码虽然短,但每一步都至关重要:

  1. 设置异常向量表:CPU 遇到中断、异常时,会查这张表。不设的话,随便一个异常系统就崩了。
  2. 设置栈指针:C 语言需要栈来存局部变量、函数调用。没栈,C 代码跑不起来。
  3. 清零 BSS 段:BSS 段存的是未初始化的全局变量。不清零的话,这些变量初始值可能是随机的。
  4. 跳转到 main:终于进入 C 世界了。main 函数里会做更复杂的初始化。
注意: 不同 SoC 的启动流程有差异。比如高通平台,Bootloader 之前还有 PBL(Primary Boot Loader),它会先初始化 DDR,再把 LK 加载到内存。所以 LK 的 start.S 里不需要初始化 DDR。

核心逻辑流程图

为了让你更直观地理解整个启动流程,我画了一张图:

Android Bootloader 启动流程 SoC 上电 → PBL(固化 ROM 代码) PBL 加载 LK 到 DDR(链接脚本指定地址) start.S:设置异常向量、栈、清零 BSS main():平台初始化、加载内核 进入 fastboot 或启动内核

避坑指南:我踩过的那些雷

做 Bootloader 开发,调试手段极其有限。没有 printf,没有 logcat,全靠 LED 灯和串口输出。我总结几个常见问题:

  • 链接脚本地址写错:代码加载地址和运行地址不一致,直接跑飞。用 objdump -d 检查反汇编,确认地址对不对。
  • 栈空间不够:Bootloader 的栈通常只有几 KB。递归调用或者局部变量太大,栈溢出,系统莫名其妙重启。
  • BSS 段没清零:全局变量初始值随机,条件判断出错。我遇到过因为 BSS 没清零,导致 USB 枚举失败,折腾了一整天。
  • 异常向量表没对齐:ARM64 要求异常向量表 2KB 对齐。不对齐的话,CPU 直接触发异常。
调试技巧: 在 start.S 里加 GPIO 翻转代码,用示波器看波形,可以精确测量每段代码的执行时间。这招我在调 DDR 初始化时序时屡试不爽。

总结

Bootloader 开发,说白了就是跟硬件打交道。源码结构要理清,链接脚本要谨慎,启动汇编要逐行理解。

我个人觉得,学 Bootloader 最好的方法就是动手改。找一块开发板,试着改链接脚本的地址,看看会出什么现象。改崩了不怕,反正有烧录工具能救回来。

嗯,今天就聊到这儿。记住一句话:Bootloader 是 Android 安全的基石,也是系统移植的第一关。搞懂它,你离底层专家就更近一步了。


公众号:蓝海资料掘金营,微信deep3321