Featured image of post qemu 模拟 Arduino UNO 环境&调试

qemu 模拟 Arduino UNO 环境&调试

字数: 893

最近在做的 GooseHeap OS 很需要对一些全局变量和寄存器进行调试,但单片机很难做调试,于是乎就开始搞鼓 qemu 跑 arduino 虚拟机。

qemu-system-avr

qemu 有 arduino uno 对应的 avr 虚拟机,虽然不够完善,但已有实现的 USART 和 16 位定时器对于 GooseHeap OS 的多任务调试已经够用。
直接用 pacman 安装:

1
sudo pacman -S qemu-system-avr

现在的 qemu-system-avr 支持仿真的微处理器可以通过 -machine help 参数查看:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
❯ qemu-system-avr -machine help
Supported machines are:
2009                 Arduino Duemilanove (ATmega168) (alias of arduino-duemilanove)
arduino-duemilanove  Arduino Duemilanove (ATmega168)
mega2560             Arduino Mega 2560 (ATmega2560) (alias of arduino-mega-2560-v3)
arduino-mega-2560-v3 Arduino Mega 2560 (ATmega2560)
mega                 Arduino Mega (ATmega1280) (alias of arduino-mega)
arduino-mega         Arduino Mega (ATmega1280)
uno                  Arduino UNO (ATmega328P) (alias of arduino-uno)
arduino-uno          Arduino UNO (ATmega328P)
none                 empty machine

可以看到 arduino-uno 的微处理器 ATmega328P 是支持的。

avr-gdb

调试使用 gdb,需要 avr-gdb :

1
sudo pacman -S avr-gdb gdb-common

avr-gdb 可以调试跑在 qemu 下的 avr 架构程序。

第一次调试

对 GooseHeapOS 进行调试,现有的 Makefile 没有加调试参数所以采用以下参数进行构建:

1
make -B CFLAGS='-mmcu=atmega328p -DF_CPU=16000000UL -Og -g3 -Wall -Wextra'

之后使用 qemu-system-avr 直接调试:

1
2
3
4
5
6
7
qemu-system-avr \
    -M uno \
    -bios main.elf \
    -display none \
    -monitor none \
    -serial stdio \
    -s -S

此时没有任何输出,因为通过 -s -S 参数开启 GDB Server 并暂停 CPU 等待调试。
因为我 gdb 用的 pwndbg,很多 gdb 配置对 avr-gdb 是不兼容的,所以用 -nx -q 不使用任何配置文件。用如下参数在新终端打开 gdb:

1
avr-gdb -nx -q main.elf

在 gdb 内用如下参数连接 qemu 的 gdb server:

1
target remote localhost:1234

可见此时停在 0x00000000 地址,地址的最开头,为中断向量表的起始点。

可以用 disassemble __vectors 反编译中断向量表:

可以看到 ATmega328P 的前 0x64 地址放着中断向量表,且每一个中断向量表占用 4 字节。每个中断向量是一个 jmp 指令。此时的 PC 指针停在 0x00000000,怎么证明呢?当然,在 gdb 下是轻而易举的。
通过 p $pc 即可打印出 pc 寄存器:

可我们的 GooseHeap OS 是从 main 开始的,但 main 之前有很多的初始化工作,可以通过 tui enable、tui layout asm 打开一个显示反汇编的窗口,然后通过 si 单步执行一步步看 avr-gcc 编译出来的程序结构。

跳转函数

单步执行显然很慢,因为我们编译程序的时候是保存了函数名的 label,gdb 可以根据函数的 label 打断点进行跳转,比如:

1
2
b main
c

这样我们就跳转到 main 函数的开头,我们就可以和以往使用 gdb 一样,对 avr 程序进行调试了。

qemu-system-avr 参数

我们刚才使用的 qemu 参数为:

1
2
3
4
5
6
7
qemu-system-avr \
    -M uno \
    -bios main.elf \
    -display none \
    -monitor none \
    -serial stdio \
    -s -S

这里简单讲下每个参数的作用:

参数 含义
-M uno -M 是 -machine 的缩写
-bios main.elf 制定加载的固件文件
-display none 不创建图形显示窗口
-monitor none 禁用 QEMU Monitor
-serial stdio 将串口映射到宿主机标准输入输出
-s -gdb tcp::1234 的简写
-S 启动时不执行 CPU 指令