前言
我们在开发软件时,免不了引入一些Bug,如何快速定位并解决这些bug 呢?工程师调试跟踪解决这些bug 的过程就像是医生给病人体检治病,医生需要借助各种医疗设备检测病人的各项指标,才能诊断病情分析病因并给出治疗方案。工程师解决这些bug,也需要借助各种调试跟踪技术,通过查看当前的执行指令、内存数据、运行日志等信息,分析出产生bug 的可能原因,再给出解决方案。
这些调试跟踪技术,可以帮我们更清晰的了解代码的执行状态,快速找到执行结果与期望结果不相符的起点或原因,比如控制逻辑或业务逻辑出现漏洞、中间结果被意外更改或损坏、低效的性能瓶颈等。
一、GDB 调试原理
我们初学程序设计时,C/C++/Java 这些静态类型语言都需要先经过编译、链接、执行过程,linux 系统常用的编译链接工具是GCC。程序设计免不了出现一些非预期的Bug,要解决这些bug 首先需要我们获得当前代码足够的执行信息,GNU 也提供了GDB 调试工具,供我们查看代码的执行信息,快速定位产生bug 的代码并解决它。
一般我们在程序开发过程中,代码编辑、编译链接、代码调试这几个工具配合使用,所以很多 IDE(Intergreated Development Environment) 常将这几部分封装在一起,比如windows 平台下的Visual Studio、嵌入式开发常用的Keil MDK 等。这些IDE 工具将常用操作以菜单按钮的形式提供,让我们专注于程序开发过程,对我们隐藏了编译链接和代码调试工具的原理,本文以GDB 为例,简单介绍下代码调试原理。
1.1 GDB 调试模型
目前软件开发主要在x86 平台上进行,但我们开发的目标程序有可能在其它平台比如ARM 上运行,为了方便我们在PC 上调试ARM 主机中运行的程序,GDB 根据调试程序和被调试程序是否运行在同一台电脑中,提供了如下两种调试模型:
-
本地调试:调试程序和被调试程序运行在同一台电脑中。
-
远程调试:调试程序运行在一台电脑中,被调试程序运行在另一台电脑中。
直接跟用户交互的可视化调试程序常有两种形式:一种是在终端窗口内手动输入调试命令,以字符形式显示调试信息;另一种是在IDE 内点击菜单按钮来代替手动输入调试命令,以图形加字符的形式显示调试信息。前一种形式更方便编写脚本实现自动化调试;后一种形式不需要记忆那么多调试命令,呈现的调试信息更直观、视觉辨识度更高,能提高点调试效率。
远程调试相比本地调试,多了一个GdbServer程序,该程序和目标程序(被调试程序)都运行在目标机(比如一个ARM 主板)中。上图中的红线表示GDB与GdbServer之间通过串口线或者网络进行通讯,用于传输GDB 调试消息的通讯协议可以称为GDB Remote Serial Protocol(GDB RSP)。
GDP RSP 既然是一个通讯协议,自然有标准的报文格式和内容要求,基本的报文格式如下图所示:
GDP RSP 报文主要包括四个部分,固定的开始字符(’$’)和结束字符(’#’),中间的调试消息数据以及最后的校验和,我们使用GDB 调试工具并不需要了解的那么详细,这里也就不展开介绍协议报文了(若想了解更详细信息,可参阅文档:Howto: GDB Remote Serial Protocol)。
1.2 GDB 与被调试程序关系
不管是本地调试还是远程调试,GDB 调试程序都需要有两个条件:
- 目标程序代码包含必要的调试信息,比如文件名、函数名、变量名、行号等符号表信息,函数堆栈、寄存器等信息。这些调试信息可以在编译阶段设置编译选项添加,比如gcc 编译工具添加"-g" 选项就可以在可执行文件中添加调试信息。由于带调试信息的可执行文件较大,嵌入式开发中资源受限,软件项目常有Debug 和Release 两个版本,前者供调试跟踪,后者更精简;
- GDB 可以控制被调试程序的执行,可以访问被调试程序的任何指令和内存数据。比如GDB 可以启动或者接管被调试程序的运行,控制被调试程序在指定条件下停止运行,查看并修改被调试程序的变量值、参数值、执行结果、执行顺序等运行数据。
我们先使用man gdb 命令查看GDB 的简单介绍与用法:
GDB 主要有三种启动方式:
- gdb program:使用GDB 开始执行被调试程序program,可通过GDB 命令控制program 的行为;
- gdb program core:使用GDB 同时执行被调试程序program 和core 文件(程序异常中止或退出时,保存的内存映像加调试信息文件,包含程序当前的内存、寄存器、堆栈等信息),便于定位分析程序异常中止或退出的原因;
- gdb attach PID (gdb -p PID):使用GDB 接管(attach)一个正在运行的被调试程序,PID 为被调试程序的process-ID(可通过pidof program 查看),可通过GDB 命令控制program 的行为。
从GDB 与被调试程序间的关系看,GDB 的三种启动方式可以分为两类:一类是由GDB 程序调用执行一个尚未运行的被调试程序program;另一类是由GDB 程序attach 接管一个正在运行的被调试程序program。
前面也谈到,GDB 进程可以控制被调试程序的执行,可以访问被调试程序的任何指令和内存数据。GDB 进程相当于是被调试程序的父进程,GDB 进程对被调试进程program 有绝对的控制权。GDB 是如何调用或者接管一个正在运行的被调试程序呢?
Linux 内核提供了一个用于进程跟踪的系统调用函数ptrace,该函数提供了一个进程(the “tracer”)监察和控制另一个进程(the “tracee”)的方法,它不仅可以监控系统调用,而且还能够检查和改变“tracee” 进程的内存和寄存器里的数据,甚至还可以拦截系统调用。GDB 进程通过系统调用函数ptrace,就可以读写被调试进程program(GDB 进程作为tracer,被调试进程作为tracee)的指令空间、数据空间、堆栈和寄存器的值,接管被调试进程program 的所有信号。这样一来,被调试进程program 的执行就被GDB 进程完全控制了,从而达到调试的目的。
我们使用man ptrace 命令查看ptrace 的简单介绍如下:
我们知道了,GDB 进程借助系统调用函数ptrace 实现对被调试进程的监察和控制,也就好理解GDB 进程是如何调用或者接管被调试进程program 的了。对于尚未运行的被调试程序,启动GDB 调试进程后,该进程会创建一个子进程(linux 系统通过fork 创建子进程)并调用ptrace 函数,ptrace 函数再由第一个参数获知是启动一个尚未运行的进程(传入参数值 PTRACE_TRACEME)还是接管一个正在运行的进程(传入参数值 PTRACE_ATTACH 或 PTRACE_SEIZE)。GDB 进程启动或接管被调试进程test 的过程图示如下(接管正在运行的进程,GDB 向其发送信号SIGSTOP,正在运行的被调试进程就会暂停执行并进入TASK_STOPED状态,等待被调试):
所以,不论是调试一个新程序,还是调试一个已经处于执行中状态的服务程序,通过ptrace系统调用,最终的结果都是:gdb程序是父进程,被调试程序是子进程,子进程的所有信号都被父进程gdb来接管,并且父进程gdb可查看、修改子进程的内部信息,包括:指令空间、数据空间、堆栈、寄存器等。
二、GDB 常用调试命令
如何使用GDB 调试程序呢?GDB 提供了一系列命令,我们可以在启动GDB 进程后通过help 命令查看,GDB 支持的调试命令类别如下:
从help 命令的返回结果可以看出,GDB 主要支持12 类调试命令,其中比较常用的有断点设置breakpoints、数据查询data、文件指定与查看files、运行控制running、堆栈查询stack、状态查看status 等六大类。如果想查看某一类具体支持哪些调试命令,可以使用比如help breakpoints 形式的命令查看详情。GDB 支持的调试命令很多,本文只介绍几个最常用的调试命令。
前面已经谈到,要想使用gdb 调试程序,被调试程序需要包含调试信息,如果使用GCC 工具链编译链接程序,则需要添加-g 参数(也可以是-Og 参数)。使用前面介绍的GDB 启动命令,可以根据启动调试后的提示信息判断被调试程序是否包含调试信息:
$ gdb test
......
# 没有调试信息
Reading symbols from test...(no debugging symbols found)...done.
# 包含调试信息
Reading symbols from test...done.
- 1
- 2
- 3
- 4
- 5
- 6
2.1 断点设置命令
启动GDB 调试程序后,一般先设置普通断点、观察断点、捕捉断点等,以便在后续调试过程中,让程序及时暂停在我们关注的地方,查看断点处的数据和状态信息。GDB 常用的断点设置命令如下(可通过help breakpoints 查看支持的断点命令列表,可进一步通过help break 查看某个具体命令的用法):
常用断点设置命令 | 命令描述 |
---|---|
break location | 在源代码指定设置location 处设置断点,程序执行到location 处暂停执行。location 可以是行号linenum、函数名function,如果不止一个源文件,还可以在前面加上文件名,比如filename:linenum 或 filename:function。 |
break location if cond | 在源代码指定位置location 处设置条件断点,程序执行到location 处判断条件condition 的真假,若条件condition 为true 则暂停执行,否则继续执行。condition 是一个布尔型表达式,location 含义跟前一条指令中的相同。 |
watch expression | 在程序执行过程中,监控某个变量或表达式(也即expression)的值,当观察到该变量或表达式的值发生变化时,则程序暂停执行。 |
catch event | 在程序执行过程中,监控某个事件的触发,比如程序抛出指定类型异常、某动态库被加载或卸载等,当捕捉到该事件发生时,则程序暂停执行。 |
info breakpoints info break | 可以查看当前调试环境中存在的所有断点,包括普通断点、观察断点以及捕捉断点。每个断点都有一个唯一的编号,可根据该编号使能或清除相应的断点。 |
disable [num] | 禁用当前调试环境中的某个断点,如果没有指定编号num 默认禁用所有断点。 |
enable [num] | 激活当前调试环境中的某个断点,如果没有指定编号num 默认激活所有断点。 |
clear location delete [num] | 可以删除指定位置location 处或指定编号num 的所有断点,如果没有指定位置location 默认清除编号最小的断点,如果没有指定编号num 默认删除所有断点。 |
下面给出这几个命令的简单示例:
......
Reading symbols from test...done.
(gdb) list #--> 显示带行号的源代码
1 #include
2
3 int add2(int a, int b)
4 {
5 return (a + b);
6 }
7
8 int main (int argc, char *argv[])
9 {
10 int n = 1;
(gdb) #--> 默认显示10行代码,可按Enter 健查看后续代码
11 int sum = 0;
12
13 while (n <= 100)
14 {
15 sum = add2(sum, n);
16 n++;
17 }
18
19 return 0;
20 }(gdb)
Line number 21 out of range; test.c has 20 lines.
(gdb) break 11 #--> 在第13行设置普通断点
Breakpoint 1 at 0x40157f: file test.c, line 11.
(gdb) break add2 #--> 在函数add2处设置普通断点
Breakpoint 2 at 0x40155a: file test.c, line 5.
(gdb) tbreak 16 #--> 在第16行设置一个一次性普通断点,常用于循环中
Temporary breakpoint 3 at 0x401598: file test.c, line 16.
(gdb) watch n #--> 设置变量n 为观察断点
No symbol "n" in current context. #--> 当前调试程序未运行,故当前调试环境没有符号n,需要先运行到符号n 定义后再设置其为观察断点
(gdb) run #--> 运行被调试程序到第一个断点处暂停
Starting program: D:\VSCoder\GDB\test.exe
[New Thread 10816.0x42a8]
[New Thread 10816.0x2b68]
Thread 1 hit Breakpoint 1, main (argc=1, argv=0x1914a0) at test.c:11
11 int sum = 0;
(gdb) watch n #--> 设置变量n 为观察断点
Hardware watchpoint 4: n
(gdb) info break #--> 查看当前调试环境中的所有断点,Num 为唯一编号、Type 为断点类型、Disp 表示断点触发后保留还是删除,Enb 表示断点处于激活还是禁用状态,Address 显示断点在内存中的地址信息,What 显示断点所在文件与行号信息
Num Type Disp Enb Address What
1 breakpoint keep y 0x000000000040157f in main at test.c:11
breakpoint already hit 1 time
2 breakpoint keep y 0x000000000040155a in add2 at test.c:5
3 breakpoint del y 0x0000000000401598 in main at test.c:16
4 hw watchpoint keep y n
(gdb) disable 1 #--> 禁用编号为1 的断点
(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep n 0x000000000040157f in main at test.c:11
breakpoint already hit 1 time
......
(gdb) enable 1 #--> 激活编号为1 的断点
(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep y 0x000000000040157f in main at test.c:11
breakpoint already hit 1 time
......
(gdb) clear #--> 清除编号最小的断点
Deleted breakpoint 1
(gdb) info break
Num Type Disp Enb Address What
2 breakpoint keep y 0x000000000040155a in add2 at test.c:5
3 breakpoint del y 0x0000000000401598 in main at test.c:16
4 hw watchpoint keep y n
(gdb) delete #--> 删除所有的断点
Delete all breakpoints? (y or n) y
(gdb) info break
No breakpoints or watchpoints.
(gdb)
- 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
2.2 变量堆栈查看命令
在被调试程序代码中设置断点,实际上就是让被调试程序暂停在我们关注的断点位置,一般断点处也是我们怀疑程序存在bug或性能瓶颈的地方。
当被调试程序停在断点处后,我们需要查看当前调试环境的状态与数据信息,比如变量当前值、调用堆栈信息、当前寄存器值等。GDB 常用的变量或堆栈信息查询命令如下:
常用变量堆栈查询命令 | 命令描述 |
---|---|
print expression | 在 GDB 调试程序的过程中,输出或者修改指定变量或者表达式的值。 |
display expr display/fmt expr | 在调试阶段查看某个变量或表达式的值,与print 命令的区别是,每次被调试程序暂停执行时,display 都会自动显示变量或表达式的值,print 则不会。 |
info display | 可以查看当前调试环境中存在的所有display 变量或表达式,每个display 变量或表达式都有唯一的编号。 |
undisplay num | 删除指定编号num 的display 变量或表达式,如果没有指定编号num 默认删除所有display 变量或表达式。 |
frame spec | 可以查看当前调试环境中特定的栈帧信息,spec 可以是栈帧编号、栈帧地址、函数名等,如果不指定参数spec 则显示当前栈帧信息。 |
up n down n | 在当前栈帧的基础上(假设当前栈帧编号为m),查看编号为m + n 或m - n 的栈帧信息。如果不指定参数n 则默认n = 1。 |
backtrace | 查看当前调试环境中所有的栈帧信息,也即从当前栈帧到main 函数的整个函数调用链信息。 |
info args info locals | 查看当前函数参数的值 查看当前函数内局部变量的信息 |
info frame | 可以查看当前栈帧中存储的详细信息,包括当前栈帧的编号及地址、当前函数及其调用者的地址、当前函数编程语言类型、当前函数参数和局部变量的地址及其值、当前栈帧寄存器值等。 |
下面给出这几个命令的简单示例:
......
(gdb) break 13
Breakpoint 1 at 0x401586: file test.c, line 13.
(gdb) break add2
Breakpoint 2 at 0x40155a: file test.c, line 5.
(gdb) print n # --> 打印变量n 的当前值
No symbol "n" in current context. # --> 当前调试程序未运行,故当前调试环境没有符号n,需要先运行到符号n 定义后再查看其当前值
(gdb) run #--> 运行被调试程序到第一个断点处暂停
Starting program: D:\VSCoder\GDB\test.exe
[New Thread 10392.0x3b60]
[New Thread 10392.0x1c54]
Thread 1 hit Breakpoint 1, main (argc=1, argv=0x7814a0) at test.c:13
warning: Source file is more recent than executable.
13 while (n <= 100)
(gdb) print n # --> 打印变量n 的当前值
$1 = 1
(gdb) display sum # --> 保持显示变量sum 的当前值
1: sum = 0
(gdb) info display # --> 查看当前调试环境中的所有display 变量或表达式信息
Auto-display expressions now in effect:
Num Enb Expression
1: y sum
(gdb) undisplay # --> 删除当前调试环境中所有的display 变量或表达式
Delete all auto-display expressions? (y or n) y
(gdb) print n=5 # --> 修改变量n 的当前值为5
$2 = 5
(gdb) info locals # --> 打印当前函数内的所有局部变量的当前值
n = 5
sum = 0
(gdb) frame # --> 打印当前栈帧信息
#0 main (argc=1, argv=0x9814a0) at test.c:13
13 while (n <= 100)
(gdb) down # --> 打印当前栈帧编号减一的栈帧信息
Bottom (innermost) frame selected; you cannot go down.
(gdb) continue # --> 继续执行被调试程序到下一个断点处暂停
Continuing.
Thread 1 hit Breakpoint 2, add2 (a=0, b=5) at test.c:5
5 return (a + b);
(gdb) backtrace # --> 打印当前调试环境中的所有栈帧信息,也即当前函数调用链信息
#0 add2 (a=0, b=5) at test.c:5
#1 0x0000000000401595 in main (argc=1, argv=0x9814a0) at test.c:15
(gdb) info args # --> 打印当前函数的所有参数值
a = 0
b = 5
(gdb) up # --> 打印当前栈帧编号加一的栈帧信息
#1 0x0000000000401595 in main (argc=1, argv=0x9814a0) at test.c:15
15 sum = add2(sum, n);
(gdb) down # --> 打印当前栈帧编号减一的栈帧信息
#0 add2 (a=0, b=5) at test.c:5
5 return (a + b);
(gdb) info frame # --> 打印当前栈帧的详细信息
Stack level 0, frame at 0x61fdf0: #--> 当前栈帧编号为0,地址为0x61fdf0
rip = 0x40155a in add2 (test.c:5); saved rip = 0x401595 #--> 当前函数的存储地址为0x40155a,它的调用者的存储地址为0x401595
called by frame at 0x61fe30 #-->当前函数调用者的栈帧地址为0x61fe30
source language c. #--> 当前函数使用C语言编写的
Arglist at 0x61fde0, args: a=0, b=5 #--> 当前函数的参数地址和参数值
Locals at 0x61fde0, Previous frame's sp is 0x61fdf0 #--> 当前函数内的局部变量存储地址
Saved registers:
rbp at 0x61fde0, rip at 0x61fde8, #--> 当前栈帧内部存储的寄存器信息
- 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
2.3 执行控制命令
为被调试程序设置了需要特别关注的断点,也有了可以查看甚至修改当前调试环境变量值、堆栈信息、寄存器值等数据的命令,还需要能自由控制被调试程序执行顺序的命令,方便进行单步调试或断点调试。GDB 常用的执行控制命令如下:
常用执行控制命令 | 命令描述 |
---|---|
start run | 都可以用来在 GDB 调试器中启动被调试程序,start 命令执行到main 函数起始位置暂停,run 命令执行到第一个断点处暂停。 |
next count step count | 都可以控制GDB 单步执行程序,count 为执行步数缺省值为1 行。执行到函数调用代码时,next 命令不考虑函数内部代码行数,将函数调用记作一行,step 命令计算调用函数内部代码的行数。 |
continue count | 控制被调试程序执行到往下数第count 个断点处暂停,也即忽略count - 1 个断点,参数count 缺省值为1,也即执行下一个断点处暂停。 |
until location | 控制被调试程序执行到特定位置location 处暂停,如果省略参数location,则执行到循环体外暂停,若无循环体则同next 命令。 |
下面给出这几个命令的简单示例:
......
(gdb) break 13
Breakpoint 1 at 0x115b: file test.c, line 13.
(gdb) start # --> 控制GDB 开始执行被调试程序,并在main 函数起始位置暂停
Temporary breakpoint 2 at 0x1141: file test.c, line 9.
Starting program: /home/paul/Desktop/GDB/test
Temporary breakpoint 2, main () at test.c:9
9 {
(gdb) continue # --> 继续执行被调试程序到下一个断点处暂停
Continuing.
Breakpoint 1, main () at test.c:13
13 while (n <= 100)
(gdb) info locals # --> 打印当前函数内的所有局部变量的当前值
n = 1
sum = 0
(gdb) step 7 # --> 控制被调试程序继续执行7 行,计算被调函数add2 内的行数
15 sum = add2(sum, n);
(gdb) info locals
n = 2
sum = 1
(gdb) next 7 # --> 控制被调试程序继续执行7 行,被调函数add2 按照一行计算
16 n++;
(gdb) info locals
n = 4
sum = 10
(gdb) next
13 while (n <= 100)
(gdb) until # --> 控制被调试程序执行到当前循环体外暂停
19 return 0;
(gdb) info locals
n = 101
sum = 5050
(gdb) quit # --> 退出GDB 调试进程
A debugging session is active.
Inferior 1 [process 5867] will be killed.
Quit anyway? (y or n) y
- 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
上述GDB 调试命令都可以使用缩写形式,比如使用b 代替break、使用c 代替continue、使用bt 代替backtrace 等,不用每次输入命令全称。
三、VS Code 可视化调试
使用GDB 命令来调试程序并没有那么直观,我们对可视化的图像信息更敏感些,因此很多IDE 都提供了可视化调试界面。这里使用比较轻巧的代码编辑器VS Code 搭配GCC 和GDB 工具链,实现可视化调试功能。
一般一个软件工程包含不止一个源文件,多个源文件的编译链接通常靠Makefile 控制。使用VS Code 编辑好工程代码和Makefile 文件后,可以在VS Code 启动配置文件launch.json 中配置GDB 调试器和预启动任务preLaunchTask,在任务配置文件tasks.json 中配置执行Makefile 文件中定义的make default 命令,实现对目标工程的可视化调试。
这里使用博文:VSCode+GCC+Makefile项目管理 中的示例代码,考虑到GNU工具链对Linux 系统支持更好,本文使用Ubuntu 20.04系统构建VS Code 可视化调试工程。
原博文是基于windows 系统编写的Makefile 文件和launch.json、tasks.json 配置文件,这里使用Linux 系统需要修改几个地方:
- 将Makefile 文件中的mingw32-make 改为make;
- 将tasks.json 文件中的mingw32-make 改为make;
- 将launch.json 文件中的"miDebuggerPath" 值改为 “/usr/bin/gdb”;
- 将launch.json 文件中的"externalConsole"值改为 false(可选,设为false后使用VS Code 终端界面进行交互);
配置好VS Code 后,进入“Run and Debug” 窗口界面,先设置断点和观察表达式,变量和调用栈帧信息会自动显示在想要区域,设置好断点和观察点后,点击“Start Debugging" 按钮开始执行调试任务,界面如下:
VS Code 不仅支持可视化变量、断点、堆栈信息列表显示,还支持执行GDB 命令以获得更多信息。继续执行到函数add 内的断点处,在DEBUG CONSOLE 界面执行命令“-exec info frame” 获得当前栈帧详细信息,图示如下:
四、GDB 远程调试
GDB 调试模型有本地调试和远程调试两种,前面主要基于本地调试介绍的,我们在使用服务器编译或嵌入式调试等场景,就需要在PC 端调试远程服务器或嵌入式板子上的程序了,如何进行GDB 远程调试呢?
在博文:LwIP开发调试环境搭建 中已经展示了在windows 系统上调试虚拟机qemu-vexpress-a9 内运行的程序,调试界面如下:
这里简单解释下GDB 远程调试的命令交互过程,首先执行脚本qemu-dbg.bat 启动qemu-vexpress-a9 虚拟机,主要命令如下:
# qemu-dbg.bat
......
start qemu-system-arm -M vexpress-a9 -kernel rtthread.elf -serial stdio -sd sd.bin -S -s
- 1
- 2
- 3
qemu-dbg 相比qemu 脚本主要多出来两个参数-S 和-s,这两个参数是什么意思呢?我们通过命令帮助查询如下:
C:\Users\paul>qemu-system-arm -h
......
-S freeze CPU at startup (use 'c' to start execution)
......
-s shorthand for -gdb tcp::1234
......
- 1
- 2
- 3
- 4
- 5
- 6
从命令帮助可知,qemu-system-arm -s 参数可以让qemu-vexpress-a9 虚拟机在tcp::1234 端口启动gdbserver 服务,相当于在虚拟机内执行了“arm-none-eabi-gdbserver localhost:1234 rtthread.elf” 命令(虚拟机与宿主机共用IP 即localhost,ARM Cortex-A9 使用的编译器为arm-none-eabi-gcc,选用gdbserver 版本应与编译器架构一致)。
启动qemu-vexpress-a9 虚拟机时,通过-s 参数启动了gdbserver 服务,接下来在宿主机内执行如下命令:
$ arm-none-eabi-gdb #--> 执行ARM 架构gdb 命令
......
(gdb) target remote localhost:1234 #--> 连接远程目标gdbserver 服务器,IP:Port 为localhost:1234
Remote debugging using localhost:1234
0x600100d0 in ?? ()
(gdb) file rtthread.elf #--> 从本地读取调试信息
A program is being debugged already.
Are you sure you want to change the file? (y or n) y
Reading symbols from rtthread.elf...done.
(gdb) break main #--> 在main 函数起始位置设置断点
Breakpoint 1 at 0x60010048: file applications\main.c, line 7.
(gdb) continue #--> 继续执行到下一个断点处暂停,也即main 函数起始位置
Continuing.
Breakpoint 1, main () at applications\main.c:7
7 printf("hello rt-thread\n");
(gdb) backtrace #--> 查看当前调试环境所有堆栈信息
#0 main () at applications\main.c:7
(gdb) info frame #--> 查看当前堆栈详细信息
Stack level 0, frame at 0x600b9780:
pc = 0x60010048 in main (applications\main.c:7); saved pc = 0x60012884
source language c.
Arglist at 0x600b977c, args:
Locals at 0x600b977c, Previous frame's sp is 0x600b9780
Saved registers:
r11 at 0x600b9778, lr at 0x600b977c
(gdb) quit #--> 退出远程调试进程
A debugging session is active.
Inferior 1 [Remote target] will be detached.
Quit anyway? (y or n) y
Detaching from program: E:\RT_Thread\rtthread_source\qemu-vexpress-a9\rtthread.elf, Remote target
Ending remote debugging.
- 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
评论记录:
回复评论: