常用的GDB的调试方法和技巧
Linux开发架构之路 2024-07-30

本文分为两大模块,第一部分记录下本人常用到的GDB的调试方法和技巧,第二部分则尝试分析GDB调试的基本原理。

一、GDB调试

要让程序能够被调试,首先得编译成调试版本,当然release版本的也能通过导入符号表来实现调试,目前还没试过。

GDB打断点用break命令,一般简写b,断点有多种形式。

1.1 行断点

可以在指定的文件的指定行里打断点,形式是:break 源文件名称 : 行号,比如:

b 源.cpp:22

1.2 函数断点

比较常用的是函数断点,因为我们在定位问题的时候,往往定位到某个关键函数,该函数可能被多次调用,被调用的位置也,那么用行断点就不太方便了,GDB可以给一个函数打上断点,打上断点后,用继续,简写c,程序执行到函数被调用就会阻塞而不需要关注它在哪个文件哪一行被调用了。 方法如下:

b 功能1

当进程阻塞之后,我们就可以用step命令,简写,来进入该函数内部,然后进一步用next或step来跟踪函数里面的代码

1.3 条件断点

在调试一些循环语句中,我们有时候需要观察某个自增变量达到一个特定值的时候,代码的行为,这个时候就需要条件变量,比如对于循环语句里,我们计数在i == 12的时候观察程序的运行,那么就可以在断点位置后面加上一个触发条件,比如:

b 源.cpp:22 如果 i == 12

现在程序只需在i==12的时候阻塞,在i取其他值的时候,程序就可以正常运行

1.4 多线程调试

有多个线程的情况时,某个函数可能会被多个线程调用,我们可以先用infothreads查看线程编号,然后程序再限定下哪个线程指定到这里需要阻塞,比如我们指定编号为3个线程:

中断源.cpp:22 线程 3 中断功能1线程3

或者我们指定仅运行当前线程,如下:

设置调度程序锁定

on就是打开,off关闭后就是运行所有线程。

注意:一般是用step进入到函数里面,确定跟踪该函数内部的执行时才使用该命令,否则其他情况线程无法切换,可能会对调试造成麻烦。

从语句就可以看出,它的意思就是设置(线程)调度关闭/开启。

因为在大型工程里面,一个函数被多个线程调用,而那些线程我们调用这个目标函数具体做什么事情我们并不关心,我们只需要在当前的线程里,(该函数也可能在一个循环里多)次调用,服务器进程经常有这种情况),当前面几次函数执行完成还没有达到我们想要的结果时,如果发生了线程切换,那会很麻烦,而限定程序不切换线程,那么就一直执行当前这个线程,那就更好定位问题了。

比如调试某个基于PG内核的数据库的SQL入口函数的时候,该函数会被十多个线程调用,而我的问题出现在主线程上,所以我需要设置线程不切换。

另外,在多进程情况下(有fork()时),GDB默认模式下,只能调试这个父进程不会跟踪子进程,不过可以设置,命令:set follow-fork-child,这样就会跟踪子进程进程了

1.5 删除断点和忽略断点

使用infobreak查看断点信息,每个断点都有个编号,当某些断点不需要时,我们可以用delete删除它,,比如删除断点3:

删除 3

也可以将某行代码上的所有断点都清除,清除:

清除源.cpp:22

如果只是暂时忽略某个断点,还可以设置忽略次数,比如忽略断点3总共12次,忽略:

忽略 3 12

2. 下一步

next简写成n,当执行到我们想要的一行代码继续往下一行代码走时就可以用该命令;

step简写成s,它也是单步执行,与next不同的是1,如果当前代码行是调用了某个函数,那么step会进入该被调用的函数里面,一般比较接近我们的问题相关的代码时,就可以用step进入函数内部,再单步调试。

3. 语法

在多线程环境下,因为每个线程都有一个栈,所以首先要切换线程,infothreads查看线程编号,加入要切换到的线程是3号,那么线程3就可以切换到3号线程。如果前面设置了关闭线程切换,那就不用管了。

查看栈帧的命令是backtrace,简写bt。它会依次从栈顶往栈底启动当前线程的栈帧,如下所示,#0即是栈顶,此时,当前线程正在执行exec_simple_query()函数,并且我们可以看到该函数被确定的参数的值


3.1回退阶段

使用up ndown n可以对栈帧进行回退和前进,想改变当前调试的函数时就很好用。如当前在栈帧0处,那么up 5切换到栈帧#5处(upcall)向上循环是往栈底走的,为了不记错乱,记成它会走到栈帧序号更大的栈帧),再向下4,那么就到了栈帧#1的位置

4. 附加和分离

我们需要经常调试一个已经在运行的进程,一般先用top命令查看其进程号,或者ps -ef | grep 进程名称查看,其中-ef可以把前台、后台的进程都显示出来。

查询到PID之后,就用gdb Attach PID调试该进程;注意,调试完成该进程后,用detach命令分离被调试进程和gdb,这样该程序将不再受gdb的控制,而gdb也可以去继续attach其它进程。

如果没有分离,那么当我们杀死gdb进程的时候,被调试的进程个体被杀死。

查看GDB的官方文档对detach的描述:

detach 当你完成对附加进程的调试后,你可以使用 detach 命令将其从 GDB 控制中释放。分离进程将继续执行。执行 detach 命令后,该进程和 GDB 再次完全独立,你就可以附加另一个进程或使用 run 启动一个进程了。执行该命令后,如果再次按 RET,detach 不会重复。如果在附加进程时退出 GDB 或使用 run 命令,则会终止该进程。默认情况下,如果你尝试执行这两件事中的任何一件,GDB 都会要求你确认;你可以使用 set confirmed 命令控制是否需要确认(请参阅可选的警告和消息部分)。

5. handle信号处理

GDB在调试进程的时候,可能会收到来自进程的各种信号,这个时候需要我们定义下GDB遇到某种信号时,做某种处理,其语法格式为:

处理 信号类型 处理方式

比如说我调试PG内核的时候,就会收到SIGUSR2,这是用户自定义信号,某个进程接收该信号时,默认的处理方式是进程终止,当因此没有在gdb调试前设置针对该信号的处理方式时,输入c后,调试并没有正常进行,而是停止了,并且打印了一些信息,这个时候就需要使用handle来处理SIGUSR2信号,如下:

处理 SIGUSR2 nostop noprint

然后再输入c去继续,就可以正常进行调试了。

6. 查看代码

gdb Attach进程之后,执行layout src会出现两个窗口,上面窗口用于看代码,打开两个窗口不能上下切换查看历史命令。

可以切换两个窗口间焦点,用fs下,这样就可以使用上下键查看历史命令了。

7. 查看函数汇编代码

反汇编函数名

8. 内存泄漏

像数据库内核这种代码量庞大的项目,可以使用静态代码检测工具去检测内存泄漏。

如果要在中小型项目中用GDB调试的时候去帮助判断是否发生内存泄漏,可以给malloc/free或者自己封装的内存申请/释放函数打上断点,并打印对应的指针的话的值,可以设置跟踪指标,比如malloc返回的指针p进行跟踪:watch p,因为它如果被释放并且被置空的话,最后是可以看到该指标为0x0的。

还可以在GDB中调用一下glibc库函数:malloc_stats()函数可以统计本进程具体的内存使用情况,精确到字节,观察in use bytes的数值变化。

二、GDB调试原理

GDB能够对程序进行调试,下面对一个系统调用:ptrace

#包括长 ptrace (枚举 __ptrace_request 请求, pid_t pid, void *addr, void *data);

第一个参数request参数指定了我们要使用ptrace的什么功能。

2.1 调试一个可执行程序测试

用GDB去运行一个程序,比如gdb ./test,或者是先进入gdb,再执行./test运行程序test,第一个参数就是PTRACE_TRACEME,顾名思义,就是“跟踪我”。

参数 pid 表示要跟踪进程的 pid,addr 表示要监视的被跟踪子进程的地址。

这个时候,原理就是开启一个GDB进程,然后GDB进程fork出一个子进程,让子进程执行PTRACE_TRACEME,然后子进程再调用execve(),如下图


此时,GDB进程其子进程就可以读取test进程的指令空间、数据空间、堆栈和寄存器的值。并且gdb进程接收了test进程的所有信号,对应系统向test进程发送的所有信号,都被gdb进程接收到。

(其实应该是内核给gdb的子进程发送信号,然后该进程给其父进程即GDB进程发送信号,父子进程间通信很容易)

2.2 GDB调试一个已经存在的程序即gdb Attach原理

我们用gdb Attach PID的时候,ptrace第一个参数确定的就是PTRACE_ATTACH,这是父进程调用attach到已经运行的子进程中;这个命令有权限的检查,普通用户进程不能attach到root进程中,但一般调试的都是普通用户进程,所以也没有遇到过问题。

这个过程就是:运行一个GDB进程,他调用ptrace()尝试attach目标进程test去,此时GDB需要给test进程发送一个信号SIGSTOP,要求test停止,这个信号是不能忽略的,然后test进程就进入TASK_STOPED状态,(用top -u用户名可以看到被gdb Attach的进程如果没有继续的话,其进程状态是t,这个就是暂停或被跟踪),然后之后状态是被跟踪状态TASK_TRACED,这个不重要,其实状态都是t,而不是Run。

这个过程的示意图如下


2.3 GDB断点原理

在某行代码处打一个断点,其实就是封装行代码的编译(是指令级别!!!)用INT 3中断指令代替,原来的代码被保存到“断点链表”中。

这个是软中断,硬中断是外设给CPU中断,让CPU停止,这个是内核在CPU待执行指令中插入的中断指令(勘误,CPU执行到int 3中断指令才不会停止,CPU只是个执行指令的机器它不会自己停止,只是此时执行中断指令,然后CPU被操作系统内核代码发出,发出那样的CPU的内核状态,然后内核会进行补不同进程的调度),所以是软中断。(都是让CPU收到中断指令,只是看是硬件发的还是软件发的)

INT n这种中断指令,CPU执行到这里时,内核调用相应的中断处理程序,对于INT 3,那就是当前进程test停止运行,将CPU占用GDB进程用。

INT 3是x86系列处理器提供的专门用于支持调试的指令。简单来说,该指令的目的就是使CPU(中断)到调试器,以供调试器对执行现场进行各种分析

这里还有个细节,就是运行到中断指令的话,这句指令不是执行完了吗,那我们到断点处,是怎么继续运行该断点处的代码的?

值得注意的是,CPU轮到GDB进程后,GDB会去断点链表里找到排序的处理指令(源代码也一样),将断点那一行的INT 3又替换回还原的代码,并且让PC指针回到返回该行。

所以我们要执行断点处的代码的话,输入指令n,就行了,而不是直接执行断点处的下一行。

PC:程序计数器,是通用注册,但有特殊用途,用于指向当前运行指令的下一条指令

往期精彩回顾



声明: 本文转载自其它媒体或授权刊载,目的在于信息传递,并不代表本站赞同其观点和对其真实性负责,如有新闻稿件和图片作品的内容、版权以及其它问题的,请联系我们及时删除。(联系我们,邮箱:evan.li@aspencore.com )
0
评论
  • 相关技术文库
  • C语言
  • 编程
  • 软件开发
  • 程序
下载排行榜
更多
评测报告
更多
广告