MTK安全启动分析1:Preloader启动汇编详解
📅 2026/7/11 6:18:05
👁️ 阅读次数
📝 编程学习
init.s文件介绍
在preloader/platform/mt6761/src/init/init.s是 MTK 平台 preloader 的AArch32 启动入口汇编,运行在芯片内部 SRAM 中(此时 DDR 内存尚未初始化),是 BROM 验签通过后跳转执行的第一段代码。
核心执行主线:保存启动参数 → CPU 硬件勘误修复 → 处理器模式/中断配置 → 缓存/总线时序优化 → 清零 BSS 段 → 初始化栈指针 → 跳转 C 语言 main 函数。
代码后半段为导出给 C 语言调用的工具函数(缓存操作、架构切换等),不属于启动顺序执行链路。
.section .text.start .equ MODE_USR ,0x10 .equ MODE_FIQ ,0x11 .equ MODE_IRQ ,0x12 .equ MODE_SVC ,0x13 .equ MODE_MON ,0x16 .equ MODE_ABT ,0x17 .equ MODE_UNDEF ,0x1B .equ MODE_SYS ,0x1F .equ I_BIT ,0x80 .equ F_BIT ,0x40 .equ INT_BIT ,0xC0 .extern sys_stack .extern sys_stack_sz .extern bl31_base_addr .extern rst_vector_base_addr /* bldr argument address */ .globl bldr_args_addr bldr_args_addr: .word 0x0 .globl _start .type _start,STT_FUNC _start: b resethandler bss1_start: .word _bss1_start bss1_end: .word _bss1_end bss2_start: .word _bss2_start bss2_end: .word _bss2_end stack: .long sys_stack stacksz: .long sys_stack_sz resethandler: LDR r6, =bldr_args_addr STR r4, [r6] MOV r0, #0 MOV r1, #0 MOV r2, #0 MOV r3, #0 MOV r4, #0 MOV r5, #0 MOV r6, #0 MOV r7, #0 MOV r8, #0 MOV r9, #0 MOV r10, #0 MOV r11, #0 MOV r12, #0 MOV sp, #0 MOV lr, #0 /* CONFIG_ARM_ERRATA_826319 */ mrc p15, 0, r8, c1, c0, 0 @ Read System Control Register into Rt bic r8, r8, #0x4 @ disable D-Cache bic r8, r8, #0x1000 @ clear I-Cache mcr p15, 0, r8, c1, c0, 0 @ Write Rt to System Control Register mrc p15, 1, r8, c15, c0, 0 @ Read L2ACTLR into Rt orr r8, r8, #0x8 @ Set Bit[3]=1, disable eviction transaction bic r8, r8, #0x1 << 14 @ Set Bit[14]]0 mcr p15, 1, r8, c15, c0, 0 @ Write Rt to L2ACTLR isb /* CONFIG_ARM_ERRATA_836870 */ /** CONFIG_ARM_ERRATA_836870=y (for 6595/6752/6735, prior to r0p4) * Prog CatC, * Non-allocating reads might prevent a store exclusive from passing * worksround: set the CPUACTLR.DTAH bit. * The CPU Auxiliary Control Register can be written only when the system * is idle. ARM recommends that you write to this register after a powerup * reset, before the MMU is enabled, and before any ACE or ACP traffic * begins. **/ mrrc p15, 0, r8, r9, c15 @ Read CPUACTLR into Rt, Rt2 orr r8, r8, #0x1 << 24 @ Set Bit[24]=1, set the CPUACTLR.DTAH bit. mcrr p15, 0, r8, r9, c15 @ Write Rt, Rt2 to CPUACTLR isb /* CONFIG_ARM_ERRATA_855873 */ mrrc p15, 0, r8, r9, c15 @ Read CPUACTLR into Rt(Low), Rt2(High) orr r9, r9, #0x1 << 12 @ Set Bit[44]=1, set the CPUACTLR.ENDCCASCI bit. mcrr p15, 0, r8, r9, c15 @ Write Rt, Rt2 to CPUACTLR isb /* set the cpu to SVC32 mode */ MRS r0,cpsr BIC r0,r0,#0x1f ORR r0,r0,#0xd3 MSR cpsr,r0 /* disable interrupt */ MRS r0, cpsr MOV r1, #INT_BIT ORR r0, r0, r1 MSR cpsr_cxsf, r0 /* * L2 write to write access 2T/3T programmable * Program before I/D bits enable * 0x0c53_0604 bit[21] set to 1’b1 * 1’b0 -> L2 write to write 2T (default) * 1’b1 -> L2 write to write 3T */ ldr r0, =0x0C530604 ldr r1, [r0] orr r1, r1, #0x1 << 21 str r1, [r0] dsb isb /* enable I+Z+SMP bits and disable D bit */ MRC p15, 0, ip, c1, c0, 0 ORR ip, ip, #0x1840 /* I+Z+SMP bits */ BIC ip, ip, #0x4 /* C bit */ MCR p15, 0, ip, c1, c0, 0 /* Make BROM connect to META mode more stable, request by Chandler.Li */ DSB ISB clear_bss1 : LDR r0, bss1_start /* find start of bss segment */ LDR r1, bss1_end /* stop here */ MOV r2, #0x00000000 /* clear */ CMP r0, r1 BEQ clear_bss2 /* clear loop... */ clbss1_l : STR r2, [r0] ADD r0, r0, #4 CMP r0, r1 BNE clbss1_l clear_bss2 : LDR r0, bss2_start /* find start of bss segment */ LDR r1, bss2_end /* stop here */ MOV r2, #0x00000000 /* clear */ CMP r0, r1 BEQ setup_stk /* clear loop... */ clbss2_l : STR r2, [r0] ADD r0, r0, #4 CMP r0, r1 BNE clbss2_l setup_stk : /* setup stack */ LDR r0, stack LDR r1, stacksz /* buffer overflow detect pattern */ LDR r2, =0xDEADBEFF STR r2, [r0] LDR r1, [r1] SUB r1, r1, #0x04 ADD r1, r0, r1 MOV sp, r1 entry : LDR r0, =bldr_args_addr B main .globl jump .type jump,STT_FUNC jump: MOV r4, r1 /* r4 argument */ MOV r5, r2 /* r5 argument */ MOV pc, r0 /* jump to addr */ .globl apmcu_icache_invalidate .type apmcu_icache_invalidate,STT_FUNC apmcu_icache_invalidate: MOV r0, #0 MCR p15, 0, r0, c7, c5, 0 /* CHECKME: c5 or c1 */ BX lr .globl apmcu_isb .type apmcu_isb,STT_FUNC apmcu_isb: ISB BX lr .globl apmcu_disable_icache .type apmcu_disable_icache,STT_FUNC apmcu_disable_icache: MOV r0,#0 MCR p15,0,r0,c7,c5,6 /* Flush entire branch target cache */ MRC p15,0,r0,c1,c0,0 BIC r0,r0,#0x1800 /* I+Z bits */ MCR p15,0,r0,c1,c0,0 BX lr .globl apmcu_disable_smp .type apmcu_disable_smp,STT_FUNC apmcu_disable_smp: MRC p15,0,r0,c1,c0,1 BIC r0,r0,#0x040 /* SMP bit */ MCR p15,0,r0,c1,c0,1 BX lr .section .text.arch64 .globl jumparch64 .type jumparch64,STT_FUNC jumparch64: /* r0 as bl31 bootarg and r1 as reserved variable for ATF */ /* setup the reset vector base address after warm reset to Aarch64 */ LDR r5, =bl31_base_addr LDR r5,[r5] LDR r6, =rst_vector_base_addr LDR r6,[r6] str r5,[r6] /* setup the excution state after warm reset: 1:Aarch64, 0:Aarch32 */ MRC p15,0,r6,c12,c0,2 orr r6, r6, #1 MCR p15,0,r6,c12,c0,2 DSB ISB /* do warm reset:reset request */ MRC p15,0,r6,c12,c0,2 orr r6, r6, #2 MCR p15,0,r6,c12,c0,2 DSB ISB .globl WFI_LOOP .type WFI_LOOP,STT_FUNC WFI_LOOP: /* enter WFI to request a warm reset */ WFI B WFI_LOOP .globl jumparch64_slt .type jumparch64_slt,STT_FUNC jumparch64_slt: /* setup the reset vector base address after warm reset to Aarch64 */ /* ldr r1,=RVBADDRESS_CPU0 */ /* ldr r1,[r1] */ /* LDR r0, =0x40000000 */ LDR r0, =0x40000000 LDR r1, =0x10200038 str r0,[r1] /* setup the excution state after warm reset: 1:Aarch64, 0:Aarch32 */ MRC p15,0,r0,c12,c0,2 orr r0, r0, #1 MCR p15,0,r0,c12,c0,2 DSB ISB /* do warm reset:reset request */ MRC p15,0,r0,c12,c0,2 orr r0, r0, #2 MCR p15,0,r0,c12,c0,2 DSB ISB /* set r0 as 0x40000300 for dtb */ ldr r0, =0x40000300 1: /* enter WFI to request a warm reset */ WFI B 1blink_descriptor.ld链接脚本:定义整个 preloader 的内存布局规则
链接脚本的核心作用是规定「代码、数据、栈、BSS 分别放在内存的什么地址、按什么顺序排列」,同时指定程序的入口点,是整个镜像的「地址地图」。
OUTPUT_ARCH(arm) ENTRY(_start) romBase = 0x00201000; bss1Base = 0x00102180; bss2Base = 0x00108C00; stackBase = 0x00200000; MEMORY { bss1 : ORIGIN = 0x00102180, LENGTH = 0x6C00 rom : ORIGIN = 0x00201000, LENGTH = 0x3F000 bss2 : ORIGIN = 0x00108C00, LENGTH = 0x5000 stack : ORIGIN = 0x00200000, LENGTH = 0xC00 } SECTIONS { /* rom region */ . = romBase; .start ALIGN(4) : { *(.text.start) } >rom . = romBase + 0x01FC; .rom_info ALIGN(4) : { *(.data.rom_info) } >rom .text ALIGN(4) : { *(.text) *(.text.*) } >rom .rodata ALIGN(4) : { *(.rodata) *(.rodata.*) } >rom .data ALIGN(4) : { *(.data) *(.data.*) } >rom .got ALIGN(4) : { *(.got) *(.got.*) } >rom __boot_end = .; /* bss1 region */ . = bss1Base; .bss1 ALIGN(16) : { _bss1_start = .; *print.o (.bss.log_sram_buf) *efi.o (.bss.gpt_sram_buf) *efi.o (.bss.gpt_sram_crc32_table) *efi.o (.bss.gpt_sram_part_info) *efi.o (.bss.gpt_sram_part_meta_info) *dramc_*.o (.bss .bss.* COMMON) /* make _bss1_end as 4 bytes alignment */ . = ALIGN(4); _bss1_end = .; } >bss1 ASSERT((SIZEOF(.bss1) < LENGTH(bss1)), "Error: .bss1 over size!") /* stack */ . = stackBase; .stack ALIGN(4) : { _stack_start = .; *(.bss.sys_stack) _stack_end = .; } >stack /* bss2 region */ . = bss2Base; .bss2 ALIGN(16) : { _bss2_start = .; *(.bss) *(.bss.*) *(COMMON) /* make _bss2_end as 4 bytes alignment */ . = ALIGN(4); _bss2_end = .; } >bss2 ASSERT((SIZEOF(.bss2) < LENGTH(bss2)), "Error: .bss2 over size!") /* Clang needs .ARM.exidx, but we do not use it for debug. */ /DISCARD/ : { *(.ARM.exidx* .gnu.linkonce.armexidx.*) } }MTK preloader 运行在芯片内部 SRAM(此时 DDR 尚未初始化),因此所有地址都对应片上 SRAM 空间,和后续DDR内存无关。
1. MEMORY 命令:划分可用内存块
脚本先定义了 4 块独立的 SRAM 内存区域,对应不同用途:
内存块 | 起始地址 | 长度 | 核心用途 |
bss1 | 0x00102180 | 0x6C00 | 第一阶段BSS,存放DDR初始化、GPT、日志缓冲区等早期初始化变量 |
bss2 | 0x00108C00 | 0x5000 | 第二阶段BSS,存放普通全局未初始化变量 |
stack | 0x00200000 | 0xC00 | 栈空间,栈向下增长 |
rom | 0x00201000 | 0x3F000 | 代码只读区,存放所有指令、只读数据、已初始化全局变量 |
2. SECTIONS 命令:段的排布规则
把各个源文件的段(.text/.data/.bss 等)按顺序分配到上述内存块中,有三个核心规则:
- 入口锚定:ENTRY(_start) 声明整个程序的入口符号是 _start,BROM 加载 preloader 并验签通过后,会直接跳转到该符号对应的地址执行。
- 启动段置顶:.start 段放在 rom 区域的最开头(地址 0x00201000),内容是所有文件的 .text.start 段——也就是你给出的汇编代码所在的段,保证启动指令是镜像的第一条有效代码。
- 符号导出:在 BSS、栈等段的首尾定义 _bss1_start、_bss1_end、_bss2_start、_bss2_end 等全局符号,供汇编代码和C代码直接引用,链接时自动填充真实地址,无需硬编码。
init.s 与链接脚本的三重绑定关系
启动汇编(init.s)不是游离的代码,它通过三个维度和链接脚本精准对应,确保被链接器放到正确的内存位置、能正确访问所有内存区域。
1. 段名绑定:启动代码固定在镜像最开头
- 汇编开头用 .section .text.start 声明自身属于 .text.start 段;
- 链接脚本中 .start ALIGN(4) : { *(.text.start) } >rom 把所有 .text.start 段放到 rom 区域的最起始位置。
这就保证了上电后 BROM 跳转的第一个地址,就是汇编里的 _start 标签,不会被其他C代码挤占位置。
2. 入口符号绑定:_start 作为程序执行起点
- 汇编中用 .globl _start 导出全局符号 _start,并作为第一段代码的标签;
- 链接脚本中 ENTRY(_start) 把该符号设为整个镜像的入口点,生成的 ELF 文件入口地址就是 _start 的物理地址。
二者配合,确保BROM跳转到镜像起始地址后,能直接执行预设的启动逻辑。
3. 地址符号联动:汇编直接使用链接脚本定义的常量
汇编代码中引用的 _bss1_start、_bss1_end、_bss2_start、_bss2_end、sys_stack 等符号,都不是硬编码的数值,而是在链接脚本中定义、链接阶段自动赋值的地址常量。
举个完整的联动例子:
- 链接脚本在 .bss1 段开头定义 _bss1_start = .;,结尾定义 _bss1_end = .;,. 代表当前地址计数器;
- 汇编里用 bss1_start: .word _bss1_start 把该地址存为一个数据变量;
- 清 BSS 时,LDR r0, bss1_start 就能直接拿到 BSS 段的真实起始地址。
这种设计的好处是:修改内存布局只需改链接脚本,汇编代码无需改动,地址自动同步,适配不同SRAM配置的芯片时非常灵活。
顺序执行主流程(从上电到进入C语言)
阶段1:入口跳转与启动参数保存
代码对应
.globl _start _start: b resethandler ; 中间是数据常量区:bss起止地址、栈地址等 resethandler: LDR r6, =bldr_args_addr STR r4, [r6] ; 清零所有通用寄存器、sp、lr执行逻辑与作用
- _start 是整个镜像的入口点(链接脚本 ENTRY(_start) 指定),BROM 验签通过后直接跳转到这里。
- 第一条指令直接跳转到 resethandler,中间的 bss1_start/bss1_end/stack 等是数据常量,存放链接脚本导出的内存地址,供后续代码读取,不会被执行。
- 保存 BROM 传入的启动参数:BROM 跳转前会把启动信息(如启动原因、设备模式等)的指针放在 r4 寄存器,这里将其存入全局变量 bldr_args_addr,后续 C 语言代码可以读取该参数。
- 清零 r0~r12、sp、lr 所有通用寄存器,构建干净的启动环境,避免上电脏数据导致异常。
阶段2:CPU 硬件勘误(Errata)修复
代码对应
三段 CONFIG_ARM_ERRATA_xxx 配置,分别操作系统控制寄存器、L2 辅助控制寄存器、CPU 辅助控制寄存器。
执行逻辑与作用
这是 ARM Cortex-A 系列 CPU 的标准硬件 bug 修复步骤,必须在 MMU、数据缓存开启前完成,是 MTK 平台的强制适配项:
- Errata 826319:关闭数据缓存(D-Cache)、清除指令缓存(I-Cache),避免启动阶段缓存一致性问题。
- L2ACTLR 配置:配置二级缓存辅助控制寄存器,关闭驱逐事务、调整总线时序,提升 L2 缓存运行稳定性。
- Errata 836870:设置 CPUACTLR 的 DTAH 位(bit24),修复“非分配读可能阻碍存储排他指令执行”的硬件缺陷。
- Errata 855873:设置 CPUACTLR 的 ENDCCASCI 位(bit44,对应 r9 寄存器的 bit12),修复缓存一致性相关的硬件 bug。
- 每次寄存器配置后执行 isb(指令同步屏障),确保配置立即生效,避免流水线预取旧指令。
阶段3:处理器模式与中断配置
代码对应
; 切换到 SVC32 模式 MRS r0,cpsr BIC r0,r0,#0x1f ORR r0,r0,#0xd3 MSR cpsr,r0 ; 关闭总中断 MRS r0, cpsr MOV r1, #INT_BIT ORR r0, r0, r1 MSR cpsr_cxsf, r0执行逻辑与作用
- 强制切换到 SVC 管理模式:ARM 处理器的最高特权模式,保证后续所有系统寄存器配置都有权限执行,是引导程序的标准启动模式。
- 关闭 IRQ、FIQ 所有中断:启动阶段异常向量表尚未初始化,中断触发会导致程序跑飞;同时底层初始化不允许被打断,必须保持原子性。
阶段4:总线时序与缓存性能配置
代码对应
- L2 写时序配置(寄存器 0x0C530604,bit21 置 1)
- 开启 I-Cache、Z 分支预测、SMP 位,关闭 D-Cache
执行逻辑与作用
- L2 写时序调整:将 L2 缓存写-写访问时序从默认 2T 改为 3T,牺牲少量性能换取更高的总线稳定性,是 MTK 平台针对 SRAM 运行场景的标准优化。
- 缓存配置:
- 开启指令缓存(I-Cache)和分支预测(Z 位):提升启动阶段代码执行速度,指令缓存不涉及内存一致性问题,可提前开启。
- 开启 SMP 位:为后续多核启动做准备,提前就绪缓存一致性协议。
- 保持数据缓存(D-Cache)关闭:此时 MMU 未配置、DDR 未初始化,开启数据缓存会导致地址映射混乱、数据一致性错误,必须等内存初始化完成后再开启。
阶段5:清零 BSS 段(分两段执行)
代码对应
clear_bss1 → clbss1_l 循环 → clear_bss2 → clbss2_l 循环执行逻辑与作用
1.BSS 段的意义:存放 C 语言中未初始化的全局变量,C 语言标准要求这类变量默认值为 0。上电后 SRAM 中是随机脏数据,必须手动清零,否则变量初始值随机会导致程序异常。
2.分两段清零的原因:对应链接脚本中的 bss1 和 bss2 两块独立 SRAM 区域:
- bss1:存放 DDR 初始化、GPT、日志缓冲区等早期核心变量,在 DDR 初始化前就会使用;
- bss2:存放普通全局变量,在基础硬件初始化完成后启用。
3.清零逻辑:逐 4 字节向内存写入 0,从段首循环到段尾,完成全段清零。
阶段6:栈指针初始化
代码对应
setup_stk: LDR r0, stack ; 栈起始地址(最低地址) LDR r1, stacksz ; 栈大小 ; 写入栈溢出检测魔数 LDR r2, =0xDEADBEFF STR r2, [r0] ; 计算栈顶(高地址),栈向下增长 LDR r1, [r1] SUB r1, r1, #0x04 ADD r1, r0, r1 MOV sp, r1执行逻辑与作用
- 栈是 C 语言运行的基础:函数调用、局部变量、返回地址都依赖栈空间,ARM 栈默认向下增长(从高地址向低地址延伸),因此 sp 栈指针初始化为栈区域的最高地址。
- 栈溢出检测:在栈的最低地址写入魔数 0xDEADBEFF,后续程序可检测该值是否被覆盖,判断是否发生栈溢出,是嵌入式启动的标准调试手段。
阶段7:跳转 C 语言主函数
代码对应
entry: LDR r0, =bldr_args_addr B main执行逻辑与作用
- 将启动参数地址放入 r0,作为 main 函数的第一个入参(ARM32 C 调用约定:前四个参数放 r0~r3)。
- 执行 B main 跳转到 C 语言编写的主函数,汇编启动阶段结束,正式进入 C 语言逻辑;后续 DDR 初始化、存储驱动、LK 加载等复杂逻辑都由 C 代码实现。
非顺序执行:导出工具函数
代码后半段的 .globl 标号函数,是导出给 C 语言调用的底层操作接口,不会在启动流程中自动执行,按需调用:
1. 基础跳转与缓存操作
- jump:直接跳转到指定地址,支持传递两个参数(r4、r5),用于跳转到下一级镜像(如 LK)。
- apmcu_icache_invalidate:失效全部指令缓存
- apmcu_disable_icache:关闭指令缓存与分支预测
- apmcu_disable_smp:关闭 SMP 一致性位用于运行时动态调整缓存配置,或切换模式前的缓存清理。
2. AArch64 架构切换函数
jumparch64、jumparch64_slt 是 64 位 MTK 平台的核心逻辑:preloader 本身运行在 32 位模式,完成硬件初始化后,需要切换到 64 位模式启动 BL31(ARM Trusted Firmware),再由 BL31 启动 LK。
切换执行流程(以 jumparch64 为例)
- 读取 bl31_base_addr(BL31 镜像入口地址),写入复位向量基地址寄存器,配置暖复位后的启动入口。
- 配置系统控制寄存器,设置暖复位后进入AArch64 执行状态(默认是 AArch32)。
- 置位复位请求位,触发 CPU 暖复位(warm reset)。
- 执行 WFI(等待中断)进入低功耗,等待复位生效;复位后 CPU 以 AArch64 模式从 BL31 入口启动,完成架构切换。
整体设计逻辑总结
- 分层递进:从最底层的硬件修复,到处理器模式配置,再到 C 语言环境搭建,逐步向上传递控制权,符合嵌入式启动的通用范式。
- 适配 SRAM 限制:所有操作都在片内 SRAM 中完成,不依赖 DDR,拆分 BSS、精简栈大小都是为了适配有限的片上内存。
- 前后衔接:向上承接 BROM 的启动参数,向下对接 C 语言主逻辑,同时预留 64 位架构切换能力,完整串联起「BROM → Preloader → BL31 → LK」的 MTK 整条启动链。
编程学习
技术分享
实战经验