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 的第一行代码,是一切的开端。
链接脚本:告诉编译器怎么排兵布阵
链接脚本(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 .
这段代码虽然短,但每一步都至关重要:
- 设置异常向量表:CPU 遇到中断、异常时,会查这张表。不设的话,随便一个异常系统就崩了。
- 设置栈指针:C 语言需要栈来存局部变量、函数调用。没栈,C 代码跑不起来。
- 清零 BSS 段:BSS 段存的是未初始化的全局变量。不清零的话,这些变量初始值可能是随机的。
- 跳转到 main:终于进入 C 世界了。main 函数里会做更复杂的初始化。
核心逻辑流程图
为了让你更直观地理解整个启动流程,我画了一张图:
避坑指南:我踩过的那些雷
做 Bootloader 开发,调试手段极其有限。没有 printf,没有 logcat,全靠 LED 灯和串口输出。我总结几个常见问题:
- 链接脚本地址写错:代码加载地址和运行地址不一致,直接跑飞。用
objdump -d检查反汇编,确认地址对不对。 - 栈空间不够:Bootloader 的栈通常只有几 KB。递归调用或者局部变量太大,栈溢出,系统莫名其妙重启。
- BSS 段没清零:全局变量初始值随机,条件判断出错。我遇到过因为 BSS 没清零,导致 USB 枚举失败,折腾了一整天。
- 异常向量表没对齐:ARM64 要求异常向量表 2KB 对齐。不对齐的话,CPU 直接触发异常。
总结
Bootloader 开发,说白了就是跟硬件打交道。源码结构要理清,链接脚本要谨慎,启动汇编要逐行理解。
我个人觉得,学 Bootloader 最好的方法就是动手改。找一块开发板,试着改链接脚本的地址,看看会出什么现象。改崩了不怕,反正有烧录工具能救回来。
嗯,今天就聊到这儿。记住一句话:Bootloader 是 Android 安全的基石,也是系统移植的第一关。搞懂它,你离底层专家就更近一步了。
公众号:蓝海资料掘金营,微信deep3321