gdb:使用总结

本文深入探讨了使用GDB进行程序调试的各种技术,包括设置断点、查看和修改变量值、跟踪线程、调试多进程和多线程。通过实例详细解释了如何使用break、n/s、print、list、backtrace、info、thread等命令,以及如何处理多进程调试中的fork和exec操作,展示了调试过程中的一些实用技巧和注意事项。
摘要由CSDN通过智能技术生成

在这里插入图片描述
在这里插入图片描述

实战

以调试 redis 源码为例介绍常用的命令讲解。

准备

  1. 下载源码并解压
wget http://download.redis.io/releases/redis-6.0.3.tar.gz
tar zxvf redis-6.0.3.tar.gz
  1. 进入 redis 源码目录并编译,注意编译时要生成调试符号并且关闭编译器优化选项。
cd redis-6.0.3
make CFLAGS="-g -O0" -j 2 (如果已经编译过先 make clean)
  • 由于 redis 是纯 C 项目,使用的编译器是 gcc,因而这里设置编译器的选项时使用的是 CFLAGS 选项;如果项目使用的语言是 C++,那么使用的编译器一般是 g++,相对应的编译器选项是 CXXFLAGS。这点请读者注意区别。
  • 另外,这里 makefile 使用了 -j 选项,其值是 2,表示开启 2 个进程同时编译,加快编译速度
  1. 编译成功后,会在 src 目录下生成多个可执行程序,其中 redis-server 和 redis-cli 是需要调试的程序。 进入 src 目录,使用 GDB 启动 redis-server 这个程序:
cd src
gdb ./redis-serve

启动gdb调试器

编写程序:

#include <stdio.h>
int main ()
{
    unsigned long long int n, sum;
    n = 1;
    sum = 0;
    while (n <= 100)
    {
        sum = sum + n;
        n = n + 1;
    }
    return 0;
}

将程序编译成可执行文件:

# 使用 GDB 调试某个可执行文件,该文件中必须包含必要的调试信息(比如各行代码所在的行号、包含程序中所有变量名称的列表(又称为符号表)等)。因此必须有-g
# -o main:生成可执行文件main
 gcc main.cpp -o main -g

启动gdb调试器调试尚未执行的程序

$ gdb main               
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-114.el7
Copyright (C) 2013 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".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/oceanstar/workspace/cpp/code/opencv_sln/main...(no debugging symbols found)...done.
(gdb) 

该指令在启动 GDB 的同时,会打印出一堆免责条款。通过添加 --silent(或者 -q、–quiet)选项,可将比部分信息屏蔽掉

$ gdb main -q
Reading symbols from /home/oceanstar/workspace/cpp/code/opencv_sln/main...(no debugging symbols found)...done.
(gdb) 

run命令

默认情况下, gdb filename 命令只是附加的一个调试文件,并没有启动这个程序,需要输入 run 命令(简写为 r)启动这个程序

在这里插入图片描述
这就是 redis-server 启动界面,假设程序已经启动,再次输入 run 命令则是重启程序。我们在 GDB 界面按 Ctrl + C 快捷键让 GDB 中断下来,再次输入 run 命令,GDB 会询问我们是否重启程序,输入 yes 确认重启。
在这里插入图片描述

continue 命令

当 GDB 触发断点或者使用 Ctrl + C 命令中断下来后,想让程序继续运行,只要输入 continue 命令即可(简写为 c)。当然,如果 continue 命令继续触发断点,GDB 就会再次中断下来。
在这里插入图片描述
在这里插入图片描述

break

break 命令(简写为 b)即我们添加断点的命令,可以使用以下方式添加断点:

  • break functionname,在函数名为 functionname 的入口处添加一个断点;
  • break LineNo,在当前文件行号为 LineNo 处添加一个断点;
  • break filename:LineNo,在 filename 文件行号为 LineNo 处添加一个断点。

这三种方式都是我们常用的添加断点的方式。

在 redis main() 函数处添加一个断点:
在这里插入图片描述
设置断点后重启程序
在这里插入图片描述
redis-server 默认端口号是 6379,绑定端口是需要调用 bind 函数,通过文件搜索可以找到相应位置文件,在 anet.c 455 行。
在这里插入图片描述
使用 break 命令在这个地方加一个断点:
在这里插入图片描述
由于程序绑定端口号是 redis-server 启动时初始化的,为了能触发这个断点,再次使用 run 命令重启下这个程序,GDB 第一次会触发 main() 函数处的断点,输入continue 命令继续运行,接着触发 anet.c:455 处的断点:
在这里插入图片描述
anet.c:455 对应的代码:
在这里插入图片描述
现在断点停在第 455 行,所以当前文件就是 anet.c,可以直接使用“break 行号”添加断点。例如,可以在第 458 行、464 行、466 行分别加一个断点,看看这个函数执行完毕后走哪个 return 语句退出,则可以执行:
在这里插入图片描述
添加好这三个断点以后,我们使用 continue 命令继续运行程序,发现程序运行到第 466 行中断下来(即触发 Breakpoint 5):
在这里插入图片描述
说明 redis-server 绑定端口号并设置侦听(listen)成功,我们可以再打开一个 SSH 窗口,验证一下,发现 6379 端口确实已经处于侦听状态了。

在这里插入图片描述

backtrace 与 frame 命令

backtrace 命令(简写为 bt)用来查看当前调用堆栈。接上,redis-server 现在中断在 anet.c:466 行,可以通过 backtrace 命令来查看当前的调用堆栈:

在这里插入图片描述
这里一共有 6 层堆栈,最顶层是 main() 函数,最底层是断点所在的 anetListen() 函数,堆栈编号分别是 #0 ~ #5 ,如果想切换到其他堆栈处,可以使用 frame 命令(简写为 f),该命令的使用方法是“frame 堆栈编号(编号不加 #)”。在这里依次切换至堆栈顶部,然后再切换回 #0 练习一下:
在这里插入图片描述

info break、enable、disable 和 delete 命令

在程序中加了很多断点,而我们想查看加了哪些断点时,可以使用 info break 命令(简写为 info b
在这里插入图片描述
通过上面的内容片段可以知道,目前一共增加了 5 个断点,相应的断点信息比如每个断点的位置(所在的文件和行号)、内存地址、断点启用和禁用状态信息也一目了然。如果我们想禁用某个断点,使用“ disable 断点编号”就可以禁用这个断点了,被禁用的断点不会再被触发;同理,被禁用的断点也可以使用“ enable 断点编号”重新启用。

在这里插入图片描述
使用 disable 1 以后,第一个断点的 Enb 一栏的值由 y 变成 n,重启程序也不会再次触发

在这里插入图片描述

如果 disable 命令和 enable 命令不加断点编号,则分别表示禁用和启用所有断点:
在这里插入图片描述
使用“delete 编号”可以删除某个断点,如 delete 2 3 则表示要删除的断点 2 和断点 3:
在这里插入图片描述
同样的道理,如果输入 delete 不加命令号,则表示删除所有断点。

list 命令

(gdb) list

  • 如果不带任何参数的话,该命令会接着打印上次 list 命令打印出代码后面的代码。
  • 如果是第一次执行 list 命令则会显示当前正在执行代码位置附近的代码。

(gdb) list -

  • 如果参数是一个减号的话,则和前面刚好相反,会打印上次 list 命令打印出代码前面的代码。

(gdb) list LOATION

  • list 命令还可以带一个代码位置作为参数,顾名思义,这样的话就会打印出该代码位置附近的代码。
  • 这个代码位置的定义和在 break 命令中定义的相同,可以是一个行号:

(gdb) list 100

  • 列出当前代码文件中第 100 行附近代码

(gdb) list tcpdump.c:450

  • 列出 tcpdump.c 文件中第 450 行附近代码

(gdb) list main

  • 列出当前代码文件中 main 函数附近代码

(gdb) list inet.c:pcap_lookupdev

  • 列出 inet.c 代码文件中指定函数附近代码

(gdb) list FIRST,LAST

  • 这里 FIRST 和 LAST 都是具体的代码位置,此时该命令将显示 FIRST 到 LAST 之间的代码。
  • 可以不指定 FIRST 或者 LAST 参数,这样的话就将显示 LAST 之前或者 FIRST 之后的代码。
  • 注意,即使只指定一个参数也要带逗号,否则就编程前面的命令,显示代码位置附近的代码了。

(gdb) set listsize COUNT

  • list 命令默认只会打印出 10 行源代码,如果觉得不够,可以使用这个命令修改:

(gdb) show listsize

  • 如果想查看listsize参数当前被设置成多少

(gdb) info functions

  • 如果你想看程序中一共定义了哪些函数,可以使用 info functions
  • 这个命令会显示程序中所有函数的名词,参数格式,返回值类型以及函数处于哪个代码文件中。

常用方法:

  • list 命令(简写为 l)可以查看当前断点处的代码
  • 再次输入 list 命令试一下,则往后查阅代码
  • 继续输入 list 指令会以递增行号的形式继续显示剩下的代码行,一直到文件结束为止。当然 list 指令还可以往前和往后显示代码,命令分别是“list + (加号)”和“list -(减号)”, 比如 list +20 和 list -20

print

通过 print 命令(简写为 p)我们可以在调试过程中方便地查看变量的值,也可以修改当前内存中的变量值。切换当前断点到堆栈 #4 ,然后打印以下三个变量。
在这里插入图片描述
这里使用 print 命令分别打印出 server.port 、server.ipfd 、server.ipfd_count 的值,其中 server.ipfd 显示 “{0 <repeats 16 times>}”,这是 GDB 显示字符串或字符数据特有的方式,当一个字符串变量或者字符数组或者连续的内存值重复若干次,GDB 就会以这种模式来显示以节约空间

print 命令不仅可以显示变量值,也可以显示进行一定运算的表达式计算结果值,甚至可以显示一些函数的执行结果值。举个例子:

  • 我们可以输入 p &server.port来输出 server.port 的地址值
  • 如果在 C++ 对象中,可以通过p this来显示当前对象的地址,也可以通过p *this来列出当前对象的各个成员变量值
  • 如果有三个变量可以相加( 假设变量名分别叫 a、b、c ),可以使用 p a + b + c 来打印这三个变量的结果值

假设 func() 是一个可以执行的函数,p func() 命令可以输出该变量的执行结果。举一个最常用的例子,某个时刻,某个系统函数执行失败了,通过系统变量 errno 得到一个错误码,则可以使用p strerror(errno)将这个错误码对应的文字信息打印出来,这样就不用费劲地去 man 手册上查找这个错误码对应的错误含义了。

print 命令不仅可以输出表达式结果,同时也可以修改变量的值,我们尝试将上文中的端口号从 6379 改成 6400 试试:
在这里插入图片描述
总结起来,利用 print 命令,我们不仅可以查看程序运行过程中的各个变量的状态值,也可以通过临时修改变量的值来控制程序的行为。

ptype

ptype ,顾名思义,其含义是“print type”,就是输出一个变量的类型。例如,我们试着输出 Redis 堆栈 #4 的变量 server 和变量 server.port 的类型:
在这里插入图片描述
可以看到,对于一个复合数据类型的变量,ptype 不仅列出了这个变量的类型( 这里是一个名叫 redisServer 的结构体),而且详细地列出了每个成员变量的字段名,方便我们去查看每个变量的类型定义。

info 和 thread 命令

info 命令是一个复合指令,可以用来查看当前进程的所有线程运行情况。

我们先使用 delete 命令删掉所有断点,然后使用 run 命令重启一下 redis-server,等程序正常启动后,我们按快捷键 Ctrl+C 中断程序,然后使用 info thread 命令来查看当前进程有哪些线程,分别中断在何处
在这里插入图片描述
在这里插入图片描述

  • 通过 info thread 的输出可以知道 redis-server 正常启动后,一共产生了 5 个线程,包括一个主线程和四个工作线程,线程编号(Id 那一列)分别是 5、4、3、2、1。

  • 注意 虽然第一栏的名称叫 Id,但第一栏的数值不是线程的 Id,第三栏括号里的内容(如 LWP 23500)中,23500 这样的数值才是当前线程真正的 Id。Light Weight Process(轻量级进程),即是我们所说的线程。

怎么知道线程哪个线程是主线程?

  • 现在有 5 个线程,也就有 5 个调用堆栈,如果此时输入 backtrace 命令查看调用堆栈,由于当前 GDB 作用在线程 1,因此 backtrace 命令显示的一定是线程 1 的调用堆栈:
    在这里插入图片描述

如何切换到其他线程呢?

  • 可以通过“thread 线程编号”切换到具体的线程上去。例如,想切换到线程 2 上去,只要输入 thread 2 即可,然后输入 bt 就能查看这个线程的调用堆栈了:

在这里插入图片描述

  • 当前作用的线程切换到线程 2 上之后,线程 2 前面就被加上了星号

在这里插入图片描述
info 命令还可以用来查看当前函数的参数值,组合命令是 info args,我们找个函数值多一点的堆栈函数来试一下:

在这里插入图片描述
上述代码片段切回至主线程 1,然后切换到堆栈 #2,堆栈 #2 调用处的函数是aeProcessEvents() ,一共有两个参数,使用 info args 命令可以输出当前两个函数参数的值,参数 eventLoop 是一个指针类型的参数,对于指针类型的参数,GDB 默认会输出该变量的指针地址值,如果想输出该指针指向对象的值,在变量名前面加上 * 解引用即可,这里使用 p *eventLoop 命令:
在这里插入图片描述
如果还要查看其成员值,继续使用 变量名 ->字段名 即可

调试多进程

(1)follow-fork-mode

set follow-fork-mode [parent|child]
  • parent:gdb默认调试的是父进程,如果参数是parent则fork之后继续调试父进程,子进程不受影响
  • child:如果想调试子进程,则修改参数为child,set follow-fork-mode child之后调试子进程,父进程不受影响。

(2)show follow-fork-mode
查看当前调试的fork进程的模式

(3)detach-on-fork
该参数表明gdb在fork之后是否断开(detach)某个进程的调试,或者交给gdb控制.

   set detach-on-fork [on|off]
  • on:断开调试follow-fork-mode调试的指定进程
  • off:gdb将控制父进程和子进程。follow-fork-mode指定的进程将被调试,另一个进程置于暂停(suspended)状态。

使用gdb调试多进程时,如果想要在进程间进行切换,那么就需要在fork调用前设置: set detach-on-fork off ,然后使用 info inferiors 来查看进程信息,得到的信息可以看到最前面有一个进程编号,使用 inferior num 来进行进程切换。

那么为什么要使用 set detache-on-fork off 呢?它的意思是在调用fork后相关进程的运行行为是怎么样的,是detache on/off ?也就是说分离出去独立运行,不受gdb控制还是不分离,被阻塞住。这里还涉及到一个设置 set follow-fork-mode [parents/child] ,就是fork之后,gdb的控制落在谁身上,如果是父进程,那么分离的就是子进程,反之亦然。如果detache-on-fork被off了,那么未受控的那个进程就会被阻塞住,进程状态为T,即处于调试状态。

(4) show detach-on-fork

gdb将每一个被调试进程的执行状态记录在一个名为inferior的结构中。一般情况下一个inferior对应一个进程,每个不同的inferior有不同的地址空间。inferior有时候会在进程没有启动的时候就存在。

(5)info inferiors

查询正在调试的进程,gdb会为他们分配唯一的Num号,其中前面带’*'号的就是正在调试的进程

(6)inferior <inferior num>

切换调试的进程为inferior num的进程处

(7)add-inferior [-copies n] [-exec executable]

  • 添加n个新的调试进程,可以用file executable来分配给inferior可执行文件。
  • 如果不指定n,则只增加一个inferior;
  • 如果不指定executable,则执行程序留空,增加后可使用file命令重新指定执行程序;这时候创建的inferior其关联的进程并没启动。

(8)clone-inferior [-copies n] [infno]

  • 复制n个编号是infno的inferior。
  • 如果不指定n的话,就只复制一个inferior;
  • 如果不指定infno,则就复制正在调试的inferior。

(9)detach inferior infno

  • 断开(detach)掉编号是infno的inferior。
  • 值得注意的是这个inferior还存在,可以再次用run命令执行它。

(10)kill inferior infno

  • kill掉infno号inferior。值得注意的是这个inferior仍然存在,可以再次用run等命令执行它。

(11)remove-inferior infno

  • 删除一个infno号的inferior。
  • 删除前需要先kill或者detach这个inferior,因为当一个inferior正在运行时不能被删除.

(12)set schedule-multiple [on|off]

  • off:只有当前inferior会执行。
  • on:全部是执行状态的inferior都会执行。
  • show schedule-multiple,查看schedule-multiple的状态。

(13)set follow-exec-mode [new|same]

  • same:当发生exec的时候,在执行exec的inferior上控制子进程。
  • new:新建一个inferior给执行起来的子进程。而父进程的inferior仍然保留,当前保留的inferior的程序状态是没有执行。
  • show follow-exec-mode,查看follow-exec-mode设置的模式。

(14)set print inferior-events [on|off]:用来打开和关闭inferior状态的提示信息。

  • show print inferior-events:查看print inferior-events设置的状态。

(15)maint info program-spaces,用来显示当前gdb管理的地址空间的数目。

除了follow-fork-mode方法还有没有其他的方法调试多进程呢?我们知道gdb有附着(attach)到正在运行的进程的功能,即attach 命令。因此我们可以利用该命令attach到子进程然后进行调试。attach命令能够应付各种各样复杂的进程系统,比如孙子/曾孙进程,比如守护进程(精灵进程).

看个例子:


#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
 
int main()
{
	pid_t id=fork();
	if(id == 0)
	{
		printf("i am child,pid=%d,ppid=%d\n",getpid(),getppid());
		exit(1);
	}
	else
	{
		sleep(1);
		printf("i am parent,pid=%d,ppid=%d\n",getpid(),getppid());
		if(wait(NULL) != -1)
		{
			printf("wait success\n");
		}
	}
	return 0;
}
 
makefile
 
testgdb:testgdb.c
	gcc -o $@ $^ -g   //因为要使用gdb调试,所以要在gcc后面添加-g选项
.PHONY:clean
clean:
	rm -f testgdb
  1. 测试follow-fork-mode和detach-on-fork命令
    在这里插入图片描述
  2. 进程间的查询和切换

在这里插入图片描述

#include <stdio.h>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>

//Parent process handle after generate a thread.
void * ParentDo(void *argv)
{
    pid_t pid = getpid();
    pthread_t tid = pthread_self();   //Get the thread-id selfly.
    char tprefix[] = "thread";

    printf("[%s]: [%d] [%s] [%lu] [%s]\n", argv, pid, tprefix, tid, "step2");  //add the second breakpoint.
    printf("[%s]: [%d] [%s] [%lu] [%s]\n", argv, pid, tprefix, tid, "step3");

    return NULL;
}

//Child process handle.
void Child()
{
    pid_t pid = getpid();
    char prefix[] = "Child";
    printf("[%s]: [%d] [%s]\n", prefix, pid, "step1");
    return;
}
//Parent process handle.
void Parent()
{
    pid_t pid = getpid();
    char cParent[] = "Parent";
    char cThread[] = "Thread";
    pthread_t pt;

    printf("[%s]: [%d] [%s]\n", cParent, pid, "step1");

    if (pthread_create(&pt, nullptr, ParentDo, cThread))
    {
        printf("[%s]: Can not create a thread.\n", cParent);
    }

    ParentDo(cParent);
    sleep(1);
}



int main(int argc, const char **argv)
{
    int pid;
    pid = fork();
    if (pid != 0)    //add the first breakpoint.
        Parent();
    else
        Child();
    return 0;
}

如果直接运行程序,那么输出的结果如下:

在这里插入图片描述

开始调试

(1) 设置调试模式和Catchpoint

设置调试父子进程,gdb跟主进程,子进程block在fork位置。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这时可以另开一个终端,使用如下命令查看当前CentOS系统所有进程的状态:发现父进程PID为10062,通过fork产生的子进程为10065:
在这里插入图片描述

待研究

调试多线程

  • info threads显示当前可以调试的所有线程,每个线程会有一个GDB为其分配的ID,后面操作线程的时候会用到这个ID。前面有*的是当前调试的线程。
  • thread ID切换当前调试的线程为指定ID的线程
  • break thread_test.c:123 thread all:在所有线程中相应的行上设置断点
  • thread apply ID1 ID2 command: 让一个或者多个线程执行GDB命令command
  • thread apply all command:让所有被调试线程执行GDB命令command
  • set scheduler-locking off|on|step:实际使用过多线程调试的人都可以发现,在使用step或者continue命令调试当前被调试线程的时候,其他线程也是同时执行的,怎么只让被调试程序执行呢?通过这个命令就可以实现这个需求。off 不锁定任何线程,也就是所有线程都执行,这是默认值。 on 只有当前被调试程序会执行。 step 在单步的时候,除了next过一个函数的情况(熟悉情况的人可能知道,这其实是一个设置断点然后continue的行为)以外,只有当前线程会执行。
  • show scheduler-locking:查看当前锁定线程的模式。

gdb对于多线程程序的调试有如下支持:
(1)线程产生通知:在产生新的线程时, gdb会给出提示信息

(gdb) r
Starting program: /root/thread
[New Thread 1073951360 (LWP 12900)]
[New Thread 1082342592 (LWP 12907)]---以下三个为新产生的线程
[New Thread 1090731072 (LWP 12908)]
[New Thread 1099119552 (LWP 12909)]

(2)查看线程:使用可以查看运行的线程。info threads

(gdb) info threads
  4 Thread 1099119552 (LWP 12940)   0xffffe002 in ?? ()
  3 Thread 1090731072 (LWP 12939)   0xffffe002 in ?? ()
  2 Thread 1082342592 (LWP 12938)   0xffffe002 in ?? ()   #1为gdb分配的线程号,对线程进行切换时,使用该号码
* 1 Thread 1073951360 (LWP 12931)   main (argc=1, argv=0xbfffda04) at thread.c:21

(3)thread THREADNUMBER切换线程

(gdb) thread 4
[Switching to thread 4 (Thread 1099119552 (LWP 12940))]#0   0xffffe002 in ?? ()
(gdb) info threads
* 4 Thread 1099119552 (LWP 12940)   0xffffe002 in ?? ()
   3 Thread 1090731072 (LWP 12939)   0xffffe002 in ?? ()
   2 Thread 1082342592 (LWP 12938)   0xffffe002 in ?? ()
   1 Thread 1073951360 (LWP 12931)   main (argc=1, argv=0xbfffda04) at thread.c:21

一般来讲,在使用gdb调试的时候,只有一个线程为活动线程,如果希望得到其他的线程的输出结果,必须使用thread命令切换至指定的线程,才能对该线程进行调试或观察输出结果。

看个例子:

#include<stdio.h>
#include<pthread.h>
 
void *pthread1_run(void *arg)
{
	int count=10;
	while(count--)
	{
		sleep(1);
		printf("i am new pthread1 %lu\n",pthread_self());
	}
	pthread_exit(NULL);
	return 0;
}
 
void *pthread2_run(void *arg)
{
	int count=10;
	while(count--)
	{
		sleep(1);
		printf("i am new pthread2 %lu\n",pthread_self());
	}
	pthread_exit(NULL);
	return 0;
}
 
int main()
{
	pthread_t tid1;
	pthread_t tid2;
	pthread_create(&tid1,NULL,pthread1_run,NULL);
	pthread_create(&tid2,NULL,pthread2_run,NULL);
	pthread_join(tid1,NULL);
	pthread_join(tid2,NULL);
	return 0;
}
 
Makefile
 
testtid:testtid.c
	gcc -o $@ $^ -lpthread -g   //-g是说明要使用gdb调试该代码,-lpthread是与线程有关函数编译链接的时候必须添加的
.PHONY:clean
clean:
	rm -f testtid
  1. 查看当前正在调试的线程并切换线程
    在这里插入图片描述
    前面的1,2,3是gdb分配的线程号,当切换线程的时候使用该线程号。最前面的’*'号说明当前正在调试的线程。

  2. 让所有被调试的线程都执行同一个命令,就是打印堆栈信息
    在这里插入图片描述

  3. 只让线程编号为1的线程打印堆栈信息

在这里插入图片描述

  1. 锁定线程并查看当前锁定的线程
    在这里插入图片描述

调试死锁

#include<unistd.h>
#include<pthread.h>

pthread_mutex_t mutex_1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex_2 = PTHREAD_MUTEX_INITIALIZER;

void *pthread_test_1(void *arg)
{
    pthread_mutex_lock(&mutex_1);
    sleep(1);   //休眠以保证pthread_test_2线程运行至持有mutex_2
    pthread_mutex_lock(&mutex_2);
    pthread_mutex_unlock(&mutex_1);
    pthread_mutex_unlock(&mutex_2);
}

void *pthread_test_2(void *arg)
{
    pthread_mutex_lock(&mutex_2);
    sleep(1);    //休眠以保证pthread_test_1线程运行至持有mutex_1
    pthread_mutex_lock(&mutex_1);
    pthread_mutex_unlock(&mutex_2);
    pthread_mutex_unlock(&mutex_1);
}

int main(void)
{
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, pthread_test_1, NULL);
    pthread_create(&tid2, NULL, pthread_test_2, NULL);

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    return 0;
}

(1) 运行程序,发现程序停止了,那么就可能死锁了
(2)pidof命令获取进程PID
在这里插入图片描述

(3)启用GDB,然后attach [PID]调试已经在运行的进程

在这里插入图片描述
(4)查看线程的堆栈信息

thread apply all bt:查看所有线程的堆栈信息

当发现有多个线程停留在 “__lll_lock_wait” 上时,很可能说明发生了死锁,或者其它问题比如持有锁的线程无法退出,导致其他线程都阻塞在了加锁上。

在这里插入图片描述
从上图可以看出pthread_test_2这个线程在等待给mutex_1上锁,pthread_test_1这个线程在等待给mutex_2上锁,还不能就此判断出发生了死锁。需要在进一步查看mutex_1和mutex_2的信息。

(5)查看锁的信息

  • 如果知道互斥量的名字,可以直接运行 "print mutex_name"命令来查看。
  • 如果不知道互斥量的名字,可以通过 “获取互斥量地址处的数据方式” 来查看

print *(pthread_mutex *)(address):就是从address所标识的地址处取出pthread_mutex类型的数据。

由上图可以看出,mutex_1被ID为9641的线程持有,而9641为pthread_test_1,mutex_2被ID为9642的线程持有,而9642为phtread_test_2。

综上:pthread_test_1持有mutex_1,等待mutex_2,pthread_test_2持有mutex_2,等待mutex_1,得出结论发生了死锁。

在这里插入图片描述

next、step、until、finish、return 和 jump 命令

这几个命令是 GDB 调试程序时最常用的几个控制流命令,因此放在一起介绍

next

next 命令(简写为 n)是让 GDB 调到下一条命令去执行,这里的下一条命令不一定是代码的下一行,而是根据程序逻辑跳转到相应的位置。
在这里插入图片描述

如果当前 GDB 中断在上述代码第 6 行,此时输入 next 命令 GDB 将调到第 11 行,因为这里的 if 条件并不满足。这里有一个小技巧,在 GDB 命令行界面如果直接按下回车键,默认是将最近一条
命令重新执行一遍,因此,当使用 next 命令单步调试时,不必反复输入 n 命令,直接回车就可以了

step

next 命令用调试的术语叫“单步步过”(step over),即遇到函数调用直接跳过,不进入函数体内部。而下面的 step 命令(简写为 s)就是“单步步入”(step into),顾名思义,就是遇到函数调用,进入函数内部。

以redis为例,使用 b main 命令在 main() 处加一个断点,然后使用 r 命令重新跑一下程序,会触发刚才加在 main() 函数处的断点,然后使用 n 命令让程序走到 spt_init(argc, argv) 函数调用处,再输入 s 命令就可以进入该函数了:

在这里插入图片描述
在这里插入图片描述

return 和 finish 命令

  • 实际调试时,我们在某个函数中调试一段时间后,不需要再一步步执行到函数返回处,希望直接执行完当前函数并回到上一层调用处,就可以使用 finish 命令。与 finish 命令类似的还有 return 命令,return 命令的作用是结束执行当前函数,还可以指定该函数的返回值。
  • 这里需要注意一下二者的区别:finish 命令会执行函数到正常退出该函数;而 return 命令是立即结束执行当前函数并返回,也就是说,如果当前函数还有剩余的代码未执行完毕,也不会执行了。

until 命令

实际调试时,还有一个 until 命令(简写为 u)可以指定程序运行到某一行停下来,还是以 redis-server 的代码为例
在这里插入图片描述
这是 redis-server 代码中 initServer() 函数的一个代码片段,位于文件 server.c 中,当停在第 2742 行,想直接跳到第 2746 行,可以直接输入 u 2746,这样就能快速执行完中间的代码。当然,也可以先在第 2746 行加一个断点,然后使用 continue 命令运行到这一行,但是使用 until 命令会更简便。

Jump 命令

jump 命令基本用法是:jump <location>,该命令会带一个参数,即要跳转到的代码位置,可以是源代码的行号:

  • (gdb)jump 555 #跳转到源代码的第 555 行的位置

可以是相对当前代码位置的偏移量:

  • (gdb)jump +10#跳转到距当前代码下 10 行的位置

也可以是代码所处的内存地址:

  • (gdb) jump *0x12345678 #跳转到位于该地址的代码

注意,在内存地址前面要加“*”。还有,jump 命令不会改变当前程序调用栈的内容,所以当你从一个函数跳到另一个函数时,当函数运行完返回进行退栈操作时就会发生错误,因此最好还是在同一个函数中进行跳转。

location 可以是程序的行号或者函数的地址,jump 会让程序执行流跳转到指定位置执行,当然其行为也是不可控制的,例如您跳过了某个对象的初始化代码,直接执行操作该对象的代码,那么可能会导致程序崩溃或其他意外行为。jump 命令可以简写成j,但是不可以简写成 jmp,其使用有一个注意事项,即如果 jump 跳转到的位置后续没有断点,那么 GDB 会执行完跳转处的代码会继续执行。举个例子:
在这里插入图片描述
假设我们的断点初始位置在行号 3 处(代码 A),这个时候我们使用 jump 6,那么程序会跳过代码 B 和 C 的执行,执行完代码 D( 跳转点),程序并不会停在代码 6 处,而是继续执行后续代码,因此如果我们想查看执行跳转处的代码后的结果,需要在行号6、7 或 8 处设置断点。

有时候也可以用来测试一些我们想要执行的代码(正常逻辑不太可能跑到),比如

在这里插入图片描述

我们想执行 12 行的代码。

b main
jump 12

就会将 else 分支执行。

disassemble 命令

当进行一些高级调试时,我们可能需要查看某段代码的汇编指令去排查问题,或者是在调试一些没有调试信息的发布版程序时,也只能通过反汇编代码去定位问题,那么disassemble 命令就派上用场了。
在这里插入图片描述

set args 和 show args 命令

很多程序需要我们传递命令行参数。在 GDB 调试中,很多人会觉得可以使用 gdb filename args 这种形式来给 GDB 调试的程序传递命令行参数,这样是不行的。正确的做法是在用 GDB 附加程序后,在使用 run 命令之前,使用“set args 参数内容”来设置命令行参数。

还是以 redis-server 为例,Redis 启动时可以指定一个命令行参数,它的默认配置文件位于 redis-server 这个文件的上一层目录,因此我们可以在 GDB 中这样传递这个参数:set args …/redis.conf(即文件 redis.conf 位于当前程序 redis-server 的上一层目录),可以通过 show args 查看命令行参数是否设置成功。

在这里插入图片描述
如果单个命令行参数之间含有空格,可以使用引号将参数包裹起来。
在这里插入图片描述
如果想清除掉已经设置好的命令行参数,使用 set args 不加任何参数即可。
在这里插入图片描述
在这里插入图片描述

tbreak 命令

tbreak 命令也是添加一个断点,第一个字母“t”的意思是 temporarily(临时的),也就是说这个命令加的断点是临时的,所谓临时断点,就是一旦该断点触发一次后就会自动删除。添加断点的方法与上面介绍的 break 命令一模一样,这里不再赘述。这里文档就不再描述,大家自行测试即可。

watch 命令

watch 命令是一个强大的命令,它可以用来监视一个变量或者一段内存,当这个变量或者该内存处的值发生变化时,GDB 就会中断下来。被监视的某个变量或者某个内存地址会产生一个 watch point(观察点)。

watch 命令的使用方式是“watch 变量名或内存地址”,一般有以下几种形式:

(1) 形式一:整型变量

int i;
watch i

(2) 形式二:指针类型

char *p;
watch p 与 watch *p

注意:

  • watch p 与watch *p是有区别的,前者是查看*(&p),是 p 变量本身;后者是 p 所指内存的内容
  • 我们需要查看地址,因为目的是要看某内存地址上的数据是怎样变化的。

(3)形式三:watch 一个数组或内存区间

char buf[128];
watch buf
  • 这里是对 buf 的 128 个数据进行了监视,此时不是采用硬件断点,而是用软中断实现的。
  • 用软中断方式去检查内存变量是比较耗费 CPU 资源的,精确地指明地址是硬件中断

注意:当设置的观察点是一个局部变量时,局部变量无效后,观察点也会失效。在观察点失效时 GDB 可能会提示如下信息:

Watchpoint 2 deleted because the program has left the block in which its expression isvalid

在这里插入图片描述

在这里插入图片描述
测试:

在这里插入图片描述
在这里插入图片描述

watch i 的问题:是可以同时去 watch,只是局部变量需要进入到相应的起作用范围才能 watch。比如 initBuf 函数的 i。

问题来了,如果要取消 watch 怎么办?
先用 info watch 查看 watch 的变量,然后根据编号使用 delete 删除相应的 watch 变量。

在这里插入图片描述

display

display 命令监视的变量或者内存地址,每次程序中断下来都会自动输出这些变量或内存的值。例如,假设程序有一些全局变量,每次断点停下来我都希望 GDB 可以自动输出这些变量的最新值,那么使用“display 变量名”设置即可。

在这里插入图片描述
上述代码中,使用 display 命令分别添加了寄存器 ebp 和寄存器 eax,ebp 寄存器分别使用十进制和十六进制两种形式输出其值,这样每次程序中断下来都会自动把这些值打印出来,可以使用 info display 查看当前已经自动添加了哪些值,使用 delete display 清除全部需要自动输出的变量,使用 delete diaplay 编号 删除某个自动输出的变量。
在这里插入图片描述
在这里插入图片描述

调试技巧

将 print 打印结果显示完整

当使用 print 命令打印一个字符串或者字符数组时,如果该字符串太长,print 命令默认显示不全的,我们可以通过在 GDB 中输入 set print element 0 命令设置一下,这样再次使用 print 命令就能完整地显示该变量的所有字符串了。

多线程下禁止线程切换

假设现在有 5 个线程,除了主线程,工作线程都是下面这样的一个函数:
在这里插入图片描述
为了能说清楚这个问题,我们把四个工作线程分别叫做 A、B、C、D。
假设 GDB 当前正在处于线程 A 的代码行 3 处,此时输入 next 命令,我们期望的是调试器跳到代码行 4 处;或者使用“u 代码行 10”,那么我们期望输入 u 命令后调试器可以跳转到代码行 10 处。

但是在实际情况下,GDB 可能会跳转到代码行 1 或者代码行 2 处,甚至代码行13、代码行 14 这样的地方也是有可能的,这不是调试器 bug,这是多线程程序的特点,当我们从代码行 4 处让程序 continue 时,线程 A 虽然会继续往下执行,但是如果此时系统的线程调度将 CPU 时间片切换到线程 B、C 或者 D 呢?那么程序最终停下来的时候,处于代码行 1 或者代码行 2 或者其他地方就不奇怪了,而此时打印相关的变量值,可能就不是我们需要的线程 A 的相关值。

为了解决调试多线程程序时出现的这种问题,GDB 提供了一个在调试时将程序执行流锁定在当前调试线程的命令:set scheduler-locking on。当然也可以关闭这一选项,使用 set scheduler-locking off。除了 on/off 这两个值选项,还有一个不太常用的值叫 step,这里就不介绍了。

条件断点

在实际调试中,我们一般会用到三种断点:普通断点、条件断点和硬件断点。

硬件断点又叫数据断点,这样的断点其实就是前面课程中介绍的用watch命令添加的部分断点(为什么是部分而不是全部,前面介绍原因了,watch 添加的断点有部分是通过软中断实现的,不属于硬件断点)。硬件断点的触发时机是监视的内存地址或者变量值发生变化

普通断点就是除去条件断点和硬件断点以外的断点。

下面重点来介绍一下条件断点,所谓条件断点,就是满足某个条件才会触发的断点,这里先举一个直观的例子:

void do_something_func(int i)
{
	 i ++;
	 i = 100 * i;
}
int main()
{
	 for(int i = 0; i < 10000; ++i)
	 {
		 do_something_func(i);
	 }
	 return 0;
}

在上述代码中,假如我们希望当变量 i=5000 时,进入 do_something_func() 函数追踪一下这个函数的执行细节。此时可以修改代码增加一个 i=5000 的 if 条件,然后重新编译链接调试,这样显然比较麻烦,尤其是对于一些大型项目,每次重新编译链接都需要花一定的时间,而且调试完了还得把程序修改回来。

有了条件断点就不需要这么麻烦了,添加条件断点的命令是 break [lineNo] if [condition],其中 lineNo 是程序触发断点后需要停下的位置,condition 是断点触发的条件。这里可以写成 break 11 if i==5000,其中,11 就是调用 do_something_fun() 函数所在的行号。当然这里的行号必须是合理行号,如果行号非法或者行号位置不合理也不会触发这个断点。

(gdb) break 11 if i==5000 
Breakpoint 2 at 0x400514: file test1.c, line 10.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /root/testgdb/test1 
Breakpoint 1, main () at test1.c:9
9 for(int i = 0; i < 10000; ++i)
(gdb) c
Continuing.
Breakpoint 2, main () at test1.c:11
11 do_something_func(i);
(gdb) p i
$1 = 5000

把 i 打印出来,GDB 确实是在 i=5000 时停下来了。

添加条件断点还有一个方法就是先添加一个普通断点,然后使用“condition 断点编号断点触发条件”这样的方式来添加。添加一下上述断点:

(gdb) b 11
Breakpoint 1 at 0x400514: file test1.c, line 11.
(gdb) info b
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000000000400514 in main at test1.c:11
(gdb) condition 1 i==5000
(gdb) r
Starting program: /root/testgdb/test1 
y
Breakpoint 1, main () at test1.c:11
11 do_something_func(i);
Missing separate debuginfos, use: debuginfo-install glibc-2.17-196.el7_4.2.x86_64
(gdb) p i
$1 = 5000
(gdb)

使用 GDB 调试多进程程序

这里说的多进程程序指的是一个进程使用 Linux 系统调用 fork() 函数产生的子进程,没有相互关联的进程就是普通的 GDB 调试,不必刻意讨论。

在实际的应用中,如有这样一类程序,如 Nginx,对于客户端的连接是采用多进程模型,当 Nginx 接受客户端连接后,创建一个新的进程来处理这一路连接上的信息来往,新产生的进程与原进程互为父子关系,那么如何用 GDB 调试这样的父子进程呢?一般有两种方法:

(1)用 GDB 先调试父进程,等子进程 fork 出来后,使用 gdb attach 到子进程上去,当然这需要重新开启一个 session 窗口用于调试,gdb attach 的用法在前面已经介绍过了

(2)GDB 调试器提供了一个选项叫 follow-fork,可以使用 show follow-fork mode 查看当前值,也可以通过 set follow-fork mode 来设置是当一个进程 fork 出新的子进程时,GDB 是继续调试父进程还是子进程(取值是 child),默认是父进程( 取值是 parent)。

(gdb) show follow-fork mode 
Debugger response to a program call of fork or vfork is "parent".
(gdb) set follow-fork child
(gdb) show follow-fork mode
Debugger response to a program call of fork or vfork is "child".
(gdb)

问题

解决gdb调试过程中打印时出现optimized out问题

用gdb调试时想打印变量值,结果出现了optimized out,打印不出变量内容,后来找到解决方案,将makefile中的编译命令中-O3优化改为-O0取消优化即可

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值