您好,欢迎来到九壹网。
搜索
您的当前位置:首页深入探索GDB调试技巧及其底层实现原理

深入探索GDB调试技巧及其底层实现原理

来源:九壹网

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

一、GDB调试

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

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

1.1 行断点

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

b source.cpp:22

1.2 函数断点

感觉更常用的是函数断点,因为我们在定位问题的时候,往往定位到某个关键函数,该函数可能被多次调用,被调用的位置也很多,那么用行断点就不太方便了,GDB可以给一个函数打上断点,打上断点后,用continue,简写c,程序执行

b source.cpp:22 if i == 12

到函数被调用处就会阻塞,而不需要关注它在哪个文件哪一行被调用了。用法如下:

b func1

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

1.3 条件断点

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

b source.cpp:22 if i == 12
那么程序只会在i==12的时候阻塞,在i取其它值的时候,程序可以正常运行

1.4 多线程调试

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

break source.cpp : 22 thread 3
或
break func1 thread 3

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

set scheduler-locking on

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

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

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

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

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

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

1.5 删除断点和忽略断点

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

delete 3

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

clear source.cpp : 22

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

ignore 3 12

2. next和step

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

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

3. 查看栈帧

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

查看栈帧的命令是backtrace,简写bt。它会依次从栈顶往栈底列出当前线程的栈帧,如下所示,#0即是栈顶,也就是说,当前线程正在执行exec_simple_query()函数,而且我们可以看到该函数被传入的参数的值

3.1回退栈帧

使用up n和down n可以对栈帧进行回退和前进,想改变当前调试的函数时很好用。如当前在栈帧0处,那么 up 5就会切换到栈帧#5处(up叫up但却是往栈底走的,为了不记忆错乱,记成它会走到栈帧序号更大的栈帧),再down 4,那么就到了栈帧#1的地方

需要C/C++ Linux服务器架构师学习资料加qun获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享

4. attach和detach

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

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

如果没有detach,那么当我们杀死gdb进程的时候,被调试的进程也会被杀死。

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

5. handle信号处理

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

handle 信号类型 处理方式

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

handle SIGUSR2 nostop noprint

然后再输入c去continue,就能正常进行调试了。

6. 查看代码

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

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

7. 查看函数汇编代码

disassemble funcName

8. 内存泄漏

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

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

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

二、GDB调试原理

GDB能够对程序进行调试,源自于一个系统调用:ptrace

#include <sys/ptrace.h>
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);

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

2.1 调试一个可执行程序test

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

这个时候,原理就是开启一个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的进程如果没有continue的话,其进程状态是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中断(break)到调试器,以供调试者对执行现场进行各种分析

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

实际上,CPU轮到GDB进程后,GDB会去断点链表里找到原先的汇编指令(源代码也一样),将断点那一行的INT 3又替换回原先的代码,而且让PC指针回退回该行。

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

PC:Program Counter,是通用寄存器,但是有特殊用途,用来指向当前运行指令的下一条指令

因篇幅问题不能全部显示,请点此查看更多更全内容

Copyright © 2019- 91gzw.com 版权所有 湘ICP备2023023988号-2

违法及侵权请联系:TEL:199 18 7713 E-MAIL:2724546146@qq.com

本站由北京市万商天勤律师事务所王兴未律师提供法律服务