GDB 调试:让程序运行中的错误无所遁形¶
来自 Linux二进制
引言¶
作为C/C++
开发人员,确保程序正常运行是根本且首要的目标。而要达成这一目标,调试是最为基础的手段。熟悉各类调试方式,能够助力我们更迅速地定位程序问题,提升开发效率。在开发进程中,倘若程序的运行结果未达预期,首要之举便是启用GDB
进行调试,在相应位置"设置断点",进而剖析缘由;当线上服务出现问题时,首先查看进程是否存在。若进程不存在,需查看是否生成了coredump
文件。若有,可借助GDB
调试该文件;若没有,则通过dmesg
来分析内核日志以探寻原因。
概念¶
GDB
(GNU Debugger
)是一个由GNU
开源组织发布的、UNIX/LINUX
操作系统下的、功能强大的程序调试工具。
它允许开发者在程序运行时查看变量的值、设置断点、单步执行代码、查看调用栈等,从而帮助开发者找出程序中的错误和优化程序的性能。
GDB
可以调试多种编程语言编写的程序,如C
、C++
、Objective-C
等。它支持在本地和远程系统上进行调试,并且可以处理多线程和多进程的程序。
总的来说,GDB
是软件开发过程中非常重要的工具,对于提高程序的质量和稳定性起着关键作用。
常用调试指令¶
1、断点¶
断点属于我们在调试过程中频繁运用的一项功能。当我们于特定位置设定断点以后,程序运行至该位置就会暂时停止,此时我们能够针对程序实施更多的操作,例如查看变量的内容、堆栈的状况等等,从而辅助我们对程序进行调试。
断点的命令经过归纳整理可大概分为以下三类:
* breakpoint
* watchpoint
* catchpoint
这些分类主要基于断点的不同类型和功能,让我们一起了解一下这些命令。
breakpoint¶
可以根据行号、函数、条件生成断点,下面是相关命令以及对应的作用列表:
命令 | 作用 |
---|---|
break | 在下一个指令处设置断点。 |
break [file]:function | 在文件file 的function 函数入口设置断点。 |
break [file]:line | 在文件file 的第line 行设置断点。 |
info breakpoints | 查看断点列表。 |
break [+-]offset | 在当前位置偏移量为[+-]offset 处设置断点。 |
break *addr | 在地址addr 处设置断点。 |
break ... if expr | 设置条件断点,仅仅在条件满足时。 |
ignore n count | "忽略断点n 次"或者"跳过n 次达到断点的执行"。 |
clear | 删除当前行的断点。 |
clear [file:]line | 删除第line 行的断点。 |
clear function | 删除所有位于function 内的断点。 |
delete n | 删除指定编号的断点。 |
enable n | 启用指定编号的断点。 |
disable n | 禁用指定编号的断点。 |
save breakpoints file | 保存断点信息到指定文件。 |
watchpoint¶
watchpoint
是一种与常规断点相似但具有独特特性的调试工具,它并非固定于某一行源代码之上,而是让GDB
在监控的表达式值发生变化时自动暂停程序的执行。这种机制允许开发者在无需预知代码执行路径的情况下,对特定变量或表达式的变化保持高度关注,从而更有效地诊断程序中的问题。
watchpoint
的实现方式主要分为硬件辅助和软件模拟两种。硬件实现的watchpoint
依赖于底层硬件系统的支持,能够更高效地检测变量的值变化。而软件实现的watchpoint
则通过模拟方式工作,即在程序的每一步执行后都检查被监控的变量值是否发生了改变。GDB
在创建新的数据断点时,会首先尝试采用硬件辅助的watchpoint
以提高效率,如果由于硬件限制或配置问题无法成功创建,则会回退到软件模拟的方式来实现。
命令 | 作用 |
---|---|
watch variable | 设置变量数据断点。 |
watch var1 + var2 | 设置表达式数据断点。 |
rwatch variable | 设置读断点,仅支持硬件实现。 |
awatch variable | 设置读写断点,仅支持硬件实现。 |
info watchpoints | 查看数据断点列表。 |
set can-use-hw-watchpoints 0 | 强制基于软件方式实现。 |
注意:
* 当监控变量为局部变量时,一旦局部变量失效,数据断点也会失效
* 如果监控的是指针变量
p
,则watch *p
监控的是p
所指内存数据的变化情况,而watch p
监控的是p
指针本身有没有改变指向最常见的数据断点应用场景:「定位堆上的结构体内部成员何时被修改」。由于指针一般为局部变量,为了解决断点失效,一般有两种方法。
命令 | 作用 |
---|---|
print \&variable | 查看变量的内存地址。 |
watch(type)address | 通过内存地址间接设置断点。 |
watch -l variable | 指定location 参数。 |
watch variable thread 1 | 仅编号为1 的线程修改变量var 值时会中断。 |
catchpoint¶
catchpoint
命令是GDB(GNU Debugger)
中用于设置捕获点(catchpoint
)的命令,它允许程序在发生特定事件时停止执行,这些事件包括异常抛出、库加载、系统调用等。以下是一些常用的catchpoint
命令及其说明:
命令 | 作用 |
---|---|
catch throw | 捕获C++ 程序中的异常抛出事件。当程序抛出异常时,将停止执行。该命令是GDB 为C++ 程序中的异常抛出事件提供的专门调试工具,而C 程序则需要通过其他方式来检测和调试错误或异常情况。 |
catch catch | 捕获C++ 程序中的异常捕获事件。当程序捕获到异常时,将停止执行。 |
catch load | 捕获动态链接库(共享库)的加载事件。当程序加载新的动态链接库时,将停止执行。 |
catch unload | 捕获动态链接库的卸载事件。当程序卸载动态链接库时,将停止执行。 |
catch fork | 捕获fork 系统调用。在Unix-like 系统中,当程序调用fork 时,将停止执行。 |
catch vfork | 捕获vfork 系统调用。类似于fork ,但在某些情况下,vfork 的行为略有不同。 |
catch exec | 捕获exec 系统调用。当程序通过exec 系列函数(如execl、execp、execvp 等)执行新程序时,将停止执行。 |
catch syscall系统调用名称或编号 | 捕获特定的系统调用。可以通过系统调用的名称、组或编号来指定要捕获的系统调用。 |
【拓展1 】查看和管理
catchpoint
可通过如下方式:*
info break
:显示所有已设置的断点(包括catchpoints
)。虽然命令是info break
,但它也会列出catchpoints
。 *delete
、disable
、enable
等命令同样适用于catchpoint
,用于删除、禁用或启用已设置的捕获点。 【拓展2 】在GDB(GNU Debugger)
中,commands
命令是一个非常强大的功能,它允许你为特定的断点、观察点或捕获点指定一系列GDB
命令,这些命令将在断点被触发时自动执行。以下是使用commands
命令的标准方法:(gdb) commands [breakpoint-number] > [command1] > [command2] > ... > end
*
[breakpoint-number]
:是可选的,表示你想要附加命令的断点的编号。如果省略,则默认应用于最近设置的断点。 *[command1]
,[command2]
, ...:是在断点触发时你希望自动执行的GDB
命令,每行一个命令,以end
作为结束标记。
2、启动与退出¶
使用GDB
调试,一般有以下几种启动与退出方式:
命令 | 作用 |
---|---|
gdb program | 最常用的启动GDB 调试程序的方式。 |
gdb --args program arg1 arg2 ... | 带参数启动程序。 |
gdb -x /path/to/gdbinit --args program args | 带自定义gdbinit 脚本启动。 |
gdb program -c coredump_file | 用GDB 查看core dump 文件,跟踪程序崩溃的原因。 |
gdb attach pid | 附加(attach )到一个已经在运行的进程号为pid 的进程上,以便进行调试。 |
gdb -p pid | 调试一个已经在运行的进程,其中pid 是你想要调试的进程的进程ID ;与gdb attach pid 效果相同,但语法更简洁。 |
quit或q | 退出GDB 调试器。 |
Ctrl+D | 退出GDB 调试器,同quit 。 |
GDB
进入进程进行调试的方式主要包括:调试可执行程序、调试正在运行的进程、调试core dump
文件以及带参数调试。这些方式涵盖了GDB
调试的主要应用场景,能够满足大多数开发者的需求。
【拓展 】
Linux
平台如何生成core dump
文件在
Linux
平台上生成core dump
文件通常是在程序崩溃时自动发生的,但这依赖于系统的几个配置选项。下面是一些步骤来确保在程序崩溃时能够生成core dump
文件:1. 设置
ulimit
参数
ulimit
命令用于限制用户在Shell
中的资源使用。要允许生成core dump
文件,你需要设置core
文件大小限制。打开终端并执行以下命令:ulimit-cunlimited
这将允许生成任意大小的
core dump
文件。如果你不想设置无限制大小,可以指定一个具体的大小,例如:ulimit-c1024
这里的
1024
代表1024KB
,即1MB
。2. 配置
core_pattern
core_pattern
决定了core dump
文件的命名和保存位置。默认情况下,core dump
文件可能会被保存在程序崩溃的位置,并命名为core
。为了更精确地控制这些文件的生成,你可以编辑/etc/sysctl.conf
文件,并加入以下行:kernel.core_pattern = /var/crash/%e.%p.%h.%t.coredump
这里的
%e
是崩溃程序的名字,%p
是进程ID
,%h
是主机名,%t
是时间戳。你可以根据需要调整这些参数。之后,运行以下命令使设置生效:sudosysctl-p
3. 确保目标目录可写
确保
core_pattern
中指定的目录对生成core dump
文件的用户是可写的。例如,你可以为/var/crash
目录设置权限:sudomkdir-p/var/crash #root是用户名,指的是系统管理员账户; # adm 是组名,它通常包含一些可以访问系统日志和其它管理信息的用户。 sudochownroot:adm/var/crash sudochmod2775/var/crash
4. 确认信号处理
默认情况下,当程序接收到
SIGSEGV
(段错误)、SIGFPE
(浮点异常)、SIGILL
(非法指令)等信号时,系统会生成core dump
文件。你也可以通过程序代码显式地请求生成core dump
文件,例如使用raise(SIGQUIT)
。5. 重新启动系统或应用
为了确保所有的设置生效,你可能需要重启你的应用程序或者整个系统。
6. 检查和分析core dump文件
一旦程序崩溃并生成了
core dump
文件,你可以使用调试器如gdb
来加载和分析这个文件,帮助你确定崩溃的原因。例如:gdb/path/to/your/executable/path/to/corefile
请确保你有正确的权限去读取
core dump
文件和执行可执行文件。遵循以上步骤,你应该能够在
Linux
平台上成功地生成和分析core dump
文件。如果遇到问题,检查系统日志(如/var/log/syslog
)可能有助于诊断原因。
3、命令行¶
在GDB(GNU Debugger)
中,run
、set args
、show args
等命令与程序的命令行参数处理紧密相关。以下是对GDB
命令行命令的总结:
命令 | 作用 |
---|---|
start | 开始执行被调试的程序,并在程序的入口点(通常是main 函数的第一条语句之前)暂停,方便您查看程序初始状态和设置断点等,以便后续进行调试。 |
run arglist | 以arglist 为参数列表运行程序,即run arg1 arg2 arg3 ... 。 |
set args arglist | 设置程序启动时接收的命令行参数,每次你使用run 命令启动程序时,这些参数都会被传入程序。 |
set args | 设置程序启动时接收的命令行参数,这里表示设置空的参数列表。 |
show args | 显示当前设置的命令行参数。 |
使用set args
来预设参数,然后使用run
命令启动程序时,这些参数会被自动传递给程序。在调试多参数或需要特定参数的程序时,set args
和run
的组合使用非常有用。show args
命令可以随时用来检查当前的参数设置,确保在运行程序前参数正确无误。
这些命令允许你在GDB
环境中灵活地控制程序的启动条件,特别是对于需要命令行参数的程序来说,这能提供很大的便利。通过这些命令,你可以模拟不同的运行环境,测试程序在不同输入下的行为,这对于调试和验证程序逻辑至关重要。
4、程序栈¶
在GDB(GNU Debugger)
中,有几个命令行工具可以用来检查和操作程序的调用栈。这些命令帮助你理解程序的执行流程,定位问题所在,以及查看函数调用的历史。以下是几个常用的与程序栈相关的GDB
命令:
命令 | 作用 |
---|---|
backtrace [n] | 显示程序崩溃或停止执行时的函数调用历史;backtrace 会列出一系列函数调用,每个调用都对应着程序中的一个位置。列表的顶部通常是导致程序停止执行的函数,而底部则是更早的调用,直到程序的入口点。n 表示来查看调用堆栈的前 n 个帧。 |
frame [n] | 选择第n 个栈帧,如果不存在,则查看当前栈帧;切换到调用栈中的不同帧(frame ),以便检查函数的局部变量、参数和返回地址等信息。 |
up [n] | up 表示回到编号更大的上一帧,即选择当前栈帧的调用者;n 表示选择当前栈帧编号+n 的栈帧。 |
down [n] | down 表示移到编号更小的下一帧,即选择被当前栈帧调用的下一个函数;n 表示选择当前栈帧编号-n 的栈帧。 |
info frame n | n 表示你想要查看的帧的编号。这会显示该函数调用的详细信息,包括参数和局部变量的值。 |
info frame [addr] | 显示当前选定的栈帧(stack frame )的信息,或者如果提供了地址参数 [addr ],则会显示指定地址处的栈帧信息。栈帧是在函数调用时创建的,它包含了函数的局部变量、参数以及返回地址等信息。通过info frame 命令,你可以检查函数调用的上下文,这对于调试程序非常有用。 |
info args | 显示当前选中栈帧的所有函数参数的值;当你在一个函数调用点设定了断点并停止程序执行时,可以通过输入info args 来查看该函数的参数。GDB 将列出每个参数的名字(如果源代码可用并且编译时包含了调试信息),以及它们的当前值。 |
info locals | 显示当前栈帧中所有局部变量的值。当程序在一个函数的断点处暂停时,使用info locals 命令可以帮助你检查这些局部变量的状态,它会列出当前函数内所有的局部变量,包括它们的名称、类型和当前值。如果函数中有多个嵌套的代码块,那么每个代码块的局部变量都会被显示出来。info locals 命令还可以接受一个可选的变量名作为参数,以便只显示特定变量的信息。例如,info locals local_var1 将只显示local_var1 的信息。 |
finish | 继续执行直到当前栈帧结束并返回到调用者。这可以让你查看一个函数的完整执行流程而不必逐行跟踪。 |
【拓展 】
info frame
命令可能显示的信息类型:* 栈级别(Stack Level):显示当前帧相对于栈顶部的级别。 * 帧地址(Frame Address):显示当前帧的内存地址。 * 指令指针(Instruction Pointer) :对于
x86
架构,这可能是EIP
或RIP
寄存器的值,显示了当前正在执行的指令地址。 * 源文件和行号(Source File and Line Number):显示了当前指令所在的源代码文件和行号。 * 保存的指令指针(Saved Instruction Pointer):这是调用当前函数的上一层函数的返回地址。 * 调用者帧地址(Caller's Frame Address):显示了调用当前函数的上一层函数的帧地址。 * 源语言(Source Language) :显示了源代码的语言,如C
或C++
。 * 参数列表(Argument List):显示了传递给当前函数的参数。例如,以下是一个
info frame
命令的输出示例:1(gdb) info frame 2Stack level 0, frame at 0xbffd0cd0: 3 eip = 0x80483ca in show3 (main.c:4); 4 saved eip = 0x80483ef 5 called by frame at 0xbffd0ce0 6 source language c. 7 Arglist at 0xbffd0cc8, args: ...
在这个例子中,我们可以看到当前的栈帧位于地址
0xbffd0cd0
,当前正在执行的指令是在main.c
文件的第4
行中的show3
函数,而上一个函数的返回地址是0x80483ef
。调用者帧地址是0xbffd0ce0
。这个地址指向调用当前函数的那个栈帧的基址,可以用来查看调用者函数的上下文信息。
在GDB
(GNU Debugger
)中,程序栈(call stack
)是追踪程序执行流程的关键工具之一。程序栈记录了程序执行过程中的函数调用历史,每个函数调用都会在栈上创建一个新的栈帧(stack frame
)。栈帧包含了函数的局部变量、函数参数、返回地址和其他相关信息。
5、多进程¶
GDB
在调试多进程程序(程序含fork
调用)时,默认只追踪父进程。可以通过命令设置,实现只追踪父进程或子进程,或者同时调试父进程和子进程。
命令 | 作用 |
---|---|
info inferiors | 显示当前被调试的进程列表,包括进程ID 和一些基本信息。 |
inferior inferior-num | 切换到指定编号的进程进行调试。 |
attach pid | 附加到一个已经运行的进程,其中pid 是进程ID 。 |
print $_exitcode | $_exitcode 是一个特殊的变量,它包含了程序退出时的退出代码。但是,$_exitcode 只在程序正常退出后才可用。如果你试图在程序仍在运行或未正常退出时使用print $_exitcode ,GDB 将无法提供退出代码,因为此时_exitcode 尚未确定。 |
set follow-fork-mode child | 设置GDB 追踪子进程。 |
set follow-fork-mode parent | 设置GDB 追踪父进程。 |
set detach-on-fork on | 设置了这个选项,当你的程序调用fork() 创建子进程时,GDB 将不会自动追踪子进程,这可以减少GDB 追踪的进程数量。 |
set detach-on-fork off | 在fork() 后同时追踪父进程和子进程。 |
在调试多进程程序时候,默认情况下,除了当前调试的进程,其他进程都处于挂起状态,所以,如果需要在调试当前进程的时候,其他进程也能正常执行,那么通过设置set schedule-multiple on
即可。
【拓展 】在
GDB
中,set schedule-multiple on
命令开启了一个特性,允许GDB
在多线程或多进程调试环境中同时调度多个线程或进程。默认情况下,GDB
在每次执行continue
,step
,next
, 或其他类似的命令时,只会调度一个线程或进程。但是,当schedule-multiple
被设置为on
时,GDB
将尝试同时推进所有活动线程或进程的执行,直到它们都遇到断点、系统调用或其他暂停点。这个特性在调试高度并发的程序时尤其有用,因为你可以观察到所有线程或进程的动态行为,而不仅仅局限于某一个线程或进程的视角。例如,在多线程程序中,你可以看到不同线程之间的交互和同步点。
set schedule-multiple on
的使用格式如下:(gdb) set schedule-multiple on
一旦启用,
GDB
将在执行命令时尽可能地并行推进所有线程或进程。但是,需要注意的是,这可能会导致调试会话变得更为复杂,因为你需要同时关注多个执行流。在多进程调试中,
schedule-multiple
的效果可能受限于GDB
当前控制的进程数以及操作系统对进程调度的限制。此外,不是所有的GDB
版本都完全支持schedule-multiple
,尤其是在处理多进程环境时。
6、多线程¶
多线程编程在日常开发中广泛使用,掌握其调试技巧对软件工程师至关重要。
使用GNU Debugger (GDB)
调试多线程程序时,有一些关键的指令可以帮助你更有效地管理线程和定位问题。以下是一些常用的GDB
命令,它们特别适用于多线程环境:
命令 | 作用 |
---|---|
info threads | 列出所有线程的ID 和状态。 |
thread num | 切换到编号为num 的线程。 |
thread apply thread-id-list command | 对特定线程列表执行一个给定的命令;thread-id-list 可以是一个单一的线程ID 、一个线程ID 的范围,或者多个线程ID 的列表。线程ID 可以通过info threads 命令获得,它会列出所有当前活动线程的信息,包括它们的ID 。假设你有两个活动线程,ID 分别是1 和2 ,你想要查看线程1 和线程2 上变量x 的值,可以这样操作:thread apply 1,2 print x 。 |
thread apply all command | 在所有线程上执行command 。其中command 是你希望在所有线程上执行的GDB 命令。例如,如果你想要查看所有线程中某个变量x 的值,thread apply all print x 。 |
break functionthread n | 在特定线程n 的特定函数function 上设置断点。 |
set scheduler-locking on | 调试多线程程序时,设置仅当前选中的线程会执行,即锁定当前选中的线程,直到你显式地解除锁定或切换到另一个线程。 |
set scheduler-locking off | 调试多线程程序时,设置所有线程都可以执行,即意味着所有线程都可以被调度,没有任何锁定,这是默认值。 |
set scheduler-locking step | 当您在当前选定的线程上使用next 、step 或finish 命令进行单步执行时,GDB 会暂时锁定当前线程,防止其他线程被调度,注意 ,只在单步执行时会锁定当前线程 。这意味着在你单步执行的过程中,其他线程不会抢占CPU 时间,从而避免了它们可能引发的意外行为或数据竞争。一旦单步执行命令完成,GDB 将解除锁定,允许其他线程再次被调度。这有助于你专注于单个线程的行为,而不必担心其他线程的干扰。 |
如果只关心当前线程,建议临时设置scheduler-locking
为on
,避免其他线程同时运行,导致命中其他断点分散注意力。
7、打印输出¶
通常情况下,在调试的过程中,我们需要查看某个变量的值,以分析其是否符合预期,这个时候就需要打印输出变量值。以下是一些常用的打印变量的GDB
命令:
命令 | 作用 |
---|---|
whatis variable | 显示变量的类型。 |
ptype variable | 查看变量详细的类型信息。 |
info variables variable | 查看定义变量variable 的文件,不支持局部变量。 |
display variable | 用于在每次程序暂停时自动显示一个变量或表达式的值。可以使用undisplay display-number 来取消显示,其中display-number 是display 命令返回的编号。 |
1)打印字符串¶
使用x/s
命令打印ASCII
字符串,如果是宽字符字符串,需要先看宽字符的长度 print sizeof(str)
。
如果长度为2
,则使用x/hs
打印;如果长度为4
,则使用x/ws
打印。
命令 | 作用 |
---|---|
x/s str | 以字符串(s )格式显示位于str 所指内存地址处的数据,即打印字符串。 |
set print elements 0 | 控制当你使用print 命令打印数组或集合时,GDB 显示的元素数量。默认情况下,GDB 会限制显示的元素数量,以避免输出过长或不必要的信息。当你设置set print elements 0 时,实际上是在告诉GDB 不要限制输出的元素数量。也就是说,GDB 将尽可能打印出整个数组或集合的所有元素,而不是只显示一部分。 |
call printf("%s\n",xxx) | 使用printf 函数来打印一个字符串变量xxx 。要注意的是,xxx 必须是一个有效的指针,指向一个有效的字符串。虽然printf 的标准返回类型是int ,表示输出的字符数量,但在某些情况下,GDB 可能无法识别这一点。为了解决这个问题,你需要显式地指定printf 的返回类型。正确的命令格式如下:call (int)printf("Your format string here\n", argument1, argument2, ...) 。 |
printf "%s\n",xxx | 同上。 |
【拓展 】在
GDB
中,x
命令是一个用于检查内存内容的强大工具,全称是examine
。它允许你以不同的格式查看内存中的数据,如字节、半字、字、双字或字符串。x
命令的语法是:x/<count><format><address>
其中:
*
<count>
是你想要查看的数据项的数量,默认为1
。 *<format>
是你希望用来显示数据的格式,比如x
表示十六进制,d
表示有符号十进制,u
表示无符号十进制,t
表示二进制,f
表示浮点数,a
表示ASCII
,s
表示字符串。 *<address>
是你想要检查的内存起始地址,可以是一个变量名(如果是指针)、一个表达式的结果或一个直接的十六进制地址。例如,
x/s str_ptr
命令是examine string at str_ptr
的简写,意味着检查并以字符串格式显示str_ptr
所指向的内存区域的内容。
x
命令的灵活性使其成为调试时检查变量状态、跟踪内存泄漏或验证数据结构完整性的重要工具。通过选择不同的格式和数量,你可以深入了解程序的内存布局和数据状态。
2)打印数组¶
在GDB
中打印数组可以通过几种不同的方式来实现。以下是一些常用的方法:
命令 | 作用 |
---|---|
print arr | 打印名为arr 的数组,默认情况下,GDB 会打印数组的一部分。 |
print *arr@num | 打印从数组开头连续num 个元素的值。 |
x/10wx arr | 打印数组的前10 个元素,在这里,/10w 指定要打印10 个字(word )的数据,x 表示以十六进制格式显示数据,arr 是数组的起始地址。w 表示以整数(word )的格式打印数据。 |
print arr[index]@num | 打印arr 数组下标从index 开始的num 个元素。 |
set print array-indexes on | 打印数组时同时显示数组元素的索引,使得输出更加易读,特别是在处理多维数组或大型一维数组时。如print arr 会输出:$1 = {[0] = 1, [1] = 2, [2] = 3, [3] = 4, [4] = 5} 。 |
3)打印指针¶
在GDB
中打印指针有两种主要方式:一种是打印指针本身的值,另一种是打印指针指向的内存内容。
命令 | 作用 |
---|---|
print ptr | 查看一个指针变量ptr 的值(即它指向的内存地址),通常是一个十六进制的内存地址。 |
print *ptr | 查看指针ptr 指向的内存内容,而不是指针本身,即指针指向的地址上存储的值。 |
print (*ptr).field_name | 使用解引用指针ptr 操作来查看结构体或类的字段。 |
print ptr->field_name | 如果结构体的字段支持直接访问,也可以这样打印结构体字段。 |
print *(struct xxx *)ptr | 查看指针ptr 指向的结构体的内容。 |
print *my_ptr_array[index] | 查看my_ptr_array 数组中位于index 位置的指针所指向的值。* 操作符的作用,它用于解引用指针,即访问指针所指向的内存位置的值。 |
print *my_array_ptr | 如果my_array_ptr 是一个指向数组的指针,你可以通过解引用这个指针来查看数组的第一个元素。 |
4)打印指定内存地址的值¶
在GDB
中,x
命令是用于检查内存区域内容的强大工具。x
是examine
的缩写,它允许你查看程序运行时的内存状态。使用x
命令的基本语法如下:
x/<count><format><address>
三个参数含义如下:
*
<count>
是要检查的元素数量。
*
<format>
是数据的显示格式。
*
<address>
是要检查的内存起始地址。
其中,<format>
参数可以是以下几种之一:
*
x
:以十六进制显示字节。
*
bx
:以十六进制显示字节,更专注于字节级别数据的显示。
*
hx
:以十六进制显示半字(half word
,通常是2
字节)。
*
wx
:以十六进制显示字(word
,通常是4
字节)。
*
gx
:以十六进制显示双字(double word
,通常是8
字节)。
*
z
:以十六进制显示四字(quad word
,通常是16
字节)。
*
c
:以ASCII
字符显示字节。
*
b
:以有符号的八进制显示字节。
*
h
:以有符号的十六进制显示半字。
*
w
:以有符号的十进制显示字。
*
g
:以有符号的十进制显示双字。
*
q
:以有符号的十进制显示四字。
*
s
:以字符串形式显示数据。
*
a
:以ASCII字符显示字节。
使用x
命令是调试时检查内存内容和变量状态的重要手段,特别是在追踪内存相关的问题时。
命令 | 作用 |
---|---|
x/8bx arr | 以十六进制打印数组arr 的前8 个byte 的值。 |
x/8wx arr | 以十六进制打印数组arr 的前8 个word 的值。 |
5)打印局部变量¶
在GDB
中打印局部变量的值可以通过几种不同的方式来实现,以下是其中的一些方法:
命令 | 作用 |
---|---|
info locals | 显示当前栈帧中所有局部变量的值。当程序在一个函数的断点处暂停时,使用info locals 命令可以帮助你检查这些局部变量的状态,它会列出当前函数内所有的局部变量,包括它们的名称、类型和当前值。如果函数中有多个嵌套的代码块,那么每个代码块的局部变量都会被显示出来。info locals 命令还可以接受一个可选的变量名作为参数,以便只显示特定变量的信息。例如,info locals local_var1 将只显示local_var1 的信息。 |
backtrace full | 是backtrace 命令的一个增强版本,它不仅显示函数调用的堆栈,还提供更详细的信息,如每个调用帧中的局部变量和参数的值。这对于深入分析程序状态和找出问题所在尤其有用。 |
bt full n | 从栈顶开始向栈底方向显示n 个栈帧及其局部变量和参数的值。 |
bt full -n | 从栈底开始向栈顶方向显示n 个栈帧及其局部变量和参数的值。 |
需要注意的是,由于backtrace full
提供了大量的细节,它可能会产生相当长的输出,特别是对于复杂的调用堆栈。因此,在使用这个命令时,你应该确保有足够的耐心去阅读和理解输出结果,同时也考虑到性能和效率方面的影响。
6)打印结构体¶
在GDB
中打印结构体可以采用几种不同的方法,这取决于结构体的复杂程度以及你想要查看的具体信息。以下是一些基本的步骤和命令,可以帮助你打印和理解结构体的内容:
命令 | 作用 |
---|---|
print myStruct | 直接打印结构体,将显示结构体的所有成员和它们的值。GDB 默认会尝试以一种可读的格式显示结构体,包括递归地显示嵌套的结构体成员。 |
print myStruct.memberX | 打印结构体的特定成员,如果你想只查看结构体中的特定成员,可以使用myStruct.memberX 这种格式,其中,memberX 是结构体中特定成员。 |
set print pretty on | 设置每行只显示结构体的一名成员,先使用该命令进行设置,再使用print 打印结构体。 |
set print null-stop on | GDB 会在遇到第一个空字符(null character ,即\0 )时停止打印字符串,这是大多数编程语言中字符串结束的标志。这是默认行为,因为在标准C/C++ 中,字符串通常是由以空字符结尾的字符数组表示的。 |
set print null-stop off | 当null-stop 被设为off ,GDB 不会在遇到第一个空字符(null character ,即\0 )时停止打印字符串。将会继续打印字符串直到达到预设的最大字符数限制,或者直到达到内存区域的末尾。 |
如果你想要在每次断点触发时自动显示结构体或其成员的值,可以使用display
命令。结合info locals
命令,你还可以查看当前作用域内所有局部变量,包括结构体变量。通过组合使用上述命令,你可以有效地在GDB
中调试和理解结构体。根据你的具体需求,选择最合适的方法来查看和分析结构体数据。
8、函数跳转¶
在GDB
中进行函数跳转,主要是指在调试过程中控制程序流,使其跳转到特定的函数或代码段执行。这可以通过几种不同的方式实现:
命令 | 作用 |
---|---|
set step-mode on | 在GDB 中,set step-mode on 命令用于改变单步调试的行为。默认情况下,当你使用step 命令时,GDB 会单步执行到下一个源代码行,但是如果遇到的函数没有调试信息(例如,系统库函数或优化掉的代码),GDB 通常不会进入这些函数的内部,而是直接跨越过去。当你设置了set step-mode on ,GDB 的行为会发生变化,它会尝试进入那些没有调试信息的函数,继续进行单步执行,直到遇到有调试信息的代码或到达下一个断点。这对于查看低级代码的执行流程或检查没有调试信息的函数内部行为非常有用。 |
finish | 当你在函数内部设置了断点并开始调试时,使用finish 命令可以让函数自然执行到底,直到它返回给调用者。这在你想要跟踪一个函数的全部执行流程,但又不想逐行执行的情况下特别有用。一旦发出finish 命令,GDB 将继续执行,直到当前函数返回。此时,GDB 将在函数返回点停下来,你可以查看函数返回后的状态,包括任何可能的返回值或函数调用栈的变化。 |
return expression | return 命令可以让你从当前正在调试的函数中提前返回,并且可以选择性地指定返回值。当你在函数内部设置了断点,并且想要模拟函数提前返回的情况,return 命令就非常有用。这对于测试函数的不同退出路径或调试函数的返回值逻辑很有帮助。其中expression 是你想要作为函数返回值的表达式。如果省略expression ,则函数将返回默认值,这通常是零或空值,具体取决于函数的返回类型。 |
call printf("%s\n",xxx) | 使用printf 函数来打印一个字符串变量xxx 。要注意的是,xxx 必须是一个有效的指针,指向一个有效的字符串。虽然printf 的标准返回类型是int ,表示输出的字符数量,但在某些情况下,GDB 可能无法识别这一点。为了解决这个问题,你需要显式地指定printf 的返回类型。正确的命令格式如下:call (int)printf("Your format string here\n", argument1, argument2, ...) 。 |
set {type} address = value | 使用set 命令结合类型和内存地址可以让你直接在内存中修改数据。这里的type 是你想要存储的值的数据类型,address 是要修改的内存地址,而value 是你想要写入的新值。set 命令的修改只在当前的调试会话中有效,一旦你退出GDB 或重启程序,这些修改将不会被保留。 |
9、其它¶
1)图形化¶
GDB
的TUI
(Terminal User Interface
)模式提供了一个图形化的界面,允许你在终端窗口中以更直观的方式进行调试。TUI
模式提供了比纯文本模式更丰富的界面,包括源代码高亮、堆栈轨迹的可视化、以及各种调试信息的面板展示等。
要在启动GDB
时直接进入TUI
模式,你可以在命令行中加入-tui
参数,如下所示:
gdb -tui your_program
或者,如果你已经在GDB
中,可以通过使用组合键Ctrl+X
然后按A
键,切换到TUI
模式。在TUI
模式下,你可以使用方向键和其它键盘快捷键来浏览和操作界面。
在TUI
模式下,你还可以使用以下命令来控制窗口的布局和焦点:
*
切换到下一个窗口 :Ctrl+X
然后按O
或者使用Focus Next
(fs n
)
*
切换到源码窗口 :Focus Source
(fs s
)
*
切换到命令窗口 :Focus Command
(fs c
)
*
切换到汇编窗口 :Focus Asm
(fs a
)
*
切换到寄存器窗口 :Focus Regs
(fs r
)
*
切换到上一个窗口 :Focus Prev
(fs p
)
*
切换到双窗口模式 :Ctrl+X
然后按2
*
切换到单窗口模式 :Ctrl+X
然后按1
*
切换到传统的命令行界面或返回TUI模式 :Ctrl+X
然后按A
另外,TUI
模式下还有一些命令可以操作界面,如下:
命令 | 作用 |
---|---|
layout src | 显示源码窗口。 |
layout asm | 显示汇编窗口。 |
layout split | 同时显示源代码和汇编代码窗口。 |
layout regs | 显示寄存器 + 源码或汇编窗口。 |
winheight src +5 | 源码窗口高度增加5 行。 |
winheight asm -5 | 汇编窗口高度减小5 行。 |
winheight cmd +5 | 命令行窗口高度增加5 行。 |
winheight regs -5 | 寄存器窗口高度减小5 行。 |
2)汇编¶
在GDB
中查看汇编代码可以通过几种不同的方式完成,具体取决于你想要查看的代码范围和上下文。以下是几种常用的方法:
命令 | 作用 |
---|---|
disassemblefunction_name | 查看某个特定函数的汇编代码,可以使用disassemble 命令,简称disas 。例如,如果你想查看名为my_function 的函数的汇编代码,可以这样操作:disassemble my_function 。 |
disassemble | 如果你想查看当前程序计数器(PC )周围的汇编代码,可以省略参数,这将显示当前执行点周围的一段汇编代码。 |
disassemble /mrfunction_name | 同时比较函数源代码和汇编代码。 |
3)调试和保存core文件¶
在GDB
中调试core dump
文件是一种非常有用的技巧,特别是当程序崩溃并且生成了core dump
文件时。core dump
文件包含了程序崩溃瞬间的内存快照和进程状态,这对于诊断问题原因至关重要。
命令 | 作用 |
---|---|
file exec_file | 当你调试未带调试信息的可执行文件时,使用fileexec_file 命令可在GDB 命令行中加载带调试信息的新编译的可执行文件,以便加载后,提供新编译的可执行文件的符号表信息给未带调试信息的可执行文件使用。注意 :二者的源码需一致,这里新编译可执行文件时仅增加-g 调试命令。 |
core core_dump_file | 加载一个特定的核心转储文件(core-dump )来调试。 |
gcore core_file | 使用gcore 命令实际上并不用于加载核心转储文件。gcore 是一个GDB 内部的命令,它的用途是在程序正在运行时创建一个核心转储文件(core dump ),记录当前进程的状态。这通常在你怀疑程序可能即将崩溃,但还没有实际崩溃的情况下使用,以便保存此时的程序状态供后续分析。 |
GDB调试案例¶
以下是一个可能会引发core dump
的C
程序示例,包含了多线程、结构体、指针操作等复杂情况:
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
//定义结构体
typedefstruct{
int\*data;
}DataStruct;
//线程函数
void*threadFunction(void*arg){
DataStruct\*ds=(DataStruct\*)arg;
//错误的指针操作,可能导致coredump
\*ds-\>data=100;
returnNULL;
}
intmain(){
pthread_tthread;
DataStructds;
ds.data=NULL;
//创建线程
if(pthread_create(\&thread,NULL,threadFunction,\&ds)!=0){
perror("pthread_createfailed");
return1;
}
//等待线程结束
if(pthread_join(thread,NULL)!=0){
perror("pthread_joinfailed");
return1;
}
return0;
}
下面是使用GDB
调试这段代码的详细流程:
* 第一步:编译代码
使用-g
标志编译代码以包含调试信息。
gcc -g crash_program.c -o crash_program -lpthread
* 第二步:运行可执行程序
[root@localhost tmp]# ./crash_program
Segmentation fault (core dumped)
*
第三步:查看coredump
文件
[root@localhost tmp]# gdb ./crash_program -c crash_program.892409.1721113020.coredump
GNU gdb (GDB) Red Hat Enterprise Linux 8.2-20.el8
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./crash_program...done.
\[New LWP 892410\]
\[New LWP 892409\]
\[Thread debugging using libthread_db enabled\]
Using host libthread_db library "/lib64/libthread_db.so.1".
Core was generated by \`./crash_program'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 threadFunction (arg=0x7ffefa858550) at crash_program.c:14
14 \*ds-\>data = 100;
\[Current thread is 1 (Thread 0x7fb85b7c1700 (LWP 892410))\]
Missing separate debuginfos, use: yum debuginfo-install glibc-2.28-251.el8.x86_64
(gdb)
通过查看coredump
文件,可以看出程序在线程函数threadFunction
中发生段错误。
也可以通过backtrace
验证。
* 第四步:查看错误发生时的堆栈跟踪
(gdb) backtrace
#0 threadFunction (arg=0x7fffffffe1a0) at crash_program.c:12
#1 0x00007ffff7bb51ca in start_thread () from /lib64/libpthread.so.0
#2 0x00007ffff78108d3 in clone () from /lib64/libc.so.6
通过backtrace
可知,栈顶是threadFunction
函数,因此,可断点错误发生在threadFunction
函数中。
* 第五步:查看源代码
(gdb) list
9
10 // 线程函数
11 void *threadFunction(void *arg) {
12 DataStruct *ds = (DataStruct *)arg;
13 // 错误的指针操作,可能导致 core dump
14 *ds->data = 100;
15 return NULL;
16 }
17
18 int main() {
(gdb)
19 pthread_t thread;
20 DataStruct ds;
21
22 ds.data = NULL;
23
24 // 创建线程
25 if (pthread_create(&thread, NULL, threadFunction, &ds)!= 0) {
26 perror("pthread_create failed");
27 return 1;
28 }
(gdb)
29
30 // 等待线程结束
31 if (pthread_join(thread, NULL)!= 0) {
32 perror("pthread_join failed");
33 return 1;
34 }
35
36 return 0;
37 }
(gdb)
* 第六步:设置断点
在threadFunction
函数中DataStruct *ds = (DataStruct *)arg;
和*ds->data = 100;
这二行设置断点。
(gdb) b 12
Breakpoint 1 at 0x40066e: file crash_program.c, line 12.
(gdb) b 14
Breakpoint 2 at 0x400676: file crash_program.c, line 14.
(gdb) info b
Num Type Disp Enb Address What
1 breakpoint keep y 0x000000000040066e in threadFunction at crash_program.c:12
2 breakpoint keep y 0x0000000000400676 in threadFunction at crash_program.c:14
* 第七步:运行程序直到断点
(gdb) run
Starting program: /tmp/test/gdb/tmp/crash_program
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
[New Thread 0x7ffff77d6700 (LWP 902893)]
[Switching to Thread 0x7ffff77d6700 (LWP 902893)]
Thread 2 "crash_program" hit Breakpoint 1, threadFunction (arg=0x7fffffffe1a0) at crash_program.c:12
12 DataStruct \*ds = (DataStruct \*)arg;
* 第八步:切换到线程
当程序因断点而停止时,GDB
可能不会自动切换到线程。使用info threads
命令查看所有线程,然后使用thread [thread number]
命令切换到你想要调试的线程。如果线程尚未开始运行,你可能需要先continue
让线程启动。根据*
可知这里已切换到线程2
,因此,无需再使用thread [thread number]
命令切换到线程2
。
(gdb) info threads
Id Target Id Frame
1 Thread 0x7ffff7fe8740 (LWP 902889) "crash_program" 0x00007ffff7bb66cd in __pthread_timedjoin_ex () from /lib64/libpthread.so.0
* 2 Thread 0x7ffff77d6700 (LWP 902893) "crash_program" threadFunction (arg=0x7fffffffe1a0) at crash_program.c:12
* 第九步:检查变量
使用print
命令观察ds
和ds->data
的值。
(gdb) print ds
$1 = (DataStruct *) 0x0
(gdb) print ds->data
Cannot access memory at address 0x0
(gdb)
从print ds->data
输出中,ds->data
指针的地址为0x0
,因此接下来的解引用这个指针是非法的,即*ds->data = 100;
操作会导致core dump
错误。
*
第十步:退出GDB
使用quit
命令退出GDB
。
(gdb) q
A debugging session is active.
Inferior 1 \[process 902889\] will be killed.
Quit anyway? (y or n) y
通过上述步骤,你将能够定位和修复由于错误的指针操作而导致的core dump
问题。在本例中,关键在于在使用指针之前确保它指向有效的内存区域。
结语¶
GDB
作为Linux
平台上不可或缺的调试利器,其重要性在软件开发领域中不言而喻。无论是面对单线程的简单调试,还是处理多线程及多进程的复杂场景,乃至分析突如其来的core dump
文件,GDB
都能以其丰富的功能助开发者一臂之力。本文通过一系列命令以及实例,不仅展示了GDB
在不同场景下的应用,更旨在引导读者掌握其基础操作,从而在日常开发中实现效率的显著提升。
然而,GDB
的功能远不止于此。从基础的断点设置、变量查看到高级的反汇编代码分析、线程切换乃至信号量跟踪,GDB
提供了一个全面的调试框架,足以满足各种调试需求。掌握GDB
,意味着掌握了驾驭代码的钥匙,使得开发者能够在复杂多变的项目中游刃有余,让编程之路更加顺畅。因此,无论你是初出茅庐的新手还是久经沙场的老将,持续探索和学习GDB
的奥秘,都将是你职业生涯中一笔宝贵的财富。