C/C++调试总结:IDE(visual studio和Dev C++)和GDB使用方法 您所在的位置:网站首页 用vs还是dev C/C++调试总结:IDE(visual studio和Dev C++)和GDB使用方法

C/C++调试总结:IDE(visual studio和Dev C++)和GDB使用方法

2023-09-13 05:37| 来源: 网络整理| 查看: 265

文章目录 一、IDE调试总结二、GDB调试教程:1小时玩转Linux gdb命令 GDB是什么?GDB是什么 GDB下载和安装教程快速安装GDB源码安装GDB 如何在Windows平台上安装GDB调试器?MinGW安装GDB调试器1、在线安装GDB调试器2、手动安装GDB调试器3、修改PATH环境变量 GDB调试C/C++程序使用GDB的前期准备启动GDB调试器 调用GDB调试器的4种方式调用GDB的方式2) 调试尚未执行的程序3) 调试正在执行的程序4) 调试执行异常崩溃的程序 GDB调试器启动可用参数 gdb run(r)命令:启动程序GDB break(b):设置断点GDB break命令GDB tbreak命令GDB rbreak 命令总结 GDB watch命令:监控变量值的变化watch命令的实现原理 GDB catch命令:建立捕捉断点GDB条件断点(condition命令)详解GDB condition命令GDB ignore命令 GDB单步调试程序GDB next 命令GDB step命令GDB until命令 如何使用GDB进行断点调试?GDB finish和return命令GDB jump命令 GDB print和display命令:查看变量的值GDB print命令GDB display命令 GDB禁用和删除断点查看当前已建好的断点GDB删除断点1) clear命令2) delete 命令 GDB禁用断点 如何用GDB调试多线程程序?GDB查看所有线程GDB调整当前线程GDB执行特定线程GDB为特定线程设置断点GDB设置线程锁 GDB调试多进程程序GDB attach命令调试进程GDB显式指定要调试的进程GDB follow-fork-mode选项GDB detach-on-fork选项

一、IDE调试总结

之前的博客已经讲解了使用IDE进行调试的方法:

C语言调试教程总结(以visual studio和Dev C++为例)

Dev C++调试程序方法详解

但是在linux环境下,一般我们是使用GDB来进行调试,下面我们就对GDB的使用方法进行讲解。

二、GDB调试教程:1小时玩转Linux gdb命令

GNU symbolic debugger,简称「GDB 调试器」,是 Linux 平台下最常用的一款程序调试器。GDB 编译器通常以 gdb 命令的形式在终端(Shell)中使用,它有很多选项,这是我们要重点学习的。

发展至今,GDB 调试器已经对 C、C++、Go、Objective-C、OpenCL、Ada 等多种编程语言提供了支持。实际场景中,GDB 更常用来调试 C 和 C++ 程序,虽然 Linux 平台下有很多能编写 C、C++ 代码的集成开发工具(IDE),但它们调试代码的能力往往都源自 GDB 调试器。

调试是开发流程中一个非常重要的环境,每个程序员都应具备调试代码的能力,尤其对于从事 Linux C/C++ 开发的读者,必须具备熟练使用 GDB 调试器的能力。这套 GDB 入门教程通俗易懂,深入浅出,能让你快速学会使用 GDB 编译器。

GDB是什么?

从现在开始,我将系统教大家学习使用 GDB,本节先解决第一个问题,即 GDB 是什么。

要知道,哪怕是开发经验再丰富的程序员,编写的程序也避免不了出错。程序中的错误主要分为 2 类,分别为语法错误和逻辑错误:

程序中的语法错误几乎都可以由编译器诊断出来,很容易就能发现并解决;逻辑错误指的是代码思路或者设计上的缺陷,程序出现逻辑错误的症状是:代码能够编译通过,没有语法错误,但是运行结果不对。对于这类错误,只能靠我们自己去发现和纠正。

也就是说,程序中出现的语法错误可以借助编译器解决;但逻辑错误则只能靠自己解决。实际场景中解决逻辑错误最高效的方法,就是借助调试工具对程序进行调试。

所谓调试(Debug),就是让代码一步一步慢慢执行,跟踪程序的运行过程。比如,可以让程序停在某个地方,查看当前所有变量的值,或者内存中的数据;也可以让程序一次只执行一条或者几条语句,看看程序到底执行了哪些代码。

也就是说,通过调试程序,我们可以监控程序执行的每一个细节,包括变量的值、函数的调用过程、内存中数据、线程的调度等,从而发现隐藏的错误或者低效的代码。

对于初学者来说,学习调试可以增加编程的功力,能让我们更加了解自己的程序,比如变量是什么时候赋值的、内存是什么时候分配的,从而弥补学习的纰漏。调试是每个程序员必须掌握的基本技能,没有选择的余地!

就好像编译程序需要借助专业的编译器,调试程序也需要借助专业的辅助工具,即调试器(Debugger)。表 1 罗列了当下最流行的几款调试器:

调试器名称特 点Remote DebuggerRemote Debugger 是 VC/VS 自带的调试器,与整个IDE无缝衔接,使用非常方便。WinDbg大名鼎鼎的 Windows 下的调试器,它的功能甚至超越了 Remote Debugger,它还有一个命令行版本(cdb.exe),但是这个命令行版本的调试器指令比较复杂,不建议初学者使用。LLDBXCode 自带的调试器,Mac OS X 下开发必备调试器。GDBLinux 下使用最多的一款调试器,也有 Windows 的移植版。

本教程讲解的就是 GDB 调试器。

GDB是什么

GDB 全称“GNU symbolic debugger”,从名称上不难看出,它诞生于 GNU 计划(同时诞生的还有 GCC、Emacs 等),是 Linux 下常用的程序调试器。发展至今,GDB 已经迭代了诸多个版本,当下的 GDB 支持调试多种编程语言编写的程序,包括 C、C++、Go、Objective-C、OpenCL、Ada 等。实际场景中,GDB 更常用来调试 C 和 C++ 程序。

Windows 操作系统中,人们更习惯使用一些已经集成好的开发环境(IDE),如 VS、VC、Dev-C++ 等,它们的内部已经嵌套了相应的调试器。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ALIsvktk-1641227989444)(http://c.biancheng.net/uploads/allimg/200212/1-2002122135363V.gif)] 图 1 GDB 的吉祥物:弓箭鱼

总的来说,借助 GDB 调试器可以实现以下几个功能:

程序启动时,可以按照我们自定义的要求运行程序,例如设置参数和环境变量;可使被调试程序在指定代码处暂停运行,并查看当前程序的运行状态(例如当前变量的值,函数的执行结果等),即支持断点调试;程序执行过程中,可以改变某个变量的值,还可以改变代码的执行顺序,从而尝试修改程序中出现的逻辑错误。

后续章节会做以上功能做详细的讲解,这里简单了解一下即可,不必深究。

正如从事 Windows C/C++ 开发的一定要熟悉 Visual Studio、从事 Java 开发的要熟悉 Eclipse 或 IntelliJ IDEA、从事 Android 开发的要熟悉 Android Studio、从事 iOS 开发的要熟悉 XCode 一样,从事 Linux C/C++ 开发要熟悉 GDB。

另外,虽然 Linux 系统下读者编写 C/C++ 代码的 IDE 可以自由选择,但调试生成的 C/C++ 程序一定是直接或者间接使用 GDB。可以毫不夸张地说,我所做那些 C/C++ 项目的开发和调试包括故障排查都是利用 GDB 完成的,调试是开发流程中一个非常重要的环节,因此对于从事 Linux C/C++ 的开发人员熟练使用 GDB 调试是一项基本要求。

“工欲善其事、必先利其器”,作为一名合格的软件开发者,至少得熟悉一种软件开发工具和调试器, 而对于 Linux C/C++ 后台开发,舍 GDB 其谁。

那么,GDB 如何安装,又该怎样使用,需要记住哪些指令呢?别急,我会后续的文章中给大家做详细的讲解。

GDB下载和安装教程

基于 Linux 系统的免费、开源,衍生出了多个不同的 Linux 版本,比如 Redhat、CentOS、Ubuntu、Debian 等。这些 Linux 发行版中,有些默认安装有 GDB 调试器,但有些默认不安装。

判断当前 Linux 发行版是否安装有 GDB 的方法也很简单,就是在命令行窗口中执行 gdb -v 命令。以本机安装的 CentOS 系统为例:

[root@bogon ~]# gdb -v bash: gdb: command not found

如上所示,执行结果为“command not found”,表明当前系统中未安装 GDB 调试器。反之,若执行结果为:

[root@bogon ~]# gdb -v GNU gdb (GDB) Red Hat Enterprise Linux (7.2-92.el6) Copyright (C) 2010 Free Software Foundation, Inc. ....... sum = sum + n; n = n + 1; } return 0; }

此源码的完整存储路径为 /tmp/demo/main.c。

本节就以此程序为例,给大家演示 GDB 调试器的基本用法。

使用GDB的前期准备

通过前面的学习我们知道,GDB 的主要功能就是监控程序的执行流程。这也就意味着,只有当源程序文件编译为可执行文件并执行时,GDB 才会派上用场。

Linux 发行版中,经常使用 GCC 编译 C、C++ 程序(有关 GCC 编译器,读者可猛击《GCC编译器》系统学习)。但需要注意的是,仅使用 gcc(或 g++)命令编译生成的可执行文件,是无法借助 GDB 进行调试的。

以 main.c 源文件为例,正常情况下,使用 GCC 编译该源代码的指令如下:

[root@bogon demo]# ls main.c [root@bogon demo]# gcc main.c -o main.exe [root@bogon demo]# ls main.c main.exe

可以看到,这里已经生成了 main.c 对应的执行文件 main.exe,但值得一提的是,此文件不支持使用 GDB 进行调试。原因很简单,使用 GDB 调试某个可执行文件,该文件中必须包含必要的调试信息(比如各行代码所在的行号、包含程序中所有变量名称的列表(又称为符号表)等),而上面生成的 main.exe 则没有。

那么,如何生成符合 GDB 调试要求的可执行文件呢?很简单,只需要使用 gcc -g 选项编译源文件,即可生成满足 GDB 要求的可执行文件。仍以 main.c 源程序文件为例:

[root@bogon demo]# ls main.c [root@bogon demo]# gcc main.c -o main.exe -g [root@bogon demo]# ls main.c main.exe

由此生成的 main.exe,即可使用 GDB 进行调试。

较早以前的 C 语言编译器也允许使用 -gg 选项来产生调试信息,但是现在版本的 GDB 不再支持这种格式产生的调试信息,所以不建议使用 -gg 选项。

值得一提的是,GCC 编译器支持 -O(等于同 -O1,优化生成的目标文件)和 -g 一起参与编译。GCC 编译过程对进行优化的程度可分为 5 个等级,分别为 O0~O4,O0 表示不优化(默认选项),从 O1 ~ O4 优化级别越来越高,O4 最高。

所谓优化,例如省略掉代码中从未使用过的变量、直接将常量表达式用结果值代替等等,这些操作会缩减目标文件所包含的代码量,提高最终生成的可执行文件的运行效率。

而相对于 -O -g 选项,对 GDB 调试器更友好的是 -Og 选项,-Og 对代码所做的优化程序介于 O0 ~ O1 之间,真正可做到“在保持快速编译和良好调试体验的同时,提供较为合理的优化级别”。

解决了如何生成满足 GDB 调试器要求的可执行文件,接下来正式学习 GDB 调试器的使用。

启动GDB调试器

在生成包含调试信息的 main.exe 可执行文件的基础上,启动 GDB 调试器的指令如下:

[root@bogon demo]# gdb main.exe GNU gdb (GDB) 8.0.1 Copyright (C) 2017 Free Software Foundation, Inc. ...... (gdb)

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

[root@bogon demo]# gdb main.exe --silent Reading symbols from main.exe...(no debugging symbols found)...done. (gdb)

无论使用以上哪种方式,最终都可以启动 GDB 调试器,启动成功的标志就是最终输出的 (gdb)。通过在 (gdb) 后面输入指令,即可调用 GDB 调试进行对应的调试工作。

GDB 调试器提供有大量的调试选项,可满足大部分场景中调试代码的需要。如表 1 所示,罗列了几个最常用的调试指令及各自的作用:

调试指令作 用(gdb) break xxx (gdb) b xxx在源代码指定的某一行设置断点,其中 xxx 用于指定具体打断点的位置。(gdb) run (gdb) r执行被调试的程序,其会自动在第一个断点处暂停执行。(gdb) continue (gdb) c当程序在某一断点处停止运行后,使用该指令可以继续执行,直至遇到下一个断点或者程序结束。(gdb) next (gdb) n令程序一行代码一行代码的执行。(gdb) print xxx (gdb) p xxx打印指定变量的值,其中 xxx 指的就是某一变量名。(gdb) list (gdb) l显示源程序代码的内容,包括各行代码所在的行号。(gdb) quit (gdb) q终止调试。

如上所示,每一个指令既可以使用全拼,也可以使用其首字母表示。另外,表 1 中罗列的指令仅是冰山一角,GDB 还提供有大量的选项,可以通过 help 选项来查看。有关 help 选项的具体用法,读者可阅读《GDB查看命令》一节,这里不再做具体赘述。

仍以 main.exe 可执行程序为例,接下来为读者演示表 1 中部分选项的功能和用法:

(gdb) l 9 sum = sum + n; 10 n = n + 1; (gdb) num++; } return 0; }

执行 gcc main.c -o main.exe -g 编译指令,获得该源程序对应的具备调试信息的 main.exe 可执行文件,并在此基础上执行:

[root@bogon demo]# ./main.exe 6 num++; 7 } 8 return 0; 9 } (gdb) b 6 FILE * fp; if((fp = fopen(argv[1],"r")) == NULL){ printf("file open fail"); } else{ printf("file open true"); } return 0; }

要知道,命令行窗口打开时默认位于 ~ (表示当前用户的主目录)路径下,假设我们就位于此目录中使用 gdb 命令启动 GDB 调试器,则在执行 main.exe 之前,有以下几项操作要做:

首先,对于已启动的 GDB 调试器,我们可以先通过 l (小写的 L)指令验证其是否已找到指定的目标程序文件:

[root@bogon ~]# gdb -q 6 printf(“file open fail”); 7 } 8 else{ 9 printf(“file open true”); 10 } (gdb) 11 return 0; 12 } (gdb)

可以看到,通过借助 file 命令,则无需重启 GDB 调试器也能指定要调试的目标程序文件。

除了 file 指令外,GDB 调试器还提供有其它的指定目标调试文件的指令,感兴趣的读者可千万 GDB 官网做详细了解,后续章节在用到时也会做详细的讲解。

通过分析 main.c 中程序的逻辑不难发现,要想其正确执行,必须在执行程序的同时给它传递一个目标文件的文件名。

总的来说,为 GDB 调试器指定的目标程序传递参数,常用的方法有 3 种: 1、启动 GDB 调试器时,可以在指定目标调试程序的同时,使用 --args 选项指定需要传递给该程序的数据。仍以 main.exe 程序为例:

[root@bogon demo]# gdb --args main.exe a.txt

整个指令的意思是:启动 GDB 调试器调试 main.exe 程序,并为其传递 “a.txt” 这个字符串(其会被 argv[] 字符串数组接收)。

2、GDB 调试器启动后,可以借助 set args 命令指定目标调试程序启动所需要的数据。仍以 main.exe 为例:

(gdb) set args a.txt

该命令表示将 “a.txt” 传递给将要调试的目标程序。

3、除此之外,还可以使用 run 或者 start 启动目标程序时,指定其所需要的数据。例如:

(gdb) run a.txt (gdb) start a.txt

以上 2 条命令都可以将 “a.txt” 传递给要调试的程序。

要知道,对于调试 /tmp/demo/ 路径下的 main.exe 文件,将其作为 GDB 调试器的工作目录,一定程度上可以提高我们的调试效率。反之,如果 GDB 调试器的工作目录和目标调试文件不在同一目录,则很多时候需要额外指明要操作文件的存储路径(例如第 1) 种情况中用 file 指令指明调试文件时就必须指明其存储位置)。

默认情况下,GDB 调试器的工作目录为启动时所使用的目录。例如在 ~ 路径下启动的 GDB 调试器,其工作目录就为 ~(当前用户的 home 目录)。当然,GDB 调试器提供有修改工作目录的指令,即 cd 指令。例如,将 GDB 调试器的工作目录修改为 /tmp/demo,则执行指令为:

(gdb) cd /tmp/demo

由此,GDB 调试器的工作目录就变成了 /tmp/demo。

某些场景中,目标调试程序的执行还需要临时修改 PATH 环境变量,此时就可以借助 path 指令,例如:

(gdb) path /temp/demo Executable and object file path: /temp/demo:/usr/local/sbin:/usr/local/bin…

注意,此修改方式只是临时的,退出 GDB 调试后会失效。

默认情况下,GDB 调试的程序会接收 set args 等方式指定的参数,同时会将输出结果打印到屏幕上。而通过对输入、输出重定向,可以令调试程序接收指定文件或者终端提供的数据,也可以将执行结果输出到文件或者某个终端上。

例如,将 main.exe 文件的执行结果输出到 a.txt 文件中,执行如下命令:

(gdb) run > a.txt

由此,在 GDB 调试的工作目录下就会生成一个 a.txt 文件,其中存储的即为 main.exe 的执行结果。

总的来说,只有将调试程序所需的运行环境搭建好后,才能使用 run 或者 start 命令开始调试。如下是一个完整的实例,演示了 GDB 调试 mian.exe 之前所做的准备工作:

[root@bogon demo]# pwd num *= 2; } printf("num=%d",num); return 0; }

程序存储在/tmp/demo/main.c文件中,并已经生成了具备调试信息的 main.exe 可执行文件:

[root@bogon demo]# ls main.c main.exe [root@bogon demo]# gdb main.exe -q Reading symbols from main.exe... (gdb) GDB break命令

break 命令(可以用 b 代替)常用的语法格式有以下 2 种。

1、(gdb) break location // b location 2、(gdb) break … if cond // b … if cond

第一种格式中,location 用于指定打断点的具体位置,其表示方式有多种,如表 1 所示。 location 的值含 义linenumlinenum 是一个整数,表示要打断点处代码的行号。要知道,程序中各行代码都有对应的行号,可通过执行 l(小写的 L)命令看到。filename:linenumfilename 表示源程序文件名;linenum 为整数,表示具体行数。整体的意思是在指令文件 filename 中的第 linenum 行打断点。+ offset - offsetoffset 为整数(假设值为 2),+offset 表示以当前程序暂停位置(例如第 4 行)为准,向后数 offset 行处(第 6 行)打断点;-offset 表示以当前程序暂停位置为准,向前数 offset 行处(第 2 行)打断点。functionfunction 表示程序中包含的函数的函数名,即 break 命令会在该函数内部的开头位置打断点,程序会执行到该函数第一行代码处暂停。filename:functionfilename 表示远程文件名;function 表示程序中函数的函数名。整体的意思是在指定文件 filename 中 function 函数的开头位置打断点。 第二种格式中,… 可以是表 1 中所有参数的值,用于指定打断点的具体位置;cond 为某个表达式。整体的含义为:每次程序执行到 … 位置时都计算 cond 的值,如果为 True,则程序在该位置暂停;反之,程序继续执行。

如下演示了以上 2 种打断点方式的具体用法:

(gdb) l 1 #include 2 int main(int argc,char* argv[]) 3 { 4 int num = 1; 5 while(num 10 rb_one(); (gdb) 11 rb_second(); 12 return 0; 13 } (gdb) rbreak rb_* (gdb) c Continuing. rb_second[Inferior 1 (process 7882) exited normally] (gdb)

可以看到,通过执行rbreak rb_*指令,找到了程序中所有以 tb_* 开头的函数,并在这些函数内部的开头位置打上了断点(如上所示,分别为第 2 行和第 5 行)。

总结

在 GDB 调试器中,为程序的适当位置打断点,是最常用的调试程序的方法。不过,本节仅介绍了如何使用 break(tbreak、rbreak)在程序中打断点,实际场景中还可以使用 catch 或者 watch 中断程序的运行,有关它们的功能和用法,会在后续章节中给大家做详细讲解。

GDB watch命令:监控变量值的变化

《GDB break命令》一节,给大家介绍了使用 break 命令在程序某一行的位置打断点。但还有一些场景,我们需要监控某个变量或者表达式的值,通过值的变化情况判断程序的执行过程是否存在异常或者 Bug。这种情况下,break 命令显然不再适用,推荐大家使用 watch 命令。

要知道,GDB 调试器支持在程序中打 3 种断点,分别为普通断点、观察断点和捕捉断点。其中 break 命令打的就是普通断点,而 watch 命令打的为观察断点,关于捕捉断点,后续章节会做详细讲解。

使用 GDB 调试程序的过程中,借助观察断点可以监控程序中某个变量或者表达式的值,只要发生改变,程序就会停止执行。相比普通断点,观察断点不需要我们预测变量(表达式)值发生改变的具体位置。

所谓表达式,就是包含多个变量的式子,比如 a+b 就是一个表达式,其中 a、b 为变量。

对于监控 C、C++ 程序中某变量或表达式的值是否发生改变,watch 命令的语法非常简单,如下所示:

(gdb) watch cond

其中,conde 指的就是要监控的变量或表达式。

和 watch 命令功能相似的,还有 rwatch 和 awatch 命令。其中:

rwatch 命令:只要程序中出现读取目标变量(表达式)的值的操作,程序就会停止运行;awatch 命令:只要程序中出现读取目标变量(表达式)的值或者改变值的操作,程序就会停止运行。

强调一下,watch 命令的功能是:只有当被监控变量(表达式)的值发生改变,程序才会停止运行。

举个例子:

(gdb) l try{ throw 100; }catch(int e){ num++; cout try{ throw num; }catch(int &e){ num++; } } cout int i = 1; int sum = 0; while(i int num =0; scanf("%d", &num); int result = myfunc(num); printf("%d", result); return 0; }

此程序存储在~/demo/main.c源文件中(~ 表示当前用户的主目录),功能是根据用户输入的 num 值,输出 12+22+…+num2 的值。

GDB next 命令

next 是最常用来进行单步调试的命令,其最大的特点是当遇到包含调用函数的语句时,无论函数内部包含多少行代码,next 指令都会一步执行完。也就是说,对于调用的函数来说,next 命令只会将其视作一行代码。

next 命令可以缩写为 n 命令,使用方法也很简单,语法格式如下:

(gdb) next count

参数 count 表示单步执行多少行代码,默认为 1 行。

举个例子:

(gdb) b 16 Breakpoint 2 at 0x40058b: file main.c, line 16. (gdb) r Starting program: /root/demo/main.exe Breakpoint 2, main () at main.c:16 16 int num =0; (gdb) n 2 int i = 1; int sum = 0; while(i int num =0; scanf("%d", &num); int result = myfunc(num); printf("%d", result); return 0; }

此程序存储在~/demo/main.c源文件中(~ 表示当前用户的主目录)。

首先,表 1 中 run、continue、list、next、print 以及 quit 命令的用法都非常简单,唯一需要注意的一点是,run 命令除了可以启动程序的执行,还可以在任何时候重新启动程序。

例如,以 main.c 为例:

[root@bogon demo]# gdb main.exe -q Reading symbols from main.exe... (gdb) l result += i; i++; } printf("result=%d\n", result); return 0; }

此程序存储在~/demo/main.c文件中(~ 代指当前系统登录用户的主目录),并且已经其编译为可供 GDB 调试的 main.exe 可执行文件:

[root@bogon demo]# gcc main.c -o main.exe -g [root@bogon demo]# ls main.c main.exe

GDB print命令

前面章节中,我们已经多次接触并使用了 print 命令,它的功能就是在 GDB 调试程序的过程中,输出或者修改指定变量或者表达式的值。

所谓表达式,简单理解就是由多个变量构成的式子。例如 a、b、c 为单个变量,a+b、a+b*c 即为表达式。

print 命令可以缩写为 p,最常用的语法格式如下所示:

(gdb) print num (gdb) p num

其中,参数 num 用来代指要查看或者修改的目标变量或者表达式。

以调试 main.exe 为例:

[root@bogon demo]# gdb main.exe -q Reading symbols from ~/demo/main.exe...done. (gdb) l 1 #include 2 int main(){ 3 int num,result=0,i=0; 4 scanf("%d", &num); 5 while(i char * thread_name = (char*)name; printf("this is %s\n",thread_name); printf("http://c.biancheng.net\n"); } int main() { pthread_t tid1,tid2; pthread_create(&tid1, NULL, thread_job, "thread1_job"); pthread_create(&tid2, NULL, thread_job, "thread2_job"); pthread_join(tid1,NULL); pthread_join(tid2,NULL); printf("this is main\n"); return 0; }

此程序的存储位置为 ~/demo/main.c。可以看到,此程序中包含 3 个线程,分别为 main 主线程、tid1 子线程和 tid2 子线程。

需要注意的是,将此程序编译为可供 GDB 调试的可执行程序时,需执行如下命令:

gcc main.c -o main.exe -g -lpthread

因为 pthread 线程库并不属于 Linux 系统中的默认库,所以编译、链接时就需要为 gcc 命令附加 -lpthread 参数。

GDB查看所有线程

info threads 命令的功能有 2 个,既可以查看当前调试环境下存在的线程数以及各线程的具体信息,也可以通过指定线程的编号查看某个线程的具体信息。

info threads 命令的完整语法格式如下:

(gdb) info threads [id…]

其中,参数 id… 作为可选参数,表示要查看的线程编号,编号个数可以是多个。

以 main.exe 程序为例:

(gdb) b 6 Breakpoint 1 at 0x11d9: file main.c, line 6. (gdb) r Starting program: ~/demo/main.exe [Thread debugging using libthread_db enabled] Using host libthread_db library “/lib/x86_64-linux-gnu/libthread_db.so.1”. [New Thread 0x7ffff7d9f700 (LWP 54283)] printf("this is child,pid = %d\n",getpid()); } else { printf("this is parent,pid = %d\n",getpid()); } return 0; }

程序的存储路径为~/demo/myfork.c。可以看到,程序中包含 2 个进程,分别为父进程(又称主进程)和使用 fork() 函数分离出的子进程。

事实上在多数 Linux 发行版系统中,GDB 并没有对多进程程序提供友好的调试功能。无论程序中调用了多少次 fork() 函数(或者 vfork() 函数),从父进程中分离出多少个子进程,GDB 默认只调试父进程,而不调试子进程。

那么问题就出现了,如何使用 GDB 调试多进程程序中的子进程呢?

GDB attach命令调试进程

首先,无论父进程还是子进程,都可以借助 attach 命令启动 GDB 调试它。attach 命令用于调试正在运行的进程,要知道对于每个运行的进程,操作系统都会为其配备一个独一无二的 ID 号。在得知目标子进程 ID 号的前提下,就可以借助 attach 命令来启动 GDB 对其进行调试。

这里还需要解决一个问题,很多场景中子进程的执行时间都是一瞬而逝的,这意味着,我们可能还未查到它的进程 ID 号,该进程就已经执行完了,何谈借助 attach 命令对其调试呢?对于 C、C++ 多进程程序,解决该问题最简单直接的方法是,在目标进程所执行代码的开头位置,添加一段延时执行的代码。

例如,将上面程序中if(pid==0)判断语句整体做如下修改:

if(pid == 0) { int num =10; while(num==10){ sleep(10); } printf("this is child,pid = %d\n",getpid()); }

可以看到,通过添加第 3~6 行代码,该进程执行时会直接进入死循环。这样做的好处有 2 个,其一是帮助 attach 命令成功捕捉到要调试的进程;其二是使用 GDB 调试该进程时,进程中真正的代码部分尚未得到执行,使得我们可以从头开始对进程中的代码进行调试。

有读者可能会问,进程都已经进行死循环了,后续代码还可以进行调试吗?当然可以,以上面示例中给出的死循环,我们只需用 print 命令临时修改 num 变量的值,即可使程序跳出循环,从而执行后续代码。

就以调试修改后的 myfork.c 程序(已将其编译为 myfork.exe 可执行文件)为例:

[root@bogon demo]# gdb myfork.exe -q Reading symbols from ~/demo/myfork.exe…done. (gdb) r Starting program: ~/demo/myfork.exe Detaching after fork from child process 5316. (gdb) p num=1 $2 = 1 (gdb) c Continuing. this is child,pid = 5376

通过执行如下命令,我们可以轻松了解到当前调试环境中 follow-fork-mode 选项的值:

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

GDB detach-on-fork选项

注意,借助 follow-fork-mode 选项,我们只能选择调试子进程还是父进程,且一经选定,调试过程中将无法改变。如果既想调试父进程,又想随时切换并调试某个子进程,就需要借助 detach-on-fork 选项。

detach-on-fork 选项的语法格式如下:

(gdb) set detach-on-fork mode

其中,mode 参数的可选值有 2 个:

on:默认值,表明 GDB 只调试一个进程,可以是父进程,或者某个子进程;off:程序中出现的每个进程都会被 GDB 记录,我们可以随时切换到任意一个进程进行调试。

和 detach-on-fork 搭配使用的,还有如表 1 所示的几个命令。

命令语法格式功 能(gdb)show detach-on-fork查看当前调试环境中 detach-on-fork 选项的值。(gdb) info inferiors查看当前调试环境中有多少个进程。其中,进程 id 号前带有 * 号的为当前正在调试的进程。(gdb) inferiors id切换到指定 ID 编号的进程对其进行调试。(gdb) detach inferior id断开 GDB 与指定 id 编号进程之间的联系,使该进程可以独立运行。不过,该进程仍存在 info inferiors 打印的列表中,其 Describution 列为 ,并且借助 run 仍可以重新启用。(gdb) kill inferior id断开 GDB 与指定 id 编号进程之间的联系,并中断该进程的执行。不过,该进程仍存在 info inferiors 打印的列表中,其 Describution 列为 ,并且借助 run 仍可以重新启用。remove-inferior id彻底删除指令 id 编号的进程(从 info inferiors 打印的列表中消除),不过在执行此操作之前,需先使用 detach inferior id 或者 kill inferior id 命令将该进程与 GDB 分离,同时确认其不是当前进程。

除表 1 罗列的这几个命令,GDB 调试其提供有其它的一些命令,由于不常用,这里不再罗列,读者可前往 GDB官网自行查看。

这里仍以调试 myfork.c 程序为例,不过为了让读者清楚地感受 detach-on-fork 选项的功能,这里需要对 else 语句块的代码进行如下修改:

else { int mnum=5; while(mnum==5){ sleep(1); } printf("this is parent,pid = %d\n",getpid()); }

也就是说,myfork.c 程序中,父进程和子进程中各拥有一个死循环。

在此基础上,进行如下调试:

(gdb) set detach-on-fork off



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有