通过 qemu+gdb 实现跨架构调试

需求

有时候身边只有 x86 架构的硬件环境,却想学习和测试 arm、mips 等其他架构特性,此时怎么办呢?众所周知,VMware 只能模拟同架构不同操作系统,对此可以通过 qemu 实现跨架构模拟。

安装 qemu-user

qemu 是一个支持跨平台虚拟化的虚拟机,有 user mode 和 system mode 两种配置方式。其中 qemu 在 system mode 配置下模拟出整个计算机,可以在 qemu 之上运行一个操作系统。qemu 的 system mode 与常见的 VMware 和 Virtualbox 等虚拟机比较相似,但是 qemu 的优势是可以跨指令集。例如,VMware 和 Virtualbox 之类的工具通常只能在 x86 计算机上虚拟出一个 x86 计算机,而 qemu 支持在 x86 上虚拟出一个 ARM 计算机。qemu 在 user mode 配置下,可以运行跟当前平台指令集不同的平台可执行程序。例如可以用 qemu 在 x86 上运行 ARM 的可执行程序,但是两个平台必须是同一种操作系统,比如 Linux。

1
sudo apt install qemu-user

安装 gdb-multiarch

gdb-multiarch 是一个经过交叉编译后的、支持多架构版本的 gdb。

1
sudo apt install gdb-multiarch

安装 aarch64 编译工具链

1
sudo apt install gcc-aarch64-linux-gnu

交叉编译测试用例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
lhx@ubuntu:~/test/qemu$ ls
hello.c
lhx@ubuntu:~/test/qemu$ cat hello.c
#include <stdio.h>
void hello()
{
printf("Hello World !\n");
}
int main()
{
hello();
return 0;
}
lhx@ubuntu:~/test/qemu$ aarch64-linux-gnu-gcc -g -static hello.c
lhx@ubuntu:~/test/qemu$ ls
a.out hello.c
lhx@ubuntu:~/test/qemu$ file a.out
a.out: ELF 64-bit LSB executable, ARM aarch64, version 1 (GNU/Linux), statically linked, BuildID[sha1]=e1fe3c59cad06eff9cab2729a00233bc10d763ce, for GNU/Linux 3.7.0, with debug_info, not stripped
lhx@ubuntu:~/test/qemu$ qemu-aarch64 ./a.out
Hello World !

开始 qemu+gdb 跨架构调试

  • 窗口 1:启动 a.out
    通过 qemu-aarch64 运行交叉编译的 a.out, 并指定 gdb 调试端口号为 1234,然后等待 gdb 远程连接。
1
2
lhx@ubuntu:~/test/qemu$ qemu-aarch64 -g 1234 ./a.out
Hello World !

-g port:该选项表示 QEMU_GDB 环境变量取值,即 等待 gdb 连接的端口号。

  • 窗口 2:gdb 远程调试
    通过 gdb-multiarch 启动 a.out,这里 a.out 用于读取和远程端一致的调试符号信息。连接上远程端口号后,便可以进行设断点、查看寄存器、反汇编等一系列调试操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
lhx@ubuntu:~/test/qemu$ ls
a.out hello.c
lhx@ubuntu:~/test/qemu$ gdb-multiarch -q a.out
Reading symbols from a.out...
(gdb) target remote localhost:1234
Remote debugging using localhost:1234
0x0000000000400558 in _start ()
(gdb) b main
Breakpoint 1 at 0x4006d4: file hello.c, line 10.
(gdb) c
Continuing.

Breakpoint 1, main () at hello.c:10
10 hello();
(gdb) s
hello () at hello.c:5
5 printf("Hello World !\n");
(gdb) n
6 }
(gdb) info registers
x0 0xe 14
x1 0x1 1
x2 0x0 0
x3 0x48bf00 4767488
x4 0xfbad2a84 4222429828
x5 0x21a 538
x6 0x10 16
x7 0x7f7f7f7f7f7f7f7f 9187201950435737471
x8 0x40 64
x9 0x3fffffff 1073741823
x10 0x20000000 536870912
x11 0x10000 65536
x12 0x48b000 4763648
x13 0x410 1040
x14 0x0 0
x15 0x48c738 4769592
x16 0x40b998 4241816
x17 0x416fc0 4288448
x18 0x0 0
x19 0x400db8 4197816
x20 0x400e80 4198016
x21 0x0 0
x22 0x400280 4194944
x23 0x489030 4755504
x24 0x18 24
x25 0x48b000 4763648
x26 0x48b000 4763648
x27 0x451000 4526080
x28 0x0 0
x29 0x4000800260 274886296160
x30 0x4006c0 4196032
sp 0x4000800260 0x4000800260
pc 0x4006c0 0x4006c0 <hello+20>
cpsr 0x60000000 1610612736
fpsr 0x0 0
fpcr 0x0 0

(gdb) bt
#0 hello () at hello.c:6
#1 0x00000000004006d8 in main () at hello.c:10
(gdb) disassemble hello
Dump of assembler code for function hello:
0x00000000004006ac <+0>: stp x29, x30, [sp, #-16]!
0x00000000004006b0 <+4>: mov x29, sp
0x00000000004006b4 <+8>: adrp x0, 0x451000 <_nl_locale_subfreeres+552>
0x00000000004006b8 <+12>: add x0, x0, #0x3e8
0x00000000004006bc <+16>: bl 0x407350 <puts>
0x00000000004006c0 <+20>: nop
0x00000000004006c4 <+24>: ldp x29, x30, [sp], #16
0x00000000004006c8 <+28>: ret
End of assembler dump.
(gdb) c
Continuing.
[Inferior 1 (process 1) exited normally]
(gdb)