小米手环固件启动链路分析 | 流水账
写在前面
本次研究对象是小米手环的RTOS嵌入式固件,这是一个典型的将系统内核与应用程序直接链接且缺乏MMU的RTOS案例。本文主要是希望与大家交流与学习,博主没有嵌入式开发的基础,再加上本人水平实在有限,过程中高度依赖大模型辅助,可能存在因模型幻觉以及本人理解错误导致的错误,欢迎批评指正。
一:解包bin文件
1:Binwalk解包
用binwalk解包名为10nfc 1.2.180.bin的固件升级包(小米之家上老哥分享的有资源),可以得到下面的文件(实际bin文件里面我记得是zip压缩包来着,时间有点久远了,记不太清了):

这么多文件实际上只有两个是可执行的程序(vela_ap.bin和vela_ota.bin),其他全是文件系统,从ota.json中可以得到印证:

vela_ota.bin:跟ota相关的程序:

vela_ap.bin:应用程序:

2:把玩文件系统
这里心血来潮把玩了一下固件里的文件系统,提取了个图标出来:

这里过程比较简单,就是用开源的romfs提取工具把文件系统解包出来,但是解包出来的还是一堆bin文件,不过有文件系统的结构了,可以猜出来一些文件是什么:

在 大老师(大模型老师)的指导下,用一个脚本把灰度图标给还原出来了:
from PIL import Image
import numpy as np
# 读取文件
with open('baseball.bin', 'rb') as f:
data = f.read()
print(f"文件总大小: {len(data)} 字节")
# 尝试不同的尺寸和格式
attempts = [
# (宽度, 高度, 格式, 描述)
(40, 40, 'RGBA', "40x40 RGBA"), # 6400字节 + 可能的4字节头
(50, 32, 'RGBA', "50x32 RGBA"), # 6400字节
(40, 40, 'RGB', "40x40 RGB"), # 4800字节
(64, 64, 'L', "64x64 灰度"),
(80, 80, 'L', "80x80 灰度"), # 6400字节
]
for width, height, mode, desc in attempts:
if mode == 'RGBA':
bytes_needed = width * height * 4
elif mode == 'RGB':
bytes_needed = width * height * 3
elif mode == 'L':
bytes_needed = width * height
else :
bytes_needed = 0
print(f"尝试 {desc}: 需要 {bytes_needed} 字节")
if len(data) >= bytes_needed:
try:
# 如果数据比需要的大,可能有多余的文件头
if len(data) > bytes_needed:
print(f" 检测到可能的多余数据({len(data)-bytes_needed}字节),尝试跳过...")
image_data = data[-bytes_needed:] # 从末尾取数据
else:
image_data = data
arr = np.frombuffer(image_data, dtype=np.uint8)
if mode == 'RGBA':
arr = arr.reshape((height, width, 4))
elif mode == 'RGB':
arr = arr.reshape((height, width, 3))
elif mode == 'L':
arr = arr.reshape((height, width))
img = Image.fromarray(arr, mode)
filename = f"baseball_{width}x{height}_{mode}.png"
img.save(filename)
print(f" ✓ 成功保存为: {filename}")
except Exception as e:
print(f" ✗ 失败: {e}")
else:
print(f" ✗ 数据不足")
没猜对尺寸其实也能看出来是个什么东西了:



二:丢进IDA,基址何处寻
1:先猜指令集架构
事已至此,反编译看看先,遂把vela_ap.bin丢给IDA,首先面临一个问题,就是选架构,这里就比较简单,结合硬件信息(一个叫BES2700的Soc)在网上找找,其实这种设备大概率就是一个Cortex-M的芯片,然后问问大模型老师,就可以猜出是ARM Thumb了,然后反编译一下验证就好了。(其实主要是具体的过程我记不清了,不然本文作为流水账肯定会写出来的(^v^))
2:基址是什么…
然后就遇到了第一个拦路虎:基地址。
我们用IDA打开vela_ap.bin,选择arm小端序:

然后就傻眼了,基地址该怎么填?

填不了只能默认了…但是…
PS:这里要选32位的(选No):

IDA识别出是Thumb了:

这里科普一下Thumb指令集:Arm架构的CPU支持两套核心指令集:32位的ARM指令集和16位的Thumb指令集。可以把Thumb指令集想象成ARM指令集的一个 “压缩精简版” ,它通过将指令长度减半,专门用来解决在资源有限的嵌入式设备中,代码体积过大、功耗过高的问题。
因为没有基址,所以这种错误的地址引用特别多:

不过,实在不去恢复好像…也没什么影响…还是会有的吧…所以…基址到底是什么…
3:找个基址燃尽了…
要说找基址似乎也没什么麻烦的…至少我看的资料都这么说…无非就是从bootloader入手啦…或者用序列匹配去匹配字符串指针啦…什么的,听起来好像蛮简单的…
但是奈何博主实在太菜,都没能成功…
Bootloader
bootloader什么的我手上好像有哎…能从vela_ota.bin里面找到么…🤔
但是vela_ota.bin的基址…我也没有啊!!!而且一堆汇编指令…桥豆麻袋,先从软柿子捏,先搁置先搁置…
序列匹配
然后博主尝试了这个很著名的方法,然后把自己燃尽了…
简单来说原理就是看程序中引用的字符串指针全部找出来,从小到大排一个序列,然后把所有字符串的偏移地址都找出来,从小到大拍一个序列,然后让这两个序列匹配上,虽然绝对地址不同,但是相对位置应该是可以一一对应上的,详情可以参考视频:揭秘VxWorks路由器破解之路
原理上来说很清晰对吧,但是理想很丰满,现实很骨感,博主尝试自己写脚本去做匹配,最后燃尽了也没能成功(还是太菜了…)。然后这个工作就搁置下来了…
求得神器
然后有一天博主因为觉得IDA的界面太丑想尝试一些新的反编译工具,在把玩 Cutter 时,发现了一个神奇的东西:

点开长这样:

这里一定不要选所有核心啊,不然电脑会很卡的。
它算了几个小时,吃完饭回来一看有结果了,本来对它不抱有希望的(○´・д・)ノ:

可以看到0x2c100000分数断崖式领先啊,至于对不对嘛,后面再揭晓。至于这个工具是用的什么算法实现的,博主没有研究,不过 Cutter 本身是开源的,应该是可以通过源码学习一下的。
PS:Cutter 是一款基于 radare2 引擎的开源逆向工程 GUI 工具,旨在通过图形化界面降低命令行反汇编框架的使用门槛,将 radare2 强大的开源分析能力与直观的可视化操作相结合,让开发者能够免费、透明地完成二进制程序的反编译、调试和安全分析。
三:从源码里找找线索
其实寻找基址最好最直接的办法是翻硬件手册,但是BES2700的硬件手册是不公开的,那该怎么办呢?可以接着找线索,小米手环的这套固件实际上是基于一款叫NuttX的开源RTOS开发的(博主就是想找RTOS的案例才找到这个固件的),叫Vela系统(从bin文件的名字也能看出来),从vela_ap.bin的结尾的字符串也可以看出一些线索:
包括:
- 芯片的名称:
best1503 - 操作系统内核:
NUTTX FLASH的基址(但是这个地址并不是我们想要的原因后面会说)FLASH_DUAL_CHIP=1:双 Flash 架构OTA_CODE_OFFSET=0x100000:OTA 固件在 Flash 内的偏移(0x100000=1MB)
别的就不知道是什么意思了,OTA 固件在 Flash 内的偏移是比较疑惑的一个点,因为后面推断出这个偏移应该就是加载vela_ap.bin的偏移地址才对(相对FLASH的基址),不知道这里是不是印证了这一点,有没有懂行的老哥说一下。
有点跑题,拉回正轨,小米的这套vela系统是有一个叫OpenVela的开源项目的,不过项目比较庞大,于是我就再次借助 大老师 的力量,叫它扒一下OpenVela 的开源仓库,看看里有没有BES2700的硬件信息。
坏消息:没找到BES2700。好消息:找到BES2600了。
1:大老师找到了什么?
OpenVela 的 BES 原厂支持在 GitHub 的 open-vela/vendor_bes 仓库:
仓库结构(摘自 README):
- chips/bes/:BES 芯片系列支持(基于 Cortex‑M,说明里写的是 BES2600)
- boards/best2003_ep/:对应 BES2600WM‑AX4F 的评估板配置(板级 defconfig、链接脚本等都在这里)
- drivers/:BES 相关驱动
README 的“Supported Hardware”只列了:
- BES2600WM‑AX4F,及其开发板 BES2600WM MAIN BOARD V1.1
- 并且强调:当前仓库的芯片目录是 bes(Cortex‑M),而 besa7(Cortex‑A 系列)不在本仓库范围
在仓库页面里搜关键字 2700 / BES2700 / memory,结果都是 “No matches found”,也就是说,至少在当前可见的代码和文档里,没有 BES2700 的任何痕迹。
那 OpenVela 里 BES2600 的内存分布定义在哪里?
在 boards/best2003_ep/scripts/Make.defs 里,有这样关键的两行:
BES_SDK_LINK_PATH ?= $(BOARD_DIR)/configs/$(CONFIG_ARCH_BOARD_CUSTOM_NAME_STR)
ARCHSCRIPT = $(BES_SDK_LINK_PATH)/$(LDSCRIPT)
链接脚本的真实位置是:boards/best2003_ep/configs/<板级配置名>/$(LDSCRIPT),也就是说,具体的 MEMORY / SECTIONS 定义不在 OpenVela 仓库里,而是藏在 BES 的 DDK(vendor/bes/$(CONFIG_BES_DDK_DIR))的链接脚本里。
找到了最那个接近的链接脚本
根据这些线索,找到了一个最接近的答案:
vendor_bes/blob/dev/boards/best2003_ep/aos_evb/configs/ap/_best1000.lds
这是 best2003_ep(BES2600 系列)“AOS_EVB 评估板”上跑在 M33 这一侧(AP 子系统) 的链接脚本模板,用来决定这一侧镜像的“各段放哪儿、各内存区多大”。它本身不是 BES2700 的脚本,但非常适合作为参考来理解 BES 这类多核 SoC 里一侧内存布局的写法。
2:里面有什么?
这个脚本里最有意义的可能就是这一段了:
MEMORY
{
ROM (rx) : ORIGIN = 0x00020000, LENGTH = 0x00010000
FLASH (r) : ORIGIN = (0x2C000000 + 0x150000), LENGTH = (0x1000000 - ((0x2C000000 + 0x150000) - 0x2C000000))
FLASHX (rx) : ORIGIN = (((0x2C000000 + 0x150000)) - 0x2C000000 + 0x0C000000), LENGTH = (0x1000000 - ((0x2C000000 + 0x150000) - 0x2C000000))
FLASH_NC (r) : ORIGIN = (((0x2C000000 + 0x150000)) - 0x2C000000 + 0x28000000), LENGTH = (0x1000000 - ((0x2C000000 + 0x150000) - 0x2C000000))
RAM (rwx) : ORIGIN = ((0x00200000 + 0 - 0x00200000 + 0x20000000) + 0), LENGTH = (((0x201C0000 + 0x00040000 - ((0x00200000 + 0 - 0x00200000 + 0x20000000) + 0) - 0 - 0x80000)) - 0) - 0x13000
RAMX (rx) : ORIGIN = ((((0x00200000 + 0 - 0x00200000 + 0x20000000) + 0)) - ((0x00200000 + 0 - 0x00200000 + 0x20000000) + 0) + (((0x00200000 + 0 - 0x00200000 + 0x20000000) + 0) - 0x20000000 + 0x00200000)), LENGTH = (((0x201C0000 + 0x00040000 - ((0x00200000 + 0 - 0x00200000 + 0x20000000) + 0) - 0 - 0x80000)) - 0) - 0x13000
FRAMX (rwx) : ORIGIN = ((((0x00200000 + 0 - 0x00200000 + 0x20000000) + 0)) - ((0x00200000 + 0 - 0x00200000 + 0x20000000) + 0) + (((0x00200000 + 0 - 0x00200000 + 0x20000000) + 0) - 0x20000000 + 0x00200000)) + (((0x201C0000 + 0x00040000 - ((0x00200000 + 0 - 0x00200000 + 0x20000000) + 0) - 0 - 0x80000)) - 0) - 0x13000, LENGTH = 0x13000
PSRAM (rwx) : ORIGIN = (0x34000000), LENGTH = 0x800000-0-0-0
PSRAM_NC (rwx) : ORIGIN = (((0x34000000)) - 0x34000000 + 0x30000000), LENGTH = 0x800000-0-0-0
PSRAMX (rwx) : ORIGIN = (((0x34000000)) - 0x34000000 + 0x14000000), LENGTH = 0x800000-0-0-0
PSRAMUHS (rwx) : ORIGIN = (0x3C000000 + 0x2000000-0), LENGTH = 0
PSRAMUHS_NC (rwx) : ORIGIN = (((0x3C000000 + 0x2000000-0)) - 0x3C000000 + 0x38000000), LENGTH = 0
PSRAMUHSX (rx) : ORIGIN = (((0x3C000000 + 0x2000000-0)) - 0x3C000000 + 0x1C000000), LENGTH = 0
}
这团代码的含义(大老师总结):
| 区域名 | 真实起始地址 | 真实长度 | 约等于大小 | 用途/属性 |
|---|---|---|---|---|
| ROM | 0x00020000 |
0x00010000 |
64 KB | 只读存储器(可能放部分底层库或BootROM补丁) |
| FLASH | 0x2C150000 |
0x00EB0000 |
~14.69 MB | Flash 物理映射区(不可直接执行,存数据) |
| FLASHX | 0x0C150000 |
0x00EB0000 |
~14.69 MB | Flash 的 Cache/XIP 映射区(代码在这里执行) |
| FLASH_NC | 0x28150000 |
0x00EB0000 |
~14.69 MB | Flash 的 Non-Cacheable 映射区(给DMA用) |
| RAM | 0x20000000 |
0x0016D000 |
~1.42 MB | 内部主 SRAM(存放全局变量、堆、栈) |
| RAMX | 0x00200000 |
0x0016D000 |
~1.42 MB | 内部 SRAM 的 Cache/TCM 映射区(零等待执行) |
| FRAMX | 0x0036D000 |
0x00013000 |
76 KB | 内部 SRAM 尾部剥离出的 Overlay 执行区 |
| PSRAM | 0x34000000 |
0x00800000 |
8 MB | 外部 PSRAM 物理映射区 |
| PSRAM_NC | 0x30000000 |
0x00800000 |
8 MB | 外部 PSRAM 的 Non-Cacheable 映射区 |
| PSRAMX | 0x14000000 |
0x00800000 |
8 MB | 外部 PSRAM 的 Cache/XIP 映射区 |
| PSRAMUHS | 0x3E000000 |
0x00000000 |
0 | 超高速 PSRAM(当前配置未启用) |
这段代码暴露了一个博主之前完全不知的玩意: ARM Cortex-M 在复杂 SoC 中的三重地址映射机制。同样是 Flash,它定义了三个区域(FLASH, FLASHX, FLASH_NC):
同一块 Flash 要映射 3 次?
对于起始地址 0x2C150000,长度约 14.69 MB 的这一块物理 Flash:
FLASH (0x2C150000):这是“裸地址”。在 ARM 架构里,直接去读 Flash 的这个地址通常是非常慢的,且往往不能直接取指令执行。它一般只用来做特殊的底层访问或烧写。FLASHX (0x0C150000):带 XIP (Execute In Place,片上执行) 和 Cache (缓存) 的映射窗口。CPU 要运行里面的代码(.text段),必须跳到0x0C000000开头的这片地址空间来跑,这样指令会被 Cache 缓存,运行速度极快。FLASH_NC (0x28150000):Non-Cacheable (非缓存) 映射。如果 CPU 把这块 Flash 当作数据盘,或者蓝牙/WiFi 的 DMA 控制器要去这块 Flash 搬数据,就必须用这个地址。如果 DMA 走 Cache 的地址,内存数据不同步就会死机。
或者这样理解:
【同一块物理 Flash 芯片】
│
├──> 映射窗口 A:0x2C000000 起始 (称作 FLASH)
│ 特点:走普通总线,没有 Cache,非常慢。
│ 权限:只读。不能在这里取指令执行!
│
├──> 映射窗口 B:0x0C000000 起始 (称作 FLASHX)
│ 特点:经过了 CPU 的 I-Cache(指令缓存)。
│ 权限:可读可执行。CPU 真正跑代码的地方!
│
└──> 映射窗口 C:0x28000000 起始 (称作 FLASH_NC)
特点:走普通总线,强制 Non-Cacheable。
权限:只读。专门给蓝牙/WiFi的 DMA 控制器去读 Flash 数据用的。
虽然这是 BES2600 的脚本,但它确实提供了一个 “BES 系列芯片内存分布模板”。
3:基址大概能确认了
之前 Cutter 算出来的基址是0x2C100000,虽然不知道 Cutter 是怎么算出来的,但是根据这个三重地址映射机制,作为可执行程序也就是FLASHX段,不妨大胆猜测,0x0C100000就是基址!
然后改基址!顺便把段名称也改了:

4:RAM段!!!
在 大老师 的悉心指导下,我又知道了一个没听说过的、在 ARM XIP 架构中的机制:
全局变量(.data 段)的初始值,是被打包在 vela_ap.bin(Flash)的末尾的。上电后,Bootloader 会把这些初始值拷贝到内部 RAM 中的一块区域,程序实际运行时是读写的地址实在RAM段。
为什么越解决问题问题反而越多了???
四:把RAM段搞定
1:还是要找到copy数据代码
按道理来说从哪里开始是.data,复制到哪里,这个肯定会在代码中体现才对,但是这段代码到底在哪里?于是接下来,博主与 大老师 展开了激烈的讨论:排除了在vela_ota.bin的可能性,捋了一遍从烧录到启动可能的过程,最后得到一条结论,cpu在上电之后,轮到vela_ota.bin中的代码开始执行时,是从第一条指令开始执行的!(虽然可能对于嵌入式专业的老哥这似乎是常识…但是对于博主这种没做过嵌入式开发的菜鸡来说确实是一个重大发现😓)
2:被大老师带跑偏但歪打正着
大老师跟博主说:ARM Cortex-M 架构有一个铁律:芯片上电复位的瞬间,CPU 会去一个固定的地方读取两个 32 位数字:第一个数字(偏移 0x00):赋值给栈指针(SP)。第二个数字(偏移 0x04):赋值给程序计数器(PC),也就是你要找的“第一条指令”的地址。这个存放 SP 和 PC 的地方,叫向量表。向量表必须放在一段内存的绝对起始地址。
然后看了一眼vela_ap.bin的前8个字节…你跟我说这是向量表?🤬
FF FF FF FF 00 00 05 00
然后我就把前 256 字节的十六进制给 大老师 看,还真看出了点东西:
把这 16 个字节按小端序(反向读 4 字节)拼起来,奇迹出现了:
00 C0 05 20 => 0x2005C000 (完美的内部 RAM 栈指针地址!)
85 00 10 0C => 0x0C100085 (完美的 FLASHX 代码区地址!最低位是 1,代表 Thumb 指令)
40 19 10 0C => 0x0C101940 (NMI 中断地址)
00 ED 00 E0 => 0xE000ED00 (绝杀证据! 这是 ARM Cortex-M 架构的“系统控制块”的固定硬件地址,通常被放在向量表的第 4 项作为硬 fault 默认处理函数)
破案了!真正的 ARM 向量表,藏在文件开头的偏移 0xB2(十进制的 178)处!
🤬虽然 大老师 很敏锐地发现了藏在其中的有效信息,但是地址都是错的,明明是在188(0xBC)字节,完了说这是真正的向量表更是在胡扯!!!!
然后一通折腾,总算有点进展。这里IDA很坑的一个地方,它没有把开头的一段正确反编译出来,要手动按’C’键去进行反编译,而且开头一段IDA按照32位的ARM指令去分析的(ROM:0C100000 CODE32)。
ROM:0C100000
ROM:0C100000 ; Segment type: Pure code
ROM:0C100000 AREA ROM, CODE, READWRITE, ALIGN=0
ROM:0C100000 ; ORG 0xC100000
ROM:0C100000 CODE32
ROM:0C100000 DCB 0xFF
ROM:0C100001 DCB 0xFF
ROM:0C100002 DCB 0xFF
ROM:0C100003 DCB 0xFF
ROM:0C100004 ; ---------------------------------------------------------------------------
ROM:0C100004 ANDEQ R0, R5, R0
ROM:0C100008 ANDEQ R0, R0, R0
ROM:0C10000C LDMDACS R6!, {R2-R4,R6,R7,R10-R12,LR,PC}^
ROM:0C100010 SMLADMI R0, R4, R8, R4
ROM:0C100010 ; ---------------------------------------------------------------------------
要改成Thumb模式,至于具体怎么操作,这里有一个 Callback,还记得这个平平无奇的弹窗吗?

修改方法就写在里面了,修改IDA的T寄存:Edit->Segments->Change Segment Register Value

改成1就好了:

然后再让大模型去分析下面的汇编代码:
FLAHX:0C100000 ; Processor : ARM
FLAHX:0C100000 ; ARM architecture: metaarm
FLAHX:0C100000 ; Target assembler: Generic assembler for ARM
FLAHX:0C100000 ; Byte sex : Little endian
FLAHX:0C100000
FLAHX:0C100000 ; ===========================================================================
FLAHX:0C100000
FLAHX:0C100000 ; Segment type: Pure code
FLAHX:0C100000 AREA FLAHX, CODE, ALIGN=0
FLAHX:0C100000 ; ORG 0xC100000
FLAHX:0C100000 CODE16
FLAHX:0C100000 DCB 0xFF
FLAHX:0C100001 DCB 0xFF
FLAHX:0C100002 DCB 0xFF
FLAHX:0C100003 DCB 0xFF
FLAHX:0C100004 ; ---------------------------------------------------------------------------
FLAHX:0C100004 MOVS R0, R0
FLAHX:0C100006 MOVS R5, R0
FLAHX:0C100008 MOVS R0, R0
FLAHX:0C10000A MOVS R0, R0
FLAHX:0C10000C BGT 0xC0FFFC8
FLAHX:0C10000E CMP R0, #0x76 ; 'v'
FLAHX:0C100010 LDR R0, loc_C100064
FLAHX:0C100012 BX R0 ; loc_C100014
FLAHX:0C100014 ; ---------------------------------------------------------------------------
FLAHX:0C100014
FLAHX:0C100014 loc_C100014 ; CODE XREF: FLAHX:0C100012↑j
FLAHX:0C100014 BL sub_C1012CC
FLAHX:0C100018 LDR R0, loc_C100068
FLAHX:0C10001A MSR.W MSP, R0
FLAHX:0C10001E LDR R0, unk_C10006C
FLAHX:0C100020 MSR.W MSPLIM, R0
FLAHX:0C100024 MOVS R0, #0
FLAHX:0C100026 MSR.W CONTROL, R0
FLAHX:0C10002A ISB.W SY
FLAHX:0C10002E BL sub_C100090
FLAHX:0C100032 BL sub_C100150
FLAHX:0C100036 BL sub_C1002CC
FLAHX:0C10003A LDR R1, byte_C100070
FLAHX:0C10003C LDR R2, byte_C100074
FLAHX:0C10003E LDR R3, byte_C100078
FLAHX:0C100040
FLAHX:0C100040 loc_C100040 ; CODE XREF: FLAHX:0C10004C↓j
FLAHX:0C100040 CMP R2, R3
FLAHX:0C100042 ITTT LT
FLAHX:0C100044 LDRLT.W R0, [R1],#4
FLAHX:0C100048 STRLT.W R0, [R2],#4
FLAHX:0C10004C BLT loc_C100040
FLAHX:0C10004E LDR R1, loc_C10007C
FLAHX:0C100050 LDR R2, loc_C100080
FLAHX:0C100052 MOVS R0, #0
FLAHX:0C100054
FLAHX:0C100054 loc_C100054 ; CODE XREF: FLAHX:0C10005C↓j
FLAHX:0C100054 CMP R1, R2
FLAHX:0C100056 ITT LT
FLAHX:0C100058 STRLT.W R0, [R1],#4
FLAHX:0C10005C BLT loc_C100054
FLAHX:0C10005E BL.W first_function ; entry point
FLAHX:0C100062 MOVS R0, R0
FLAHX:0C100064
FLAHX:0C100064 loc_C100064 ; DATA XREF: FLAHX:0C100010↑r
FLAHX:0C100064 MOVS R5, R2
FLAHX:0C100066 LSRS R0, R2, #0x10
FLAHX:0C100068
FLAHX:0C100068 loc_C100068 ; DATA XREF: FLAHX:0C100018↑r
FLAHX:0C100068 MCR p0, 4, R2,c0,c5, 0
FLAHX:0C100068 ; ---------------------------------------------------------------------------
FLAHX:0C10006C unk_C10006C DCB 0x80 ; DATA XREF: FLAHX:0C10001E↑r
FLAHX:0C10006D DCB 0xF2
FLAHX:0C10006E DCB 0x15
FLAHX:0C10006F DCB 0x20
FLAHX:0C100070 byte_C100070 DCB 0x14 ; DATA XREF: FLAHX:0C10003A↑r
FLAHX:0C100071 DCB 0xCC
FLAHX:0C100072 DCB 0x75
FLAHX:0C100073 DCB 0x2C ; ,
FLAHX:0C100074 byte_C100074 DCB 0x68 ; DATA XREF: FLAHX:0C10003C↑r
FLAHX:0C100075 DCB 0xBE
FLAHX:0C100076 DCB 6
FLAHX:0C100077 DCB 0x20
FLAHX:0C100078 byte_C100078 DCB 0x30 ; DATA XREF: FLAHX:0C10003E↑r
FLAHX:0C100079 DCB 0xCF
FLAHX:0C10007A DCB 7
FLAHX:0C10007B DCB 0x20
FLAHX:0C10007C ; ---------------------------------------------------------------------------
FLAHX:0C10007C
FLAHX:0C10007C loc_C10007C ; DATA XREF: FLAHX:0C10004E↑r
FLAHX:0C10007C LDM R7!, {R4,R5}
FLAHX:0C10007E MOVS R0, #7
FLAHX:0C100080
FLAHX:0C100080 loc_C100080 ; DATA XREF: FLAHX:0C100050↑r
FLAHX:0C100080 ADD R3, SP, #0x2D0
FLAHX:0C100082 MOVS R0, #0xC
FLAHX:0C100084
FLAHX:0C100084 loc_C100084 ; CODE XREF: FLAHX:0C10008C↓j
FLAHX:0C100084 NOP
FLAHX:0C100086 NOP
FLAHX:0C100088 NOP
FLAHX:0C10008A NOP
FLAHX:0C10008C B loc_C100084
FLAHX:0C10008C ; ---------------------------------------------------------------------------
FLAHX:0C10008E DCB 0
FLAHX:0C10008F DCB 0
FLAHX:0C100090
FLAHX:0C100090 ; =============== S U B R O U T I N E =======================================
FLAHX:0C100090
FLAHX:0C100090
FLAHX:0C100090 sub_C100090 ; CODE XREF: FLAHX:0C10002E↑p
FLAHX:0C100090 LDR R2, =0x2005C000
FLAHX:0C100092 MOVS R3, #0
FLAHX:0C100094 MOV R1, R2
FLAHX:0C100094 ; End of function sub_C100090
FLAHX:0C100094
FLAHX:0C100096
FLAHX:0C100096 ; =============== S U B R O U T I N E =======================================
FLAHX:0C100096
FLAHX:0C100096
FLAHX:0C100096 sub_C100096
FLAHX:0C100096 PUSH {R4,R5,LR}
FLAHX:0C100098 LDR R4, =0xC100085
FLAHX:0C10009A LDR R5, =unk_C101940
FLAHX:0C10009C
FLAHX:0C10009C loc_C10009C ; CODE XREF: sub_C100096+18↓j
FLAHX:0C10009C CMP R3, #0xF
FLAHX:0C10009E ITE GT
FLAHX:0C1000A0 MOVGT R0, R4
FLAHX:0C1000A2 LDRLE.W R0, [R5,R3,LSL#2]
FLAHX:0C1000A6 ADDS R3, #1
FLAHX:0C1000A8 CMP R3, #0x5E ; '^'
FLAHX:0C1000AA STR.W R0, [R1],#4
FLAHX:0C1000AE BNE loc_C10009C
FLAHX:0C1000B0 LDR R3, =0xE000ED00
FLAHX:0C1000B2 STR R2, [R3,#8]
FLAHX:0C1000B4 DSB.W SY
FLAHX:0C1000B8 POP {R4,R5,PC}
FLAHX:0C1000B8 ; End of function sub_C100096
FLAHX:0C1000B8
FLAHX:0C1000B8 ; ---------------------------------------------------------------------------
FLAHX:0C1000BA DCB 0
FLAHX:0C1000BB DCB 0xBF
FLAHX:0C1000BC dword_C1000BC DCD 0x2005C000 ; DATA XREF: sub_C100090↑r
FLAHX:0C1000C0 dword_C1000C0 DCD 0xC100085 ; DATA XREF: sub_C100096+2↑r
FLAHX:0C1000C4 off_C1000C4 DCD unk_C101940 ; DATA XREF: sub_C100096+4↑r
FLAHX:0C1000C8 off_C1000C8 DCD 0xE000ED00 ; DATA XREF: sub_C100096+1A↑r
还记得之前找到的那个“向量表”吗?sub_C100096 函数把这个函数把这个“向量表”搬运了!
ROM:0C100096 sub_C100096
ROM:0C100096 PUSH {R4,R5,LR}
ROM:0C100098 LDR R4, =0xC100085 ; R4 = 默认的弱函数地址
ROM:0C10009A LDR R5, =dword_C101940 ; R5 = 强函数向量表数组的起点
ROM:0C10009C loc_C10009C
ROM:0C10009C CMP R3, #0xF ; 比较索引,是否大于 15
ROM:0C10009E ITE GT
ROM:0C1000A0 MOVGT R0, R4 ; 如果 > 15,用默认弱函数 (0xC100085)
ROM:0C1000A2 LDRLE.W R0, [R5,R3,LSL#2] ; 如果 <= 15,从强函数表里取真实地址
ROM:0C1000A6 ADDS R3, #1 ; 索引 +1
ROM:0C1000A8 CMP R3, #0x5E ; 一共循环 94 次 (0x5E)
ROM:0C1000AA STR.W R0, [R1],#4 ; 把算好的函数地址,写入到 R1 指向的内存(RAM),R1 自动+4
ROM:0C1000AE BNE loc_C10009C ; 继续循环
ROM:0C1000B0 LDR R3, =0xE000ED00 ; R3 = ARM 架构的系统控制块基地址 (SCB)
ROM:0C1000B2 STR R2, [R3,#8] ; 把 R2 的值,写入偏移 8 的位置!
ROM:0C1000B4 DSB.W SY ; 数据同步屏障
ROM:0C1000B8 POP {R4,R5,PC}
这段代码在干什么?!
-
搬运向量表:它在把 Flash 里的中断向量表,一份一份地拷贝到 RAM 里(目标地址是传入的 R1 参数)。Cortex-M 允许把向量表重定向到 RAM,这样可以在运行时动态修改中断处理函数。
-
处理弱函数/强函数:这是 NuttX 操作系统的标准操作。前 16 个中断(系统异常)有特殊的处理逻辑,后面的用默认的。
-
终极一击(点亮 VTOR):0xE000ED00 + 8 就是 Cortex-M 的 VTOR(Vector Table Offset Register,向量表偏移寄存器)这句 STR R2, [R3,#8] 的意思就是:“CPU 大人,以后遇到中断,别去 Flash 里找处理函数了,去 R2(RAM)里找吧!”
0x0C100085 这里的代码干了什么?
首先纠正一个 ARM 架构的微小细节:在 ARM Cortex-M 中,向量表第二项(复位向量)的最低位如果是 1,表示接下来的代码是 Thumb 指令。所以,CPU 真正要跳转去执行的代码地址是 0x0C100084(把末尾的 1 去掉)。
FLAHX:0C100084 loc_C100084 ; CODE XREF: FLAHX:0C10008C↓j
FLAHX:0C100084 NOP
FLAHX:0C100086 NOP
FLAHX:0C100088 NOP
FLAHX:0C10008A NOP
FLAHX:0C10008C B loc_C100084
如果 CPU 真的跳转到 0x0C100084,发生的事情非常简单:芯片会瞬间死机,卡死在这里永远不动了。这是一个经典的死循环陷阱。大老师猜测它只是操作系统里的一个弱函数占位符,如果某个中断没有被分配具体的处理函数,系统就会把向量表里对应的位置填上这个的地址。万一触发了这个没用的中断,CPU 就跑到这里执行四个 NOP 然后死循环,目的是“安全宕机,防止跑飞”。
到了这里似乎又陷入了死胡同,但是还是有一点是值得高兴的:基址的推测应该是八九不离十的,因为正正好好0x0C100084这里就是4个NOP的开始,这种“正正好好”应该能说明关于基址的推测是正确的。虽然采用这个基址之后还会有一些段(像蓝牙要用的存储空间之类的?maybe吧)因为没有恢复而出现引用错误,但通过这里的交叉验证应该能确定基址就是0x0C100000。
3:正确反编译了,那就重新分析一遍吧
大老师 重新分析了一遍按照Thumb指令正确反编译的指令,然后向我吹牛说:
恭喜你!!!你终于把这段最硬核的启动代码完整地扒出来了!
这段代码简直是教科书级别的 ARM Cortex-M33 启动流程。它不仅完美无瑕,而且直接回答了我们寻找已久的所有问题。
现在,我带你像外科医生一样,逐段解剖这段代码,你会看到整个芯片是如何从上电瞬间“活”过来的:
😓咱也不知道下面的分析对不对,有没有大佬帮忙看一下:
- 第一阶段:跳过头部,进入真正的入口 (0x0C10000E),芯片内部的 BootROM 跑完后,会跳到 0x0C100000,但前 14 个字节是文件头。真正的第一条指令在 0x0C10000E;
- 第二阶段:Cortex-M33 特有的特权级与堆栈初始化 (0x0C100014)。
- 第三阶段:.data 搬运循环,这就是你跋山涉水寻找的 .data 搬运代码! 逻辑极其清晰,一个标准的 memcpy 汇编实现。
- 第四阶段:附赠的 .bss 清零循环,未初始化的全局变量会被放在 .bss 段,启动时必须全部填 0。这段代码干的就是这个。
- 第五阶段:跳入入口函数 (0x0C10005E),汇编部分彻底结束,这里跳转到了极其遥远的 0xC52FC1C。这大概率就是 NuttX 的 nx_start() 或者 main 函数所在的地址。
所以…搬运.data 的代码就这么水灵灵的找到了?还顺便找到了入口函数?所以上电之后到第一个函数之前起作用的代码就是这些,上面的截多了:
FLAHX:0C100000 ; Processor : ARM
FLAHX:0C100000 ; ARM architecture: metaarm
FLAHX:0C100000 ; Target assembler: Generic assembler for ARM
FLAHX:0C100000 ; Byte sex : Little endian
FLAHX:0C100000
FLAHX:0C100000 ; ===========================================================================
FLAHX:0C100000
FLAHX:0C100000 ; Segment type: Pure code
FLAHX:0C100000 AREA FLAHX, CODE, ALIGN=0
FLAHX:0C100000 ; ORG 0xC100000
FLAHX:0C100000 CODE16
FLAHX:0C100000 DCB 0xFF
FLAHX:0C100001 DCB 0xFF
FLAHX:0C100002 DCB 0xFF
FLAHX:0C100003 DCB 0xFF
FLAHX:0C100004 ; ---------------------------------------------------------------------------
FLAHX:0C100004 MOVS R0, R0
FLAHX:0C100006 MOVS R5, R0
FLAHX:0C100008 MOVS R0, R0
FLAHX:0C10000A MOVS R0, R0
FLAHX:0C10000C BGT 0xC0FFFC8
FLAHX:0C10000E CMP R0, #0x76 ; 'v'
FLAHX:0C100010 LDR R0, loc_C100064
FLAHX:0C100012 BX R0 ; loc_C100014
FLAHX:0C100014 ; ---------------------------------------------------------------------------
FLAHX:0C100014
FLAHX:0C100014 loc_C100014 ; CODE XREF: FLAHX:0C100012↑j
FLAHX:0C100014 BL sub_C1012CC
FLAHX:0C100018 LDR R0, loc_C100068
FLAHX:0C10001A MSR.W MSP, R0
FLAHX:0C10001E LDR R0, unk_C10006C
FLAHX:0C100020 MSR.W MSPLIM, R0
FLAHX:0C100024 MOVS R0, #0
FLAHX:0C100026 MSR.W CONTROL, R0
FLAHX:0C10002A ISB.W SY
FLAHX:0C10002E BL sub_C100090
FLAHX:0C100032 BL sub_C100150
FLAHX:0C100036 BL sub_C1002CC
FLAHX:0C10003A LDR R1, byte_C100070
FLAHX:0C10003C LDR R2, byte_C100074
FLAHX:0C10003E LDR R3, byte_C100078
FLAHX:0C100040
FLAHX:0C100040 loc_C100040 ; CODE XREF: FLAHX:0C10004C↓j
FLAHX:0C100040 CMP R2, R3
FLAHX:0C100042 ITTT LT
FLAHX:0C100044 LDRLT.W R0, [R1],#4
FLAHX:0C100048 STRLT.W R0, [R2],#4
FLAHX:0C10004C BLT loc_C100040
FLAHX:0C10004E LDR R1, loc_C10007C
FLAHX:0C100050 LDR R2, loc_C100080
FLAHX:0C100052 MOVS R0, #0
FLAHX:0C100054
FLAHX:0C100054 loc_C100054 ; CODE XREF: FLAHX:0C10005C↓j
FLAHX:0C100054 CMP R1, R2
FLAHX:0C100056 ITT LT
FLAHX:0C100058 STRLT.W R0, [R1],#4
FLAHX:0C10005C BLT loc_C100054
FLAHX:0C10005E BL.W first_function ;
重新整理一遍整个流程:
- 0x0C100000 ~ 0x0C10000A:复位向量与对齐填充
- 0x0C10000C ~ 0x0C100012:条件校验与入口重定向
- 0x0C100014 ~ 0x0C10002A:核心系统初始化
- 0x0C10002E ~ 0x0C10003A:外设与运行时初始化
- 0x0C10003A ~ 0x0C10004C:.data 段拷贝(FLASH → RAM)
- 0x0C10004E ~ 0x0C10005C:.bss 段清零(RAM)
- 0x0C10005E:跳转至主函数
.data 段拷贝的核心逻辑如下:
LDR R1, byte_C100070 ; R1 = .data 在 Flash 中的起始地址(源)
LDR R2, byte_C100074 ; R2 = .data 在 RAM 中的起始地址(目标)
LDR R3, byte_C100078 ; R3 = .data 在 RAM 中的结束地址
loc_C100040:
CMP R2, R3
ITTT LT
LDRLT.W R0, [R1], #4 ; 若 R2<R3,从源读 4 字节到 R0,R1+=4
STRLT.W R0, [R2], #4 ; 写入目标,R2+=4
BLT loc_C100040 ; 循环直到 R2 >= R3
从哪搬:0x2C75CC14(Flash 里的全局变量初始值仓库)
搬到哪:0x2006BE68(RAM 里的家)
搬多少:一直搬到 0x2007CF30 为止
从这里可以看出,RAM的基址就是0x20000000没跑了。然后算一下从0x2006BE68到0x2007CF30大小是0x110C8个字节,我们从0x2C75CC14(这是FLASH的地址,对应到FLASHX就是0x0C75CC14)往后数0x110C8个字节看一下,也就是0xC76DCDC处:

非常的Amazing啊,正好是那段字符串的开头!也就是说.data被我们找到了!
接下来就很简单了,创建一个Segment改名叫RAM,基址设置为0x20000000,写一个脚本把数据复制过去就好了。
五:验证一下那个入口函数
找到最后跳转的那个疑似“入口函数”的伪码,大模型给出了一些猜测:
void __noreturn first_function()
{
int v0; // r0
int v1; // r0
__int64 v2; // [sp+0h] [bp-20h]
// 第一阶段:配置某种硬件上下文/内存映射窗口
// 传入的 0x40000000 和 0x40080000 极大概率是 TCM (紧耦合内存)
// 或者是某两个协核之间的共享内存物理基地址。
v2 = sub_C504A80(0, 0x40000000, 0, 1074266112, 0, 0, 0, 0x40000000, 0, 1074266112);
// 第二阶段:将配置好的上下文交给底层系统
sub_C52EDC8(v2, HIDWORD(v2));
// 第三阶段:固件签名/版本校验!(极度可疑)
// 0x2C73AA0A 绝对是一个 Feature Flag 或者是固件校验 Magic Number。
// 这个函数很可能在检查:"我现在跑的这个固件版本对不对?有没有被篡改?"
v0 = sub_C52EE12(745777674);
// 第四阶段:空函数 (可能是留给调试的 Hook)
v1 = nullsub_9(v0);
// 第五阶段:系统级初始化 (时钟、中断最终裁剪等)
sub_C534A20(v1);
// 第六阶段:【真正的 NuttX 启动点】
nx_start_inlined(); //后面改的函数名
}
然后进到最后那个函数验证一下,这是一个伪代码长达 500 多行的超级函数,这么长的函数会是系统的 main 函数么?
void __noreturn sub_C13FAC8()
{
int v0; // r2
char v1; // r3
_DWORD *v3; // r3
_DWORD *v4; // r0
void (__fastcall *v5)(_DWORD *, __int64 *); // r2
int v6; // r1
int v7; // r0
int v8; // r2
char *v9; // r3
_DWORD *v10; // r1
_DWORD *v11; // r0
int v12; // r0
int i; // r3
int v18; // r0
_DWORD *v20; // r0
int v21; // r3
_DWORD *v22; // r4
int v23; // r3
_DWORD *v24; // r5
int v25; // r0
int v26; // r1
int v27; // r1
int v28; // r2
int v29; // r0
_DWORD *v30; // r0
int v31; // r3
_DWORD *v32; // r5
int v33; // r3
void (__fastcall **v34)(_DWORD *, __int64 *); // r3
void (__fastcall *v35)(_DWORD *, __int64 *); // r2
int v36; // r3
_DWORD *v37; // r6
int v38; // r3
_DWORD *v39; // r0
_DWORD *v40; // r4
_DWORD *v41; // r6
int v42; // r5
char *v43; // r3
int v44; // r4
int v45; // r2
_QWORD *v46; // r0
int v47; // r3
_DWORD *v48; // r2
_DWORD *v49; // r0
_DWORD *v50; // r4
int v51; // r0
int v52; // r1
int j; // r3
_DWORD *v54; // r6
int v55; // r4
char *v56; // r5
int v57; // r3
int v58; // r4
int v59; // r5
int v60; // r3
int v61; // r0
_DWORD *v62; // r8
int v63; // r3
void (__fastcall *v64)(int); // r3
_DWORD *v65; // r4
_DWORD *v66; // r5
int v67; // r10
int v68; // r11
int v69; // r0
int v70; // s16
int v71; // r3
__int64 v72; // r2
_QWORD *v73; // r1
char *v74; // r3
int v75; // r1
_DWORD *v76; // r2
_DWORD *v77; // r0
_DWORD *v78; // r4
int v79; // r3
int v80; // r0
int *v81; // r5
int v82; // r0
int v83; // r3
int v84; // r4
int v85; // r3
int v86; // r0
int v87; // r4
int v88; // r0
int v89; // r4
int v90; // r6
int v91; // r0
int v92; // r5
void (__fastcall *v93)(int, int); // r3
int v94; // r0
int v95; // r0
int v96; // r0
int v97; // r0
int v99; // r0
int v101; // r4
int v102; // r0
int v103; // r0
int v104; // r0
int v105; // r0
int v106; // r0
int v107; // [sp+10h] [bp+0h]
int v108; // [sp+14h] [bp+4h]
__int64 v109; // [sp+18h] [bp+8h] BYREF
__int64 v110; // [sp+20h] [bp+10h] BYREF
int v111; // [sp+28h] [bp+18h]
LOBYTE(dword_2007D278[307]) = 1;
sub_C534B60(&dword_2007D278[2], 0, 176);
dword_2007D278[5] = 0;
LOBYTE(dword_2007D278[9]) = 3;
dword_2007D278[7] = nx_start_inlined;
dword_2007D278[8] = nx_start_inlined;
HIWORD(dword_2007D278[9]) = 6;
sub_C16513C(&dword_2007D278[38], 744329432, 31);
v0 = 744329840;
v1 = dword_2C5D9270[2 * LOBYTE(dword_2007D278[9]) + 1];
dword_2007D278[0] = &dword_2007D278[38];
_NF = (v1 & 8) != 0;
if ( (v1 & 8) != 0 )
{
v3 = (_DWORD *)dword_2007D278[21];
v0 = dword_2C5D9270[2 * LOBYTE(dword_2007D278[9])];
}
else
{
v3 = (_DWORD *)dword_2C5D9270[2 * LOBYTE(dword_2007D278[9])];
}
if ( _NF )
v3 = (_DWORD *)((char *)v3 + v0);
dword_2007D278[3] = 0;
dword_2007D278[2] = *v3;
if ( dword_2007D278[2] )
{
*(_DWORD *)(dword_2007D278[2] + 4) = &dword_2007D278[2];
*v3 = &dword_2007D278[2];
}
else
{
*v3 = &dword_2007D278[2];
v3[1] = &dword_2007D278[2];
}
dword_2007D278[514] = &dword_2007D278[2];
LOBYTE(dword_2007D278[307]) = 2;
dword_2007D278[305] = sub_C1683BC(744326864, 1014152256, 9192384);
dword_2007D278[237] = sub_C1683BC(744326869, 537766904, 542340);
dword_2007D278[306] = 4;
v4 = (_DWORD *)sub_C168A04(16);
v5 = (void (__fastcall *)(_DWORD *, __int64 *))v4;
dword_2007D278[311] = v4;
if ( !v4 )
{
v6 = 494;
LABEL_10:
v7 = 744326874;
goto LABEL_11;
}
v23 = dword_2007D278[306];
*v4 = &dword_2007D278[2];
if ( sub_C13E0E4(&dword_2007D278[2], BYTE2(dword_2007D278[9]), v4, v23) < 0 )
{
v5 = 0;
v6 = 509;
goto LABEL_10;
}
*(_DWORD *)(*(_DWORD *)(dword_2007D278[4] + 52) + 16) = dword_2007D278;
sub_C165A20(&dword_2007D278[2]);
sub_C13CB98(&dword_2007D278[2]);
sub_C13B330(&dword_2007D278[2]);
*(_BYTE *)(dword_2007D278[4] + 12) = 3;
v8 = 0;
LOBYTE(dword_2007D278[307]) = 3;
v9 = (char *)&unk_2007D134;
dword_2007D278[292] = 0;
v10 = &unk_2007D134;
unk_2007D234 = &unk_2007D134;
do
{
v11 = v10;
++v8;
v10 = v9;
*v11 = v9;
v9 += 16;
}
while ( v8 != 16 );
((void (*)(void))sub_C13BBF0)();
dword_2007D278[511] = sub_C4DDE6C(744419906);
v12 = sub_C13ABB8(537313512);
for ( i = 0; i != 94; ++i )
dword_2007D278[2 * i + 49] = sub_C13E1AC;
_R3 = 128;
__asm { MSR.W BASEPRI, R3 }
sub_C52FA08(v12, sub_C13E1AC, 537383740);
sub_C13AB58(11, sub_C166EFC, 0);
sub_C1657A0(11);
MEMORY[0xE000ED1C] = MEMORY[0xE000ED1C] & 0xFFFFFF | 0x60000000;
sub_C13AB58(12, sub_C16A454, 0);
sub_C1657A0(12);
sub_C166928(12);
sub_C13AB58(3, sub_C16B078, 0);
sub_C1657A0(3);
sub_C13AB58(4, sub_C16AE3C, 0);
sub_C1657A0(4);
sub_C13AB58(5, sub_C16AD14, 0);
sub_C1657A0(5);
sub_C13AB58(6, sub_C16AF40, 0);
sub_C1657A0(6);
v18 = sub_C534CE8(5, 0);
sub_C165AB8(v18);
_R3 = 240;
__asm { MSR.W BASEPRI, R3 }
__enable_irq();
v20 = (_DWORD *)sub_C168A04(40);
v22 = v20;
if ( !v20 )
{
sub_C16A3A4(3, 744326890, 744328757, v21);
dword_2007D278[308] = 0;
__und(0xFFu);
}
*v20 = 744329576;
v24 = v20 + 1;
v25 = sub_C534B60(&dword_2007D278[544], 0, 52);
dword_2007D278[822] = &dword_2007D278[544];
dword_2007D278[545] = sub_C51667C(v25);
sub_C516854();
sub_C5166BC(*(_DWORD *)(dword_2007D278[822] + 4), 1, sub_C163ED4);
LOBYTE(dword_2007D278[544]) = *(_DWORD *)(dword_2007D278[822] + 4);
LOBYTE(dword_2007D278[548]) = 1;
BYTE1(dword_2007D278[544]) = 0;
dword_2007D278[547] = 0;
dword_2007D278[546] = 0;
dword_2007D278[550] = 0;
v26 = dword_2007D278[545];
*v24 = dword_2007D278[544];
v24[1] = v26;
v24[2] = 0;
v24[3] = 0;
v24 += 4;
v27 = dword_2007D278[549];
v28 = dword_2007D278[550];
*v24 = dword_2007D278[548];
v24[1] = v27;
v24[2] = v28;
dword_2006BE68[10] = -1673527297;
v29 = sub_C5166B8();
LOBYTE(dword_2007D278[554]) = 1;
dword_2007D278[823] = v29;
v30 = (_DWORD *)sub_C168A04(88);
v32 = v30;
if ( v30 )
{
*v30 = v22;
v37 = v30 + 1;
sub_C161C08(v30 + 1);
v38 = sub_C4F1F1C(744326988, 744329536, v32);
if ( v38 >= 0 )
{
LABEL_24:
dword_2007D278[308] = v22;
v34 = (void (__fastcall **)(_DWORD *, __int64 *))*v22;
v35 = *(void (__fastcall **)(_DWORD *, __int64 *))(*v22 + 16);
if ( v35 )
{
v35(v22, &v109);
}
else
{
v5 = *v34;
if ( !*v34 )
{
v6 = 326;
v7 = 744324581;
goto LABEL_11;
}
v5(v22, &v110);
v109 = 10000 * v110 + v111 / 100000;
}
v36 = v109;
if ( HIDWORD(v109) )
v36 = -1;
dword_2006BE68[10] = v36;
sub_C13D6D0(537382704);
sub_C516A18(0);
LOWORD(dword_2006BE68[156]) = 1;
dword_2007D278[513] = &dword_2006BE68[155];
dword_2006BE68[157] = 0;
dword_2006BE68[158] = 0;
LOBYTE(dword_2007D278[512]) = 1;
v5 = (void (__fastcall *)(_DWORD *, __int64 *))dword_2006BE68[155];
BYTE2(dword_2006BE68[156]) = 0;
if ( !dword_2006BE68[155] )
{
v6 = 825;
v7 = 744327079;
goto LABEL_11;
}
v39 = (_DWORD *)sub_C168A04(24);
v40 = v39;
if ( v39 )
{
*v39 = &dword_2006BE68[155];
v41 = v39 + 1;
sub_C161C08(v39 + 1);
*((_WORD *)v40 + 10) = 0;
sub_C15CE7C(&v110, 16, 744327092, 0);
v39 = (_DWORD *)sub_C4F1F1C(&v110, 744329704, v40);
if ( (int)v39 < 0 )
{
sub_C162294(v41);
v39 = (_DWORD *)sub_C1685B4(v40);
}
}
v42 = 0;
sub_C4DDDD0(v39);
v43 = (char *)&dword_2007D278[312];
unk_2007D240 = 0;
unk_2007D244 = 0;
do
{
v44 = unk_2007D240;
v43[5] = 1;
*(_DWORD *)v43 = 0;
if ( v44 )
*unk_2007D244 = v43;
else
unk_2007D240 = v43;
++v42;
unk_2007D244 = v43;
v43 += 96;
}
while ( v42 != 8 );
unk_2007CF48 = 0;
unk_2007CF4C = 0;
dword_2007D278[532] = 0;
dword_2007D278[533] = 0;
dword_2007D278[534] = 0;
dword_2007D278[535] = 0;
dword_2007D278[536] = 0;
dword_2007D278[537] = 0;
dword_2007D278[540] = 0;
dword_2007D278[541] = 0;
dword_2007D278[538] = 0;
dword_2007D278[539] = 0;
v5 = (void (__fastcall *)(_DWORD *, __int64 *))((int (*)(void))sub_C13A944)();
if ( !v5 )
{
v6 = 210;
LABEL_44:
v7 = 744327103;
goto LABEL_11;
}
v5 = (void (__fastcall *)(_DWORD *, __int64 *))sub_C13A944(&dword_2007D278[536], 8, 2);
if ( !v5 )
{
v6 = 216;
goto LABEL_44;
}
v5 = (void (__fastcall *)(_DWORD *, __int64 *))sub_C13A982(&dword_2007D278[540], 4, 0);
if ( !v5 )
{
v6 = 222;
goto LABEL_44;
}
v5 = (void (__fastcall *)(_DWORD *, __int64 *))sub_C13A982(&dword_2007D278[538], 8, 2);
if ( !v5 )
{
v6 = 228;
goto LABEL_44;
}
sub_C4DDD72(537312888, 0, v5);
sub_C4DDD72(537312896, 2, v45);
v46 = (_QWORD *)sub_C168D30(384);
if ( v46 )
{
HIDWORD(v72) = &dword_2006BE68[8];
v73 = v46 + 48;
do
{
LODWORD(v72) = dword_2006BE68[8];
*v46 = v72;
*(_DWORD *)(v72 + 4) = v46;
dword_2006BE68[8] = v46;
v46 += 6;
}
while ( v46 != v73 );
}
v47 = 0;
v48 = &unk_2007CFB4;
v49 = (_DWORD *)unk_2007CFB0;
do
{
v50 = v49;
++v47;
v49 = v48;
*v48 = v50;
v48 += 6;
}
while ( v47 != 16 );
unk_2007CFB0 = 537383196;
v51 = sub_C13BBF0(v49);
v52 = unk_2007CF68;
unk_2007CF68 = dword_2006BE68;
dword_2006BE68[0] = v52;
sub_C13BA24(v51);
for ( j = 0; j != 4096; j += 4 )
*(_DWORD *)(j + 537293688) = -559038737;
v54 = &dword_2007D278[564];
v55 = 0;
v56 = (char *)&dword_2007D278[564];
do
{
v57 = (char)v55++;
sub_C1441A4(v56, 744460961, 0, v57);
v56 += 120;
}
while ( v55 != 4 );
v58 = 0;
v59 = 537386280;
do
{
v60 = (char)v58++;
v61 = sub_C1441A4(v59, 744460961, 1, v60);
v59 += 120;
}
while ( v58 != 4 );
v62 = &dword_2006BE68[12];
v108 = 0;
while ( 2 )
{
v63 = v62[66];
if ( v63 )
{
v64 = *(void (__fastcall **)(int))(v63 + 4);
if ( v64 )
v64(v61);
}
v62[66] = 744329408;
sub_C4FEF24(v62 + 67);
v65 = v54;
v66 = v54;
v107 = 0;
do
{
v67 = v66[8];
v68 = *((char *)v66 + 36);
v69 = sub_C146C80(&dword_2006BE68[64 * v67 + 79]);
v5 = (void (__fastcall *)(_DWORD *, __int64 *))v65[16];
v70 = v69;
if ( !v5 )
{
v71 = v66[10];
if ( v71 == -1 )
{
v6 = 459;
v7 = 744327127;
goto LABEL_11;
}
v66[10] = v71 + 1;
if ( !v71 )
{
v65[12] = 0;
v74 = (char *)&dword_2006BE68[64 * v67 + 20 + 2 * v68];
v75 = *((_DWORD *)v74 + 1);
v76 = v65 + 11;
v65[11] = v75;
if ( v75 )
{
*(_DWORD *)(v75 + 4) = v76;
*((_DWORD *)v74 + 1) = v76;
}
else
{
*((_DWORD *)v74 + 1) = v76;
*((_DWORD *)v74 + 2) = v76;
}
sub_C146B24(v65, 1);
}
}
if ( sub_C13AF08(v65 + 14) <= 69999 )
sub_C13D6D0(v65 + 14);
sub_C146CAC(&dword_2006BE68[64 * v67 + 79], v70);
v61 = sub_C146E6C(v67);
v66 += 30;
_ZF = v107++ == 3;
v65 += 30;
}
while ( !_ZF );
v62 += 64;
_ZF = v108 == 480;
v54 += 120;
v108 += 480;
if ( !_ZF )
continue;
break;
}
sub_C4F1F1C(744327150, 744329664, 537313524);
sub_C4F1F1C(744327160, 744329760, 0);
sub_C4F1F1C(744327169, 744329328, 0);
dword_2007D278[504] = 101;
dword_2007D278[505] = 12713984;
dword_2007D278[506] = -905969664;
dword_2007D278[507] = 97;
sub_C4F1F1C(744327179, 744329968, 0);
sub_C4F1F1C(744327192, 744329484, 0);
sub_C4F1F1C(744327202, 744329624, 0);
v77 = (_DWORD *)sub_C168A04(28);
v77[1] = sub_C14164A;
v78 = v77;
v77[2] = sub_C141652;
*v77 = sub_C141640;
v77[3] = sub_C141620;
if ( sub_C143278(744327212, v77) < 0 )
{
sub_C1685B4(v78);
sub_C16A3A4(3, 744327225, 744328984, v79);
}
v80 = sub_C168A04(16);
v81 = (int *)v80;
if ( v80 )
{
*(_DWORD *)(v80 + 12) = sub_C145BEA;
v82 = sub_C168A04(32);
v84 = v82;
if ( !v82 )
{
sub_C16A3A4(3, 744327269, 744328921, v83);
goto LABEL_87;
}
*(_BYTE *)(v82 + 28) = 8;
*(_DWORD *)(v82 + 24) = v81;
*v81 = v82;
*(_DWORD *)(v82 + 16) = v82 + 16;
*(_DWORD *)(v82 + 20) = v82 + 16;
sub_C161C08(v82);
if ( sub_C4F1F1C(744327302, 744329444, v84) < 0 )
{
sub_C162294(v84);
sub_C1685B4(v84);
LABEL_87:
sub_C1685B4(v81);
sub_C16A3A4(3, 744327317, 744328894, v85);
}
}
v86 = sub_C168A04(24);
v87 = v86;
if ( v86 )
{
sub_C161C08(v86);
*(_DWORD *)(v87 + 16) = v87 + 16;
*(_DWORD *)(v87 + 20) = v87 + 16;
if ( sub_C4F1F1C(744327363, 744330008, v87) < 0 )
{
sub_C162294(v87);
sub_C1685B4(v87);
}
}
if ( sub_C146DF8(537313464) )
{
v5 = 0;
v6 = 646;
v7 = 744327376;
goto LABEL_11;
}
dword_2007D278[808] = 0;
dword_2007D278[806] = 744330088;
LOWORD(dword_2007D278[812]) = 0;
LOBYTE(dword_2007D278[807]) = 10;
v88 = sub_C168A04(44);
v89 = v88;
if ( v88 )
{
v90 = v88 + 20;
sub_C161C08(v88 + 20);
*(_DWORD *)(v89 + 40) = &dword_2007D278[806];
v91 = sub_C168D44(744327391);
*(_DWORD *)(v89 + 36) = v91;
if ( !v91 )
{
LABEL_100:
sub_C162294(v90);
sub_C1685B4(v89);
goto LABEL_105;
}
if ( sub_C4F1F1C(744327391, 744330048, v89) < 0 )
{
sub_C1685B4(*(_DWORD *)(v89 + 36));
goto LABEL_100;
}
v92 = *(_DWORD *)(v89 + 40);
if ( !*(_BYTE *)(v89 + 16) )
{
*(_DWORD *)(v89 + 12) = sub_C141AAC;
sub_C146DF8(v89);
*(_BYTE *)(v89 + 16) = 1;
v93 = *(void (__fastcall **)(int, int))(*(_DWORD *)v92 + 16);
if ( v93 )
v93(v92, 60000);
(**(void (__fastcall ***)(int))v92)(v92);
}
}
LABEL_105:
v5 = (void (__fastcall *)(_DWORD *, __int64 *))dword_2007D278[4];
LOBYTE(dword_2007D278[307]) = 4;
if ( !dword_2007D278[4] )
{
v6 = 68;
v7 = 744327406;
goto LABEL_11;
}
v94 = sub_C4DDDA0(744327169, 3);
if ( v94 )
{
if ( v94 > 0 )
sub_C4F3104(*(_DWORD *)(dword_2007D278[508] + 8), v94);
}
else
{
sub_C38FF70(dword_2007D278[508], 0, 1);
sub_C38FF70(dword_2007D278[508], 0, 2);
if ( sub_C13B384(&dword_2007D278[2]) >= 0 )
{
LOBYTE(dword_2007D278[307]) = 5;
sub_C4E2A44(744393316, 744569844);
sub_C4E4C20(744327435, 224, 537383500);
v95 = sub_C4E4C20(744327442, 100, 537384720);
v96 = sub_C13AB44(v95);
v97 = sub_C13EF28(744327449, 2, 240, 0, 10240, sub_C1405A0, 0, v96);
if ( v97 > 0 )
{
_R6 = 128;
LOBYTE(dword_2007D278[307]) = 6;
v99 = sub_C13BA24(v97);
while ( 1 )
{
LOBYTE(_R5) = __get_CPSR();
__asm { MSR.W BASEPRI, R6 }
sub_C13BBF0(v99);
v101 = sub_C1441E4(0);
v102 = sub_C146CD0(0, v101);
if ( v102 )
{
LABEL_117:
sub_C534800(v102);
}
else
{
switch ( v101 )
{
case 0:
goto LABEL_117;
case 1:
v104 = sub_C52FE24();
v105 = sub_C534800(v104);
sub_C52FE3C(v105);
break;
case 2:
v106 = 0;
goto LABEL_121;
case 3:
v106 = 1;
LABEL_121:
sub_C4F7D04(v106);
break;
default:
break;
}
}
v103 = sub_C146CD0(0, -1);
_R5 = (unsigned __int8)_R5;
v99 = sub_C13BA24(v103);
__asm { MSR.W BASEPRI, R5 }
}
}
v5 = 0;
v6 = 367;
v7 = 744327460;
LABEL_11:
sub_C161678(v7, v6, v5);
}
}
v5 = 0;
v6 = 652;
goto LABEL_10;
}
sub_C16A3A4(3, 744327001, 744329009, v38);
sub_C162294(v37);
sub_C1685B4(v32);
}
else
{
sub_C16A3A4(3, 744326940, 744329009, v31);
}
sub_C16A3A4(3, 744327040, 744328757, v33);
goto LABEL_24;
}
大老师找到了几个号称是“决定性”的证据,证明了它就是 NuttX 操作系统的内核启动函数被编译器暴力内联后的产物:
证据 1:启动状态机
在 NuttX 官方源码 sched/init/nx_start.c 中,有一个著名的全局变量 g_nx_initstate 用来记录启动阶段。在这个伪代码中,我们看到了完美的对应:
LOBYTE(dword_2007D278[307]) = 1; // 状态 1
...
LOBYTE(dword_2007D278[307]) = 2; // 状态 2
...
LOBYTE(dword_2007D278[307]) = 6; // 最终态 6 (OS_RUNNING)
0x2007D278 就是 NuttX 的全局系统控制块!
证据 2:内核内存池分配
代码里疯狂出现 sub_C168A04(16)、sub_C168A04(40) 等。这根本不是业务逻辑,这是操作系统在启动时通过 kmm_malloc 动态创建任务控制块(TCB)、信号量等内核对象。
证据 3:中断控制器绑定
_R3 = 128;
__asm { MSR.W BASEPRI, R3 } // 屏蔽低优先级中断
sub_C13AB58(11, sub_C16A454, 0); // 绑定中断号 11
sub_C1657A0(11); // 使能中断 11
...
sub_C13AB58(3, sub_C16B078, 0);
这是极度底层的 irq_attach 操作,在为操作系统的硬件中断挂载 C 语言处理函数。
证据 4:驱动注册的常量
满屏的 sub_C4F1F1C(744326988, 744329536, obj)。这些 744xxxxx 绝对不是乱码,它们是被混淆或 Hash 化的设备路径 ID(比如对应 /dev/uart0 等)。这是在执行 NuttX 的 register_driver。
证据 5:最后的无限死循环
在函数的最后 30 行,我们看到了操作系统的灵魂:
v97 = sub_C13EF28(..., sub_C1405A0, ...); // 创建初始任务
if ( v97 > 0 ) {
LOBYTE(dword_2007D278[307]) = 6; // 状态机进入 OS_RUNNING
while ( 1 ) { // 【系统级死循环】
__asm { MSR.W BASEPRI, R6 } // 进入临界区
sub_C13BBF0(v99); // 上下文切换
v101 = sub_C1441E4(0); // 获取下一个任务
sub_C534800(v102); // 调度执行
__asm { MSR.W BASEPRI, _R5 } // 退出临界区
}
}
没有任何一个正常的 main 函数会在内联汇编里死循环操作 BASEPRI(中断优先级屏蔽寄存器)。这是操作系统内核的 Idle 任务(空闲任务)底层实现! 从这一秒起,多任务调度器正式运转。
为什么它会这么长?
因为厂商开启了 LTO(链接时优化),把原本分散在 up_initialize()、board_initialize()、nx_bringup() 等几十个 C 文件里的初始化代码,全部“拍扁”内联到了一个函数里。
在 NuttX 这种高级 RTOS 里,根本没有传统意义上的 main 函数。你的蓝牙协议栈、音频解码代码,全都被封装成了“任务”。启动链路的大致全貌已经浮出水面:
0xFF头文件 -> 硬件特权级初始化 -> FPU/向量表配置 -> .data/.bss内存搬运 -> BES定制校验层 -> NuttX nx_start -> 进入 Idle 死循环调度。
接下来我们要去哪找业务逻辑?操作系统已经跑起来了,不用再管这个函数了,去寻找那些由 task_create 创建出来的线程!去全局搜索 sub_C13EF28(任务创建函数)被调用的地方,找到那些作为参数传进去的函数指针(比如上文提到的 sub_C1405A0),那里才是真正通往应用层的大门。
分析到这里就没有再继续了,主要不知道后面能拿来干嘛,后面有机会的话再更新吧。
更新
用科恩实验室的BinaryAI分析了一波,发现固件里主要还是只包含了一些基础组件以及JavaScript和Lua的解释器?所以业务逻辑还是要看文件系统里存着的脚本或者字节码了吧😔。

识别出Lua和JS的函数可信度还是蛮高的:


值得一提的是,BinaryAI并没有“命中”NuttX的系统内核,可能是因为FreeRTOS同为RTOS有些函数跟NuttX很像,没有足够的区分度吧,但单个函数还是识别出来很多来自NuttX的:

博客主要是为了记录学习中遇到的问题,仅供参考,可能由于技术有限以及大模型幻觉会出现错误,欢迎指正。
博客内容仅限于技术学习、学术研究与安全交流目的,旨在探讨软件工作原理,所有案例分析均基于公开可获取的信息。
博客可能引用或转载第三方技术文章、代码片段或观点。此类内容仅代表原作者立场。转载内容将尽力注明出处,如涉及版权异议,请联系以便及时处理。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)