hfcrwx

Home

debug

Published Mar 07, 2022

Linux C/C++代码调试

前言

https://github.com/SimpleSoft-2020/book_debug.git

第1章 C/C++调试基本知识

1.1 BUG与Debug

发现问题的根源比想出解决方案更重要。找到问题的根源是一种技能,也更有价值。

1.2 为什么选择C/C++

本书主要介绍如何调试C/C++代码,如果掌握了本书的调试方法与技巧,其他语言的调试也能够驾轻就熟。

如果掌握了VC软件的调试方法与技巧,就可以轻松地迁移到Dev-C上进行调试,甚至能够平滑地迁移到Eclipse中调试Java代码—尽管界面有所不同,但是软件调试的核心是相同的。

第3章 Linux系统gdb调试基本功能

3.1 Linux C/C++编程基本知识

无论在哪里编写代码,最后都要到Linux系统中编译、调试和运行。

3.1.1 开发环境安装

在Linux系统中开发C/C++程序时,我们用到的工具一般是gcc和g++。gcc和g++既有区别,又有联系,下面进行简单介绍。

GCC(GNU Compiler Collection)即GNU编译工具集,有编译器、链接器、组装器等,主要用来编译C和C++语言,也可以编译Objective-C和Objective-C++程序。注意,这里的GCC是大写的,代表的是GNU编译工具集。

而gcc(GNU C Compiler)(注意,这里是小写的)代表的是GNU C语言编译器;g++代表的是GNU C++语言编译器。但是从本质上讲,gcc和g++并不是真正的编译器,它们也只是GCC里面的两个工具,在编译C/C++程序时,它们会调用真正的编译器对代码进行编译。可以简单地这样理解:gcc工作的时候会调用C编译器;g++工作的时候会调用C++编译器。二者的部分区别如下。

·文件后缀名的处理方式不同:gcc会将后缀为.c的文件当作C程序,将后缀为.cpp的文件当作C++程序;g++会将后缀为.c和.cpp的都当成C++程序。因为C和C++语法上有一些区别,所以有时候通过g++编译的程序不一定能通过gcc编译。要注意的是,gcc和g++都可以用来编译C和C++代码。

·链接方式不同:gcc不会自动链接C++库(比如STL标准库),g++会自动链接C++库。

·预处理器宏不同:g++会自动添加一些预处理器宏,比如__cplusplus,但是gcc不会。所以,如果要开发纯C语言的程序,可以使用gcc;如果要开发C/C++程序,而且还要使用STL标准库。为了开发的便利性,建议使用g++。

1.CentOS上安装gcc和g++

yum -y install gcc gcc-c++
gcc -v
g++ -v

2.在Ubuntu上安装gcc和g++

apt-get -y install gcc g++

3.1.2 开发第一个C/C++程序

1.使用C语言编写HelloWorld程序

helloworld.c

#include <stdio.h>

int main() {
  printf("hello, world\n");
  return 0;
}

gcc -o helloworld helloworld.c
g++ -o helloworld helloworld.c

2.使用C++语言编写HelloWorld程序

helloworldplus.cpp

#include <iostream>

int main() {
  std::cout << "hello, world" << std::endl;
  return 0;
}

g++ -o helloworldplus helloworldplus.cpp
gcc -o helloworldplus helloworldplus.cpp -lstdc++
gcc -c helloworldpuls.cpp

3.使用Makefile

目标:依赖关系
	命令
	命令

注意,依赖关系和命令可以书写为一行,命令之间要用分号隔开。一般采用写为多行的方式,以便查看。写为多行时,命令前面的空白不是空格,而是按下Tab键形成的。

Makefile的目标文件命名为main.o、student.o,这与gcc编译结果的真实目标文件是一致的,但这不是必需的,目标文件可以任意命名,只要符合Makefile的命名规则即可

3.2 gdb简介

3.2.1 gdb的安装

gdb -v

3.2.2 gdb常用功能概览

支持的功能 描述
断点管理 设置断点、查看断点等
调试执行 逐语句、逐过程执行
查看数据 在调试状态下查看变量数据、内存数据等
运行时修改变量值 在调试状态下修改某个变量的值
显示源代码 查看源代码信息
搜索源代码 对源代码进行查找
调用堆栈管理 查看堆栈信息
线程管理 调试多线程程序,查看线程信息
进程管理 调试多个进程
崩溃转储(core dump)分析 分析 core dump 文件
调试启动方式 用不同方式调试进程,比如加载参数启动、附加到进程等

3.3 调试执行

3.3.1 启动调试

chapter_3.3

gdb chapter_3.3
[root@iZ2ze7qslbwa07f03lfmegZ chapter_3.3]# gdb chapter_3.3
GNU gdb (GDB) Red Hat Enterprise Linux 8.3-3.el7
Copyright (C) 2019 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".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from chapter_3.3...
(gdb) list
198			int throw_num = 50;
199			printf("throw\n");
200	                throw 10;
201	        }
202	        catch(...)
203	        {
204			int catch_num = 100;
205	                printf("catch ...\n");
206	        }
(gdb) r
Starting program: /root/book_debug/chapter_3.3/chapter_3.3 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
name is test_try_catch,10
throw
catch ...
[New Thread 0x7ffff6efd700 (LWP 5852)]
线程函数开始
启动新线程:0
[New Thread 0x7ffff66fc700 (LWP 5853)]
线程函数开始
启动新线程:1
[New Thread 0x7ffff5efb700 (LWP 5854)]
线程函数开始
启动新线程:2
[New Thread 0x7ffff56fa700 (LWP 5855)]
线程函数开始
启动新线程:3
[New Thread 0x7ffff4ef9700 (LWP 5856)]
启动新线程:4
线程函数开始
[New Thread 0x7ffff46f8700 (LWP 5857)]
启动新线程:5
线程函数开始
[New Thread 0x7ffff3ef7700 (LWP 5858)]
线程函数开始
启动新线程:6
[New Thread 0x7ffff36f6700 (LWP 5859)]
线程函数开始
启动新线程:7
[New Thread 0x7ffff2ef5700 (LWP 5860)]
线程函数开始
启动新线程:8
[New Thread 0x7ffff26f4700 (LWP 5861)]
线程函数开始
启动新线程:9
线程函数结束
线程函数结束
[Thread 0x7ffff6efd700 (LWP 5852) exited]
[Thread 0x7ffff66fc700 (LWP 5853) exited]
线程函数结束
[Thread 0x7ffff5efb700 (LWP 5854) exited]
线程函数结束
[Thread 0x7ffff56fa700 (LWP 5855) exited]
线程函数结束
[Thread 0x7ffff4ef9700 (LWP 5856) exited]
线程函数结束
[Thread 0x7ffff46f8700 (LWP 5857) exited]
线程函数结束
[Thread 0x7ffff3ef7700 (LWP 5858) exited]
线程函数结束
[Thread 0x7ffff36f6700 (LWP 5859) exited]
线程函数结束
[Thread 0x7ffff2ef5700 (LWP 5860) exited]
线程函数结束
level is 1,str is call_fun_test_1,name is call_fun_test_1
level is 2,str is call_fun_test_2,name is call_fun_test_2
str is test,number is 305419896,node id is 100,test end
0 1 2 3 4 5 6 7 8 9 this is a test string arr test done
a is 10,x is 100,str is test
quit fun
execute test_fun_x
test fun x
execute test_fun_x
test fun x
execute test_fun_x
test fun x
execute test_fun_x
test fun x
execute test_fun_x
test fun x
execute test_fun_x
test fun x
execute test_fun_x
test fun x
execute test_fun_x
test fun x
execute test_fun_x
test fun x
execute test_fun_x
test fun x
i is 10
str is test
test_1 test_fun
test_2 test_fun
传入的参数信息为:
参数 0=/root/book_debug/chapter_3.3/chapter_3.3
会员管理系统
1:录入会员信息
q:退出
[Thread 0x7ffff26f4700 (LWP 5861) exited]
q
[Inferior 1 (process 5848) exited normally]
(gdb) q
[root@iZ2ze7qslbwa07f03lfmegZ chapter_3.3]#

问题

Missing separate debuginfos, use: debuginfo-install glibc-2.17-325.el7_9.x86_64

解决

// yum install glibc-2.17-325.el7_9.x86_64
yum install yum-utils
debuginfo-install glibc-2.17-325.el7_9.x86_64

问题

[root@iZ2ze7qslbwa07f03lfmehZ ~]# debuginfo-install glibc-2.17-325.el7_9.x86_64
Loaded plugins: fastestmirror
Loading mirror speeds from cached hostfile
Could not find debuginfo for main pkg: glibc-2.17-325.el7_9.x86_64
Could not find debuginfo pkg for dependency package nss-softokn-freebl-3.67.0-3.el7_9.x86_64
No debuginfo packages available to install

解决

创建 /etc/yum.repos.d/CentOS-Debuginfo.repo

# CentOS-Debug.repo
#
# The mirror system uses the connecting IP address of the client and the
# update status of each mirror to pick mirrors that are updated to and
# geographically close to the client.  You should use this for CentOS updates
# unless you are manually picking other mirrors.
#

# All debug packages from all the various CentOS-7 releases
# are merged into a single repo, split by BaseArch
#
# Note: packages in the debuginfo repo are currently not signed
#

[base-debuginfo]
name=CentOS-7 - Debuginfo
baseurl=http://debuginfo.centos.org/7/$basearch/
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-Debug-7
enabled=1
#
yum clean all
yum makecache
debuginfo-install glibc-2.17-325.el7_9.x86_64

问题

warning: File "/usr/local/lib64/libstdc++.so.6.0.26-gdb.py" auto-loading has been declined by your `auto-load safe-path' set to "$debugdir:$datadir/auto-load:/usr/share/gdb/auto-load".
To enable execution of this file add
	add-auto-load-safe-path /usr/local/lib64/libstdc++.so.6.0.26-gdb.py
line to your configuration file "/root/.gdbinit".
To completely disable this security protection add
	set auto-load safe-path /
line to your configuration file "/root/.gdbinit".
For more information about this security protection see the
"Auto-loading safe path" section in the GDB manual.  E.g., run from the shell:
	info "(gdb)Auto-loading safe path"

解决

/root/.gdbinit

add-auto-load-safe-path /usr/local/lib64/libstdc++.so.6.0.26-gdb.py

3.3.2 启动调试并添加参数

gdb chapter_3.3
set args admin passwd
r

3.3.3 附加到进程

/chapter_3.3
[root@iZ2ze7qslbwa07f03lfmegZ chapter_3.3]# ps aux | grep chapter_3.3
root      6170  0.0  0.0  48564  1764 pts/2    S+   16:06   0:00 ./chapter_3.3
root      6238  0.0  0.0 112816   996 pts/0    S+   16:08   0:00 grep --color=auto chapter_3.3
[root@iZ2ze7qslbwa07f03lfmegZ chapter_3.3]# gdb attach 6170

此时,可以在gdb中输入gdb相关的命令,比如设置断点等。此时整个chapter_3.3程序处于暂停状态,需要在gdb中输入命令c继续运行,程序才能恢复为正常状态。我们在gdb中输入c,使程序继续运行。然后在chapter_3.3窗口输入q退出程序,此时,gdb也能检测到程序已经退出。然后在gdb窗口输入q,退出gdb调试。

3.4 断点管理

3.4.1 设置断点

普通断点、条件断点、数据断点

1.在源代码的某一行设置断点

break 文件名:行号
break chapter_3.3.cpp:49
run
list
print

2.为函数设置断点

break 函数名
break add_member
c
1

如果有多个函数名相同,只是参数不同,为同名函数设置断点会怎样呢?gdb会为所有同名函数都设置断点,这一点其实很重要,尤其是在C++的函数重载中,因为只看代码很难区分到底会调用哪一个函数。但是为函数设置断点后,就不用担心到底会执行哪一个函数。因为每个函数都会被设置断点,无论是哪一个函数被调用,都会命中。

b test_fun
b test_1::test_fun
b test_fun()

3.使用正则表达式设置函数断点

rb 正则表达式
rbreak 正则表达式
rb test_fun*

4.通过偏移量设置断点

当前代码执行到某一行时,如果要为当前代码行的前面某一行或者后面某一行设置断点,就可以使用这个功能来达到快速设置断点的目的。

b +偏移量
b -偏移量
Thread 1 "chapter_3.3" hit Breakpoint 10, main (argc=1, argv=0x7fffffffe0e8) at chapter_3.3.cpp:232
232		test_fun(10);
(gdb) b +5
Breakpoint 11 at 0x401a93: file chapter_3.3.cpp, line 237.
(gdb) b -5
Breakpoint 12 at 0x401a21: file chapter_3.3.cpp, line 228.
(gdb) b -4
Note: breakpoint 12 also set at pc 0x401a21.
Breakpoint 13 at 0x401a21: file chapter_3.3.cpp, line 228.

5.设置条件断点

b 断点 条件
b chapter_3.3.cpp:90 if i==900
info b
r
print i
b cond_fun_test if a==10
b cond_fun_test if str="test"

6.在指令地址上设置断点

如果调试程序没有符号信息,而我们又想在某些地方设置断点时,则可以使用在指令地址上设置断点的功能。语法如下:

b *指令地址

先使用无调试符号的方式生成可执行文件。对Makefile稍做修改,去除-g选项,使得生成的可执行文件不包含调试符号信息。启动gdb并调试chapter_3.3,然后在测试函数cond_fun_test上设置一个断点。因为没有调试符号信息,所以第一步先获得cond_fun_test函数的地址,执行下述命令:

(gdb) p cond_fun_test
$1 = {<text variable, no debug info>} 0x4014b1 <cond_fun_test(int, char const*)>
(gdb) b *0x4014b1
Breakpoint 1 at 0x4014b1
(gdb) 

7.设置临时断点

顾名思义,临时断点是指这个断点是临时的,只命中一次,然后会被自动删除,后续即使代码被多次调用也不会再次命中。语法如下:

tbreak 断点
tb 断点
(gdb) tb test_fun_x
Temporary breakpoint 1 at 0x401465: file chapter_3.3.cpp, line 84.
(gdb) info b
Num     Type           Disp Enb Address            What
1       breakpoint     del  y   0x0000000000401465 in test_fun_x() at chapter_3.3.cpp:84
(gdb) r
(gdb) info b
No breakpoints or watchpoints.

3.4.2 启用/禁用断点

如果一个断点被禁用,则该断点不会被命中,但是它仍然会在断点列表中显示。我们仍然可以通过info b来查看被禁用的断点,也可以通过启用断点命令来重新启用被禁用的断点。

disable 断点编号
enable 断点编号

在禁用断点后,断点的Enb标志变成n,启用以后又恢复为y。

也可以对一个范围内的断点执行启用或禁用操作,比如禁用编号为4~10的断点,则可以使用下述命令:

disable 4-10

启用断点也是一样的。例如要启用编号为4~10的断点,则可以使用下述命令:

enable 4-10

3.4.3 启用断点一次

这是启用断点的一种变化用法,在启用断点时,可以只启用一次,命中一次后会自动禁用,不会再次命中。它与临时断点相似,临时断点只会命中一次,命中一次之后就会自动删除。启用断点一次的不同之处在于断点启用后,虽然只会命中一次,但是不会被删除,而是被禁用。

enable once 断点编号
b test_fun_x
disable 1
enable once 1
i b
r
i b

3.4.4 启用断点并删除

这同样是启用断点的一种变化用法,即如果断点被启用,当下次命中该断点后,会自动删除。该功能与临时断点相似,相当于把一个被禁用的断点转换为临时断点。语法如下:

enable delete 断点编号
b test_fun_x
disable 1
enable delete 1
i b
r
i b

3.4.5 启用断点并命中N次

这也是启用断点的一种变化用法,即启用断点后可以命中N次,但是命中N次后,该断点就会被自动禁用,不会再次命中。语法如下:

enable count 数量 断点编号
b test_fun_x
disable 1
enable count 5 1
i b
r
c
c
c
c
i b

3.4.6 忽略断点前N次命中

与条件断点类似,即在设置断点时可以指定接下来的N次命中都忽略,直到第N+1次命中时运行才暂停。语法如下:

ignore 断点编号 次数

希望在第8次被调用时能够命中,前7次的调用都被忽略,则使用以下命令:

ignore 1 7
b test_fun_x
i b
ignore 1 7
i b
r

3.4.7 查看断点

info breakpoints
info break
info b
i b
info breakpoint 2
info break 2
info b 2
i b 2

查看断点信息的命令由info和breakpoint两个命令组合而成,这两个命令有多种组合方式。info可以写为两种形式:info和i。breakpoint可以写为3种形式:breakpoint、break和b。因此,一共有6种组合形式,例如,i breakpoint也是查看断点的有效命令。

3.4.8 删除断点

1.删除所有断点:delete

delete

2.删除指定断点:delete断点编号

delete 5
delete 5 6

3.删除指定范围的断点:delete范围

delete 5-7
delete 5-7 10-12

4.删除指定函数的断点:clear函数名

clear test_fun

5.删除指定行号的断点:clear行号

clear chapter_3.3.cpp:107
clear 107

删除断点命令clear和delete是有区别的。delete命令是全局的,不受栈帧的影响;clear命令受到当前栈帧的制约,删除的是将要执行的下一处指令的断点。delete命令可以删除所有断点,包括观察点和捕获点等;clear命令不能删除观察点和捕获点。

3.5 程序执行

3.5.1 启动程序

启动程序的命令为run或者r,一般用于调试一个程序。r命令只在使用gdb启动被调试的程序时执行一次。

然后进入gdb的调试窗口,这时程序被暂停,可以执行设置启动参数、设置断点等操作。然后在gdb中输入run启动程序,直到遇到第一个命中的断点为止,程序才会中断。

3.5.2 继续运行

继续运行可以使用命令continue或者c。当程序处于中断状态时,比如已经命中断点,则可以执行continue命令恢复或者继续运行程序,直到遇到下一个断点为止,

3.5.3 继续运行并跳过当前断点N次

continue 次数

当第一次在test_fun_x中暂停时,如果想忽略接下来的8次命中(包括当前这一次),则可以使用命令:

continue 8

那么,继续执行时会忽略接下来的7次断点命中,在第9次命中的时候暂停。

Thread 1 "chapter_3.3" hit Breakpoint 1, test_fun_x () at chapter_3.3.cpp:84
84		printf("test fun x\n");
(gdb) continue 8
Will ignore next 7 crossings of breakpoint 1.  Continuing.
(gdb) i b
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x0000000000401465 in test_fun_x() at chapter_3.3.cpp:84
	breakpoint already hit 9 times

3.5.4 继续运行直到当前函数执行完成

finish

在程序启动以后,输入1进行会员信息的录入操作,此时命中断点,在函数add_member处中断。然后输入finish完成该函数的调试。程序会提示输入会员名和会员年龄。输入完成后,因为该函数执行完成,所以在调用该函数的地方中断。

3.5.5 单步执行

step
s

当进入到断点所在代码时,可以输入step或者s命令执行该行代码。如果该行代码有函数调用,会直接进入该函数内部继续执行;如果没有函数调用,则直接执行下一行。

3.5.6 逐过程执行

next
n

单步执行也可以称之为逐语句执行。逐语句和逐过程执行命令后面都可以跟一个数字参数,例如s 5或者n 5,表示往后执行5行代码,遇到s会进入函数,遇到n则不会进入函数。如果在往后执行的过程中遇到了断点,首先会在断点处中断。

3.6 查看当前函数参数

info args
i args

3.7 查看/修改变量的值

print 变量名
p 变量名

如果要修改查看到的变量值

print 变量名=值

3.7.1 使用gdb内嵌函数

在使用print或者p命令时,可以直接使用gdb内嵌的一些函数(比如C函数),比如sizeof、strcmp等,也可以使用一些常见的表达式。

(gdb) p sizeof(int)
$1 = 4
(gdb) p sizeof(long)
$2 = 8
(gdb) p sizeof(void*)
$3 = 8
(gdb) p 12 * 12
$4 = 144
(gdb) p 12 > 10
$5 = 1
(gdb) p strcmp("123", "12")
'__strcmp_sse42' has unknown return type; cast the call to its declared return type
(gdb) p (int)strcmp("123", "12")
$6 = 51
(gdb) p strlen("test string")
'__strlen_sse2_pminub' has unknown return type; cast the call to its declared return type
(gdb) p (int)strlen("test string")
$7 = 11
(gdb) p sizeof(NODE)
$8 = 64
(gdb) p sizeof(test_1)
$9 = 16
(gdb) p sizeof(test_2)
$10 = 16
(gdb) set $f1 = fopen("test.txt", "w+")
(gdb) p $f1
$1 = (_IO_FILE *) 0x41ac40
(gdb) set $str = "this is a test string\n"
(gdb) p $str
$2 = "this is a test string\n"
(gdb) set $write_size = fwrite($str, 1, strlen($str), $f1)
(gdb) p $write_size
$3 = 22
(gdb) set $res = fclose($f1)
(gdb) p $res
$4 = 0
[root@iZ2ze7qslbwa07f03lfmegZ chapter_3.3]# cat test.txt
this is a test string

3.7.2 查看结构体/类的值

(gdb) b 42
(gdb) r
(gdb) p new_node
$1 = (NODE *) 0x41c290
(gdb) p new_node->ID
$2 = 0
(gdb) p new_node->Name
$3 = "hello\000,\367\377\177", '\000' <repeats 29 times>
(gdb) p new_node->age
$4 = 1
(gdb) p *new_node
$5 = {ID = 0, Name = "hello\000,\367\377\177", '\000' <repeats 29 times>, age = 1, prev = 0x41b880, next = 0x0}

在gdb中输入set print null-stop命令,设置字符串的显示规则,即遇到结束符时停止显示。通过设置之后,再次执行p*new_node命令,Name部分不会再显示空字符

(gdb) set print null-stop
(gdb) show print null-stop
Printing of char arrays to stop at first null char is on.
(gdb) p *new_node
$6 = {ID = 0, Name = "hello", age = 1, prev = 0x41b880, next = 0x0}

如果结构体的成员比较多,这种显示仍然会杂乱无章,不方便查看每一个成员的数据,也就是说还不够漂亮(pretty)。gdb还提供了一个使显示更加漂亮的选项,命令为set printpretty。

(gdb) set print pretty
(gdb) p *new_node
$7 = {
  ID = 0, 
  Name = "hello", 
  age = 1, 
  prev = 0x41b880, 
  next = 0x0
}
(gdb) p *test
$8 = {
  _vptr.test_1 = 0x404328 <vtable for test_1+16>, 
  x = 10, 
  y = 100
}

3.7.3 查看数组

(gdb) b print_arr_test
Breakpoint 1 at 0x4014fc: file chapter_3.3.cpp, line 104.
(gdb) r
Thread 1 "chapter_3.3" hit Breakpoint 1, print_arr_test () at chapter_3.3.cpp:104
104		int iarr[]={0,1,2,3,4,5,6,7,8,9};
(gdb) n
105		const char *strarr[]={"this","is","a","test","string"};
(gdb) n
106		for(unsigned long i=0;i<sizeof(iarr)/sizeof(int);i++)
(gdb) p iarr
$1 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
(gdb) p strarr
$2 = {0x40410a "this", 0x40410f "is", 0x404112 "a", 0x404114 "test", 0x404119 "string"}

控制数组显示的命令为set print array on,其中on可以省略,默认情况下为off,因此在gdb中执行set print array命令,以便能够在显示数组时更方便查看。

(gdb) set print array on
(gdb) p iarr
$5 =   {0,
  1,
  2,
  3,
  4,
  5,
  6,
  7,
  8,
  9}
(gdb) p strarr
$6 =   {0x40410a "this",
  0x40410f "is",
  0x404112 "a",
  0x404114 "test",
  0x404119 "string"}

3.8 自动显示变量的值

这对于需要观察那些不停变化的变量值来说,使用p命令就不太方便了,因为需要使用多次。gdb还有另外一个display命令,每次程序暂停都可以自动显示变量值。语法如下:

display 变量名

后面可以跟多个变量名,比如display {var1,var2,var3}。

(gdb) b cond_fun_test
(gdb) r
Thread 1 "chapter_3.3" hit Breakpoint 1, cond_fun_test (a=10, str=0x404114 "test") at chapter_3.3.cpp:97
97		int x = a * a;
(gdb) display x
1: x = 0
(gdb) n
98		printf("a is %d,x is %d,str is %s\n",a,x,str);
1: x = 100
(gdb) n
a is 10,x is 100,str is test
99		x *=2;
1: x = 100
(gdb) n
100		printf("quit fun\n");
1: x = 200

如果display命令后面跟多个变量名,则必须要求这些变量的长度相同(比如都是整型变量)。如果长度不相同,则需要分开使用。可以在gdb中输入info display命令来查看已经设置的自动显示的变量信息。

(gdb) display {x, a, str}
1: {x, a, str} = <error: array elements must all be the same size>
(gdb) display {x, a}
2: {x, a} = {0, 10}
(gdb) display str
3: str = 0x404114 "test"
(gdb) n
98		printf("a is %d,x is %d,str is %s\n",a,x,str);
1: {x, a, str} = <error: array elements must all be the same size>
2: {x, a} = {100, 10}
3: str = 0x404114 "test"
(gdb) info display
Auto-display expressions now in effect:
Num Enb Expression
1:   y  {x, a, str}
2:   y  {x, a}
3:   y  str
undisplay 1
delete display 1
undisplay
delete display

除了删除自动显示,还可以暂时禁用自动显示,在需要的时候可以再次启用某些变量的自动显示。

disable display 1
enable display 1

3.9 显示源代码

因为加上编译选项-g后,生成的可执行文件中包含调试信息,并且保存了对应的源文件信息(只是保存了源文件名等信息),所以在查看源代码时,要确保对应的源文件存在,否则无法查看。

l
l -
set listsize 20
list add_member
list 文件名:行号
list 行号

3.10 查看内存

x /选项 地址
b 140
r
(gdb) p str
$1 = 0x404114 "test"
(gdb) x str
0x404114:	0x74736574
(gdb) x /s str
0x404114:	"test"
(gdb) x /d str
0x404114:	116
(gdb) x /4d str
0x404114:	116	101	115	116
(gdb) p 0x12345678
$1 = 305419896
(gdb) p number
$2 = 305419896
(gdb) p &number
$3 = (int *) 0x7fffffffdf74
(gdb) x /4x &number
0x7fffffffdf74:	0x78	0x56	0x34	0x12
(gdb) x /4x 0x7fffffffdf74
0x7fffffffdf74:	0x78	0x56	0x34	0x12
struct TEST_NODE {
  char gender[3];
  int ID;
  char name[7];
};
(gdb) x /16s node
0x41c320:	"男"
0x41c324:	"d"
0x41c326:	""
0x41c327:	""
0x41c328:	"海洋"
0x41c32f:	""
0x41c330:	"\364\026@"
0x41c334:	""
0x41c335:	""
0x41c336:	""
0x41c337:	""
0x41c338:	"Q\002"
0x41c33b:	""
0x41c33c:	""
0x41c33d:	""
0x41c33e:	""
(gdb) p sizeof(TEST_NODE)
$4 = 16

3.11 查看寄存器

寄存器是CPU内部用来存放数据的一些区域,是CPU内部的高速存储单元,用来临时存放一些参与计算的数据,比如函数参数、程序指针等。CPU可以直接操作寄存器中的值,且速度要比访问内存快得多。

寄存器主要分为通用寄存器、指针寄存器、段寄存器和标志寄存器等。

·通用寄存器(General Purpose Register):尽管通用寄存器是通用的,可以存储任意数据,但大多时候主要用来存储操作数和运算结果等信息。比如,32位通用寄存器对应的为EAX、EBX、ECX、EDX、ESP、EBP、ESI、EDI;64位通用寄存器对应的为RAX、RBX、RCX、RDX、RSP、RBP、RSI、RDI等。其中,EBP(RBP)是基指针寄存器,可以直接访问栈中的数据;ESP(RSP)是栈指针寄存器;只能访问栈顶的数据。

·指针寄存器(Pointer Register):又称为指令寄存器,用来存放指令指针。32位为EIP,64位为RIP。

·段寄存器(Segment Register):用来存储段数据的段值,比如存储数据段的段值、代码段的段值等。段寄存器主要有CS(代码段)、DS(数据段)、ES(附加段)、FS(通用段)、GS(通用段)、SS(栈段)。

·标志寄存器(RFLAGS Register):显示程序状态的寄存器,主要有CF(Carry Flag,进位或者错位)、AF(Adjust Flag,辅助进位)、ZF(Zero Flag,零标志)等。

在gdb中,指令寄存器$rip指向当前执行的代码位置,栈指针寄存器$rsp指向当前栈顶,通用寄存器会存储一些变量的值、函数参数以及函数返回值等。

我们在编译时删除-g参数

b cond_fun_test
r
(gdb) p a
$2 = {i = {0, 1045149306}, d = 1.2904777690891933e-08}
(gdb) p str
No symbol "str" in current context.

一般情况下,函数的参数会存放在寄存器中,所以我们用查看寄存器的方式来查看传递的参数是什么。查看寄存器的命令如下:

info registers
i r

也可以在r后面指定寄存器的名称,比如rax、rbx等,使其只显示特定寄存器的值。而下述命令则显示所有寄存器的值,包括浮点寄存器等。

info all-registers

第一个参数存储在寄存器rdi中,第二个参数存储在寄存器rsi中。

(gdb) i r rdi
rdi            0xa                 10
(gdb) i r rsi
rsi            0x404114            4210964
(gdb) x /s $rsi
0x404114:	"test"
(gdb) x /s 0x404114
0x404114:	"test"

rdi, rsi, rdx, rcx, r8, r9

3.12 查看调用栈

当程序进行函数调用时,这些调用信息(比如在哪里调用等)称为栈帧。每一个栈帧的内容还包括调用函数的参数、局部变量等。所有栈帧组成的信息称为调用栈(或者调用堆栈)。

当程序刚开始运行时,只有一个栈帧,即主函数main。每调用一个函数,就产生一个新的栈帧;当函数调用结束时(即从函数返回后),该函数的调用随之结束,该栈帧也结束。如果该函数是一个递归函数,则调用该函数会产生多个栈帧。

3.12.1 查看栈回溯信息

查看栈回溯信息的命令是backtrace。执行该栈回溯命令后,会显示程序执行到什么位置、包含哪些帧等信息。每一帧都有一个编号,从0开始。0表示当前正在执行的函数,1表示调用当前函数的函数,以此类推。栈回溯是倒序排列的。下面来演示backtrace命令的用法。

b call_fun_test_2
r
backtrace

也可以执行命令来查看指定数量的栈帧:

bt 栈帧数量

这对于调用栈帧比较多的情况很有用处,可以忽略掉不太关心的那些栈帧。比如执行bt 2,则只显示两个栈帧。

bt 2

如果想查看1和2这两个帧应该怎样做呢?可以使用命令bt -2。

bt -2

如果bt后面跟的是一个正数,则从0开始计数。如果是一个负数,则从最大的栈帧编号开始倒序计数,但是最后显示时还是按照从小到大的编号顺序显示,只是显示的栈帧不同。比如一共有10个帧,编号为0~9,如果执行bt 4,则显示的帧为0~3;如果执行命令bt-4,则显示的帧编号为6~9。

3.12.2 切换栈帧

可以通过“frame栈帧号”的方式来切换栈帧。为什么要切换栈帧呢?因为每一个栈帧所对应的程序的运行上下文都不同,比如栈帧1的局部变量和栈帧2的局部变量都不相同,只有切换到某个具体的栈帧之后才能查看该栈帧对应的局部变量信息。比如栈回溯中,共有3个栈帧,我们想查看栈帧号为2(也就是main函数中所对应)的信息,则执行命令即可切换到2号帧:

frame 2

或者

f 2

这时我们可以查看该帧对应的一些变量信息,比如局部变量number和name的值。

(gdb) bt
#0  call_fun_test_2 (level=2, str=0x404167 "call_fun_test_2") at chapter_3.3.cpp:145
#1  0x00000000004016ed in call_fun_test_1 (level=1, str=0x40419a "call_fun_test_1") at chapter_3.3.cpp:156
#2  0x00000000004019fb in main (argc=1, argv=0x7fffffffe108) at chapter_3.3.cpp:218
(gdb) f 2
#2  0x00000000004019fb in main (argc=1, argv=0x7fffffffe108) at chapter_3.3.cpp:218
218		call_fun_test_1(1,"call_fun_test_1");
(gdb) p number
$1 = 100
(gdb) p name
$2 = 0x40422b "main"
(gdb) f 1
#1  0x00000000004016ed in call_fun_test_1 (level=1, str=0x40419a "call_fun_test_1") at chapter_3.3.cpp:156
156		call_fun_test_2(level + 1,"call_fun_test_2");
(gdb) p number
$3 = 101
(gdb) p name
$4 = 0x40419a "call_fun_test_1"

除使用print查看局部变量外,还可以使用info locals来查看当前帧的所有局部变量的值,也可以使用info args来查看当前帧所有的函数参数。

(gdb) info locals
number = 101
name = 0x40419a "call_fun_test_1"
(gdb) info args
level = 1
str = 0x40419a "call_fun_test_1"

还可以使用命令up和down来切换帧。up和down都是基于当前帧来计数的。比如,当前帧号为1,up 1则切换到2号帧,down 1则切换到0号帧。

(gdb) f 1
#1  0x00000000004016ed in call_fun_test_1 (level=1, str=0x40419a "call_fun_test_1") at chapter_3.3.cpp:156
156		call_fun_test_2(level + 1,"call_fun_test_2");
(gdb) up 1
#2  0x00000000004019fb in main (argc=1, argv=0x7fffffffe108) at chapter_3.3.cpp:218
218		call_fun_test_1(1,"call_fun_test_1");
(gdb) info locals
t1 = 0x0
t3 = {x = 4201439, y = 0}
number = 100
name = 0x40422b "main"
test = 0x403d8d <__libc_csu_init+77>
test2 = 0x2
test3 = {<test_1> = {_vptr.test_1 = 0xe9000004a0, x = 65535, y = 1}, <No data fields>}
(gdb) down 2
#0  call_fun_test_2 (level=2, str=0x404167 "call_fun_test_2") at chapter_3.3.cpp:145
145		int number = 102;
(gdb) i locals
number = 10
name = 0x41c310 "\220"

还可以使用以下命令来切换帧:

f 帧地址

其中,帧地址是栈帧所对应的地址。如果程序崩溃,栈回溯信息可能会遭到破坏,这时就可以使用该命令来进行栈帧切换。

3.12.3 查看帧信息

可以使用info frame命令(包括前面介绍的info locals和infoargs命令)来查看帧的详细信息,还可以使用info frame命令来查看具体的某一帧的详细信息。比如要查看编号为1的帧的详细信息,可以直接使用info frame 1(可以简写为i f 1)命令,而不用先进行帧的切换操作。

Thread 1 "chapter_3.3" hit Breakpoint 1, call_fun_test_2 (level=2, str=0x404167 "call_fun_test_2")
    at chapter_3.3.cpp:145
145		int number = 102;
(gdb) bt 
#0  call_fun_test_2 (level=2, str=0x404167 "call_fun_test_2") at chapter_3.3.cpp:145
#1  0x00000000004016ed in call_fun_test_1 (level=1, str=0x40419a "call_fun_test_1") at chapter_3.3.cpp:156
#2  0x00000000004019fb in main (argc=1, argv=0x7fffffffe0e8) at chapter_3.3.cpp:218
(gdb) i f 1
Stack frame at 0x7fffffffdf90:
 rip = 0x4016ed in call_fun_test_1 (chapter_3.3.cpp:156); saved rip = 0x4019fb
 called by frame at 0x7fffffffe010, caller of frame at 0x7fffffffdf60
 source language c++.
 Arglist at 0x7fffffffdf80, args: level=1, str=0x40419a "call_fun_test_1"
 Locals at 0x7fffffffdf80, Previous frame's sp is 0x7fffffffdf90
 Saved registers:
  rbp at 0x7fffffffdf80, rip at 0x7fffffffdf88
(gdb) info frame 2
Stack frame at 0x7fffffffe010:
 rip = 0x4019fb in main (chapter_3.3.cpp:218); saved rip = 0x7ffff6f20555
 caller of frame at 0x7fffffffdf90
 source language c++.
 Arglist at 0x7fffffffe000, args: argc=1, argv=0x7fffffffe0e8
 Locals at 0x7fffffffe000, Previous frame's sp is 0x7fffffffe010
 Saved registers:
  rbx at 0x7fffffffdff8, rbp at 0x7fffffffe000, rip at 0x7fffffffe008

帧的详细信息包括帧地址、rip地址、函数名、函数参数等信息。这里可以用f 帧地址命令来切换帧地址,前面在3.12.2节中提到过。这个帧地址也可以用到i f命令中,比如使用i f 0x7fffffffe400 可以查看2号帧的详细信息。

3.13 线程管理

(gdb) b 179
Breakpoint 1 at 0x401813: file chapter_3.3.cpp, line 179.
(gdb) r
Starting program: /root/book_debug/chapter_3.3/chapter_3.3 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
name is test_try_catch,10
throw
catch ...
[New Thread 0x7ffff6efd700 (LWP 13497)]
线程函数开始
启动新线程:0
[New Thread 0x7ffff66fc700 (LWP 13498)]
线程函数开始
启动新线程:1
[New Thread 0x7ffff5efb700 (LWP 13499)]
线程函数开始
启动新线程:2
[New Thread 0x7ffff56fa700 (LWP 13500)]
线程函数开始
启动新线程:3
[New Thread 0x7ffff4ef9700 (LWP 13501)]
线程函数开始
启动新线程:4
[New Thread 0x7ffff46f8700 (LWP 13502)]
线程函数开始
启动新线程:5
[New Thread 0x7ffff3ef7700 (LWP 13503)]
线程函数开始
启动新线程:6
[New Thread 0x7ffff36f6700 (LWP 13504)]
线程函数开始
启动新线程:7
[New Thread 0x7ffff2ef5700 (LWP 13505)]
线程函数开始
启动新线程:8
[New Thread 0x7ffff26f4700 (LWP 13506)]
线程函数开始
启动新线程:9

Thread 1 "chapter_3.3" hit Breakpoint 1, start_threads (thread_num=10) at chapter_3.3.cpp:179
warning: Source file is more recent than executable.
179	    for (auto& thread : threads)

3.13.1 查看所有线程信息

(gdb) info threads
  Id   Target Id                                       Frame 
* 1    Thread 0x7ffff7feb740 (LWP 13493) "chapter_3.3" start_threads (thread_num=10) at chapter_3.3.cpp:179
  2    Thread 0x7ffff6efd700 (LWP 13497) "chapter_3.3" 0x00007ffff7bcde9d in nanosleep ()
    at ../sysdeps/unix/syscall-template.S:81
  3    Thread 0x7ffff66fc700 (LWP 13498) "chapter_3.3" 0x00007ffff7bcde9d in nanosleep ()
    at ../sysdeps/unix/syscall-template.S:81
  4    Thread 0x7ffff5efb700 (LWP 13499) "chapter_3.3" 0x00007ffff7bcde9d in nanosleep ()
    at ../sysdeps/unix/syscall-template.S:81
  5    Thread 0x7ffff56fa700 (LWP 13500) "chapter_3.3" 0x00007ffff7bcde9d in nanosleep ()
    at ../sysdeps/unix/syscall-template.S:81
  6    Thread 0x7ffff4ef9700 (LWP 13501) "chapter_3.3" 0x00007ffff7bcde9d in nanosleep ()
    at ../sysdeps/unix/syscall-template.S:81
  7    Thread 0x7ffff46f8700 (LWP 13502) "chapter_3.3" 0x00007ffff7bcde9d in nanosleep ()
    at ../sysdeps/unix/syscall-template.S:81
  8    Thread 0x7ffff3ef7700 (LWP 13503) "chapter_3.3" 0x00007ffff7bcde9d in nanosleep ()
    at ../sysdeps/unix/syscall-template.S:81
  9    Thread 0x7ffff36f6700 (LWP 13504) "chapter_3.3" 0x00007ffff7bcde9d in nanosleep ()
    at ../sysdeps/unix/syscall-template.S:81
  10   Thread 0x7ffff2ef5700 (LWP 13505) "chapter_3.3" 0x00007ffff7bcde9d in nanosleep ()
    at ../sysdeps/unix/syscall-template.S:81
  11   Thread 0x7ffff26f4700 (LWP 13506) "chapter_3.3" 0x00007ffff7bcde9d in nanosleep ()
    at ../sysdeps/unix/syscall-template.S:81

当前进程共有11个线程,编号为1~11,其中1号线程前面有一个*号,表示1号线程是当前线程。每个线程信息还包含执行位置,即处于哪个文件的哪一行代码处。2~11号这10个线程都处于系统nanosleep.S文件的第81行,因为每个线程都在调用sleep函数,所以需要等待3秒。

3.13.2 切换线程

当前线程很重要,因为很多命令都是针对当前线程有效。比如,查看栈回溯的bt命令、查看栈帧的f命令等都是针对当前线程。如果想要查看某个线程堆栈的相关信息,必须要先切换到该线程。

切换线程的命令如下:

thread 线程ID

线程ID就是前面提到的线程的标号,如1~11就是线程的编号。如果想要切换到2号线程,则执行以下命令(也可以使用简写命令t 2)可以将2号线程切换为当前线程:

thread 2

再执行i threads命令查看当前线程是否已经切换到2号线程。

(gdb) t 2
[Switching to thread 2 (Thread 0x7ffff6efd700 (LWP 13497))]
#0  0x00007ffff7bcde9d in nanosleep () at ../sysdeps/unix/syscall-template.S:81
81	T_PSEUDO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS)
(gdb) i threads
  Id   Target Id                                       Frame 
  1    Thread 0x7ffff7feb740 (LWP 13493) "chapter_3.3" start_threads (thread_num=10) at chapter_3.3.cpp:179
* 2    Thread 0x7ffff6efd700 (LWP 13497) "chapter_3.3" 0x00007ffff7bcde9d in nanosleep ()
    at ../sysdeps/unix/syscall-template.S:81
  3    Thread 0x7ffff66fc700 (LWP 13498) "chapter_3.3" 0x00007ffff7bcde9d in nanosleep ()
    at ../sysdeps/unix/syscall-template.S:81
  4    Thread 0x7ffff5efb700 (LWP 13499) "chapter_3.3" 0x00007ffff7bcde9d in nanosleep ()
    at ../sysdeps/unix/syscall-template.S:81
  5    Thread 0x7ffff56fa700 (LWP 13500) "chapter_3.3" 0x00007ffff7bcde9d in nanosleep ()
    at ../sysdeps/unix/syscall-template.S:81
  6    Thread 0x7ffff4ef9700 (LWP 13501) "chapter_3.3" 0x00007ffff7bcde9d in nanosleep ()
    at ../sysdeps/unix/syscall-template.S:81
  7    Thread 0x7ffff46f8700 (LWP 13502) "chapter_3.3" 0x00007ffff7bcde9d in nanosleep ()
    at ../sysdeps/unix/syscall-template.S:81
  8    Thread 0x7ffff3ef7700 (LWP 13503) "chapter_3.3" 0x00007ffff7bcde9d in nanosleep ()
    at ../sysdeps/unix/syscall-template.S:81
  9    Thread 0x7ffff36f6700 (LWP 13504) "chapter_3.3" 0x00007ffff7bcde9d in nanosleep ()
    at ../sysdeps/unix/syscall-template.S:81
  10   Thread 0x7ffff2ef5700 (LWP 13505) "chapter_3.3" 0x00007ffff7bcde9d in nanosleep ()
    at ../sysdeps/unix/syscall-template.S:81
  11   Thread 0x7ffff26f4700 (LWP 13506) "chapter_3.3" 0x00007ffff7bcde9d in nanosleep ()
    at ../sysdeps/unix/syscall-template.S:81

确认当前线程是2号线程后,即可使用堆栈命令来查看信息。比如执行bt命令查看栈回溯信息。

(gdb) bt
#0  0x00007ffff7bcde9d in nanosleep () at ../sysdeps/unix/syscall-template.S:81
#1  0x000000000040214b in std::this_thread::sleep_for<long, std::ratio<1l, 1l> > (__rtime=...)
    at /opt/rh/devtoolset-9/root/usr/include/c++/9/thread:378
#2  0x000000000040175a in do_work (arg=0x7fffffffdf2c) at chapter_3.3.cpp:166
#3  0x00000000004039b2 in std::__invoke_impl<void, void (*)(void*), int*> (
    __f=@0x41ac30: 0x4016f4 <do_work(void*)>) at /opt/rh/devtoolset-9/root/usr/include/c++/9/bits/invoke.h:60
#4  0x0000000000403926 in std::__invoke<void (*)(void*), int*> (__fn=@0x41ac30: 0x4016f4 <do_work(void*)>)
    at /opt/rh/devtoolset-9/root/usr/include/c++/9/bits/invoke.h:95
#5  0x0000000000403895 in std::thread::_Invoker<std::tuple<void (*)(void*), int*> >::_M_invoke<0ul, 1ul> (
    this=0x41ac28) at /opt/rh/devtoolset-9/root/usr/include/c++/9/thread:244
#6  0x0000000000403850 in std::thread::_Invoker<std::tuple<void (*)(void*), int*> >::operator() (this=0x41ac28)
    at /opt/rh/devtoolset-9/root/usr/include/c++/9/thread:251
#7  0x0000000000403834 in std::thread::_State_impl<std::thread::_Invoker<std::tuple<void (*)(void*), int*> > >::_M_run (this=0x41ac20) at /opt/rh/devtoolset-9/root/usr/include/c++/9/thread:195
#8  0x0000000000403a20 in execute_native_thread_routine ()
#9  0x00007ffff7bc6ea5 in start_thread (arg=0x7ffff6efd700) at pthread_create.c:307
#10 0x00007ffff6ffcb0d in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:111

因为当前栈帧在系统nanosleep中,所以我们看到的大部分栈帧都是thread相关的函数。然后再执行finish命令退出sleep函数,回到do_work函数,这个时候就可以使用命令i locals来查看当前帧的局部变量信息。

(gdb) finish
Run till exit from #0  0x00007ffff7bcde9d in nanosleep () at ../sysdeps/unix/syscall-template.S:81
std::this_thread::sleep_for<long, std::ratio<1l, 1l> > (__rtime=...)
    at /opt/rh/devtoolset-9/root/usr/include/c++/9/thread:378
378		while (::nanosleep(&__ts, &__ts) == -1 && errno == EINTR)
(gdb) 
Run till exit from #0  std::this_thread::sleep_for<long, std::ratio<1l, 1l> > (__rtime=...)
    at /opt/rh/devtoolset-9/root/usr/include/c++/9/thread:378
do_work (arg=0x7fffffffdf2c) at chapter_3.3.cpp:167
167	    std::cout << "线程函数结束" << std::endl;
(gdb) i locals
local_data = 0

3.13.3 为线程设置断点

可以通过断点命令break或者b来为特定线程设置断点,命令语法如下:

break 断点 thread 线程ID

比如,要为2号和3号线程在代码167行处设置断点,可以使用以下命令:

b 167 thread 2
b 167 thread 3

这会为线程2和线程3在代码167行处设置断点。通过i b命令也可以发现,只有线程2和线程3会在这里命中断点,其他线程执行到这里时不会命中。

(gdb) b 167 thread 2
Breakpoint 2 at 0x40175a: file chapter_3.3.cpp, line 167.
(gdb) b 167 thread 3
Note: breakpoint 2 (thread 2) also set at pc 0x40175a.
Breakpoint 3 at 0x40175a: file chapter_3.3.cpp, line 167.
(gdb)  i b
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x0000000000401813 in start_threads(int) at chapter_3.3.cpp:179
	breakpoint already hit 1 time
2       breakpoint     keep y   0x000000000040175a in do_work(void*) at chapter_3.3.cpp:167 thread 2
	stop only in thread 2
3       breakpoint     keep y   0x000000000040175a in do_work(void*) at chapter_3.3.cpp:167 thread 3
	stop only in thread 3

3.13.4 为线程执行命令

在查看线程信息时,还可以为一个线程或者多个线程执行命令。也就是说可以为指定线程执行命令,比如为2号线程执行info args命令。为线程执行命令的语法如下:

thread apply 线程号 命令

比如,我们可以为2号和3号线程执行print命令,查看它们对应的变量local_data的值。相应的命令如下:

(gdb) thread apply 2 3 p local_data

Thread 2 (Thread 0x7ffff6efd700 (LWP 13497)):
$1 = 0

Thread 3 (Thread 0x7ffff66fc700 (LWP 13498)):
$2 = 1

或者

(gdb) thread apply 2 3 i locals

Thread 2 (Thread 0x7ffff6efd700 (LWP 13497)):
local_data = 0

Thread 3 (Thread 0x7ffff66fc700 (LWP 13498)):
local_data = 1

为使所有线程都执行命令,需要将线程号写为all,使所有线程都执行相同的命令。查看每个线程的栈回溯,即执行bt命令,

(gdb) thread apply all bt

Thread 11 (Thread 0x7ffff26f4700 (LWP 13506)):
#0  do_work (arg=0x7fffffffdf2c) at chapter_3.3.cpp:167
#1  0x00000000004039b2 in std::__invoke_impl<void, void (*)(void*), int*> (__f=@0x41c330: 0x4016f4 <do_work(void*)>) at /opt/rh/devtoolset-9/root/usr/include/c++/9/bits/invoke.h:60
#2  0x0000000000403926 in std::__invoke<void (*)(void*), int*> (__fn=@0x41c330: 0x4016f4 <do_work(void*)>) at /opt/rh/devtoolset-9/root/usr/include/c++/9/bits/invoke.h:95
#3  0x0000000000403895 in std::thread::_Invoker<std::tuple<void (*)(void*), int*> >::_M_invoke<0ul, 1ul> (this=0x41c328) at /opt/rh/devtoolset-9/root/usr/include/c++/9/thread:244
#4  0x0000000000403850 in std::thread::_Invoker<std::tuple<void (*)(void*), int*> >::operator() (this=0x41c328) at /opt/rh/devtoolset-9/root/usr/include/c++/9/thread:251
#5  0x0000000000403834 in std::thread::_State_impl<std::thread::_Invoker<std::tuple<void (*)(void*), int*> > >::_M_run (this=0x41c320) at /opt/rh/devtoolset-9/root/usr/include/c++/9/thread:195
#6  0x0000000000403a20 in execute_native_thread_routine ()
#7  0x00007ffff7bc6ea5 in start_thread (arg=0x7ffff26f4700) at pthread_create.c:307
#8  0x00007ffff6ffcb0d in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:111

Thread 10 (Thread 0x7ffff2ef5700 (LWP 13505)):
#0  do_work (arg=0x7fffffffdf2c) at chapter_3.3.cpp:167
#1  0x00000000004039b2 in std::__invoke_impl<void, void (*)(void*), int*> (__f=@0x41c030: 0x4016f4 <do_work(void*)>) at /opt/rh/devtoolset-9/root/usr/include/c++/9/bits/invoke.h:60
#2  0x0000000000403926 in std::__invoke<void (*)(void*), int*> (__fn=@0x41c030: 0x4016f4 <do_work(void*)>) at /opt/rh/devtoolset-9/root/usr/include/c++/9/bits/invoke.h:95
#3  0x0000000000403895 in std::thread::_Invoker<std::tuple<void (*)(void*), int*> >::_M_invoke<0ul, 1ul> (this=0x41c028) at /opt/rh/devtoolset-9/root/usr/include/c++/9/thread:244

用于查看所有线程栈回溯信息的命令thread apply all bt非常有用,尤其是在大型程序的调试过程中,比如死锁的调试。

3.14 其他

3.14.1 观察点

很多时候,程序只有在一些特定条件下才会出现BUG,比如某个变量的值发生变化时,或者几个因素同时发生变化时。观察点(watchpoint)或者监视点可以用来发现或者定位该类型的BUG。可以设置为监控一个变量或者一个表达式的值,当这个值或者表达式的值发生变化时程序会暂停,而不需要提前在某些地方设置断点。

在某些系统中,gdb是以软观察点的方式来实现的。通过单步执行程序的方式来监控变量的值是否发生改变,每执行一步就会检查变量的值是否发生变化。这种做法会比正常执行慢上百倍,但有时为了找到不容易发现的BUG,这是值得的。而在有些系统中(比如Linux),gdb是以硬件方式实现观察点功能,这并不会降低程序运行的速度。设置观察点的语法如下:

watch 变量或者表达式

当为变量或者一个表达式设置观察点后,该变量或者表达式的值在发生变化时,程序会发生中断,并且在变量或者表达式发生改变的地方暂停。这时可以使用各种命令查看线程或者栈帧等信息。

(gdb) watch count==5
Hardware watchpoint 1: count==5
(gdb) r
Starting program: /root/book_debug/chapter_3.3/chapter_3.3 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
name is test_try_catch,10
throw
catch ...
[New Thread 0x7ffff6efd700 (LWP 8386)]
线程函数开始
启动新线程:0
[New Thread 0x7ffff66fc700 (LWP 8387)]
线程函数开始
启动新线程:1
[New Thread 0x7ffff5efb700 (LWP 8388)]
线程函数开始
启动新线程:2
[New Thread 0x7ffff56fa700 (LWP 8389)]
线程函数开始
启动新线程:3
[New Thread 0x7ffff4ef9700 (LWP 8390)]
线程函数开始
[Switching to Thread 0x7ffff4ef9700 (LWP 8390)]

Thread 6 "chapter_3.3" hit Hardware watchpoint 1: count==5

Old value = false
New value = true
do_work (arg=0x7fffffffdf2c) at chapter_3.3.cpp:166
166	    std::this_thread::sleep_for(std::chrono::seconds(3));
(gdb) p count
$1 = 5

读取观察点的语法如下:

rwatch 变量或者表达式

当该变量或者表达式被读取时,程序会发生中断。

(gdb) rwatch count
Hardware read watchpoint 2: count
(gdb) c
Continuing.
启动新线程:4
线程函数结束
线程函数结束
[Thread 0x7ffff56fa700 (LWP 8389) exited]
[Thread 0x7ffff66fc700 (LWP 8387) exited]
线程函数结束
线程函数结束
[New Thread 0x7ffff46f8700 (LWP 8988)]
[Thread 0x7ffff5efb700 (LWP 8388) exited]
[Thread 0x7ffff6efd700 (LWP 8386) exited]
线程函数开始
启动新线程:5
[New Thread 0x7ffff3ef7700 (LWP 8989)]
线程函数开始
启动新线程:6
[New Thread 0x7ffff36f6700 (LWP 8990)]
[Switching to Thread 0x7ffff46f8700 (LWP 8988)]

Thread 7 "chapter_3.3" hit Hardware read watchpoint 2: count

Value = 5
0x0000000000401722 in do_work (arg=0x7fffffffdf2c) at chapter_3.3.cpp:164
164	    int local_data = count;

读写观察点的语法如下:

awatch 变量或者表达式

无论这个变量是被读取还是被写入,程序都会发生中断,即只要遇到这个变量就会发生中断。

(gdb) awatch local_data
Hardware access (read/write) watchpoint 3: local_data
(gdb) c
Continuing.
启动新线程:7
[New Thread 0x7ffff2ef5700 (LWP 9828)]
[Switching to Thread 0x7ffff3ef7700 (LWP 9784)]

Thread 8 "chapter_3.3" hit Hardware read watchpoint 2: count

Value = 5
0x0000000000401722 in do_work (arg=0x7fffffffdf2c) at chapter_3.3.cpp:164
164	    int local_data = count;
(gdb) 
Continuing.
启动新线程:8
线程函数结束
线程函数开始
[New Thread 0x7ffff26f4700 (LWP 9853)]
[Switching to Thread 0x7ffff46f8700 (LWP 9750)]

Thread 7 "chapter_3.3" hit Hardware access (read/write) watchpoint 3: local_data

Old value = 0
New value = 5
do_work (arg=0x7fffffffdf2c) at chapter_3.3.cpp:165
165	    count++;

查看所有观察点的语法如下:

info watchpoints
(gdb) i watchpoints
Num     Type           Disp Enb Address            What
10      hw watchpoint  keep y                      count thread 3
	stop only in thread 3
	breakpoint already hit 1 time
11      hw watchpoint  keep y                      count thread 7
	stop only in thread 7

在显示的所有观察点中,每个观察点的序号、类型、命中次数以及观察的变量等信息都会显示出来,一目了然。

禁用/启用/删除观察点命令的语法格式如下:

delete/disable/enable 观察点编号

观察点是一种特殊的断点,因此可以通过管理断点的方式来管理观察点,比如i b命令可以查看所有的断点和观察点。delete、disable、enable等命令也适用于观察点。

观察点和其他断点是统一管理的,编号是唯一的。

3.14.2 捕获点

捕获点(catchpoint)指的是程序在发生某事件时,gdb能够捕获这些事件并使程序停止执行。该功能可以支持很多事件,比如C++异常、载入动态库等。语法如下:

catch 事件

可以捕获的事件如下所示。

·throw:在C++代码中执行throw语句时程序会中断。

·catch:当代码中执行到catch语句块时会中断,也就是说代码捕获异常时会中断。

·exec、fork、vfork:调用这些系统函数时会中断,主要适用于HP-UNIX。

·load/unload:加载或者卸载动态库时。

(gdb) catch throw
Catchpoint 1 (throw)
(gdb) catch catch
Catchpoint 2 (catch)
(gdb) i b
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x00007ffff788a310 exception throw
	breakpoint already hit 1 time
2       breakpoint     keep y   0x00007ffff78891f0 exception catch
	breakpoint already hit 1 time

捕获点也是一种特殊的断点,因此可以使用管理断点的命令来管理捕获点,比如使用i b命令来查看所有断点,包括捕获点。

(gdb) r
Starting program: /root/book_debug/chapter_3.3/chapter_3.3 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".
name is test_try_catch,10
throw

Catchpoint 1 (exception thrown), __cxxabiv1::__cxa_throw (obj=0x41aca0, tinfo=0x407d20 <typeinfo for int@@CXXABI_1.3>, dest=0x0) at /usr/src/debug/gcc-9.1.0/libstdc++-v3/libsupc++/eh_throw.cc:80
80	  __cxa_eh_globals *globals = __cxa_get_globals ();
(gdb) bt
#0  __cxxabiv1::__cxa_throw (obj=0x41aca0, tinfo=0x407d20 <typeinfo for int@@CXXABI_1.3>, dest=0x0) at /usr/src/debug/gcc-9.1.0/libstdc++-v3/libsupc++/eh_throw.cc:80
#1  0x0000000000401977 in test_try_catch (number=10) at chapter_3.3.cpp:200
#2  0x00000000004019d3 in main (argc=1, argv=0x7fffffffe0e8) at chapter_3.3.cpp:214
(gdb) f 1
#1  0x0000000000401977 in test_try_catch (number=10) at chapter_3.3.cpp:200
200	                throw 10;
(gdb) i locals
throw_num = 50
local_data = 10
name = 0x40420c "test_try_catch"
(gdb) c
Continuing.

Catchpoint 2 (exception caught), __cxxabiv1::__cxa_begin_catch (exc_obj_in=0x41ac80) at /usr/src/debug/gcc-9.1.0/libstdc++-v3/libsupc++/eh_catch.cc:42
42	  _Unwind_Exception *exceptionObject
(gdb) bt
#0  __cxxabiv1::__cxa_begin_catch (exc_obj_in=0x41ac80) at /usr/src/debug/gcc-9.1.0/libstdc++-v3/libsupc++/eh_catch.cc:42
#1  0x000000000040197f in test_try_catch (number=10) at chapter_3.3.cpp:202
#2  0x00000000004019d3 in main (argc=1, argv=0x7fffffffe0e8) at chapter_3.3.cpp:214
(gdb) f 1
#1  0x000000000040197f in test_try_catch (number=10) at chapter_3.3.cpp:202
202	        catch(...)
(gdb) i locals
local_data = 10
name = 0x40420c "test_try_catch"

命令catch还有一个对应的命令tcatch,意思是临时捕获,即只捕获一次,命中后会自动删除该捕获点(与临时断点类似)。

3.14.3 搜索源代码

search 正则表达式
forward-search 正则表达式
(gdb) search member
17	int member_id = 0;
(gdb) 
18	void add_member()
(gdb) 
36		new_node->ID = member_id++;
(gdb) 
252				add_member();
(gdb) 
Expression not found
reverse-search 正则表达式
(gdb) reverse-search member
36		new_node->ID = member_id++;
(gdb) 
18	void add_member()
(gdb) 
17	int member_id = 0;
(gdb) 
Expression not found

3.14.4 查看变量类型

可以使用命令ptype来查看变量或者结构体以及类等详细信息。语法如下:

ptype 可选参数 变量或者类型

其中,可选参数用来控制显示信息,变量或者类型可以是任意的变量,也可以是定义的数据类型,比如类、结构体、枚举等。

其中的可选参数如下所示。

·/r:表示以原始数据的方式显示,不会代替一些typedef定义。

·/m:查看类时,不显示类的方法,只显示成员变量。

·/M:与/m相反,显示类的方法(默认选项)。

·/t:不打印类中的typedef数据。

·/o:打印结构体字段的偏移量和大小。

(gdb) ptype node_head
type = struct NODE {
    int ID;
    char Name[40];
    int age;
    NODE *prev;
    NODE *next;
} *
(gdb) ptype /o node_head
type = struct NODE {
/*    0      |     4 */    int ID;
/*    4      |    40 */    char Name[40];
/*   44      |     4 */    int age;
/*   48      |     8 */    NODE *prev;
/*   56      |     8 */    NODE *next;

                           /* total size (bytes):   64 */
                         } *
(gdb) ptype TEST_NODE
type = struct TEST_NODE {
    char gender[3];
    int ID;
    char name[7];
}
(gdb) ptype /o TEST_NODE
/* offset    |  size */  type = struct TEST_NODE {
/*    0      |     3 */    char gender[3];
/* XXX  1-byte hole */
/*    4      |     4 */    int ID;
/*    8      |     7 */    char name[7];
/* XXX  1-byte padding */

                           /* total size (bytes):   16 */
                         }
(gdb) ptype test_1
type = class test_1 {
  private:
    int x;
    int y;

  public:
    test_1(void);
    ~test_1();
    virtual void test_fun(void);
}
(gdb) ptype test_2
type = class test_2 : public test_1 {
  public:
    test_2(void);
    ~test_2();
    virtual void test_fun2(void);
    virtual void test_fun(void);
}
(gdb) ptype /o test_1
/* offset    |  size */  type = class test_1 {
                         private:
/*    8      |     4 */    int x;
/*   12      |     4 */    int y;

                           /* total size (bytes):   16 */
                         }
(gdb) p sizeof(test_1)
$1 = 16
(gdb) p sizeof(void*)
$2 = 8

还有一个简单的命令whatis用来查看变量或者表达式的类型,只是打印的信息比较简单

(gdb) whatis number
type = int
(gdb) whatis name
type = const char *
(gdb) whatis call_fun_test_1
type = int (int, const char *)
(gdb) ptype call_fun_test_1
type = int (int, const char *)

3.14.5 跳转执行

在调试过程中,很多时候我们希望某些代码能够被反复执行,因为我们希望能够多次查看问题,以便更加仔细地观察问题。有时候又希望直接跳过某些代码,比如环境的问题、不能满足某些条件、部分代码没有意义或者会执行失败等。

这就是跳转执行,即不按照代码的流程逐行执行,而是按照我们期望的方式执行。命令语法如下:

jump 位置

命令中的位置可以是代码行或者某个函数的地址。

(gdb) b 42
(gdb) r
(gdb) b 36
Breakpoint 2 at 0x4013b0: file chapter_3.3.cpp, line 36.
(gdb) jump 36
Continuing at 0x4013b0.

Thread 1 "chapter_3.3" hit Breakpoint 2, add_member () at chapter_3.3.cpp:36
36		new_node->ID = member_id++;

如果想跳过某些代码行,直接进入到某一行或者某个函数去执行,仍然可以使用jump命令去完成。

jump add_member
(gdb) b add_member
(gdb) r
(gdb) C-c
(gdb) jump add_member

3.14.6 窗口管理

tui enable
tui disable

·命令窗口:gdb命令输入和结果输出的窗口,该窗口始终是可见的。

·源代码窗口:显示程序源代码的窗口,会随着代码的执行自动显示代码对应的行。

·汇编窗口:汇编窗口也会随着代码的执行而变化,显示代码对应的汇编代码行。

·寄存器窗口:显示寄存器的值。

显示下一个窗口:

layout next

显示前一个窗口:

layout prev

只显示源代码窗口:

layout src

只显示汇编窗口:

layout asm

显示源代码和汇编窗口:

layout split

显示寄存器窗口,与源代码以及汇编窗口一起显示:

layout regs

设置窗口为活动窗口,以便能够响应上下滚动键:

focus next | prev | cmd | src | asm

刷新屏幕:

refresh

更新源代码窗口:

update

3.14.7 调用Shell命令

(gdb) shell ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 00:16:3e:03:82:fe brd ff:ff:ff:ff:ff:ff
    inet 172.29.233.89/20 brd 172.29.239.255 scope global dynamic eth0
       valid_lft 315257476sec preferred_lft 315257476sec
    inet6 fe80::216:3eff:fe03:82fe/64 scope link 
       valid_lft forever preferred_lft forever
(gdb) !ls -l
total 896
-rwxr-xr-x 1 root root   210392 Mar 27 12:10 chapter_3.3
-rw-r--r-- 1 root root     4831 Mar 25 20:53 chapter_3.3.cpp
-rw-r--r-- 1 root root       31 Mar 27 12:10 chapter_3.3.d
-rw-r--r-- 1 root root   365008 Mar 27 12:10 chapter_3.3.o
-rw------- 1 root root 34177024 Mar 24 21:42 core.1394
-rw-r--r-- 1 root root      837 Mar 16 21:40 Makefile

3.14.8 assert宏使用

调试程序时经常用到Linux系统中的assert宏。C与C++的宏定义不大相同,但是它们都在assert.h中进行了定义。C语言的宏定义如下:

void assert(int expression);

在C语言中,assert是一个函数,并不是一个宏;在C++语言中,assert是一个宏。

如果程序是直接运行的,没有启动调试器,则assert的expression值为false时,程序直接终止。

chapter_3.3: chapter_3.3.cpp:221: int main(int, char**): Assertion `t1 != __null' failed.
Aborted (core dumped)

如果是以调试器的方式启动程序,在遇到assert失败时会在失败处中断,这样我们就可以很方便地查看当时的状态,确定assert失败的原因。

chapter_3.3: chapter_3.3.cpp:221: int main(int, char**): Assertion `t1 != __null' failed.
[Thread 0x7ffff26f4700 (LWP 1292) exited]

Thread 1 "chapter_3.3" received signal SIGABRT, Aborted.
0x00007ffff6f34387 in __GI_raise (sig=sig@entry=6) at ../nptl/sysdeps/unix/sysv/linux/raise.c:55
55	  return INLINE_SYSCALL (tgkill, 3, pid, selftid, sig);
(gdb) bt
#0  0x00007ffff6f34387 in __GI_raise (sig=sig@entry=6) at ../nptl/sysdeps/unix/sysv/linux/raise.c:55
#1  0x00007ffff6f35a78 in __GI_abort () at abort.c:90
#2  0x00007ffff6f2d1a6 in __assert_fail_base (fmt=0x7ffff7088f60 "%s%s%s:%u: %s%sAssertion `%s' failed.\n%n", assertion=assertion@entry=0x404256 "t1 != __null", file=file@entry=0x404246 "chapter_3.3.cpp", line=line@entry=221, 
    function=function@entry=0x404230 "int main(int, char**)") at assert.c:92
#3  0x00007ffff6f2d252 in __GI___assert_fail (assertion=0x404256 "t1 != __null", file=0x404246 "chapter_3.3.cpp", line=221, function=0x404230 "int main(int, char**)") at assert.c:101
#4  0x0000000000401a35 in main (argc=1, argv=0x7fffffffe0e8) at chapter_3.3.cpp:221
(gdb) f 4
#4  0x0000000000401a35 in main (argc=1, argv=0x7fffffffe0e8) at chapter_3.3.cpp:221
221		assert(t1 != NULL);
(gdb) l
216		int number = 100;
217		const char* name ="main";
218		call_fun_test_1(1,"call_fun_test_1");
219		test_memory();
220		print_arr_test();
221		assert(t1 != NULL);
222		cond_fun_test(10,"test");
223		test_loop();
224		
225		
(gdb) p t1
$1 = (test_1 *) 0x0

3.14.9 gdb常用命令列表