gdb介绍


GDB 的全称是GNU Debuger,是linux 底下的一种免費的debug程序,没有界面,当然Linux也有带界面的比如cgdb、kdbg、ddd和insight debugger,在使用gdb调试的时候我们需要在编译程序的时候生成调试信息,比如:

gcc -Wall -g3 -o test  test.c

gcc生成调试信息的一些选项:

  • -g:该选项可以利用操作系统的“原生格式(native format)”生成调试信息。GDB 可以直接利用这个信息,其它调试器也可以使用这个调试信息

  • -ggdb:使 GCC 为 GDB 生成专用的更为丰富的调试信息,但是,此时就不能用其他的调试器来进行调试了 (如 ddx)

-g 和 -ggdb 也是分级别的

  • -g2:这是默认的级别,此时产生的调试信息包括扩展的符号表、行号、局部或外部变量信息。

  • -g3:包含级别2中的所有调试信息,以及源代码中定义的宏,以及C++中的内联函数(inline)。

  • -g1:级别1(-g1)不包含局部变量和与行号有关的调试信息,因此只能够用于回溯跟踪和堆栈转储之用。回溯跟踪指的是监视程序在运行过程中的函数调用历史,堆栈转储则是一种以原始的十六进制格式保存程序执行环境的方法,两者都是经常用到的调试手段。

一般我比较习惯使用-g3。

启动gdb


调试可执行文件:

$gdb <program>

program也就是你的执行文件,一般在当前目录下。

调试core文件(core是程序非法执行后core dump后产生的文件):

$gdb <program> <core dump file>
$gdb program core.11127

调试服务程序:

$gdb <program> <PID>
$gdb hello 11127

如果你的程序是一个服务程序,那么你可以指定这个服务程序运行时的进程ID。gdb会自动attach上去,并调试他。program应该在PATH环境变量中搜索得到。

gdb命令


启动gdb后,进入到交互模式,通过以下命令完成对程序的调试;注意高频使用的命令一般都会有缩写,熟练使用这些缩写命令能提高调试的效率;

运行相关命令

  • run:简记为 r ,其作用是运行程序,当遇到断点后,程序会在断点处停止运行,等待用户输入下一步的命令,run后可以带一些命令,比如我们要调试brks程序,我们第一步是gdb brks,进入gdb后,我们输入run log.conf去运行程序。

  • continue (简写c ):继续执行,到下一个断点处(或运行结束)

  • next:(简写 n),单步跟踪程序,当遇到函数调用时,也不进入此函数体;此命令同 step 的主要区别是,step 遇到用户自定义的函数,将步进到函数中去运行,而 next 则直接调用函数,不会进入到函数体内。

  • step (简写s):单步调试如果有函数调用,则进入函数;与命令n不同,n是不进入调用的函数的。

  • until:当你厌倦了在一个循环体内单步跟踪时,这个命令可以运行程序直到退出循环体。

  • until+行号: 运行至某行,不仅仅用来跳出循环。

  • finish: 运行程序,直到当前函数完成返回,并打印函数返回时的堆栈地址和返回值及参数值等信息。

  • call 函数(参数):调用程序中可见的函数,并传递“参数”,如:call gdb_test(55)

  • quit:简记为 q ,退出gdb

设置断点

  • break n (简写b n):在第n行处设置断点 (可以带上代码路径和行号: b OAGUPDATE.cpp:578)

  • b fn1 if a>b:条件断点设置

  • break func(break缩写为b):在函数func()的入口处设置断点,如:break cb_button

  • delete 断点号n:删除第n个断点

  • disable 断点号n:暂停第n个断点

  • enable 断点号n:开启第n个断点

  • clear 行号n:清除第n行的断点

  • info b (info breakpoints) :显示当前程序的断点设置情况

  • delete breakpoints:清除所有断点:

  • thread [id]:切换到具体的线程id,一般切换到具体的线程后再执行bt等操作。

查看源代码

  • list :简记为 l ,其作用就是列出程序的源代码,默认每次显示10行。

  • list 行号:将显示当前文件以“行号”为中心的前后10行代码,如:list 12

  • list 函数名:将显示“函数名”所在函数的源代码,如:list main

  • list :不带参数,将接着上一次 list 命令的,输出下边的内容。

打印表达式

  • print 表达式:简记为 p ,其中“表达式”可以是任何当前正在被测试程序的有效表达式,比如当前正在调试C语言的程序,那么“表达式”可以是任何C语言的有效表达式,包括数字,变量甚至是函数调用。

  • print a:将显示整数 a 的值

  • print ++a:将把 a 中的值加1,并显示出来

  • print name:将显示字符串 name 的值

  • print gdb_test(22):将以整数22作为参数调用 gdb_test() 函数

  • print gdb_test(a):将以变量 a 作为参数调用 gdb_test() 函数

  • display 表达式:在单步运行时将非常有用,使用display命令设置一个表达式后,它将在每次单步进行指令后,紧接着输出被设置的表达式及值。如: display a

  • watch 表达式:设置一个监视点,一旦被监视的“表达式”的值改变,gdb将强行终止正在被调试的程序。如: watch a

  • whatis :查询变量或函数

  • info function: 查询函数

  • 扩展info locals: 显示当前堆栈页的所有变量

表达式格式化输出

  • set print address [on/off]:打开地址输出,当程序显示函数信息时,GDB会显出函数的参数地址。系统默认为打开的
  • set print array [on/off]:打开数组显示,打开后当数组显示时,每个元素占一行,如果不打开的话,每个元素则以逗号分隔。这个选项默认是关闭的
  • set print elements:这个选项主要是设置数组的,如果你的数组太大了,那么就可以指定一个来指定数据显示的最大长度,当到达这个长度时,GDB就不再往下显示了。如果设置为0,则表示不限制。
  • set print null-stop:当显示字符串时,遇到结束符则停止显示。这个选项默认为off
  • set print pretty [on/off]:当GDB格式化输出结构体

查询运行信息

  • where/bt :当前运行的堆栈列表;

  • bt backtrace 显示当前调用堆栈

  • up/down 改变堆栈显示的深度

  • set args 参数:指定运行时的参数

  • show args:查看设置好的参数

  • info program: 来查看程序的是否在运行,进程号,被暂停的原因。

  • info threads: 查看所有线程

分割窗口

  • layout:用于分割窗口,可以一边查看代码,一边测试:

  • layout src:显示源代码窗口

  • layout asm:显示反汇编窗口

  • layout regs:显示源代码/反汇编和CPU寄存器窗口

  • layout split:显示源代码和反汇编窗口

  • Ctrl + L:刷新窗口

GDB TUI—在 GDB 中显示程序源码

官方参考文档: http://sourceware.org/gdb/onlinedocs/gdb/TUI.html
 

开启 GDB TUI 模式

方法一: 使用 gdbtui 命令或者 gdb-tui 命令开启一个调试。

gdbtui -q 需要调试的程序名

方法二: 直接使用 GDB 调试代码,在需要的时候使用切换键 Ctrl + x,然后按 a ( 两个步骤是连在一起执行),进入常规 GDB 和 GDB TUI 的来回切换。
 

GDB TUI 模式常用窗口

默认情况下, GDB TUI 模式会显示 command 窗口和 source 窗口,如上图所示,还有其他窗口,如下列举的四个常用的窗口:

  • ( cmd) command 命令窗口,可以输入调试命令
  • ( src) source 源代码窗口, 显示当前行、断点等信息
  • ( asm) assembly 汇编代码窗口
  • ( reg) register 寄存器窗口

可以通过“ layout + 窗口类型”命令来选择自己需要的窗口,例如,在 cmd 窗口输入 layout asm 则可以切换到汇编代码窗口。

layout 命令还可以用来修改窗口布局,在 cmd 窗口中输入 help layout,常见的有:

Usage: layout prev | next | <layout_name>
Layout names are:
    src : Displays source and command windows.
    asm : Displays disassembly and command windows.
    split : Displays source, disassembly and command windows.
    regs : Displays register window. If existing layout
            is source/command or assembly/command, the
            register window is displayed. If the
            source/assembly/command (split) is displayed,
            the register window is displayed with
            the window that has current logical focus.

另外,可以通过 winheight 命令修改各个窗口的大小,如下所示:

(gdb) help winheight
Set the height of a specified window.
Usage: winheight <win_name> [+ | -] <#lines>
Window names are:
src : the source window
cmd : the command window
asm : the disassembly window
regs : the register display

##将代码窗口的高度扩大 5 行代码
winheight src + 5

##将代码窗口的高度减小 4 代码
winheight src - 4

常用快捷键

C-代码 Ctrl 键,下面介绍的皆为组合键,比如:
C-x a,即是先按 Ctrl + x 两个键,然后再去按 a 键。

组合键说明
C-x a 或 C-x A进入或退出 TUI 模式
C-x 1这里是数字 1,仅在一个窗口中使用 TUI 布局
C-x 2使用至少两个窗口的 TUI 布局。
C-x o

切换活动窗口,当窗口被选中时则可以使用上下左右键
键, 上翻、下翻键进行查看信息查看, 刷新则用 Ctrl+L

(信息显示混乱是使用该组合键进行刷新)。

C-x s单键模式和 TUI 模式的切换。
单键模式可以直接通过快捷键调试程序。

注:单键模式的调试提供了常用的调试快捷键。

单键模式对应的快捷键:

快捷键说明
c继续执行
d向下层查看堆栈
u向上层查看堆栈
f执行完当前函数后返回,和常规 gdb 的 finish 命令一致
n和常规 gdb 的 next 命令一致。
s和常规 gdb 的 step 命令一致。
istepi. The shortcut letter ‘i’ stands for “step
Into”
onexti. The shortcut letter ‘o’ stands for “step
Over”
r和常规 gdb 的 run 命令一致。
v查看局部变量
w提示当前调试的位置
q退出单键模式

窗口焦点切换

快捷键方式说明
C-x o下一窗口
输入命令方式说明
focus src 或 fs s源码窗口
focus cmd 或 fs c命令窗口
focus asm 或 fs a汇编窗口
focus regs 或 fs r寄存器窗口
focus next 或 fs n下一窗口
focus prev 或 fs p上一窗口

GCC 优化级别

gcc 默认提供了 5 级优化选项:

  • -O/-O0:无优化(默认)
  • -O1:使用能减少目标文件大小以及执行时间并且不会使编译时间明显增加的优化。该模式在编译大型程序的时候会花费更多的时间和内存。在-O1 下:编译会尝试减少代码体积和代码运行时间,但是并不执行会花费大量时间的优化操作。
  • -O2: 包含-O1 的优化并增加了不需要在目标文件大小和执行速度上进行折衷的优化。GCC 执行几乎所有支持的操作但不包括空间和速度之间权衡的优化,编译器不执行循环展开以及函数内联。这是推荐的优化等级,除非你有特殊的需求。 -O2 会比-O1 启用多一些标记。与-O1 比较该优化-O2 将会花费更多的编译时间当然也会生成性能更好的代码。
  • -Os:专门优化目标文件大小,执行所有的不增加目标文件大小的-O2 优化选项。同时-Os 还会执行更加优化程序空间的选项。这对于磁盘空间极其紧张或者 CPU 缓存较小的机器非常有用。但也可能产生些许问题,因此软件树中的大部分 ebuild 都过滤掉这个等级的优化。使用-Os 是不推荐的。
  • -O3: 打 开 所 有 -O2 的 优 化 选 项 并 且 增 加 -finline-functions, -funswitch-loops,-fpredictive-commoning, -fgcse-after-reload and -ftree-vectorize 优化选项。这是最高最危险的优化等级。用这个选项会延长编译代码的时间,并且在使用 gcc4.x 的系统里不应全局启用。自从 3.x 版本以来 gcc 的行为已经有了极大地改变。在 3.x, -O3 生成的代码也只是比-O2 快一点点而已,而 gcc4.x 中还未必更快。用-O3 来编译所有的软件包将产生更大体积更耗内存的二进制文件,大大增加编译失败的机会或不可预知的程序行为(包括错误)。这样做将得不偿失,记住过犹不及。在 gcc 4.x.中使用-O3 是不推荐的。
     

coredump是什么


  程序异常退出时,会产生一个core文件,该文件记录了程序运行时的内存,寄存器状态,堆栈指针,内存管理信息还有各种函数调用堆栈信息等,我们可以理解为是程序工作当前状态存储生成的一个文件,通过工具分析这个文件,我们可以定位到程序异常退出的时候对应的堆栈调用等信息,找出问题所在并进行及时解决。

  上面说当程序运行过程中异常终止或崩溃时会发生 core dump,但还没说到什么具体的情景程序会发生异常终止或崩溃,例如我们使用 kill -9 命令杀死一个进程会发生 core dump 吗?实验证明是不能的,那么什么情况会产生呢?   Linux 中信号是一种异步事件处理的机制,每种信号对应有其默认的操作,你可以在 这里 查看 Linux 系统提供的信号以及默认处理。默认操作主要包括忽略该信号(Ingore)、暂停进程(Stop)、终止进程(Terminate)、终止并发生core dump(core)等。如果我们信号均是采用默认操作,那么,以下列出几种信号,它们在发生时会产生 core dump,这个可以通过man 7 signal:   当然不仅限于上面的几种信号。这就是为什么我们使用 Ctrl+z 来挂起一个进程或者 Ctrl+C 结束一个进程均不会产生 core dump,因为前者会向进程发出 SIGTSTP 信号,该信号的默认操作为暂停进程(Stop Process);后者会向进程发出SIGINT 信号,该信号默认操作为终止进程(Terminate Process)。同样上面提到的 kill -9 命令会发出 SIGKILL 命令,该命令默认为终止进程。而如果我们使用 Ctrl+\ 来终止一个进程,会向进程发出 SIGQUIT 信号,默认是会产生 core dump 的。还有其它情景会产生 core dump, 如:程序调用 abort() 函数、访存错误、非法指令等等。

前期设置

  1. 设置core文件生成的目录,其中%e表示程序文件名,%p表示进程ID,否则会在程序的当前目录生成dore文件;

echo /data/coredump/core.%e.%p >/proc/sys/kernel/core_pattern   
  1. 当前执行程序的用户对core目录有写权限且有足够的空间存储core文件;

  2. 生成不受限制的core文件,ulimit -c unlimited,这个只会对当前的终端有效,如果想永久有效,需要修改文件:/etc/security/limits.conf,增加一行,如下红圈所示:

使用gdb调试coredump文件

产生了 core 文件,我们该如何使用该 Core 文件进行调试呢?Linux 中可以使用 GDB 来调试 core 文件,命令如下:

gdb program core

program是程序名称,core是coredump文件。OK,我们来动手吧,动手,动手,动手,当你看见一个前凸后翘的美女时,你先去了解她,制定攻略,那接下来是不是要动手实践呢?so 动手,只看书不动手永远很难了解哪些理论,即使水平再高也需要动手解决问题,才能有更深刻的认识,其实,也就是说再漂亮的女人,也需要个男人对她动粗,你说是不是?   比如: 那都有哪些操作命令可以调试这个coredump文件呢?

  • bt或者where查看调用栈信息 如果你要查看某一层的信息,你需要切换当前的栈,一般来说,程序停止时,最顶层的栈就是当前栈,如果你要查看栈下面层的详细信息,首先要做的是切换当前栈。

  • frame <n>和 f <n>: n是一个从0开始的整数,是栈中的层编号。比如:frame 0,表示栈顶,frame 1,表示栈的第二层。

  • up <n>: 表示向栈的上面移动n层,可以不打n,表示向上移动一层。

  • down <n>: 表示向栈的下面移动n层,可以不打n,表示向下移动一层。

上面的命令,都会打印出移动到的栈层的信息。如果你不想让其打出信息。你可以使用这三个命令:

select-frame <n> 对应于 frame 命令。
up-silently <n> 对应于 up 命令。
down-silently <n> 对应于 down 命令。

查看当前栈层的信息,你可以用以下GDB命令:

  • frame 或 f   会打印出这些信息:栈的层编号,当前的函数名,函数参数值,函数所在文件及行号,函数执行到的语句。

  • info frame 和 info f   这个命令会打印出更为详细的当前栈层的信息,只不过,大多数都是运行时的内内地址。比如:函数地址,调用函数的地址,被调用函数的地址,目前的函数是由什么样的程序语言写成的、函数参数地址及值、局部变量的地址等等。

奔溃日志

生成coredump去用gdb调试还是依赖我们是否生成调试信息,如果我们的没有添加-g选项就麻烦了,此时我们可以怎么办呢?   上文说过程序崩溃(core)了是由一些信号触发的,这些信号也许是因为用户操作,也许是我们程序有一些错误的指令(比如对空指针的赋值,除数为零)触发的。那么我们程序实际上是可以捕获这些信号的,然后去打印堆栈的信息,how to do?   

signal是一个截取信号的函数,它的申明如下:

#include <signal.h>
​
typedef void (*sighandler_t)(int);
​
sighandler_t signal(int signum, sighandler_t handler);

signal截取信号,然后调用回调函数handler处理信号,那么其实我们就可以在handler函数里打印堆栈信息,backtrace获取堆栈的大小,并且把堆栈的信息放在array指向的内存中,然后调用backtrace_symbols把堆栈中的函数信息转换成函数名。例如:

int main(int argc, char** argv)
{
    signal(SIGSEGV, handle_segv); // SIGSEGV    11  Core Invalid memory reference
    signal(SIGABRT, handle_segv); // SIGABRT    6       Core Abort signal from
    signal(SIGINT, handle_segv);
    signal(SIGTSTP, handle_segv);
    signal(SIGTERM, handle_segv);
​
    //...
    
    return 0;
}
void handle_segv(int signum)
{
    void *array[100];
    size_t size;
    char **strings;
    size_t i;
    
    signal(signum, SIG_DFL); /* 还原默认的信号处理handler */
​
    size = backtrace (array, 100);
    strings = (char **)backtrace_symbols (array, size);
    
    // 这里是打印到日志中,其实也可以打印到某个文件中
    LOG_INFO("Launcher received SIG: %d Stack trace:\n", signum);
    for (i = 0; i < size; i++)
    {
        LOG_ERROR("%d %s \n",i,strings[i]);
    }
​
    free (strings);
    //g_launcher->stop();
}
从上文,我们也可以看出我们在程序崩溃前可以做一些善后处理。

最后,链接一本电子书给大家介绍 | 100个gdb小技巧

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐