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, 二进制分析, 云安全运维, 反汇编器, 安全规则引擎, 底层开发, 指令解码器, 无后门, 逆向工具