软件开发的一个很重要的工作就是测试程序和排除错误,特别是在一个大程序中,编程错误(bug)是不可避免的。程序可能会返回错误的结果,因无穷循环而死机,甚至因内存操作不当导致系统崩溃。找出这些错误并消灭它们,就需要“调试”程序。
仅仅研究源代码很难找出太多bug的。程序的测试版本如果能够提供额外的信息输出,会有很大帮助的,也可以增加状态输出语句,以显示运行时变量内容和其他信息。也可以使用调试器排除运行时错误。
调试器是一个程序,可以在一个严密的环境中执行另一个程序。本文介绍一个威力强大和应用广泛的错误排除程序:GNU调试器,也称为GDB。
本文将从三个部分来阐述GDB的使用:
- 启用GDB:介绍在启动GDB调试时如何设置gdb调试参数和模式,及给被调程序传递参数;
- 使用GDB命令:介绍在GDB调试程序中如何查看状态信息、断点使用、分析堆栈、显示数据、观察点;
- 在GDB内分析内核文件:core dump文件的生成和使用GDB如果分析core dump文件;
符号信息
GDB是一个符号式命令行调试器。所谓“符号式”,意思是在执行程序的时候可以使用变量名和函数名,就好像它们是源代码中名称一样。为了显示和翻译这些名称,调试器需要变量类型和函数类型相关的信息,以及哪条机器指令对应到哪行源代码的信息。这类信息会以符号表的形式出现,当使用-g选项进行编译和连接时就会产生符号表,符号表被包含在可执行文件中:
$ gcc -g gdb_test.c
在包含多个源代码文件的大型程序中,必须在编译每个模块的时候都使用-g选项。
启动GDB
在shell命令行中输入gdb命令就可以启动GDB。GDB支持许多命令行选项和自变量:
gdb [选项] [执行文件 [核心文件 | 进程ID]]
比方说,下面的命令直接启动调试器,不显示启动信息:
在此例中,上述命令行没有指定要被调试的可执行文件的名称。你可以事后使用调试器的file命令来指定可执行文件,具体可以参考本文“使用GDB命令”一节。
命令行自变量
一般来说, 我们在GDB命令行中指定被调试程序。在下面的范例中,此GDB命令会加载a.out可执行文件,以进行调试:
可以在程序名称后面指定要测试的进程ID,或者指定核心文件的名称。在下面的范例中程序名称的数字是进程ID(也就是PID) :
此命令会使得GDB连接到系统中正在执行的一个进程,此进程的名称为a.out,并且进程的ID是1001。如果GDB找到这样的进程,可以按下Ctrl+C开始调试过程。但是如果此调试器在目前的工作目录找到一个文件名为1001,那么就会将此自变量翻译成核心文件的名称,而不是进程ID。关于核心文件的调试,可以参考本文的“在GDB分析核心文件” 一节。
命令行选项
GDB的大多数命令行选项都有短和长两种格式。下面列表和后续各节会列表常用的选项。也可以把选项的长格式截断,只要能够区分即可。有些选项需要自变量,例如 -tty device,此选项和自变量可以使用空格或等号来分割,例如-tty=/dev/tty6。选项前置可以是一个或两个连字号,例如-quiet和--quiet是一样的。
- --version、-v
GDB输出版本和版权信息,然后马上结束、不会再继续进行调试。
- --quiet、--silent
GDB开始一个交互式的调试过程,不显示版本和版权信息。
- --help、-h
GDB显示命令行的语法,以及选项的简洁描述,然后就直接结束,不会进行调试。
传递自变量被调试程序
GDB有一个特别的命令行选项,用来区分“调试器”和“被调试程序”的命令行:
- --args
使用--args选项把命令行自变量传入“被调试程序”。
--args选项后面必须立即接着“被调试程序”。此命令可以包含程序名称,然后接着“被调试程序”的自变量,自变量的次序和不调试时执行此程序的次序一样。如果想同时为GDB指定选项,必须把这些选项放在--args之前,换句说,--args必须是GDB最后出现的选项。
也可以在启动GDB之后再指定被调试程序,应用使用交互式的命令run或set args。
选择文件
下面的命令行选项告诉GDB要使用哪个输入文件:
- -symbols filename、-s filename
如果调试符号表没有被包含在可执行文件中,可以使用-symbols选项加载一个独立的符号表文件。GDB会从这个文件读取符号表信息。
- -exec filename、-e filename
选项指定要被调试的可执行文件。
- -se filename
所指定的文件是希望使用GDB测试的文件,并且有符号表。这个选项通常是没有必要的;如果GDB命令行包含一个文件名称,并且此文件名称并非任何选项的自变量,那么GDB会将这样的文件视为-se选项的自变量。
- -core filename、-c filename、-c number、-pid number、-p number
-core选项和-pid选项实际上是同义的。如果这两个选项中任何一个自变量时十进制数,GDB就是连接到拥有此进程ID的正在执行的进程(如果有的话)。如果没有任何正在执行的进程具有此进程ID,GDB会试着打开一个以数字为名称的核心文件。如果希望强制GDB打开一个核心文件,其名称为一个十进制数,可以在文件名称前面加上路径。比方说:gdb -p ./32436 要求GDB打开目前目录下名称为32436的核心文件,不管是否有正在执行的进程具备此PID。
如-se选项一样,-core选项常常也是没有必要的。如果GDB命令行包含第二个文件名称(不是任何选项的自变量),GDB会把该文件当作-core选项的自变量。
选择用户界面
在GDB的命令行模式中,“被调试程序”的控制可能会输出调试器自己的命令和调试信息。如果此行为给你带来麻烦,可以指定别的设备作为“被调试程序”输入输出之用。
- -tty device,-t device
调试器使用device作为“被调试程序”的标准输入和输出。在下面的范例中,myprog标准的I/O流会输出到终端/dev/tty5。
- -windows、-w
GDB也可能内置集成的GUI,分别提供源代码、汇编码,CPU寄存器等显示窗口。-w选项命令GDB在执行时使用GUI(如果有的话)。如果没有GUI,这个选项就没有任何作用。
- -nowindow、-nw
-nw选项会命令GDB在控制台模式下执行,即使GUI可用的。如果本来就不支持GUI,那么这个选项就完成没影响。
- -tui
借助gdb -h所显示的说明,已经--tui选项,GDB会提供一个文本时用户界面(TUI),以管理控制台窗口。此TUI是一个程序模块,使用了curses连接库。
-tui 选项会让GDB一开始时就具备文本式全屏幕用户界面(TUI)。在默认情况下包含两个窗口:顶部窗口显示C源代码文件,左边空白处显示当前代码和断点。下部是一个命令窗口,显示(gdb)命令提示符和诊断输出。也可以打开第三个窗口,已显示汇编语言的程序代码,或者CPU寄存器的内容。
执行命令脚本
- -command command_file、-x command_file
-command或-x选项指示GDB一开始时就执行“指定文件内”的命令。“命令文件”是一个文本文件,文件内的每一个都是GDB命令。空白行和以#开头的命令都会被直接忽略。如果希望调试器执行一个或多个命令文件,执行后自动结束,那么就使用-batch选项,搭配使用-x或-command选项。
- -batch
-batch选项命令GDB在执行完所有命令文件(以-x指定)后结束。如果没有发生错误,GDB的结束状态是0,状态不为0表示有错误。
初始化文件
一开始的时候,GDB一般会处理一个名为.gdbinit的初始化文件(如果有此文件的话)。某些系统有特别的调试器配置,这个初始化文件可以有不同的名称。初始话文件内部语法和命令文件一样,包含GDB命令、注释和空白行。GDB会处理在目前目录和home目录下找到的初始化文件,按照如下顺序处理初始化文件、命令行和命令文件:
- 一开始,GDB先读取home目录的初始化文件(如果有的话),然后执行文件内容的指令。在windows系统上,GDB根据环境变量的HOME决定home目录是什么。这个初始化文件通常包含“设定调试器配置”的命令,例如set listsize 5,限制list指令的默认输出只能有五行。
- 接下来,GDB处理命令行选项。不过在-x或-command选项中指定的任何命令文件都不会再这个时间点被执行。
- 如果目前的目录不是你的home目录,并且如果目前的目录包含初始化文件,则GDB会执行改文件内命令。通常目前的目录包含“一个开发中程序”的相关文件,并且区域的初始化文件.gdbinit包含可以设置调试器配置的命令,以满足程序的特殊需求。
- 最后,在-x或-command选项中被指定的命令文件会被GDB执行。可以利用下面的选项,让GDB忽略任何.gdbinit文件:
-nx、-n
命令GDB忽略所有的初始化文件。此选项不需要参数。
使用GDB命令
在开始时, 调试器会提示输入命令,比方说,设定断点,以及执行调试程序。对GDB所有的每个命令都是“以一个命令关键字开始的”。除了关键字,命令剩下的部分就是此命令的自变量。可以截断任何关键字,只要能保证识别此关键字,不会和其他关键字混淆即可。比方说,q(或qu 或 qui)来表示quit命令。
如果输入一个空命令(也就是说,如果在GDB命令提示符的后面直接按下回车键),那么GDB会自动重复执行上一个命令(只要有价值的话)。比方说,GDB会自动地重复执行step和next命令,但是不会自动重复执行run名。
常用命令列表
命令名称 | 说明 |
help | 帮助函数,简写h |
info | 查看被调程序的状态信息 |
show | 查看调试器的状态信息 |
file | 指定调试程序 |
set args | 指定调试程序的自变量 |
run | 开始运行调试程序,后面可以加参数 |
kill | 终止被调试程序 |
quit | 退出调试器程序 |
list | 查看源代码,简写l |
break | 设立断点,简写b |
next | 单步调试(逐过程,函数直接执行),简写n |
step | 单步调试(逐语句,如果有函数符号表,就会跳进函数内),简写s |
显示某个表达式的值,简写p | |
continue | 继续执行,知道遇到下一个断点或程序尾端,简写c |
quit | 停止执行调试器,简写q |
finish | 结束当前函数调用 |
ptype | 查看变量类型 |
backtrace | 查看函数的调用的栈帧和层级关系,简写bt |
frame | 切换函数栈帧 |
display | 设置跟踪变量 |
undisplay | 取消设置跟踪变量,使用变量的编号 |
调试范例
在使用gdb调试时,使用该范例来展示gdb调试的基本功能。程序gdb_test.c包含一个逻辑错误,我们通过这个程序来逐步演示gdb如何跟踪此类错误。
#include <stdio.h>
void swap(int *p1,int *p2);
int main()
{
int a = 10,b = 20 ;
printf("The old values: a = %d; b = %d.\n",a,b);
swap(&a,&b);
printf("The new values: a = %d; b = %d.\n",a,b)
}
void swap(int *p1,int *p2)
{
int *p = p1;
p1 = p2;
p2 = p1;
}
命令补充
GDB调试器可以帮助你节省很多输入工作,可以补充命令名称、变量名称、文件名称,或函数名称。只要输入前几个字符,然后按下Tab键即可。
显示命令的帮助说明
GDB提供帮助函数(help),它把命令分成许多类,以方便查找需要的命令。当输入help命令(或h),并且没有自变量时,GDB会显示命令类别列表:
如想查看特定分类的命令,输入help,后面跟着分类名称。如想了解某个命令如何运行,则输入help,后面跟着命令名称。
状态信息
如想展示“调试器状态信息”和“被调试程序状态信息”,GDB提供info和show命令。也可以使用help命令查看状态命令说明:
调试器状态信息
show命令显示关于调试器自身的多种信息。我们使用help看看show的帮助信息:
下面的命令显示GDB日志(logging)的有关信息:
show命令所显示的大多数设置,都可以使用set命令来修改。比方说,下面的命令会启用logging功能:
被调试程序状态信息
如果没有自变量列表,info命令会列出关于被调试程序的所有可以查询信息:
当给info指定一个自变量,GDB会展示对应的信息。类似与命令行,这些自变量可以是缩写,如下面命令所示:
info all-reg
此命令显示“所有处理器寄存器”的内容,包括浮点和向量寄存器。 info register命令显示CPU寄存器的内容,不包含浮点或向量寄存器。
下面的命令显示“关于目前源代码文件”的信息。也就是,包含“正执行函数”的源代码文件:
在调试器中执行一个程序
可以使用下面的GDB命令在调试器中控制程序执行:
- file filename
如果没有在GDB命令行中指定被调试程序,可以在GDB提示符中指定,这个时候回用到file命令。GDB从执行文件中读取符号表,在输入run命令后开始执行程序。如果文件名称不在调试器的工作目录之下,调试器会在环境变量PATH所指定的路径下搜索。
- set args [arguments]
如果没有在GDB命令行中指定“被调试程序”的命令行自变量,可以在GDB提示符下指定,这个时候要用到set args命令。比方说下面的命令指定两个自变量:
如果想清除自变量列表,但不设置新的自变量,可以输入set args命令,后面不带自变量。
- show args
上述命令显示目前为程序设置的命令行自变量。
- run [arguments]
当使用run命令(r)开始执行一个程序,也可以为它指定命令行自变量。在类Unix-like的系统上,自变量可以包含shell的通配符,比如*和$。也可以使用<、>和>>来重定向程序的输入和输出。调试器使用shell来翻译此程序的命令行,所以任何通配符都和在正常的shell环境中执行时具有相同的效果。
如果run命令没有自变量,则被启动的程序会把目前设定的自变量当作其自变量。如果尚未使用任何命令(包含run命令,GDB命令行,或者set args命令等)为此程序设定任何自变量,那么run命令启动此程序也就没有自变量。
一旦利用run命令启动一个程序,它就会持续执行,直到结束,或者遇到调试器设定的断点。程序也可能会被信号中断。
- kill
如果找到一个活多个需要更正的错误,如想在调试器之外编辑和重新编译此程序,那么应该先终止调试器。可以使用kill命令终止调试器。
只要没有关闭调试器程序,设置(包含程序的命令行自变量)都会被保留。下次使用run命令执行程序时,GDB会发现可执行文件已经被修改过,于是重新加载。
显示源代码
可以在调试器中显示一个程序的源代码,相应的命令是list(简写为l)。在默认情况下,GDB一次展示十行代码,前面五行是“接下来要执行语句”之前的五行代码。list命令可以有自变量,让你指定希望显示的程序部分:
- list filename: line_number
显示源代码,以此行代码为中心。
- list line_umber
如果没指定源代码文件,那么显示的这几行代码是目前正在执行的源代码。
- list from, [to]
如想显示特定范围的源代码,可以使用from和to自变量,两者都可以是行号或者函数名称。如果没有指定to自变量,list会从from开始显示默认行数的代码。比方说,下面的命令会显示main函数的前10行:
list main
- list function_name
把“指定函数”的第一行代码当作中间行来显示源代码。
- list、l
如果没有自变量,list命令会显示更多行的源代码,而起始点就是上次list命令停止的地方。如果另一个命令是从上一次list命令开始执行,那么新的list命令也会显示改命令为中心的源代码。比方说,如果调试器刚刚因为遇到断点而中断执行,下一个list命令就会显示以此断点为中央行的源代码。
如果尚未开始执行程序,第一个没有自变量的list命令会显示已main()为中心行的源代码。如下所示:
- set listsize number
设置list命令默认的输出行数。
- show listsize
显示list命令默认的输出行数。
断点使用
当到达断点时,调试器会中断程序的执行,并显示设置断点的是哪一行代码。精确地说,所显示的这一行代码就是尚未执行的,但接下来要执行的那一行代码。一旦程序流被一个断点所中断,你可以使用GDB命令逐行执行程序,并显示变量和寄存器的内容。
也可以为断点设置条件,这样的话,当到达断点时,调试器只会在符号条件的情况下才会真正地中断程序执行。
设置断点
可以使用break命令(b)来设置断点。存在多种设置断点位置的方法。下面是最常见的做法:
- break [filename:] line_number
在目前源代码文件(或者filename文件)的某一行代码设定断点。
- break function
在某函数的第一行代码处设定点断。
- break
在下一条要执行的语句处设置断点。换句话说,程序流下次到达现在这个位置时,将会自动中断(更精确地说,不带自变量的break命令,会在目前堆栈帧的下一条语句处设置断点)。
设置断点范例
在某一个设置断点,如我们在第7行设置断点,命令如下:
在某函数设置断点,如我们在swap函数设置点断,命令如下:
在执行的情况下,直接设置断点,可见断点被设置在下一条要执行的语句处,如下:
设置临时断点范例
临时断点设置方法跟设置断点方法一致,只是命令使用tbreak,如下面范例所示:
查看断点
可以使用info命令来显示目前有哪些已经被定义的断点,如下所示:
可见已列出刚刚设置的四个断点。左边显示的中断数字可以被用在其他命令中(列如删除或禁用一个断点),以帮助标识断点。在上表中,Enb(Enabled的缩写)字段中的字母y表示"yes",表示断点被启用的,第三个字段名称Disp,是"disposition"的意思,表示下次程序流执行到此断点时,次断点会被保留或删除。如果某个断点时临时的,GDB会在到达这个语句之后尽快自动删除它,如表中最后一个断点即是临时断点。
删除、禁用和忽略断点
下面的命令需要一个断点或一个断点范围作为自变量。断点范围可以表示为使用连字好(减号)连接起来的两个断点编号,例如1-3:
- delete [bp_number | range]、d [bp_number | range]
可以删除某个(或某范围的)断点。不带自变量的delete命令会删除所有的断点。
- disable [bp_number | range]
禁用某个(或范围的)断点。如果没有指定任何自变量,此命令会影响所有的断点。禁用断点往往比删除断点更实用。
- enable [bp_number | range]
启用某个(或范围的)断点,如果没有指定任何自变量,此命令会影响所有的断点。
- ignore bp_number iterations
要求GDB共计iterations次忽略编号为bp_number的断点,而不停止执行。这个忽略命令需要两个自变量:断点的编号和忽略的次数
禁用断点范例
如下展示了通过断点编号和编号范围禁用断点, 当禁用后,表中的Enb字段变为“n”:
启用断点范例
如下展示了通过断点编号和编号范围启用断点,当启用后,表中的Enb字段变为“y”:
忽略断点范例
如下展示了忽略“5”次断点编号“3”:
删除断点范例
如下展示了通过断点编号和编号范围删除断点,及操作的断点数的变化:
条件式中断
通常,调试启会在遇到断点时尽快中断程序。如果断点有附加条件,那么GDB只有在满足条件的情况下才会真正中断程序执行。当设置断点时,你可以指定一个中断条件,做法是在中断指令后面附加一个if关键字:
break [position] if expression
在此语法中,position可以是一个函数名称或者一个行号,有没有文件名称皆可,做法类似于没有中断条件的断点。 条件可以是C语言的任何表达式,但表达式本身必须是数量类型的,并且可以包含函数调用。如下所示,在下一条要执行的语句处设置条件断点:
此命令会使得调试器在晕倒程序第15行代码时检测 *p1、*p2的值是否相等,如果条件成立的话,中断程序执行。
修改条件式断点
如果已经在某个位置设定了断点,可以使用条件式命令在相同的位置增加一个断点,或改变其为中断条件:
condition bp_number [experssion]
experssion自变量变成bp_number断点的新条件。如下修改上面断点编号为6的条件:
如果想删除中断条件,使用不附加任何条件自变量的condition命令即可:
在中断后继续执行
当中断和分析完程序后,可以恢复执行程序,做法有多种:可以逐行执行,或者执行到下一个断点,或者执行到特定的位置。在中断之后,可以使用下面所列的命令继续执行:
- continue [passes]、c [passes]
允许程序执行,直到下一个断点时停止,或者如果没有断点就让程序执行到最后自然停止。passes自变量是一个数字,可以指示“目前的断点被忽略多少次再次被GDB中断”。如果程序目前的断点在循环内,这特别有用。
- step [lines]、s [lines]
执行程序目前的这一行代码,在下一行之前就停下来。step命令可以有自变量,此自变量是正的数字,代表“要执行多少行之后才停”。然而,如果在执行完所有指定代码行之前先遇到断点,还是会被中断的。如果使用这种方式进行调试过程中遇到函数调用,并且GDB具有此函数符号表和行号信息,则跟踪进入函数内容,并在函数内第一行代码处停下来。许多开发工具把这样的做法称为单步跳入。
- next [lines]、n [lines]
和step方法一样,但是执行到函数调佣时,不会跟踪进入函数内,而是一口气把整个函数执行完,把函数调用当作一个简单的语句。许多开发工具把这样的做法称为单步跳过。
- finish
如果想继续执行到目前函数结束,则应该使用finish命令。此命令允许程序继续执行到目前的函数结束,然后停止在程序流将控制权交回“此函数的调用者”的地方。在者一点,GDB会显示此函数的返回值,以及下一行语句。许多开发工具将这样的做法称为单步跳出。
分析堆栈
“调用堆栈”常常被简称为堆栈,这是一种内存的组织方式,以后进先出(LIFO)的原则处理数据。每次程序流进入一个函数调用时,它会在堆栈上面建立一个名为“堆栈帧”的数据结构。堆栈帧不知包含调用者的地址,还包含寄存器的值,使得程序执行完此函数后,可以把执行权交回先前的函数。堆栈帧内还记录函数自变量和局部变量。当函数返回时,堆栈帧所拥有的内存也会被释放。
显示调用轨迹
当调试器中端程序执行时,如果能得知此时函数调用的次序,往往会很有帮助。GDB提供可以显示此信息,这就是调用轨迹,显示目前每个正在进行的函数调用,以及他们的自变量。如想显示调用轨迹,可以使用backtrace命令(bt)。此命令有两个更常见的同义字where和info stack(info s).
当gdb_test.c程序在swap()函数内被中断,下面的范例显示当时的调用轨迹:
上述输出结果显示,在main()函数的第9行调用了swap()函数。调试器记录堆栈帧的编号,从最后一个到最前面一个,以便于目前函数的堆栈帧始终是第0号。编号最大的堆栈帧一定是main()函数。
在调用轨迹输出中,堆栈帧后面跟着一个地址,这就是返回地址;也就是在返回后要执行的一个指令。然而,如果程序停止的地方正是此地址,则此地址会被忽略不显示。
我们可以使用添加一个factoria()递归函数,来演示调用轨迹的回溯:
#include <stdio.h>
void swap(int *p1,int *p2);
int factorial(int n);
int main()
{
int a = 10,b = 20 ;
printf("The old values: a = %d; b = %d.\n",a,b);
swap(&a,&b);
printf("The new values: a = %d; b = %d.\n",a,b);
return 0;
}
void swap(int *p1,int *p2)
{
int *p = p1;
p1 = p2;
p2 = p1;
factorial(5);
}
int factorial(int n)
{
if( n <= 1)
return 1;
return n * factorial(n -1);
}
下面的GDB输出显示递归调用 factorial()后调用堆栈的最终情况。最后一个函数调用发生在n等于1的时候。为了在factorial()函数进行最后一次递归调用时中断程序,我们可以设置一个断点,中断条件为 n == 1:
此范例的轨迹回溯显示:在main()函数开始时,要求的值是5!(5的阶乘)。 factorial()函数随机递归调用自己以计算4!、3!、2!、最后1!。
显示和改变目前的堆栈帧
大多数堆栈命令都使用来处理目前的堆栈帧。比方说,可以在目前的堆栈帧中,通过使用局部变量名称来获取其地址。当有多个堆栈帧可以使用时,GDB会让你从中选择。
当调试器在断点处停止程序执行,目前的堆栈帧就是目前函数所对应的堆栈帧,也就是调用轨迹中编号为0的堆栈帧。使用frame命令可以显示目前的堆栈帧,或者选择不同的堆栈帧:
frame [编号]
可以使用frame命令(f)选择和显示某个堆栈帧。没有自变量的frame命令(未指定堆栈帧)直接显示“目前堆栈帧”的信息。
frame命令的输出总包含两行文字:第一行包含函数名称和其自变量,第二行指明目前的源代码是第几行代码(一定在对应的函数内)。
在下一个范例中,gdb_test程序被中断在swap()函数:
f 1 命令会选择“调用目前函数”的函数所对应的堆栈帧。 在上述范例中,此堆栈帧对应到main函数。一旦此堆栈帧被选择,main()函数的局部变量就可以通过名字来存取,如上面的p a命令所示。
显示自变量和局部变量
info命令有三个子命令,可以帮助先死目前堆栈帧的内容:
- info frame
显示目前堆栈帧的相关信息,包含其返回值,和被存储的寄存器值。
- info locals
列出函数的局部变量,以及这些变量目前的值。
- info args
列出对应函数调用的自变量值。
下面的列子中,程序被中断在swap()函数开头的地方。swap()函数是更新后的版本:
void swap(int *p1,int *p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
factorial(5);
}
在GDB调试窗口运行对应的命令会显示下面信息:
第三行rip 寄存器包含要被执行的下一个机器指令的地址,对应到16行。第10行rip 寄存器指到目前的堆栈帧。下面还分别展示了info args和info locals命令显示结果。
显示数据
通常,你可以使用print命令显示变量和表达是的值,除此之外,还可以使用x命令来检查无名的内存区域。
显示表达式的值
可以把C表达式当作print或p命令的自变量:
p[/format] [expression]
此命令会计算表达式和显示结果值。如果print命令没有表达式自变量,则会再次显示 之前的值,但不会重新计算这个值。也可以指定不同的输出格式,如果希望这么做的话。
使用/format选项可以为此表达式指定适当的输出格式。如果没有指定输出格式,print会自行选用适当的格式。
print命令中的表达式也可以具有赋值作用。如下面的范例所示,目前的堆栈帧对应到gdb_test程序的main()函数:
在此范例中,第二个print命令中 a=40表达式,把40赋值给变量a。也可以使用set命令来改变这个值:
此print命令把一个表达式的值显示成变量$i的值,其中i是一个正的值。可以在后续的命令中使用这些变量,如下面的范例所示:
在此命令调用factorial()函数时,使用的自变量是$9变量的值,然后函数的返回结果再乘以2,并将结果保存到一个新的变量($10)内。
也可以使用p和set命令,在GDB中定义新的变量,变量名必须以美元符开始。比方说 set $var = *ptr命令建立一个姓的变量,名称为$var,并且赋予它的值是“目前ptr指针所指到的值”。“调试器的变量”和“被调试程序的变量”之间是有区分的。GDB也将CPU寄存器的值存储在变量中,把标准的寄存器名称当作变量的名称,但前面多一个美元符。
如果想存取其他堆栈帧的变量,并且不想改变目前的堆栈帧,应该把函数名称和两个冒号放在变量名之前,如下面的范例所示:
输出格式
在print命令中,可选的/format自变量的写法是“一个斜线”后面接着“一个字母”,使用这种方法来指定输出格式。可用的字母类似于printf()函数格式化字符串自变量的转化修饰符。比方说,print /x命令会以十六进制整数的方式显示目前的值。
如果有必要的话,print命令会将值转化成适当的类型。下面的列表描述整数值的所有的格式选项:
d | 十进制计数法,这是整数表达式的默认格式 |
u | 十进制计数法,无符号整数类型 |
x | 十六进制计数法 |
o | 八进制计数法 |
t | 二进制计数法。不要把它和x命令的选项b相混淆 |
c | 字符,和十进制计数法的字符码一起显示 |
如下面的范例所示,格式选项紧紧跟在p命令之后,两者中间没有空格:
在本范例中,只要print命令没有用做自变量的表达式,都会显示相同的值,但是格式则因为是否使用/format自变量而有所变化。
对于非整数表达式,print命令还支持另外两个选项:
a | 以十六机制的格式显示地址,同时显示“此地址下最接近的有名称内存”的位差(如果有的话) |
f | 把表达式的bit模式翻译成浮点数,并显示。 |
下面是使用这些格式选项的范例,此时目前的堆栈帧为swap函数:
显示内存区域
使用x命令可以检查无名的内存区域。此命令自变量包含此内存区域的开始地址和空间大小,以及一个可选的输出格式:
x [/nfu] [address]
此命令显示从指定的地址开始的内存区域的内容,还可以使用/nfu选项来返回指定的内容区域的空间大小和输出格式。address自变量可以是任何表达式,只要结果是有效的地址即可。如果省略address自变量,x会显示“上一次x或者print命令所显示的内存”的下一个内存区域。
/nfu自变量最多可以包含三个部分,全都是可选的:
- n 十进制数,指定要显示多少个内存单元。每个内存单元的空间大小都是由/nfu选项的第三个部分(u)所指定的,n默认值是1.
- f 格式修饰符,可能是print命令所支持的格式之一,后者是下面两个格式修饰符之一:
s 将指定地址的数据当成“空字符结尾的字符串”显示出来。
i 以汇编语言的形式显示机器指令。
- u /nfu自变量的第三个部分,也就是u选项,定义所显示的每个内存单位的空间大小,并且可以使用下面的值:
b 一个字节
h 两个字节(半个字)
w 四个字节(一个字)
g 八个字节(一个巨大字)
u的默认值是最初的w,稍后可以使用x命令设定新值。使用格式化选项s或i来设置内存单元的大小是没有意义的。如果这样的话,GDB会直接忽略此选项。
下面的范例展示x命令的用法。在这些范例中,假设如下变量被定义在目前的作用域内:
观察点
在GDB中,可以通过设定观察点来监视针对变量的读写操作。观察点类似于断点,但是并没有被绑定到特定某行代码。如果设置某个变量的观察点,那么GDB就会在其值改变时停止程序。事实上,使用GDB不仅可以观察个别变量的值,也可以观察表达式的值。可以使用watch、rwatch、awatch命令来设定不同种类的观测点,它们的语法都一样:
- watch expression
当表达式的值改变时,调试器停止程序执行。
- rwatch expression
当程序读取“和此表达式相关”的任何对象时,调试器停止程序执行。
- awatch expression
当程序读取或修改“和此表达式相关”的任何对象时,调试器停止执行。
观察点最常用的用途是观察程序“何时”会修改某个变量。当一个被观察的变量的值改变时,GDB会显示变量的旧值与新值,以及下一行要执行的程序。我们利用范例程序来演示观察点的用法。
在设置某局部变量的观测点之前,必须先执行程序,知道程序流进入该变量作用域内才行。因此,我们开始执行此程序,一直到一个断点(第9行),之后为变量a设定一个观察点,并且继续执行程序,具体操作如下:
因为在使用swap()函数交互值时, p1指向变量a,当执行*p1 = *p2;语句时,调试器会停止程序,并显示下一个要执行的语句。
继续执行这个范例,我们为变量tmp设置一个“读取观察点”,该观察点被包含进断点列表中,使用info breakpoints(或info b)就可以检查此列表:
当程序离开一个语句块,也就是说,当程序流执行时越过一个大括号},调试器就会自动删除涉及到“不在作用域内布局变量”的所有表达式观察点。
当为一个具有多个变量的表达式设定观测点时,调试器的每次存取这些变量时,GDB会停止程序。下面我们为表达式a+b设定一个读取观察点:
如果现在让此程序继续,调试器会在下一个“读取a或b”的语句处停止。这条语句时第9行的printf。因为此语句同时读取a和b,调试器两次停止,每次都显示a+b表达式的值:
在GDB内分析核心文件
核心文件(core file)也被称为core dump,是一个包含“进程所使用内存映射”的文件。当一个程序不正常地终止时,Unix系统通常会将核心文件写到工作目录下(在Unix系统下,可以使用man signal命令了解信号触发的核心文件)。通过命令行将核心文件的名称传递给GDB,就可以研究进程结束时的状态。
“分析核心文件”的做法和一般调试的做法类似,不同的地方在于:程序已经停止在某个点,不能使用run、step、next、continue等命令让它继续执行。我们使用一个“死后验尸”的范例来展示如何使用其他GDB命令进行程序调试。
生成core方法
产生core dump的条件,首先需要确定当前会话的ulimit -c,若为0,则不会产生对应的core dump文件,需要进行修改和设置,我们需要让core文件能够产出,设置core大小为无限:
更改core dump生成路径
我们可以调用如下命令设置生成路径:
echo ./core.%e.%p> /proc/sys/kernel/core_pattern
将更改core文件生成路径,自动但在当前工作目录下, %e表示程序名, %p表示进程id。具体操作如下(注意需要使用root权限进行设置):
测试 core
首先我们新建一个文件myprog.c,并编写如下代码:
#include <stdio.h>
int main()
{
int i = 0;
scanf("%d",i);
printf("hello,world %d\n",i );
return 0;
}
使用gcc -g myprog.c -o myprog命令进行编译,并调用执行,具体操作如下:
可见,在当前工作目录下,生成了core.myprog.8221文件,然后使用gdb调试该core文件,具体操作如下:
Program terminated with signal SIGSEGV告诉我们信号中断了我们的程序,发生了端错误。这个时候可以敲命令 backtrace(bt) 查看函数的调用的栈帧和层级关系。
通过查看堆栈信息可知,是在main()函数的第6行发生错误, 如果scanf函数没有问题,即我们调用时参数问题导致。