
Reverse engineering the TI AM3358 boot ROM
从我第一次拿到几块从垃圾堆里救出来的 Beaglebone Black 板子算起,大概已经过去十八个月了。不幸的是,这些板子无法立即正常工作。这是我第一次接触这类板子,或者说第一次接触任何单板计算机,所以我不确定问题是出在我自己的操作上,还是板子本身有问题(也许这正是它们当初被扔进垃圾堆的原因)。我花了相当长的时间和大量精力,才让这些板子真正开始启动,但我他妈的做到了。以下就是我在这个过程中学到的东西。
我在这个仓库里包含了一些实用工具,以及一个从 Ghidra 导出的 xml 文件,里面包含了我目前通过逆向获得的所有符号。我使用了这篇文章中的方法进行导出,而不包含实际固件,以避免任何版权问题,以防万一。如果你想自己调试引导 ROM,你反正已经接好了 JTAG,可以自己转储引导 ROM(从 0x20000 到 0x2BFFF)。
要加载这些符号:
0x20000,同时可以将块名称设置为 bootrom。main(),或者 MMC/SD 卡引导处理程序。首先,我知道这些是标准 beaglebone black 的定制版本,所以早期我判断问题可能出在板子本身缺少某些东西,比如板标识符。当我用 balenaEtcher 格式化标准 SD 卡并启动时,看到的只是什么都没有。我原本期望板上的 LED 开始闪烁,并且期望连接 UART 转 USB 线缆后能看到 U-Boot 过程。然而,UART 一片寂静。如果我移除 SD 卡,它会反复输出字母 C,这是 UART/串行引导的预期行为。它确实在 尝试 启动,而 SD 卡改变了这种行为,但我没有更多的可见信息。网上大多数故障排查都以 U-Boot 输出作为诊断问题的起点。我想我没有那种奢侈了。
我觉得此时连接一个调试探针是值得的。不幸的是,我没有匹配现有引脚封装的接头,所以我自己做了一个。
beaglebone 板子上有一个设计编号为 P2 的接头,引出了 JTAG 连接。我将一些导线连接到这个接头,再接到一个母头上,这样我就可以通过我的 J-Link 与它通信。



启动 Ozone(Segger 调试器)后,我配置了 J-Link,并首先尝试找到入口点。我原以为复位暂停(reset-halt)会把我带到需要的位置,这就是我得出了(错误)假设——入口点是 0x2148a 的原因,尽管我确实注意到这并不一致。后来,我意识到 AM335x 板子与 J-Link 的复位暂停配合得并不好,所以实际上会有几百个时钟周期的延迟,导致我不确定地落在了某个引导处理程序内部。(我最终通过为 TI 的 Code Composer Studio 编写一个 GEL 文件绕过了这个问题,CCS 支持 J-Link 调试——复位时,PC 寄存器被设置为复位处理程序,寄存器被清零,指令模式被强制为 ARM。)
从 TI 论坛的一个帖子(AM335x: TI employees, where can I get the ROM Bootloader source code/symbols?)中,我获得了一些调试符号:SPI Initialize 位于 0x231e0,SPI ReadSectors 位于 0x23230,而 0x24bfa 是一个执行 UART 读取的例程。我想这算是不小的帮助。我注意到引导失败后会在 0x402f0440 处陷入无限循环,这是一个死循环。嗯,距离引导 ROM 的其他部分相当远,一定是在 RAM 或什么地方。大概是时候去看看技术参考手册(TRM)了!
TRM 的第 26 章包含大量关于引导的信息。我们得到了如下的引导 ROM 视图:

描述:
公共 ROM 代码的架构如图 26-1 所示。它采用自上而下的方法分为三个主要层:高层、驱动层和硬件抽象层(HAL)。一层通过统一接口与较低层通信。 高层负责公共 ROM 代码的主要任务:看门狗和时钟配置以及主引导例程。 驱动层根据接口规范为任何引导设备实现逻辑和通信协议。 最后,HAL 实现用于与硬件基础设施 IP 交互的最低层代码。终端引导设备连接到器件 IO 焊盘。

图 26-2 说明了公共 ROM 代码引导过程的高层流程。在此器件上,公共 ROM 代码在安全启动(由安全 ROM 代码执行)完成后启动。随后,ROM 代码作为公共启动过程的一部分执行平台配置和初始化。引导设备列表基于 SYSBOOT 引脚创建。引导设备可以是存储器引导设备(焊接的闪存或临时引导设备,如存储卡),也可以是连接到主机的外设接口。 引导过程的主循环遍历引导设备列表,并尝试从当前选定的引导设备搜索映像。如果找到有效的引导映像并成功执行,或者看门狗超时,则退出此循环。在 HS 器件上,映像执行之前会执行映像认证过程。认证过程失败会导致分支到安全 ROM 中的“死循环”(等待看门狗复位)。
内存映射!异常向量!流程图!这一节包含大量信息。我的工作轻松了 很多。
此时,我使用 JTAG 探针将固件下载到几个不同的文件中,并开始将内容加载到 Ghidra 中。似乎没有以方便格式提供的 SVD 文件或其他寄存器映射,这真的很不幸,因为这意味着我需要手动定义内存区域、寄存器以及所有内容。这是一个繁琐的过程,但过了一会儿,我就有了一个 Python 脚本,可以用来为 AM3358 向 Ghidra 加载符号。又少了一件需要担心的事情!
似乎我拥有的文件可以映射为:
有趣的是,位于 0x402f_0440 的无限循环位于内部 SRAM 中“下载映像”的顶部,而异常向量则存储在其他地方。也许这以后会是一个重要的线索……
复位时,私有引导 ROM 处理安全相关事务,并分支到 0x2 0000,其中包含复位向量。第一条指令是一个分支到 0x2 08d0 的指令,这必定是入口点。它不是 BX 指令,所以可以推测,此时我们仍然处于 Arm 模式。
这是第一个运行的代码,所以它并不完全是一个带参数的“函数”,而更像是一个编译生成的启动脚本。第一个基本块:```arm ldr r4,[->Peripherals::CM_PER] mov r0,#0x2c ldr r6,[r4,r0]=>CM_PER.CM_PER_OCMCRAM_CLKCTRL mov r6,#0x2 str r6,[r4,r0]=>CM_PER.CM_PER_OCMCRAM_CLKCTRL mov r0,#0x2c poll: ldr r6,[r4,r0]=>CM_PER.CM_PER_OCMCRAM_CLKCTRL cmp r6,#0x2 bne poll
此块将 OCMC RAM 时钟设置为启用:
1. 设置 `CM_PER_OCMCRAM_CLKCTRL=0x2`
2. 检查寄存器是否已设置;如果没有,则持续轮询
`CM_PER_OCMCRAM_CLKCTRL` 寄存器使用位 0 和位 1 作为 `MODULEMODE` 字段,将此设置为 `=0x2` 会启用 OCMC RAM 的时钟。
下一个基本块:```arm
ldr r0,[PTR_control_status]
ldr r0,[r0,#0x0]=>control_status
and r0,r0,#0x700
mov r0,r0, lsr #0x8
cmp r0,#0x3
bne skip
ldr r0,[PTR_control_status]
ldr r0,[r0,#0x0]=>control_status
cpy r6,r0
and r0,r0,#0x1f
cmp r0,#0x1f
bleq GPMIC_init
skip: ...
此块执行以下操作:
(control_status & 0x700) >> 8 == 0x3,如果不满足则跳过control_status & 0x1f == 0x1f,如果是,则在将 control_status 加载到 r6 后调用函数 GPMC_init下一个块设置协处理器:```arm msr cpsr_c,#0xd3 ldr r4,[->Exceptions::ROM_RESET_VECTOR] mcr p15,0x0,r4,cr12,cr0,0x0 bl LAB_00020934 bl LAB_00020938 bl LAB_0002093c bl LAB_00020940 bl LAB_00020944 bl LAB_00020948 bl LAB_0002094c bl LAB_00020950 mrc p15,0x0,r0,cr1,cr0,0x0 orr r0,r0,#0x800 mcr p15,0x0,r0,cr1,cr0,0x0 b LAB_000207f0
此块中的操作:
1. 将 `11010011b` 移入 CPSR 控制字段(`I=1`,`F=1`,`T=0`,`MODE=10011`)
1. `I` 是中断禁用位,`F` 是快速中断禁用位(因此 `I=F=1` 表示中断被禁用)
2. `T` 是 Thumb 模式,设置为 `0`
3. `MODE=10011` 将处理器模式设置为 Supervisor 模式([ref](https://developer.arm.com/documentation/ddi0406/b/System-Level-Architecture/The-System-Level-Programmers--Model/ARM-processor-modes-and-core-registers/ARM-processor-modes?lang=en#CIHGHDGI))
4. 更多信息请参阅 [此处](https://developer.arm.com/documentation/ddi0406/b/System-Level-Architecture/The-System-Level-Programmers--Model/ARM-processor-modes-and-core-registers/Program-Status-Registers--PSRs-)
2. 将地址加载到 ROM 复位向量
3. 访问 [协处理器 15](https://developer.arm.com/documentation/den0013/d/ARM-Processor-Modes-and-Registers/Registers/Coprocessor-15)(系统控制协处理器)的安全扩展寄存器 `c12`,并将 ROM 复位向量加载到 `VBAR`(向量基地址寄存器)

4. 看起来像是 `nop` 跳转?为什么用 `bl` 而不是 `b`?
5. 启用分支预测(将系统控制寄存器 `SCTLR` 的第 11 位置位)

*VMSA 实现中的 CP15 `c1` 寄存器(系统控制寄存器)*
`SCTLR` 寄存器说明:
> SCTLR 提供系统的顶层控制,包括其存储系统。
> 该寄存器属于虚拟内存控制寄存器功能组。
参见 TRM 第 B4-1687 页。第 11 位是*分支预测启用*位,将其置位即表示启用[分支预测](https://developer.arm.com/documentation/ddi0406/b/System-Level-Architecture/Common-Memory-System-Architecture-Features/Caches/Branch-predictors)。
6. 调用一个函数(通过分支到调用指令)
### 位于 `0x20894` 的函数(`__main`)
此函数由另一个早期例程分支进入。我认为它会在调用 `FUN_0002889c`(后来发现是 `main()`!)之前初始化栈,可能还会初始化定时器或看门狗。```arm
ldr sp,[->RESERVED_EXCEPTION_BRANCH] ; 0x4030ce00
blx load_stack_1
ldr r12,[DWORD_1]
add r12,r12,pc
tst r12,#0x1
adrne lr,0x208bd
cpyeq lr,pc
bx r12 ;=>init_timers_maybe
adr r12,0x208bd
bx r12 ;=>LAB_000208bc
000208bc bl FUN_0002889c
000208c0 ddw 0x109
000208c4 addr RESERVED_EXCEPTION_BRANCH
...
RESERVED_EXCEPTION_BRANCH:
ldr pc=>LAB_00020090,[PTR_LAB_4030ce20]
; 20090 is a dead loop
这里执行的操作是:
r0,r1,r2,r3,r4,lr 压入堆栈(内存映射中的公共堆栈)
load_stack_1 先跳转到一个空函数 bx lr,然后从堆栈中弹出 r0,r1,r2,r3,r4,pc(基本上将这些数据放回寄存器,并将 lr 中的值放入 pc 以返回)pc + 0x109 是否为奇数,如果是偶数则将 0x208bd 加载到 lr,否则将 pc 复制到 lrFUN_0002889c这应该是启动流程图中提到的 __main() 函数:

这将使下一个函数成为 main 函数。
如图 26-8 顶部所示,一旦 CPU 完成安全启动初始化,它就会跳转到公共 ROM 代码复位向量。进入公共模式后,在系统启动时,CPU 会执行公共侧初始化和堆栈设置(编译器自动生成的 C 初始化或“分散加载”)。然后配置看门狗定时器 1(设置为三分钟),执行系统时钟配置。最后跳转到启动例程。
0x209b0)当调用 main 时,SP 寄存器指向 0x4030ce00。这是堆栈的起始位置,堆栈向下增长至 0x4030 b800;由于在压入 4 个寄存器(相差 16 字节或 4 个字)后,我们指向地址 0x4030 cdf0,因此我们使用的是 全递减堆栈,正如 AAPCS 中所定义。也就是说,SP 指向堆栈上最近的字,并向下增长。
以下是反编译的 main() 函数:```c
int main()
{
uint local_10;
uint local_c;
local_c = 0; local_10 = 0; check_stack_prm(&local_10); update_coldreset_tracing_vector(local_10); update_current_tracing_vector(1); main_clock_init(6,0); watchdog_softreset(); watchdog_write_disable_seq_data2(); set_watchdog(300000); if ((local_10 & 1) != 0) { update_current_tracing_vector(2); local_c = local_c & 0xffff | 1; } timer_func_1(); clock_init_func_4(&local_c); run_booting_loop(&local_c,local_10 & 0xff); return 0; }
对我来说最有趣的是位于 `0x20a10` 处的 `run_booting_loop` 函数。
### X-Loader 笔记
在完成启动过程并进入包含 "ISSW"、"CHSETTINGS" 和 "X-LOADER" 等字符串的部分后,我开始寻找这些字符串在 U-Boot 相关环境中可能出现的其他位置。我偶然发现了[这个帖子](https://forum.xda-developers.com/t/discussion-on-the-boot-loader-cracked.1378886/),里面的人们在逆向或破解 Nook 固件,而 [x-loader 源代码](https://github.com/joelagnel/x-loader/blob/f3c74bc9b01dac58e553393d6ec1041353f2f1f7/scripts/signGP.c) 中包含对 `CHSETTINGS` 之类内容的引用。通过四处查看,"ISSW" [似乎](https://github.com/u-boot/u-boot/blob/master/doc/README.ti-secure) 指的是从非内存设备启动。
回顾初始化文档中的高层代码:

值得关注的有:
- RNDIS
- FAR
- XMODEM
- BOOTP
- TFTP
- DFT
也许该再次尝试实时调试了。考虑到有大量我无法理解的数据,尝试逆向所有这些结构体可能会非常痛苦...
太好了!通过使用我找到的向量手动设置 PC 和 SP,实时调试可以正常工作了:

根据源代码和跟踪向量,我能够绘制出各种启动选项及其对应的设备编号。这在后来非常有用,因为在 SD/MMC 启动处理程序中设置断点时,我必须区分 MMC0 (8) 和 MMCSD1 (9)。
| Type | Device | Device ID |
| ---------- | ----------------- | ----------- |
| 内存 | XIP (MUX2) | 1 |
| 内存 | XIP w/WAIT (MUX2) | 2 |
| 内存 | XIP (MUX1) | 3 |
| 内存 | XIP w/WAIT (MUX1) | 4 |
| 内存 | NAND | 5 |
| 内存 | MMCSD1 | 7, 9 (eMMC) |
| 内存 | NAND_I2C | 10 |
| 内存 | MMC0 | 8, 12 (SD) |
| 外设 | UART0 | 16 |
| 外设 | USB | 20 |
| 外设 | GPGMAC0 | 22 |
有关处理器启动方式的信息,请参阅 TRM 和 [这个 Stack Exchange 上的回答](https://stackoverflow.com/a/31252989/8565545)。总结如下:
1. 引导 ROM 已识别 SD 卡上的 MLO (Mmc LOader) 文件,并将其复制到 SRAM
2. 这是二级程序加载器,一个较小的引导加载程序,用于初始化完整的 RAM,并将完整的 U-Boot 二进制文件复制到其中执行
3. U-Boot 二进制文件运行后,我们(或者说 U-Boot)最终启动内核
### `run_booting_loop()`
这是主启动循环。它会无限运行,或者一直运行到执行分支到另一个将被加载到 RAM 中的引导加载程序为止。
过程开始,没有跟踪向量更新:
- 查找设备类型
- 如果设备类型为 5(安全设备),则执行其他初始化
- 运行 `build_boot_list(int,buffer[],data[],int)`
- `buffer[]` 被初始化为 `0xff`,`data[]` 包含设备类型(可能)```c
void run_booting_loop(uint32_t *r0_config,undefined4 param_2,undefined4 param_3,
undefined4 default_list)
{
int iVar1;
uint j;
uint i;
int device_type;
byte alt_list [12];
undefined4 boot_status;
byte boot_list [8];
uint8_t local_buffer [8];
update_current_tracing_vector(3);
/* Device type is 3 */
lookup_device_type(&device_type);
if ((device_type == AM335X_HIGH_SECURITY) && (iVar1 = return_zero_4(), iVar1 != 0)) {
init_something_1_small(&STATIC_DATA_1);
}
/* param1 = 1
param2 = 4030 ebc4
param3 = 4030 ebb4 */
build_boot_list(*(ushort *)r0_config,boot_list,alt_list,default_list);
do {
i = 0;
local_buffer[0] = 0xff;
local_buffer[1] = 0xff;
local_buffer[2] = 0xff;
local_buffer[3] = 0xff;
do {
if (boot_list[i] - 1 < 12) {
update_current_tracing_vector(4);
/* No return unless there is an error */
boot_device_1(r0_config,boot_list[i],local_buffer);
}
else if (boot_list[i] - 65 < 8) {
update_current_tracing_vector(5);
watchdog_write_disable_seq_data2();
boot_status = 0xffffffff;
boot_device_2((uint32_t)r0_config,boot_list[i],&boot_status,local_buffer);
watchdog_write_enable_seq_data2();
if (boot_status != 0xffffffff) {
local_buffer[0] = (undefined)boot_status;
local_buffer[1] = boot_status._1_1_;
local_buffer[2] = boot_status._2_1_;
local_buffer[3] = boot_status._3_1_;
if ((boot_status & 0xffff00ff) == 0xf0030006) {
update_current_tracing_vector(9);
boot_list[i + 1] = (byte)(boot_status >> 8);
}
else if (boot_status != 0xf0030002) {
update_current_tracing_vector(8);
j = 0;
do {
if (63 < boot_list[j]) {
boot_list[j] = 0;
}
j = j + 1 & 0xff;
} while (j < 8);
}
}
}
i = i + 1 & 0xff;
} while (i < 8);
update_current_tracing_vector(6);
} while( true );
}
我发现 0x23d7a 处有一个函数,我将其命名为 boot_into_SRAM(),这是跳转到 SRAM 之前最后调用的函数,之后便进入异常处理程序。之前,我保存了一份不同的 RAM 状态,但在再次进行实时调试时(现在已过去一年多,2024 年 7 月),我意识到一定是发生了什么。这块板子成功地从 SD 卡读取数据,并在运行从卡中加载的代码!为了验证这一点,我必须在 SRAM 中找到与 SD 卡上相同的字节。事实证明,在 am335x-evm-linux-sdk-bin-.../board-support/prebuilt-images/ 中有一个名为 u-boot-spl.bin-am335x-evm 的二进制文件,该二进制文件中的代码与 SRAM 中出现的代码一致。我们已经进入了 uboot SPL!
我们已经成功启动到 SRAM,现在我想了解一下 UART 终端,它应该会显示有关 uboot 的信息。硬件连接如下。

使用 CuteCom 以 115200 @ 8-N-1 连接到设备,在没有插入 SD 卡的情况下,它只会反复输出 C。

但当插入 SD 卡时,UART 不会输出任何内容,没有任何消息或字符。故障一定是出现在启动过程太早的阶段?但现在我也知道它正在执行什么代码(并且我拥有其源代码),我应该能够为其构建一些调试符号,并开始一个正常的调试会话。这可能并不简单,我必须确保以相同的方式编译代码,可能需要一些时间来确切了解 SDK 加载到我的 SD 卡上的内容以及如何构建它。
由于 U-Boot 会向 UART 输出文本,而我什么也没看到,我猜测我们在 SPL 中的某个地方捕获到了一个异常。
此时,我花了非常多的时间在 Ghidra 中逆向并整理引导 ROM 的反编译源码,研究结构体以及每个数据成员在各函数中的使用方式,有时还涉及嵌套,这给我制造了各种麻烦。在处理这件事的同时,我觉得也是时候开始调试和编译我自己的代码了。毕竟我们已经在 RAM 中了,为什么不加载 SPL 符号,看看发生了什么?
你可以使用 Ozone 进行调试,这是 Segger 为 J-Link 提供的调试器。你还可以使用 TI 自己的 Code Composer Studio (CCS),或者其 VSCode 风格的“轻量”版本 CCS Theia。我按照这个视频成功构建了 SDK 中的所有内容:Sitara Linux Board Porting Series: Module 6。需要构建三个组件:
我按照上述系列中的 Module 7 视频 进行操作,并成功让一切运转起来,有几点说明:
s_init() 已不再存在有符号了?太棒了。现在我可以看到执行过程中发生了什么,从复位处理程序 reset() 开始,也能看到异常最终发生的位置。为了追踪问题,我在 0x402f 0440 处的异常处理程序上设置了断点,并检查了链接寄存器,其中仍保存着最近一次函数的地址。结果发现该地址是 0x402f 76ce,虽然它似乎并不总是一致。是什么导致了错误?
注意:对于调试,按照 Module 7 视频,先运行到 0x402f 0400,然后 执行 Load Memory() 这一步。每次重新启动都必须这样做。
我们进入函数 device_probe()(0x402f 74c4),然后又进入其他几个函数?然后从 do_setup_dpll()(分支位于 0x402f 07fc)开始,我们没有退出,所以继续深入该函数。单步执行后,我们回到了 crt0.S 中的 _main(),位于 0x402f 14e0。看起来我们可能正在退出 board_init_f(),然后继续执行 spl_relocate_stack_gd()。这个调用没有退出。我们到达了 dm_fixup_for_gd_move()。其中有一条指令在 0x402f 76c2 处执行失败。我想问题就在这里:它试图访问 0x81ff ff20,显然不行。我预感 SDRAM 配置存在问题。我找到了 TI 论坛上所有与类似问题相关的帖子,并发现有五六条帖子包含了一些有助于解决问题的提示。我得出结论,这可能与 (a) EMIF 调优或 (b) 软件电平校准有关。
我的板子如下所示。

内存芯片来自 Micron,而我手头的 BeagleBone Black(rev C3)原理图使用的是 Kingston DDR3 内存,具体型号为 D2516EC4BXGGB。DDR3 是 U12,我们可以使用 Micron 标记解码页面 来查找该部件:
为了确保这块芯片是正常的,我首先简单地检查了电源是否正常供应。数据手册规定其电压必须为 1.5V +/- 0.075V。我在板子底部的 R6 两端测得电压为 1.506V。我们有两个测试点,TP1 和 TP2。
如果以后有用,这里列出几个测试点。
硬件设计页面上详细描述了内存电路。
来检查一下时钟使能线。我们可以测量 R96 的两端,一端应接地,另一端应保持高电平。
确认,CKE 上有 1.5V 电压。
下一步是检查时钟信号。我尽可能做了一些工作,使用 tinySA,天线大致指向 RAM 芯片的方向。通过这种“嗅探”方式,我相当确信时钟信号是存在的,至少目前足够。
接下来,是时候看看外部存储器接口了。经常遇到的一个概念是 GEL 文件。这是一种由 Texas Instruments 为 Code Composer Studio 开发的一种解释型语言,全称是 General Extension Language。
GEL 文件包含在 DDR 内存配置工具 中。
好的!我尽可能遵循调优过程,最终成功找到了 GEL 文件的最佳值。```
The Slave Ratio Search Program Values are...
PARAMETER MAX | MIN | OPTIMUM | RANGE
DATA_PHY_RD_DQS_SLAVE_RATIO 0x071 | 0x005 | 0x03b | 0x06c DATA_PHY_FIFO_WE_SLAVE_RATIO 0x1b3 | 0x046 | 0x0fc | 0x16d DATA_PHY_WR_DQS_SLAVE_RATIO 0x0f7 | 0x01a | 0x088 | 0x0dd DATA_PHY_WR_DATA_SLAVE_RATIO 0x137 | 0x05a | 0x0c8 | 0x0dd
尝试在内存浏览器中调整 RAM 相关设置……喔!成功了!
所以内存看起来确实能工作,但 SPL 仍然失败,那么 SPL 初始化 SDRAM 的方式可能有问题?啊,对了,调优流程当然不止这些!你实际上还得更新 SPL……
我们现在越来越接近了。文件 `board.c` 通过检查我们拥有什么类型的板子来初始化 DDR。但对于这块板子,所有函数(`board_is_evm_sk()`、`board_is_icev2()`、`board_is_bone_lt()` 等)都返回 false,因此它默认使用 `config_ddr(266, ...)`,其中 266 是时钟频率(MHz),而它 *应该* 是 400 MHz。这肯定是个问题。
`board_is_bone_lt` 应该被绕过,让它始终返回 true。我这样做了,进展更进了一步,但有些事让我困扰。将新文件 MLO 加载到 SD 卡上不起作用,尽管直接加载程序运行得很好。这到底是怎么回事?我能看出被加载到 SRAM 中的代码并不是我编译的那份代码。事实上,我甚至格式化了 SD 卡,但默认的 SPL 似乎还是被加载到了 SRAM 中!我验证过,使用另一张 SD 卡时引导过程不会尝试继续。因此,引导加载程序肯定是在 SD 卡上寻找引导分区,然后将执行权转移到 SRAM,但它还没有把数据复制过去?唉。它到底是从哪来的??
这个问题给我带来了不小的麻烦。我删除了所有分区,将 MBR 清零,将引导分区清零,还尝试了不同的 SD 卡,但只有我的卡仍然能够引导,所以卡上一定还存有 *某些* 可引导数据,存放在某个地方。最后,我把整张卡全部清零,才终于结束了这场疯狂。
在此时我还了解到了在排查引导 ROM 问题时可以访问的**追踪向量**。这对逆向工程也极有帮助,因为我知道所有追踪调用的来源,然后可以根据这些调用分配函数名等。我制作了一个电子表格来解释这些追踪向量,并用它快速理解当我更改卡参数时引导过程如何变化。果然,在我反复格式化 SD 卡时,它一直声称找到了 CHSETTINGS,直到我绝望地把整张卡都清零。
要尝试以 TI 处理器的方式读取该卡,可以使用 `dd`。使用块大小=512,并通过跳过前 `n` 个扇区来指定第一个扇区(可以尝试用 GParted 查看是哪个)。示例:第一个扇区是 2048,设备是 `sda`,我们只读取第一个扇区:```
sudo dd if=/dev/sda1 of=/home/sam/sector2048 bs=512 skip=2048 count=1
我用这个直接从 SD 卡下载了 MBR 和启动分区起始位置的镜像,这两者在之后都会派上用场。
我在 Ghidra 中的逆向工作让我找到了 SD 卡引导处理函数,现在我可以看到向卡发送 SD 命令的函数,并且可以单步执行查看卡的响应内容。我以为自己找对了地方,并且以为卡返回的全是零。后来我才知道,我可能是在单步调试 eMMC 处理程序(同一个处理程序,但设备 ID 不同),或者有其他问题,因为 SD 卡本身并没有任何问题。尽管如此,我觉得是时候了解一下这些卡的工作原理了。
我很好奇为什么每次块请求时卡都会返回全零。很明显,卡的功能和软件都可以读取它,因为过去已经这样做过。尽管如此,是时候接好线,用逻辑分析仪看看了。稍微做点微焊接,用 UV 固化环氧树脂固定 30awg 线,再夹上我的 Saleae,就有了能用的东西。

我使用这个分析器来分析数据。首先,我在未插入卡的情况下进行了尝试。

前几条命令的时钟频率为 120 kHz。显然,卡没有响应(因为它不在那里)。``` CMD0, arg=\0 GO_IDLE_STATE CMD8, arg=\x01\xAA SEND_EXT_CSD CMD55, arg=\0 APP_CMD CMD1, arg=\0 SEND_OP_COND ... CMD0, arg=\0 GO_IDLE_STATE CMD8, arg=\x01\xAA SEND_EXT_CSD CMD55, arg=\0 APP_CMD CMD1, arg=\0 SEND_OP_COND
当实际插入卡片时,配置后频率会跳到约 6 MHz。

在克服了使用 SD 模式遇到的一些崩溃问题后(提示:即使是这张 SD 卡,你也应该使用 MMC 模式),我能够确认该卡提供的数据是合理的。之后我便不再纠结于此,因为我加倍努力去理解 SD 卡启动处理程序,并意识到读取到的数据 *确实* 是正确的,而且与我手动 `dd` 该卡得到的数据完全相同!唉,这是一次有趣的插曲,也让我确信卡片工作正常。
### 找出问题
数据来自地址 `0x4030c928`(一个栈变量,512 字节数组),是在针对地址 `0x0000` 和设备 `8` 运行 `0x25c2e` 处的分支之后得到的(参见 `0x4030d00c` 处的静态数据)。进入我称之为 `MBR_detection` 的方法后,程序会检查魔数 `0xaa55`。首先它加载后两个字节 `0xaa`,然后是第一个 `0xaa`。```
r0 = data[0x1ff]
r1 = data[0x1fe]
orr r0,r1,r0,lsl #8
sub r1,r0,#0xaa00
subs r1,#0x55
bne <return FAIL>
这成功了。然而,下一项检查失败了:``` r0 = data[0xc] => 0 r1 = data[0xb] => 0 orr r0,r1,r0, lsl #8 cmp r0,#0x200 bne
反编译器伪代码:```c
if (
data[0x1fe] != 0x55aa ||
data[0xb] != 0x200 ||
(data[0xd] != 1 && // bit 0
data[0xd] != 2 && // bit 1
data[0xd] != 4 && // bit 2
data[0xd] != 8 && // bit 3
data[0xd] != 0x10 && // bit 4
data[0xd] != 0x20 && // bit 5
data[0xd] != 0x40 && // bit 6
data[0xd] != 0x80) // bit 7
)
{
return 1;
}
这会检查 0xb(即 11)处的字节是否等于 0x200,并检查 0xd 处的字节是否等于单比特值。两个条件都必须满足,否则返回失败。
在检测函数返回 1 之后,引导处理程序接着尝试将其作为 MBR 读取,并加载第一个分区的偏移量,以查看该分区是否为可引导分区。过程如下:```C
// Call block read function
// mmc_block_read_something(boot_device *dev,blk_read_struct blk)
ret = ((code *)blockread_struct->block_read_func)(blockread_struct->device_ptr,&block_read_info);
if (ret != 0) {
return 1;
}
// Check if device doesn't use MBR
ret = MBR_check_bootable_partition((partition_struct *)block_data,blockread_struct);
if (ret != 0) {
// It uses MBR, try each partition in the partition entries for a bootable
// partition
ret = MBR_check_entries(block_data,blockread_struct);
if (ret != 0) {
return 1;
}
ret = MBR_parse_entries(block_data,&blockread_struct->part_entry);
if (ret != 0) {
return 1;
}
// Get bootable partition offset
block_read_info = (blk_read_struct *)(blockread_struct->part_entry).first_sect_pos;
uStack_220 = 1;
pbStack_21c = block_data;
// Call block read function
// mmc_block_read_something(boot_device *dev,blk_read_struct blk)
ret = ((code *)blockread_struct->block_read_func)
(blockread_struct->device_ptr,&block_read_info);
if (ret != 0) {
return 1;
}
// Try and verify bootable partition again
ret = MBR_check_bootable_partition((partition_struct *)block_data,blockread_struct);
if (ret != 0) {
return 1;
}
}
所以现在我跳到它在 `0x800` 读取数据的地方,得到了正确的转储。但即使数据正确,MBR 检测方法仍然返回 1(而且那里有几层间接寻址,我不得不一路追查,啧),所以问题一定出在这里。
现在到了最后冲刺阶段。从 SD 卡进行的第一次内存读取是 MBR,它最多包含四个分区表项,参见 TRM 表 26-20、26-21。引导分区的分区表项表明该分区包含 `0x40000` 个扇区。但分区文件系统(参见 TRM 表 26-23)不知为何显示只有 `0x3fff8` 个扇区,启动 ROM 检测到这一差异并失败。
作为实验,我跳过了导致问题的那个跳转指令(结果可能会很糟……但愿没事……),程序确实继续运行了,虽然我不确定最终跑到了哪里,看起来像是垃圾数据。但忽略这一点,设备居然真的启动了!在 `0x402f0400`(已加载镜像的起始位置)设置断点,一切正常运行。也许是时候接上 UART 了?UART 很好!
至于 SD 卡的问题?我在 Unix SE 上问了[一个问题](https://unix.stackexchange.com/questions/781715/why-do-the-mbr-partition-entry-and-partition-filesystem-disagree-on-the-number-o/781755),但在当前问题上没得到太多帮助(不过还是得到了一些有用的信息)。从那以后:我终于修好了!在检查用于构建 FAT16 文件系统的 `mkfs` 命令时,我注意到[另一个参考](https://blog.billvanleeuwen.ca/porting-u-boot-onto-the-beaglebone)使用了 `-a` 标志,它会禁用对齐。这就是关键。加上该标志并重新构建后,扇区计数一致了(`0x40000`),系统也能启动了。我猜启动 ROM 不支持那种对齐方式。
现在插上卡后,我得到了下面这些消息,进入启动循环。万岁!只要弄清楚为什么内核没有启动,我们就成功了!所有那些工作终于可能换来几块可用的 beaglebone 板子。值得吗?谁知道呢。```
U-Boot SPL 2021.01-00001-gc59bf25a382-dirty (Jul 24 2024 - 20:38:49 -0400)
Trying to boot from MMC1
U-Boot 2021.01-00001-gc59bf25a382-dirty (Jul 28 2024 - 20:36:46 -0400)
CPU : AM335X-GP rev 2.1
Model: TI AM335x BeagleBone Black
DRAM: 512 MiB
WDT: Started with servicing (60s timeout)
NAND: 0 MiB
MMC: OMAP SD/MMC: 0, OMAP SD/MMC: 1
Loading Environment from FAT... *** Warning - bad CRC, using default environment
<ethaddr> not set. Validating first E-fuse MAC
Net: eth2: ethernet@4a100000, eth3: usb_ether
Hit any key to stop autoboot: 2 <0x08><0x08><0x08> 1 <0x08><0x08><0x08> 0
WARNING: Could not determine device tree to use
switch to partitions #0, OK
mmc0 is current device
SD/MMC found on device 0
Failed to load 'boot.scr'
Failed to load 'uEnv.txt'
switch to partitions #0, OK
mmc0 is current device
Scanning mmc 0:1...
libfdt fdt_check_header(): FDT_ERR_BADMAGIC
<0x1b>7<0x1b>[r<0x1b>[999;999H<0x1b>[6n<0x1b>8Scanning disk [email protected]...
Scanning disk [email protected]...
** Unrecognized filesystem type **
Found 4 disks
No EFI system partition
BootOrder not defined
EFI boot manager: Cannot load any image
switch to partitions #0, OK
mmc0 is current device
SD/MMC found on device 0
4997632 bytes read in 353 ms (13.5 MiB/s)
Failed to load '/boot/undefined'
Starting kernel ...
那么,设备树有问题("WARNING: Could not determine device tree to use")。这是我第一次接触内核和内核启动,所以我完全不知道那是什么意思。
按我的理解,"启动内核"涉及以下步骤:
内核启动过程中的一个重要方面是设备树。它存储在针对该板卡的 .dtb 文件(设备树二进制文件;与 .dts 设备树源文件相对)中。
我现在的问题是 U-Boot 没有加载该板卡的设备树,因为日志中没有出现 reading /am335x-boneblack.dtb 消息。相反,我们看到了 WARNING: Could not determine device tree to use。所以这是很好的证据!猜测是由于缺少 EEPROM 板卡 ID。
关于它如何获取板卡信息的一些细节,可以在 TI 论坛上的这个帖子中找到。
那么,U-Boot 是如何知道如何配置自身并正确启动的呢?在我们构建的 U-Boot 源码中,有一个名为 configs/ 的文件夹,里面存储着针对各种不同板卡的 defconfig 文件。这些文件定义了 U-Boot 的各种配置参数,包括启动命令,可能看起来像这样:```
if test ${boot_fit} -eq 1;
then run update_to_fit;
fi;
run findfdt;
run init_console;
run envboot;
run finduuid;
run distro_bootcmd
我们通过运行 `make <boardname>_config` 目标来定义使用哪个配置。函数 `findfdt` 用于识别当前运行的板卡,并正确配置设备树。它看起来像这样(定义在 `am335x_evm.h` 中):```
"findfdt="\
"if test $board_name = A335BONE; then " \
"setenv fdtfile am335x-bone.dtb; fi; " \
"if test $board_name = A335BNLT; then " \
"setenv fdtfile am335x-boneblack.dtb; fi; " \
"if test $board_name = A335PBGL; then " \
"setenv fdtfile am335x-pocketbeagle.dtb; fi; " \
"if test $board_name = BBBW; then " \
"setenv fdtfile am335x-boneblack-wireless.dtb; fi; " \
"if test $board_name = BBG1; then " \
"setenv fdtfile am335x-bonegreen.dtb; fi; " \
"if test $board_name = BBGW; then " \
"setenv fdtfile am335x-bonegreen-wireless.dtb; fi; " \
"if test $board_name = BBBL; then " \
"setenv fdtfile am335x-boneblue.dtb; fi; " \
"if test $board_name = BBEN; then " \
"setenv fdtfile am335x-sancloud-bbe.dtb; fi; " \
"if test $board_name = A33515BB; then " \
"setenv fdtfile am335x-evm.dtb; fi; " \
"if test $board_name = A335X_SK; then " \
"setenv fdtfile am335x-evmsk.dtb; fi; " \
"if test $board_name = A335_ICE && test $ice_mii = rmii; then " \
"setenv fdtfile am335x-icev2.dtb; fi; " \
"if test $board_name = A335_ICE && test $ice_mii = mii; then " \
"setenv fdtfile am335x-icev2-prueth.dtb; fi; " \
"if test $fdtfile = undefined; then " \
"echo WARNING: Could not determine device tree to use; fi; \0" \
如果我们想要默认行为,我们只需要更改 board_name 变量,对吧?嗯,也许不行,或者说至少我不知道更改它的最佳位置在哪里。但是,虽然设置 board_name 本身没有起作用,但我实际上还更新了默认的 .dtb 文件,而这样做成功了!```
_____ _____ _ _
| _ |___ ___ ___ ___ | _ |___ ___ ||__ | |
| | _| .'| . | . | | | | . | | | -| _| _|
|||| |__,| || || || ||| ||||
|| |___|
Arago Project http://arago-project.org am335x-evm ttyS0
Arago 2021.09 am335x-evm ttyS0
am335x-evm login: root
root@am335x-evm:~#
终于,我们到了终端。我的那些废旧板子活了!
### 修复缺失的 EEPROM ID
作为最后一步,我将把正确的板卡 ID 写入 EEPROM,这在 Linux 用户空间内很容易完成。根据 SPL 源码,各种板卡 ID 如下:
- `A335BONE` - Beaglebone 板卡
- `A335BNLT` - Beaglebone Black 板卡
- `A335PBGL`
- `A335X_SK`
- `A33515BB`
- `A335_ICE`
此外还有一个可选的板卡修订版本。根据原理图,EEPROM 位于 I2C0 上,芯片本身(在我的原理图上为 24LC32A,尽管它标注为 '256Kx8)提供的 I2C 设备地址为 `0x50`(二进制 `b1010` 后跟 `000` 芯片地址,因为 5 引脚封装没有额外的地址引脚)。最后一点需要注意:WP 引脚通过 10k 上拉电阻被拉高,因此默认启用写保护;在任何写入操作之前需要将其拉低,否则它会应答但不会写入任何内容。
可以通过内核在 `/sys/bus/i2c/devices/0-0050` 访问 EEPROM,其中有一个名为 `eeprom` 的文件。因此,在 WP 引脚被拉低的情况下(将顶部靠近 DC 插孔的 TP4 接地),只需要几次 `echo` 调用即可。Beaglebone Black 系统参考手册中说明了格式。我根据[此处](https://groups.google.com/g/beagleboard/c/di5O5JCl4yw)进行了调整。```sh
root@am335x-evm:~# cat fix_eeprom.sh
#!/bin/bash
# Fix board ID EEPROM
EEPROM_FILE=/tmp/eeprom.tmp
EEPROM=/sys/bus/i2c/devices/0-0050/eeprom
# header bytes
echo -ne "\xaa\x55\x33\xee" > ${EEPROM_FILE}
# Board ID
echo -n "A335BNLT" >> ${EEPROM_FILE}
# serial number (I left this basically as the template)
echo -n "000C24wwBBoxxxx" >> ${EEPROM_FILE}
dd if=${EEPROM_FILE} of=${EEPROM}
使用 less 确认,从现在开始我们应该可以正常运行默认 SD 卡。所有板子的 EEPROM 写入后,可以回滚对 SPL 和 U-Boot 配置的更改。
| 区域 | 起始地址 | 长度 |
|---|
| 引导 ROM(公共) | 0x4002_0000 | 0xBFFF |
| 引导 ROM(公共,别名) | 0x0002_0000 | 0xBFFF |
| 内部 SRAM | 0x402F_0400 | 0xFC00 |
| L3 OCM0 | 0x4030_0000 | 0x10000 |
| 测试点 | 连接 | 原理图页面 | 板面 |
|---|
| TP1 | DGND | 2 (D1) | 顶部 |
| TP2 | VDD_MPUON (VDD_MPU_MON) | 5 (C4) | 顶部 |
| TP3 | TESTOUT | 5 (B2) | 顶部 |
| TP4 | Board ID WP | 11 (B1) | 顶部 |