4、第一个仿真脚本:使用Avatar2仿真一个简单的ARM Cortex-M3固件,理解基本流程
好,我们正式开始动手了。
前面几章我们把Avatar2的环境搭好,也聊了聊它的设计哲学。说实话,光看文档和理论,总觉得隔着一层纱。我个人的习惯是,不管三七二十一,先跑通一个最简单的例子再说。哪怕只是点个灯,也能让你对整个框架有个直观的感受。
这一章,我们就来写第一个仿真脚本。目标很简单:用Avatar2仿真一个ARM Cortex-M3的固件。这个固件本身没什么复杂的逻辑,就是读写几个寄存器,然后进入死循环。但通过这个流程,你能把Avatar2的核心工作流摸清楚——创建架构、加载固件、启动仿真、观察状态。
4.1 准备工作:一个最小的Cortex-M3固件
首先,我们需要一个目标固件。这里我准备了一个非常简单的例子,用汇编写的,直接操作内存地址。
为什么用汇编?因为Cortex-M3启动后,第一条指令就是从0x00000004地址取PC指针,然后开始执行。用C语言的话,编译器会帮你做很多初始化工作,反而把核心逻辑藏起来了。我们做逆向的,就是要看最底层的东西。
; minimal_cm3.s
; 一个最简单的Cortex-M3固件
; 功能:向0x40000000地址写入0xDEADBEEF,然后死循环
.syntax unified
.cpu cortex-m3
.thumb
.global _start
.section .text
_start:
; 设置栈指针(随便给个地址)
ldr sp, =0x20001000
; 向0x40000000写入0xDEADBEEF
ldr r0, =0x40000000
ldr r1, =0xDEADBEEF
str r1, [r0]
; 死循环
loop:
b loop
.section .isr_vector
.word 0x20001000 ; 初始栈指针
.word _start ; 复位向量
编译成二进制文件:
arm-none-eabi-as -mcpu=cortex-m3 -mthumb minimal_cm3.s -o minimal_cm3.o
arm-none-eabi-ld -Ttext=0x00000000 -o minimal_cm3.elf minimal_cm3.o
arm-none-eabi-objcopy -O binary minimal_cm3.elf minimal_cm3.bin
嗯,这里要注意一点:链接脚本里指定了起始地址为0x00000000,这是Cortex-M3的默认启动地址。如果你用别的地址,记得调整。
4.2 编写第一个Avatar2仿真脚本
现在,我们来写Python脚本。这个脚本会完成以下几件事:
- 创建一个Avatar2仿真引擎实例
- 添加一个ARM Cortex-M3架构
- 创建一个内存映射,把固件加载进去
- 设置仿真器的起始状态
- 运行几条指令,观察结果
直接上代码:
#!/usr/bin/env python3
# first_avatar_script.py
import logging
from avatar2 import *
# 配置日志,方便调试
logging.basicConfig(level=logging.INFO)
# 1. 创建Avatar实例
avatar = Avatar(arch=ARM_CORTEX_M3, output_directory='/tmp/avatar_output')
# 2. 添加一个Cortex-M3架构的target
target = avatar.add_target(
name='cm3_target',
arch=ARM_CORTEX_M3,
# 这里我们使用QEMU作为后端仿真器
# 因为QEMU对Cortex-M3支持得比较好
backend=QEMU,
# 指定CPU型号
cpu_model='cortex-m3'
)
# 3. 创建内存映射
# 先创建一个ROM区域,存放固件
rom = avatar.add_memory(
name='rom',
size=0x1000, # 4KB
address=0x00000000,
permissions='r-x', # 读+执行
file='minimal_cm3.bin' # 加载固件文件
)
# 再创建一个RAM区域,用于栈和变量
ram = avatar.add_memory(
name='ram',
size=0x1000, # 4KB
address=0x20000000,
permissions='rw-' # 读+写
)
# 4. 初始化仿真器
avatar.init_targets()
# 5. 设置初始状态
# 对于Cortex-M3,复位后PC从0x00000004读取
# 但Avatar2/QEMU会自动处理向量表,所以我们直接设置PC
target.set_register('pc', 0x00000008) # 跳过向量表,直接到代码
# 6. 运行几步,观察效果
print("=== 开始仿真 ===")
# 先读取初始的寄存器状态
r0_init = target.get_register('r0')
r1_init = target.get_register('r1')
pc_init = target.get_register('pc')
print(f"初始状态: PC=0x{pc_init:08X}, R0=0x{r0_init:08X}, R1=0x{r1_init:08X}")
# 单步执行一条指令
target.step()
pc_after_step1 = target.get_register('pc')
print(f"执行一步后: PC=0x{pc_after_step1:08X}")
# 再执行一步(加载地址到R0)
target.step()
r0_after = target.get_register('r0')
print(f"执行两步后: R0=0x{r0_after:08X} (应该为0x40000000)")
# 再执行一步(加载立即数到R1)
target.step()
r1_after = target.get_register('r1')
print(f"执行三步后: R1=0x{r1_after:08X} (应该为0xDEADBEEF)")
# 再执行一步(STR指令,写入内存)
target.step()
# 读取内存地址0x40000000的值
mem_value = target.read_memory(0x40000000, 4)
print(f"执行四步后: 内存[0x40000000] = 0x{int.from_bytes(mem_value, 'little'):08X} (应该为0xDEADBEEF)")
# 7. 清理
avatar.shutdown()
print("=== 仿真结束 ===")
4.3 运行脚本,看看效果
把上面的脚本保存为 first_avatar_script.py,然后运行:
python3 first_avatar_script.py
如果一切顺利,你会看到类似这样的输出:
=== 开始仿真 ===
初始状态: PC=0x00000008, R0=0x00000000, R1=0x00000000
执行一步后: PC=0x0000000A
执行两步后: R0=0x40000000 (应该为0x40000000)
执行三步后: R1=0xDEADBEEF (应该为0xDEADBEEF)
执行四步后: 内存[0x40000000] = 0xDEADBEEF (应该为0xDEADBEEF)
=== 仿真结束 ===
看到这个输出,说明你的第一个Avatar2仿真脚本跑通了!
我个人第一次跑通的时候,说实话挺激动的。虽然只是写了个内存,但你能感觉到,这个框架真的在模拟一个完整的CPU,指令一条条在执行,状态在变化。这种感觉,跟看文档是完全不一样的。
4.4 核心流程拆解
我们来拆解一下这个脚本背后的逻辑。说白了,Avatar2的仿真流程就四步:
Avatar2仿真核心四步曲
- 创建引擎:Avatar(arch=...) —— 确定你要仿真的架构
- 添加目标:add_target() —— 指定后端仿真器(QEMU、Unicorn、PyPanda等)
- 构建内存:add_memory() —— 定义地址空间,加载固件
- 运行与交互:step()、get_register()、read_memory() —— 控制执行,观察状态
这四步,几乎覆盖了所有Avatar2仿真场景。不管你是仿真一个简单的MCU,还是一个复杂的SoC,流程都是这样。只是后面的内存映射会更复杂,交互会更频繁。
这里我画了一张流程图,帮你把整个逻辑串起来:
4.5 避坑指南:我踩过的几个坑
第一次跑这个脚本,你可能会遇到一些问题。我把自己踩过的坑列出来,你遇到了可以直接对照:
常见问题与解决方案
- QEMU后端找不到:确保你安装了
qemu-system-arm。Avatar2默认会调用系统的QEMU。如果没装,会报错说找不到可执行文件。 - 固件加载地址不对:Cortex-M3的向量表必须从0x00000000开始。如果你把固件加载到其他地址,PC会从错误的位置取值,导致仿真直接崩溃。
- 权限设置错误:代码段要设置
permissions='r-x',RAM要设置permissions='rw-'。如果代码段没有执行权限,QEMU会报段错误。 - 寄存器名字写错:ARM的寄存器名字是
r0、r1、pc、sp,注意大小写。我一开始写成R0,结果报错说找不到寄存器。
调试小技巧
如果你不确定仿真器当前的状态,可以多打印一些信息。比如:
# 打印所有通用寄存器的值
for reg in ['r0', 'r1', 'r2', 'r3', 'r4', 'pc', 'sp', 'lr']:
val = target.get_register(reg)
print(f"{reg} = 0x{val:08X}")
这样能帮你快速定位问题出在哪一步。
4.6 小结:你学到了什么
这一章,我们亲手写了一个Avatar2仿真脚本。你学会了:
- 如何创建Avatar2引擎实例
- 如何添加一个Cortex-M3目标,并指定QEMU作为后端
- 如何创建内存映射,加载固件文件
- 如何单步执行指令,读取寄存器和内存
说白了,这就是Avatar2的“Hello World”。虽然简单,但五脏俱全。后面的章节,我们会在这个基础上,加入外设仿真、动态分析、甚至多核调试。但核心的骨架,就是这四步。
嗯,我建议你把这段代码多跑几遍,改改参数试试。比如换个固件,或者试试读取不同的内存地址。只有亲手折腾过,才能真正理解每个API的作用。
公众号:蓝海资料掘金营,微信deep3321