chgroeling/armv7m-decoder
GitHub: chgroeling/armv7m-decoder
一个基于 YAML 规格自动生成的 ARMv7-M Thumb 指令解码器与反汇编器,提供与 objdump 兼容的输出并可作为 Python 库使用。
Stars: 0 | Forks: 0
# armv7m-decoder
ARMv7-M (Thumb) 架构的指令解码器和反汇编器,
使用 Python 编写。解码器本身由
[decoder-forge](https://github.com/chgroeling/decoder-forge) 根据指令集的 YAML
描述生成;反汇编器输出的 UAL 汇编代码
与 `arm-none-eabi-objdump` 保持一致。
Python 3.12+ · GPL-3.0-only · 0.1.0
## 为什么会有这个项目
我一直想构建一个能正确模拟小型微控制器的模拟器。这是迈向该目标的第一步:一个用于 ARMv7-M
架构的反汇编器。
为什么选择 Python?有何不可。反正整个解码器都是由 decoder-forge 自动生成的。对于个人项目而言,Python 非常简单。我知道它速度慢——但我的目标是开发速度快,且少有容易出错的细节。
- Python 易于扩展
- 无需编译任何内容
- 生态系统庞大
也许有一天这个反汇编器会有一个 C++ 版本。我们拭目以待。
## 覆盖范围
据我所知,ARMv7-M 指令集是完整的:包含 260
条指令和 369 种编码,取自 Armv7-M Architecture Reference
Manual (ARM DDI 0403E.e),包括浮点扩展。其中每一条指令都有对应的反汇编格式化器,因此没有任何一条指令会退回到直接打印其原始字段的状态。
我还没有发现哪个应该被解码的字却没有被解码。相比于一份长达 700 页的手册的全面验证,这个结论显得有些单薄,所以请将其理解为“目前未发现缺失”,而不是“已完全验证”——如果您发现遗漏,请提交一个 issue。
仅包含解码和反汇编功能。此处不执行任何指令;开发模拟器是下一步的工作,而不是当前这一步。
## 安装说明
尚未发布到 PyPI,因此请从代码库安装:
```
uv add git+https://github.com/chgroeling/armv7m-decoder
```
或者,如果您希望检出代码并进行开发:
```
git clone https://github.com/chgroeling/armv7m-decoder
cd armv7m-decoder
uv sync
```
## 命令行
```
armv7m-decoder decode firmware.bin --start-address 0x0
```
输出格式为 objdump 的列表格式——地址、指令字节、汇编代码:
```
0: b510 push {r4, lr}
2: 2400 movs r4, #0
4: 2800 cmp r0, #0
6: bf08 it eq
8: 2401 moveq r4, #1
a: 6843 ldr r3, [r0, #4]
c: f20d 154f addw r5, sp, #335 @ 0x14f
10: b2da uxtb r2, r3
12: bd10 pop {r4, pc}
```
| 选项 | 含义 |
| --- | --- |
| `--start-address` | 文件中开始解码的偏移量(默认为 `0x0`) |
| `--out-file` | 将列表输出到文件而不是 stdout |
| `--max-instructions` | 在反汇编指定数量的指令后停止 |
## 库
解码一个字需要两步,因为 Thumb 在解码任何内容之前,会根据其第一个半字来确定指令的宽度:
```
from armv7m_decoder import Context, decode, disassemble, instr_size
ctx = Context()
size = instr_size(0x2401) # SIZE_16BIT
result = decode(0x2401, ctx, size)
# MOV_immediate(encoding=, sideeffects=0, d=4,
# setflags=True, imm32=1, carry=0)
disassemble(result, size) # 'movs\tr4, #1'
```
`instr` 精确包含 `size` 位:对于 16 位指令是一个单纯的半字,对于 32 位指令则是一个完整的字(第一个半字位于高位半字区)。`decode` 仅返回指令本身——每条指令都对应一个 dataclass,其中包含一个 `encoding` 成员,用于指示匹配了哪种形式,如果没有匹配项,则返回 `NoMatch`。
### 遍历流
`IT` 会使接下来的最多四条指令变为条件执行,且这些指令的条件和 `S` 位并不在它们各自的编码中——这两者均来自 ITSTATE。因此,对流的解码是带状态的:在 `Context.istate` 中维护 ITSTATE,将相同的值传递给 `disassemble`,以便它能拼写出 `moveq` 而不是 `mov`,并在每条指令执行后更新它:
```
from armv7m_decoder import next_itstate
size = instr_size(hw1)
istate = ctx.istate
result = decode(instr, ctx, size)
asm = disassemble(result, size, offset, istate)
ctx.istate = next_itstate(istate, result)
```
该 size 字段记录了编码是否匹配,这正是确保在未匹配时流依然能保持同步的原因:一个返回 `NoMatch` 的字仍然会被整个跳过,而不是将其第二个半字作为独立的指令进行解码。
### 副作用
该架构会将某些字标记为 `UNDEFINED`、`UNPREDICTABLE` 或 `SEE
`。这样的字依然会被完整解码:每个字段都会被填充,并且这些标记会出现在指令的 `sideeffects` 成员中(`SIDEFFECT_UNDEFINED`、`SIDEFFECT_UNPREDICTABLE`、`SIDEFFECT_SEE`、`SIDEFFECT_NONE`)。如何处理这些情况由调用者决定。
`disassemble` 无论如何都会拼写出这样的字,并在其前面加上标记:
```
ldrb.w fp, [sp], #161
it al
```
一个字可以带有多个标记,每个标记按 `see`、`undefined`、`unpredictable` 的顺序命名——关于该字的最强烈的断言排在最前面。完全没有匹配任何编码的字会显示为 ``;CLI 会将其转换为 objdump 的 `@ instruction: 0x…` 注释,因为这是仍然保留该字本身的地方。
## 与 objdump 的对比
该反汇编器已与 `arm-none-eabi-objdump` 进行了逐行对比,上面的列表与 objdump 对于相同字节的输出完全一致。
有一类指令是刻意设计为不同的。objdump 打印协处理器 1 和 2 的访问时,使用了 FPA 的助记符——FPA 是 ARM 在 90 年代初期的浮点加速器,它存在于这两个协处理器端口上,并将其指令编码为普通的 `LDC`/`STC`/`CDP`/`MRC`:
```
ecf0 0102 ldfe f0, [r0], #8 # objdump
ecf0 0102 ldcl 1, cr0, [r0], #8 # armv7m-decoder
```
这些是相同的 32 位,因此没有反汇编器能区分它们——其名称取决于连接到端口上的设备,而单凭这个字是无法得知的。ARMv7-M 没有 FPA,并且 objdump 自带的汇编器拒绝为 Cortex-M 目标使用 `ldfe`,因此本包打印出 Armv7-M ARM 定义的 `LDC` 形式。所有其他协处理器编号的输出都与 objdump 逐字符完全一致。
## 开发
```
uv sync # Install dependencies
uv run pytest # Run the test suite
uv run ruff check # Lint
uv run ruff format # Format
uv run python -m armv7m_decoder._generate # Regenerate the decoder
```
`formats/armv7-m.yaml` 是指令集的真理来源。
重新生成时会使用 decoder-forge 的新输出重写 `src/armv7m_decoder/_decoder.py`。该文件已被提交且是自包含的,因此无需安装 decoder-forge 即可运行本包——只有重新生成时才需要它。
`AGENTS.md` 更详细地记录了项目的分层结构及其背后的决策。
## 许可证
GPL-3.0-only。详见 [LICENSE](LICENSE)。
标签:ARM架构, Python, 二进制分析, 云安全运维, 反汇编器, 安全规则引擎, 底层开发, 指令解码器, 无后门, 逆向工具