跳转至

GDB 调试:让程序运行中的错误无所遁形

来自 Linux二进制

引言

作为C/C++开发人员,确保程序正常运行是根本且首要的目标。而要达成这一目标,调试是最为基础的手段。熟悉各类调试方式,能够助力我们更迅速地定位程序问题,提升开发效率。在开发进程中,倘若程序的运行结果未达预期,首要之举便是启用GDB进行调试,在相应位置"设置断点",进而剖析缘由;当线上服务出现问题时,首先查看进程是否存在。若进程不存在,需查看是否生成了coredump文件。若有,可借助GDB调试该文件;若没有,则通过dmesg来分析内核日志以探寻原因。

概念

GDBGNU Debugger)是一个由GNU开源组织发布的、UNIX/LINUX操作系统下的、功能强大的程序调试工具。

它允许开发者在程序运行时查看变量的值、设置断点、单步执行代码、查看调用栈等,从而帮助开发者找出程序中的错误和优化程序的性能。

GDB可以调试多种编程语言编写的程序,如CC++Objective-C等。它支持在本地和远程系统上进行调试,并且可以处理多线程和多进程的程序。

总的来说,GDB是软件开发过程中非常重要的工具,对于提高程序的质量和稳定性起着关键作用。

常用调试指令

1、断点

断点属于我们在调试过程中频繁运用的一项功能。当我们于特定位置设定断点以后,程序运行至该位置就会暂时停止,此时我们能够针对程序实施更多的操作,例如查看变量的内容、堆栈的状况等等,从而辅助我们对程序进行调试。

断点的命令经过归纳整理可大概分为以下三类:

* breakpoint

* watchpoint

* catchpoint

这些分类主要基于断点的不同类型和功能,让我们一起了解一下这些命令。

breakpoint

可以根据行号、函数、条件生成断点,下面是相关命令以及对应的作用列表:

命令 作用
break 在下一个指令处设置断点。
break [file]:function 在文件filefunction函数入口设置断点。
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++程序中的异常抛出事件。当程序抛出异常时,将停止执行。该命令是GDBC++程序中的异常抛出事件提供的专门调试工具,而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。 * deletedisableenable等命令同样适用于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)中,runset argsshow args等命令与程序的命令行参数处理紧密相关。以下是对GDB命令行命令的总结:

命令 作用
start 开始执行被调试的程序,并在程序的入口点(通常是main函数的第一条语句之前)暂停,方便您查看程序初始状态和设置断点等,以便后续进行调试。
run arglist arglist为参数列表运行程序,即run arg1 arg2 arg3 ...
set args arglist 设置程序启动时接收的命令行参数,每次你使用run命令启动程序时,这些参数都会被传入程序。
set args 设置程序启动时接收的命令行参数,这里表示设置空的参数列表。
show args 显示当前设置的命令行参数。

使用set args来预设参数,然后使用run命令启动程序时,这些参数会被自动传递给程序。在调试多参数或需要特定参数的程序时,set argsrun的组合使用非常有用。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架构,这可能是EIPRIP寄存器的值,显示了当前正在执行的指令地址。 * 源文件和行号(Source File and Line Number):显示了当前指令所在的源代码文件和行号。 * 保存的指令指针(Saved Instruction Pointer):这是调用当前函数的上一层函数的返回地址。 * 调用者帧地址(Caller's Frame Address):显示了调用当前函数的上一层函数的帧地址。 * 源语言(Source Language) :显示了源代码的语言,如CC++。 * 参数列表(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。这个地址指向调用当前函数的那个栈帧的基址,可以用来查看调用者函数的上下文信息。

GDBGNU Debugger)中,程序栈(call stack)是追踪程序执行流程的关键工具之一。程序栈记录了程序执行过程中的函数调用历史,每个函数调用都会在栈上创建一个新的栈帧(stack frame)。栈帧包含了函数的局部变量、函数参数、返回地址和其他相关信息。

5、多进程

GDB在调试多进程程序(程序含fork调用)时,默认只追踪父进程。可以通过命令设置,实现只追踪父进程或子进程,或者同时调试父进程和子进程。

命令 作用
info inferiors 显示当前被调试的进程列表,包括进程ID和一些基本信息。
inferior inferior-num 切换到指定编号的进程进行调试。
attach pid 附加到一个已经运行的进程,其中pid是进程ID
print $_exitcode $_exitcode是一个特殊的变量,它包含了程序退出时的退出代码。但是,$_exitcode只在程序正常退出后才可用。如果你试图在程序仍在运行或未正常退出时使用print $_exitcodeGDB将无法提供退出代码,因为此时_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分别是12,你想要查看线程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 当您在当前选定的线程上使用nextstepfinish命令进行单步执行时,GDB会暂时锁定当前线程,防止其他线程被调度,注意只在单步执行时会锁定当前线程 。这意味着在你单步执行的过程中,其他线程不会抢占CPU时间,从而避免了它们可能引发的意外行为或数据竞争。一旦单步执行命令完成,GDB将解除锁定,允许其他线程再次被调度。这有助于你专注于单个线程的行为,而不必担心其他线程的干扰。

如果只关心当前线程,建议临时设置scheduler-lockingon,避免其他线程同时运行,导致命中其他断点分散注意力。

7、打印输出

通常情况下,在调试的过程中,我们需要查看某个变量的值,以分析其是否符合预期,这个时候就需要打印输出变量值。以下是一些常用的打印变量的GDB命令:

命令 作用
whatis variable 显示变量的类型。
ptype variable 查看变量详细的类型信息。
info variables variable 查看定义变量variable的文件,不支持局部变量。
display variable 用于在每次程序暂停时自动显示一个变量或表达式的值。可以使用undisplay display-number来取消显示,其中display-numberdisplay命令返回的编号。

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表示ASCIIs表示字符串。 * <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命令是用于检查内存区域内容的强大工具。xexamine的缩写,它允许你查看程序运行时的内存状态。使用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的前8byte的值。
x/8wx arr 以十六进制打印数组arr的前8word的值。

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被设为offGDB不会在遇到第一个空字符(null character,即\0)时停止打印字符串。将会继续打印字符串直到达到预设的最大字符数限制,或者直到达到内存区域的末尾。

如果你想要在每次断点触发时自动显示结构体或其成员的值,可以使用display命令。结合info locals命令,你还可以查看当前作用域内所有局部变量,包括结构体变量。通过组合使用上述命令,你可以有效地在GDB中调试和理解结构体。根据你的具体需求,选择最合适的方法来查看和分析结构体数据。

8、函数跳转

GDB中进行函数跳转,主要是指在调试过程中控制程序流,使其跳转到特定的函数或代码段执行。这可以通过几种不同的方式实现:

命令 作用
set step-mode on GDB中,set step-mode on命令用于改变单步调试的行为。默认情况下,当你使用step命令时,GDB会单步执行到下一个源代码行,但是如果遇到的函数没有调试信息(例如,系统库函数或优化掉的代码),GDB通常不会进入这些函数的内部,而是直接跨越过去。当你设置了set step-mode onGDB的行为会发生变化,它会尝试进入那些没有调试信息的函数,继续进行单步执行,直到遇到有调试信息的代码或到达下一个断点。这对于查看低级代码的执行流程或检查没有调试信息的函数内部行为非常有用。
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)图形化

GDBTUITerminal 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 dumpC程序示例,包含了多线程、结构体、指针操作等复杂情况:

#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命令观察dsds->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的奥秘,都将是你职业生涯中一笔宝贵的财富。

跳转到 Cubox 查看