写程序难免会遇到 Bug,这时我们就需要 GDB 来对程序进行调试了。调试需要在编译的时候,加上一些调试相关的信息,也就是说,需要指定 -g 选项。如:
$ gcc hello.c -o hello -g
其中 -g
选项表示生成调试信息。
当你在编译程序时加上
-g
选项,gcc
会在生成的可执行文件中包含额外的调试信息。这些调试信息主要是关于源代码和目标代码之间的映射关系等内容。例如,它会记录变量的名称、类型、位置,函数的入口点等信息。这些调试信息对于使用调试工具(如
gdb
)来调试程序非常重要。调试工具可以利用这些信息来帮助你查看变量的值、设置断点、单步执行等操作。如果没有-g
选项生成的调试信息,调试工具将很难准确地提供这些功能,因为它们无法知道源代码和目标代码之间的详细对应关系。
当我们生成了调试信息后,我们进入GDB开始对程序进行调试。
进入GDB调试界面
$ gdb executable_name # 不设置任何命令行参数
$ gdb --args executable_name [arg]...
比如:
$ gdb foo
$ gdb --args foo arg1 arg2 arg3
当然,我们也可以先进入调试界面,然后再设置命令行参数,如下所示:
$ gdb foo
(gdb) set args arg1 arg2 arg3
调试程序
查看源代码
我们可以用 list/l 命令查看源代码:
格式:
list/l [文件名:][行号|函数名]
常见用法:
(gdb) list # 下翻源代码
(gdb) list - # 上翻源代码
(gdb) list 20 # 查看20行附近的源代码
(gdb) list main # 查看main函数附近的源代码
(gdb) list scanner.c:20 # 查看scanner.c文件第20行附近的源代码
(gdb) list scanner.c:scanToken # 查看scanner.c文件scanToken函数附近的源代码
设置断点
我们可以用 break/b 命令设置断点:
格式:
break/b [文件名:][行号|函数名]
常见用法:
(gdb) break 20 # 在第20行设置断点
(gdb) break main # 在main函数的开头设置断点
(gdb) break scanner.c:20 # 在scanner.c文件的第20行设置断点
(gdb) break scanner.c:scanToken # 在scanner.c文件的scanToken函数开头设置断点
查看断点
我们可以用 info break/i b 命令查看断点信息:
格式:
info break/i b
常见用法:
(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000555555554e1d in main at main.c:79
2 breakpoint keep y 0x0000555555555a99 in scanToken at scanner.c:282
输出解释
Num:断点编号,你可以用这个编号来删除或禁用特定的断点。
Type:断点类型,这里是
breakpoint
。breakpoint :这是最常见的类型,表示一个普通的断点。当程序执行到指定的位置时,程序会暂停。
watchpoint:表示一个观察点,用于监视某个变量或内存地址的值是否发生变化。当变量的值发生变化时,程序会暂停。
read watchpoint:表示一个只读观察点,用于监视某个变量或内存地址是否被读取。当变量的值被读取时,程序会暂停。
access watchpoint:表示一个访问观察点,用于监视某个变量或内存地址是否被读取或写入。当变量的值被读取或写入时,程序会暂停。
catchpoint:表示一个捕获点,用于捕获特定的事件,如异常、信号、系统调用等。当指定的事件发生时,程序会暂停。
tracepoint:表示一个跟踪点,用于在程序运行时收集数据,而不会暂停程序。跟踪点通常用于性能分析和数据收集。
breakpoint (pending):表示一个尚未解析的断点。这通常发生在指定的文件或行号在当前上下文中不可用时。
breakpoint (internal):表示一个内部断点,通常由
gdb
内部使用,用户通常不需要直接操作这些断点。breakpoint (shadow):表示一个影子断点,通常用于多线程环境,用于在多个线程中设置相同的断点。
Disp:表示断点的显示方式,
keep
表示断点在程序退出时不会自动删除。Enb:表示断点是否启用,
y
表示启用,n
表示禁用。Address:断点的内存地址。
What:断点的具体位置,包括文件名和行号。
删除断点
我们可以用 delete/d 命令删除断点:
格式:
delete/d [n] -- 删除所有断点或n号断点
常见用法:
(gdb) delete 2 # 删除2号断点
(gdb) d # 删除所有断点
启动调试
我们可以用 run/r 命令启动调试:
(gdb) r
该命令只是用于启动调试,而不是用于跳跃。
继续
continue/c 命令可以运行到逻辑上的下一个断点处:
(gdb) c
假如有图中两个断点,当我们在第4行断点停住时,我们可以按c直接执行到第27行断点中间不会再停顿。
忽略断点n次
我们可以用 ignore 命令来指定忽略某个断点多少次,这在调试循环的时候非常有用:
格式:
ignore N COUNT
常见用法:
(gdb) ignore 1 10 # 忽略1号断点10次单步调试
有如下程序时,当我们需要在循环中设置断点调试时,我们可以使用ignore来跳过多次的断点
#include<stdio.h>
void fun(){
int t = 0;
for(int i = 1; i <= 10; i++){
t++;
printf("%d\n",t);
}
}
int main(int argc, char const *argv[])
{
fun();
return 0;
}
step/s 命令可以用来进行单步调试,即遇到函数调用会进入函数。
(gdb) step
跳出函数
我们可以使用 finish 命令执行完整个函数:
(gdb) finish
当使用finish
命令时,gdb
会继续执行当前函数,直到该函数返回,而不会在当前函数内的其他断点处暂停。这意味着,即使当前函数中还有其他断点,finish
命令也会忽略这些断点,直接执行到函数返回。
逐过程
next/n 命令表示逐过程,也就是说遇到函数调用,它不会进入函数,而是把函数调用
看作一条语句。
(gdb) n
监视
print/p 命令可以打印表达式的值:
格式:
print/p EXP
如:
(gdb) print PI*r*r
print/p 命令还可以改变变量的值:
格式:
print/p EXP=VAL
比如:
(gdb) print r=2.0
我们可以用 display 命令自动展示表达式的值:
display
命令用于在程序每次暂停时自动显示指定的表达式的值。这是一个非常有用的工具,可以帮助你跟踪变量的变化,而无需手动输入print
命令来查看它们的值。
格式:
display EXP # 自动展示EXP
info display # 显示所有自动展示的表达式信息
undisplay [n] # 删除所有或[n]号自动展示的表达式
常见用法:
(gdb) display r
(gdb) display PI*r*r
(gdb) info display
Auto-display expressions now in effect:
Num Enb Expression
1: y r
2: y PI*r*r
(gdb) undisplay 2
(gdb) undisplay
Delete all auto-display expressions? (y or n)
输出解释:
1: x = 0:表示编号为1的
display
表达式显示变量x
的值为0。Auto-display expressions now in effect:表示当前生效的自动显示表达式。
Num:表示
display
表达式的编号。Enb:表示是否启用该
display
表达式,y
表示启用,n
表示禁用。Expression:表示
display
表达式的内容。
在每次跳跃到某个断点时,都会实时显示表达式的值
(gdb) print t = 5
$10 = 5
(gdb) c
Continuing.
5
Breakpoint 7, fun () at test3.c:6
6 t++;
1: t = 5
2: i = 3
(gdb) c
Continuing.
Breakpoint 6, fun () at test3.c:7
7 printf("%d\n",t);
1: t = 6
2: i = 3
(gdb) c
Continuing.
6
我们还可以通过命令查看参数和局部变量的值:
我们还可以通过命令查看参数和局部变量的值:
(gdb) info args # 查看函数的参数
(gdb) info locals # 查看函数所有局部变量的值
info args
命令用于显示当前函数的参数及其值。这个命令非常有用,特别是在你想要查看当前函数的参数值时,而无需手动输入print
命令。
显示当前函数的参数及其值:
info args
命令会列出当前函数的所有参数及其当前值。方便调试:在调试过程中,特别是当你在函数内部时,这个命令可以帮助你快速了解函数的输入参数。
例如从如下的程序,在执行到ptint_hello函数时,使用 info args 时会显示输入的参数x和y的值.使用info local 会显示函数内的局部变量值,执行到函数外时不再显示。
#include <stdio.h>
void print_hello(int x, int y) {
printf("Hello, world! x = %d, y = %d\n", x, y);
int t = 1;
}
int main() {
print_hello(10, 20);
return 0;
}
查看内存
我们可以用 x 命令查看内存的值(一般用得很少,了解即可):
格式:
x/nFU
其中, n为一个整数,表示查看n个单元的内存
F表示输出格式:
常用的输出格式有:
o(octal),
x(hex),
d(decimal),
u(unsigned decimal),
t(binary),
f(float),
c(char),
...
默认输出格式为x(hex)。
U表示内存单元:
b(byte), h(halfword, 2 bytes), w(word, 4 bytes), g(giant, 8bytes)
默认单位为w(word)
常见用法:
(gdb) x/4dw arr
0x7fffffffe3a0: 0 1 2 3
(gdb) x/4xb &i
0x7fffffffe38c: 0x37 0x25 0x00 0x00
# 其中i=9527
退出GDB
quit/q 命令可以退出 GDB。
(gdb) q
调试coredump文件
通常情况下,程序异常终止时,会产生 Coredump 文件。Coredump 文件类似飞机上的"黑匣子",它会保留程序"失事"瞬间的一些信息,通常包含寄存器的状态、栈调用情况等。Coredump 文件常用于辅助分析和 Debug。
1. 确保生成coredump
文件
在Linux系统中,默认情况下可能不会生成coredump
文件,需要手动开启:
首先查看是否允许生成coredump文件
$ ulimit -a
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
...
$ ulimit -c unlimited # 将core文件的大小临时设置为不受限制
此命令将允许生成无限大的coredump
文件。如果需要指定coredump
文件的生成路径和格式,可以编辑/proc/sys/kernel/core_pattern
文件。
设置Coredump文件的格式
sudo vim /etc/sysctl.conf #打开配置文件
# 在里面添加下面这句话
kernel.core_pattern = %e_core_%s_%t # 自定义格式%e:executable-name,%s:signal, %t:time
# 其中 %e 表示异常的文件名 %s表示发生异常产生的终止信号 %t 表示发生异常的时间
sudo sysctl -p #表示让配置生效
2. 启动gdb
并加载coredump
文件
使用以下命令启动gdb
并加载coredump
文件:
gdb <可执行程序路径> <coredump文件路径>
例如:
./my_program #先执行程序,产生coredump报错信息
gdb my_program core.1234 #然后进入GDB调试
3. 查看堆栈信息
在gdb
中输入bt
(backtrace)命令,查看程序崩溃时的堆栈信息:
bt
这将显示从导致程序崩溃的位置开始的完整函数调用堆栈。
4. 查看变量和寄存器
查看变量值
print <变量名>
查看寄存器状态
info registers
5. 切换栈帧
如果需要查看特定栈帧中的变量,可以使用frame
命令切换栈帧:
frame <栈帧号>
然后使用print
命令查看该栈帧中的变量。
6. 查看源代码
如果可执行文件包含调试信息(编译时使用-g
选项),可以使用list
命令查看崩溃点附近的源代码:
list
7. 多线程调试
如果程序是多线程的,可以使用以下命令查看所有线程:
info threads
然后切换到特定线程:
thread <线程号>
并使用bt
命令查看该线程的堆栈。
8. 分析动态库
如果崩溃发生在动态库中,可能需要加载动态库的符号信息。可以使用add-symbol-file
命令加载符号文件:
add-symbol-file <符号文件路径> <加载地址>
加载地址可以通过info sharedlibrary
命令获取。