手把手带你学会使用GDB进行调试

目录

一:GDB调试器

1:调试信息和调试原理

二:GDB调试方法

1:直接调试目标程序 

2:附加进程

3:调试core文件

三:常用简介

四:调试Redis源码进行实战

1:run

2:continue

3:break(打断点)

4:backtrace 与 frame 命令

5:info break、enable、disable 和 delete 命令

6:list 命令

7:print 和 ptype 命令

8:ptype 命令

 9:info 和 thread 命令

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

1:next 命令

2:step 命令

3:return 和 finish 命令

4:until 命令

5:Jump 命令

11:set args 和 show args 命令

12:tbreak 命令

13:watch 命令

14:display 命令

五:调试技巧

1:将printf打印的结果显示完整

2:多线程下禁止线程切换

3:条件断点

4:使用GDB调试多进程程序


一:GDB调试器

GDB(GNU 项目调试器)可以让您了解程序在执行时“内部” 究竟在干些什么,以及在程序发生崩溃的瞬间正在做什么。
GDB 做以下 4 件主要的事情来帮助您捕获程序中的 bug:
1:在程序启动之前指定一些可以影响程序行为的变量或条件
2:在某个指定的地方或条件下暂停程序
3:在程序停止时检查已经发生了什么
4:在程序执行过程中修改程序中的变量或条件,这样就可以体验修复一个 bug的成果,并继续了解其他 bug

1:调试信息和调试原理

        一般要调试某个程序,为了能清晰地看到调试的每一行代码、调用的堆栈信息、变量名和函数名等信息,需要调试程序含有调试符号信息。使用 gcc 编译程序时,如果加上 -g 选项即可在编译后的程序中保留调试符号信息。举个例子,以下命令将生成一个带调试信息的程序 hello_server:

#include <stdio.h>

void print_value(int val)
{
    printf("print_value val:%d\n", val);
}

int main()
{
    int i = 0;
    i++;
    printf("main i:%d\n", i);
    print_value(i++);
    print_value(++i);
    return 0;
}

//gcc -g -o hello hello.c   生成可执行程序

我们将这个可执行程序通过gdb进行调试操作,通过下图中的操作,进行简单的调试,当出现用红圈圈起来的地方,就证明我们已经成功调试了。

二:GDB调试方法

我们启动GDB调试一共三种方法:

gdb filename                          直接调试目标程序

gdb attach pid                        附加进程

gdb filename corename         调试core文件

1:直接调试目标程序 

我们上面进行直接的调试就是使用的第一种方法

gdb ./hello

2:附加进程

        当我们已经启动了一个程序之后,我们不想对这个程序进行重启,想直接对此程序进行调试,那我们就使用附加进程的方式进行调试。那下面我们使用redis的例子来进行调试:

我们先启动redis

redis-server redis.conf

启动之后我们查找这个redis的进程id

ps aux | grep redis
xin         3358  0.1  0.0  54884  4540 ?        Ssl  01:24   0:00 redis-server 127.0.0.1:6379
xin         3402  0.0  0.0   9040   648 pts/0    S+   01:24   0:00 grep --color=auto redis

然后我们就可以进行调试了

gdb attach 3358

当我们执行后出现下面的提示,就说明我们的权限不够,需要使用sudo的权限。

ptrace: Operation not permitted.

下面是正常的启动,并且使用了c让程序进行下去

sudo gdb attach 3358

GNU gdb (Ubuntu 9.2-0ubuntu1~20.04.2) 9.2
Copyright (C) 2020 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-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"...
attach: No such file or directory.
Attaching to process 3358
[New LWP 3360]
[New LWP 3361]
[New LWP 3362]
[New LWP 3363]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
--Type <RET> for more, q to quit, c to continue without paging--c
0x00007f824361268e in epoll_wait (epfd=5, events=0x7f8243162080, maxevents=10128, timeout=100) at ../sysdeps/unix/sysv/linux/epoll_wait.c:30
30      ../sysdeps/unix/sysv/linux/epoll_wait.c: No such file or directory.

当我们想结束这次的调试,而且对进程redis没有任何影响,也就是说让这个程序继续运行,所以我们在GDB的命令行中执行:detach命令让GDB与调试器分离,这样就可以自己运行了。

(gdb) detach
Detaching from program: /usr/local/bin/redis-server, process 3358
[Inferior 1 (process 3358) detached]
(gdb) q

3:调试core文件

        当我们的服务器运行一段时间后崩溃掉,我们需要找到是哪里出现了问题,那我们就需要core文件了,但是我们的core文件时默认不开启的,因此需要我们先打开,这个可以看别人的文章。当我们打开之后,我们使用一个程序生成一个core文件。

#include <stdio.h>

int main(void)
{
    printf("hello world! dump core for set value to NULL pointer/n");
    *(char *)0 = 0;
    return 0;
}

三:常用简介

命令名称         命令缩写       命令说明
run              r             运行一个程序
continue         c             让暂停的程序继续运行
next             n             运行到下一行
step             s             如果有调用函数,进入调用的函数内部,相当于 step into
until            u             运行到指定行停下来
finish           fi            结束当前调用函数,到上一层函数调用处
return           return        结束当前调用函数并返回指定值,到上一层函数调用处
jump             j             将当前程序执行流跳转到指定行或地址
print            p             打印变量或寄存器值
backtrace        bt            查看当前线程的调用堆栈
frame            f             切换到当前调用线程的指定堆栈,具体堆栈通过堆栈序号指定
thread           thread        切换到指定线程
break            b             添加断点
tbreak           tb            添加临时断点
delete           del           删除断点
enable           enable        启用某个断点
disable          disable       禁用某个断点
watch            watch         监视某一个变量或内存地址的值是否发生变化
list             l             显示源码
info             info          查看断点 / 线程等信息
ptype            ptype         查看变量类型
disassemble      dis           查看汇编代码
set              args          设置程序启动命令行参数
show             args          查看设置的命令行参数

四:调试Redis源码进行实战

我们下载好redis之后,我们进入到src的目录中,然后我们找到redis-server,开始调试这个程序:gdb ./redis-server:

1:run

我们输入run,将程序跑起来

7823:M 31 Dec 2024 03:45:21.256 # Server initialized
7823:M 31 Dec 2024 03:45:21.256 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect.
[New Thread 0x7ffff6e8f700 (LWP 7827)]
[New Thread 0x7ffff668e700 (LWP 7828)]
[New Thread 0x7ffff5e8d700 (LWP 7829)]
[New Thread 0x7ffff568c700 (LWP 7830)]
7823:M 31 Dec 2024 03:45:21.278 * Ready to accept connections

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

2:continue

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

^C
Thread 1 "redis-server" received signal SIGINT, Interrupt.
0x00007ffff7d6c68e in epoll_wait (epfd=5, events=0x7ffff78e3e00, maxevents=10128, timeout=100) at ../sysdeps/unix/sysv/linux/epoll_wait.c:30
30      ../sysdeps/unix/sysv/linux/epoll_wait.c: No such file or directory.
(gdb) c
Continuing.

3:break(打断点)

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

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

1:打断点

我们在main函数上打断点然后进行重启。它会提示是否进行重启

(gdb) b main
Breakpoint 1 at 0x555555596b80: file server.c, line 6151.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/xin/game/0voice_im/redis-6.2.5/src/redis-server 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, main (argc=1, argv=0x7fffffffe068) at server.c:6151
6151    int main(int argc, char **argv) {
(gdb) 

因为redis的端口是6379,并且在anet.c的441行,因此我们在这个位置也打个断点。

(gdb) b anet.c:441
Note: breakpoint 2 also set at pc 0x55555559af2b.
Breakpoint 3 at 0x55555559af2b: file anet.c, line 441.
/path/to/redis.conf
7379:M 02 Nov 14:49:04.621 * Increased maximum number of open files to 10032 (it was
originally set to 1024).
Breakpoint 2, anetListen (err=0x5555558d4930 <server+560> "", s=6,
sa=0x5555558d99a0, len=28, backlog=511)
at anet.c:441
441 if (bind(s,sa,len) == -1) 

4:backtrace 与 frame 命令

backtrace 命令(简写为 bt)用来查看当前调用堆栈。可以通过 backtrace 命令来查看当前的调用堆栈,我们使用main函数那里的断点:

(gdb) bt
#0  0x00007ffff7d6c68e in epoll_wait (epfd=5, events=0x7ffff78e3e00, maxevents=10128, timeout=100) at ../sysdeps/unix/sysv/linux/epoll_wait.c:30
#1  0x000055555559a6f6 in aeApiPoll (tvp=<optimized out>, eventLoop=0x7ffff78230f0) at ae_epoll.c:113
#2  aeProcessEvents (eventLoop=eventLoop@entry=0x7ffff78230f0, flags=flags@entry=27) at ae.c:395
#3  0x000055555559aa2d in aeMain (eventLoop=0x7ffff78230f0) at ae.c:487
#4  0x0000555555596eaa in main (argc=<optimized out>, argv=<optimized out>) at server.c:6396
(gdb) f 0
#0  0x00007ffff7d6c68e in epoll_wait (epfd=5, events=0x7ffff78e3e00, maxevents=10128, timeout=100) at ../sysdeps/unix/sysv/linux/epoll_wait.c:30
30      in ../sysdeps/unix/sysv/linux/epoll_wait.c
(gdb) f 1
#1  0x000055555559a6f6 in aeApiPoll (tvp=<optimized out>, eventLoop=0x7ffff78230f0) at ae_epoll.c:113
113         retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,
(gdb)  f 4
#4  0x0000555555596eaa in main (argc=<optimized out>, argv=<optimized out>) at server.c:6396
6396        aeMain(server.el);
(gdb) 

这里一共有 4 层堆栈,最顶层是 main() 函数,最底层是断点所在的 epoll_wait 函数,堆栈编号分别是 #0 ~ #3 ,如果想切换到其他堆栈处,可以使用 frame 命令(简写为 f),该命令的使用方法是“frame 堆栈编号(编号不加 #)”。在这里依次切换至堆栈顶部。

5:info break、enable、disable 和 delete 命令

在程序中加了很多断点,而我们想查看加了哪些断点时,可以使用 info break 命令(简写为 info b)

(gdb) info b
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000555555591308 in main at server.c:3709
breakpoint already hit 1 time
2 breakpoint keep y 0x0000555555585909 in anetListen at anet.c:441
breakpoint already hit 1 time
3 breakpoint keep y 0x0000555555585955 in anetListen at anet.c:444
4 breakpoint keep y 0x00005555555859a3 in anetListen at anet.c:450
5 breakpoint keep y 0x00005555555859aa in anetListen at anet.c:452
breakpoint already hit 1 time

        通过上面的内容片段可以知道,目前一共增加了 5 个断点,相应的断点信息比如每个断点的位置(所在的文件和行号)、内存地址、断点启用和禁用状态信息也一目了然。如果我们想禁用某个断点,使用“disable 断点编号”就可以禁用这个断点了,被禁用的断点不会再被触发;同理,被禁用的断点也可以使用“enable 断点编号”重新启用:

我们发现下面的keep选项变成n了。

(gdb) disable 1
(gdb) info b
Num Type Disp Enb Address What
1 breakpoint keep n 0x0000555555591308 in main at server.c:3709
breakpoint already hit 1 time
2 breakpoint keep y 0x0000555555585909 in anetListen at anet.c:441
breakpoint already hit 1 time
3 breakpoint keep y 0x0000555555585955 in anetListen at anet.c:444
4 breakpoint keep y 0x00005555555859a3 in anetListen at anet.c:450
5 breakpoint keep y 0x00005555555859aa in anetListen at anet.c:452
breakpoint already hit 1 time

如果 disable 命令和 enable 命令不加断点编号,则分别表示禁用和启用所有断点:

使用“delete 编号”可以删除某个断点,如 delete 2 3 则表示要删除的断点 2 和断点,如果输入 delete 不加命令号,则表示删除所有断点。

6:list 命令

list:如果不带任何参数的话,该命令会接着打印上次 list 命令打印出代码后面的代码。如果是第一次执行 list 命令则会显示当前正在执行代码位置附近的代码。
list - :如果参数是一个减号的话,则和前面刚好相反,会打印上次 list 命令打印出代码前面的代码。
list LOATION:list 命令还可以带一个代码位置作为参数,顾名思义,这样的话就会打印出该代码位置附近的代码。这个代码位置的定义和在 break 命令中定义的相同,可以是一个行号。
list 100  : 列出当前代码文件中第 100 行附近代码。
list tcpdump.c:450  : 列出 tcpdump.c 文件中第 450 行附近代码,也可以是一个函数名。
list main  :列出当前代码文件中 main 函数附近代码。
list inet.c:pcap_lookupdev  :列出 inet.c 代码文件中指定函数附近代码。
list 命令还可以指定要显示代码的具体范围 :list FIRST,LAST

        这里 FIRST 和 LAST 都是具体的代码位置,此时该命令将显示 FIRST 到 LAST 之间的代码。可以不指定 FIRST 或者 LAST 参数,这样的话就将显示 LAST 之前或者 FIRST 之后的代码。注意,即使只指定一个参数也要带逗号,否则就编程前面的命令,显示代码位置附近的代码了。
list 命令默认只会打印出 10 行源代码,如果觉得不够,可以使用如下命令修改:

set listsize COUNT :这样的话,下次 list 命令就会显示 COUNT 行源代码了。如果想查看这个参数当前被设置成多少,可以使用如下命令:show listsize

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

代码继续往后显示 10 行,也就是说,第一次输入 list 命令会显示断点处前后的代码,继续输入 list 指令会以递增行号的形式继续显示剩下的代码行,一直到文件结束为止。当然 list 指令还可以往前和往后显示代码,命令分别是“list + (加号)”和“list- (减号)”

7:print 和 ptype 命令

通过 print 命令(简写为 p)我们可以在调试过程中方便地查看变量的值,也可以修改当前内存中的变量值。切换当前断点到堆栈 #4 ,然后打印以下三个变量:

(gdb) p server.port
$1 = 6379
(gdb) p server.ipfd
$2 = {0 <repeats 16 times>}
(gdb) p server.ipfd_count
$3 = 0

        这里使用 print 命令分别打印出 server.port 、server.ipfd 、server.ipfd_count 的值,其中 server.ipfd 显示 “{0 \<repeats 16 times\>}”,这是 GDB 显示字符串或字符数据特有的方式,当一个字符串变量或者字符数组或者连续的内存值重复若干次,GDB 就会以这种模式来显示以节约空间。

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

print 命令不仅可以输出表达式结果,同时也可以修改变量的值,我们尝试将上文中的端口号从 6379 改成 6400 试试:

(gdb) p server.port=6400
$24 = 6400
(gdb) p server.port
$25 = 6400

总结起来,利用 print 命令,我们不仅可以查看程序运行过程中的各个变量的状态值,也可以通过临时修改变量的值来控制程序的行为。

8:ptype 命令

        ptype ,顾名思义,其含义是“print type”,就是输出一个变量的类型。例如,我们试着输出 Redis 堆栈 #4 的变量 server 和变量 server.port 的类型

(gdb) ptype server
type = struct redisServer {
    pid_t pid;
    char *configfile;
    char *executable;
    char **exec_argv;
    int hz;
    redisDb *db;
    ...省略部分字段...
(gdb) ptype server.port
type = int

 9:info 和 thread 命令

        在前面使用 info break 命令查看当前断点时介绍过,info 命令是一个复合指令,还可以用来查看当前进程的所有线程运行情况。下面以 redis-server 进程为例来演示一下,使用 delete 命令删掉所有断点,然后使用 run 命令重启一下 redis-server,等程序正常启动后,我们按快捷键 Ctrl+C 中断程序,然后使用 info thread 命令来查看当前进程有哪些线程,分别中断在何处:

  1    Thread 0x7ffff7c48f80 (LWP 11985) "redis-server"    0x00007ffff7d6c68e in epoll_wait (epfd=5, events=0x7ffff78e3e00, maxevents=10128, 
    timeout=100) at ../sysdeps/unix/sysv/linux/epoll_wait.c:30
  2    Thread 0x7ffff6e8f700 (LWP 11986) "bio_close_file"  futex_wait_cancelable (private=<optimized out>, expected=0, 
    futex_word=0x555555764cc8 <bio_newjob_cond+40>) at ../sysdeps/nptl/futex-internal.h:183
  3    Thread 0x7ffff668e700 (LWP 11987) "bio_aof_fsync"   futex_wait_cancelable (private=<optimized out>, expected=0, 
--Type <RET> for more, q to quit, c to continue without paging--c
    _newjob_cond+88>) at ../sysdeps/nptl/futex-internal.h:183
  4    Thread 0x7ffff5e8d700 (LWP 11988) "bio_lazy_free"   futex_wait_cancelable (private=<optimized out>, expected=0, futex_word=0x555555764d28 <bio_newjob_cond+136>) at ../sysdeps/nptl/futex-internal.h:183
  5    Thread 0x7ffff568c700 (LWP 11989) "jemalloc_bg_thd" futex_wait_cancelable (private=<optimized out>, expected=0, futex_word=0x7ffff7a073b0) at ../sysdeps/nptl/futex-internal.h:183

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

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

        怎么知道线程哪个线程是主线程,现在有 5 个线程,也就有 5 个调用堆栈,如果此时输入 backtrace 命令查看调用堆栈,由于当前 GDB 作用在线程 1,因此 backtrace命令显示的一定是线程 1 的调用堆栈。

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

(gdb) info thread
  Id   Target Id                                          Frame 
* 1    Thread 0x7ffff7c48f80 (LWP 3445) "redis-server"    0x0000555555596eaa in main (argc=<optimized out>, argv=<optimized out>) at server.c:6396
  2    Thread 0x7ffff6e8f700 (LWP 3517) "bio_close_file"  futex_wait_cancelable (private=<optimized out>, expected=0, futex_word=0x555555764cc8 <bio_newjob_cond+40>)
    at ../sysdeps/nptl/futex-internal.h:183
  3    Thread 0x7ffff668e700 (LWP 3518) "bio_aof_fsync"   futex_wait_cancelable (private=<optimized out>, expected=0, futex_word=0x555555764cf8 <bio_newjob_cond+88>)
    at ../sysdeps/nptl/futex-internal.h:183
  4    Thread 0x7ffff5e8d700 (LWP 3519) "bio_lazy_free"   futex_wait_cancelable (private=<optimized out>, expected=0, futex_word=0x555555764d28 <bio_newjob_cond+136>)
    at ../sysdeps/nptl/futex-internal.h:183
  5    Thread 0x7ffff568c700 (LWP 3520) "jemalloc_bg_thd" futex_wait_cancelable (private=<optimized out>, expected=0, futex_word=0x7ffff7a073b0)
    at ../sysdeps/nptl/futex-internal.h:183
(gdb) bt
#0  0x00007ffff7d6c68e in epoll_wait (epfd=5, events=0x7ffff78e3e00, maxevents=10128, timeout=100) at ../sysdeps/unix/sysv/linux/epoll_wait.c:30
#1  0x000055555559a6f6 in aeApiPoll (tvp=<optimized out>, eventLoop=0x7ffff78230f0) at ae_epoll.c:113
#2  aeProcessEvents (eventLoop=eventLoop@entry=0x7ffff78230f0, flags=flags@entry=27) at ae.c:395
#3  0x000055555559aa2d in aeMain (eventLoop=0x7ffff78230f0) at ae.c:487
#4  0x0000555555596eaa in main (argc=<optimized out>, argv=<optimized out>) at server.c:6396
(gdb) thread 3
[Switching to thread 3 (Thread 0x7ffff668e700 (LWP 3518))]
#0  futex_wait_cancelable (private=<optimized out>, expected=0, futex_word=0x555555764cf8 <bio_newjob_cond+88>) at ../sysdeps/nptl/futex-internal.h:183
183     ../sysdeps/nptl/futex-internal.h: No such file or directory.
(gdb) info thread
  Id   Target Id                                          Frame 
  1    Thread 0x7ffff7c48f80 (LWP 3445) "redis-server"    0x00007ffff7d6c68e in epoll_wait (epfd=5, events=0x7ffff78e3e00, maxevents=10128, timeout=100)
    at ../sysdeps/unix/sysv/linux/epoll_wait.c:30
  2    Thread 0x7ffff6e8f700 (LWP 3517) "bio_close_file"  futex_wait_cancelable (private=<optimized out>, expected=0, futex_word=0x555555764cc8 <bio_newjob_cond+40>)
    at ../sysdeps/nptl/futex-internal.h:183
* 3    Thread 0x7ffff668e700 (LWP 3518) "bio_aof_fsync"   futex_wait_cancelable (private=<optimized out>, expected=0, futex_word=0x555555764cf8 <bio_newjob_cond+88>)
    at ../sysdeps/nptl/futex-internal.h:183
  4    Thread 0x7ffff5e8d700 (LWP 3519) "bio_lazy_free"   futex_wait_cancelable (private=<optimized out>, expected=0, futex_word=0x555555764d28 <bio_newjob_cond+136>)
    at ../sysdeps/nptl/futex-internal.h:183
  5    Thread 0x7ffff568c700 (LWP 3520) "jemalloc_bg_thd" futex_wait_cancelable (private=<optimized out>, expected=0, futex_word=0x7ffff7a073b0)
    at ../sysdeps/nptl/futex-internal.h:183
(gdb) bt
#0  futex_wait_cancelable (private=<optimized out>, expected=0, futex_word=0x555555764cf8 <bio_newjob_cond+88>) at ../sysdeps/nptl/futex-internal.h:183
#1  __pthread_cond_wait_common (abstime=0x0, clockid=0, mutex=0x555555764d68 <bio_mutex+40>, cond=0x555555764cd0 <bio_newjob_cond+48>) at pthread_cond_wait.c:508
#2  __pthread_cond_wait (cond=cond@entry=0x555555764cd0 <bio_newjob_cond+48>, mutex=mutex@entry=0x555555764d68 <bio_mutex+40>) at pthread_cond_wait.c:647
#3  0x000055555560d70a in bioProcessBackgroundJobs (arg=0x1) at bio.c:209
#4  0x00007ffff7e47609 in start_thread (arg=<optimized out>) at pthread_create.c:477
#5  0x00007ffff7d6c353 in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95

         info 命令还可以用来查看当前函数的参数值,组合命令是 info args。我们接着上面的线程三继续看这些函数的参数。当我们想查看一个指针的类型的时候,使用P*即可查看。如果还要查看其成员值,继续使用 变量名 ->字段名 即可,在前面学习 print 命令时已经介绍过了

(gdb) info args
private = <optimized out>
expected = 0
futex_word = 0x555555764cf8 <bio_newjob_cond+88>
(gdb) p * futex_word
$3 = 0

上面介绍的是 info 命令最常用的三种方法,更多的方法使用 help info 查看。

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

        这几个命令是 GDB 调试程序时最常用的几个控制流命令,因此放在一起介绍。next命令(简写为 n)是让 GDB 调到下一条命令去执行,这里的下一条命令不一定是代码的下一行,而是根据程序逻辑跳转到相应的位置。

1:next 命令

 我们使用一个程序来展示一下这个n的用法。

#include <stdio.h>

int main()
{
    int a =0;
    if(a == 7)
    {
        printf("111\n");
    }

    int b = 10;
    printf("b=%d",b);
    return 0;
}
Reading symbols from ./ceshi1...
(gdb) b 6
Breakpoint 1 at 0x117c: file ceshi1.c, line 6.
(gdb) r
Starting program: /home/xin/game/1.4.4-vscode-gdb/ceshi1 

Breakpoint 1, main () at ceshi1.c:6
6           if(a == 7)
(gdb) n
11          int b = 10;
(gdb) n
12          printf("b=%d",b);
(gdb) n
13          return 0;
(gdb)

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

Breakpoint 1, main () at ceshi1.c:6
6           if(a == 7)
(gdb) n
11          int b = 10;
(gdb) 
12          printf("b=%d",b);
(gdb) 
13          return 0;
(gdb) 

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

2:step 命令

#include <stdio.h>

void add (int *a)
{
    (*a)++;
    printf("a=%d\n",*a);
}

int main()
{
    int a =0;
    if(a == 7)
    {
        printf("111\n");
    }

    add(&a);

    int b = 10;
    printf("a=%d\n",a);
    printf("b=%d\n",b);
    return 0;
}

我们使用n会跳过函数,但是使用s会进入函数体中。

Reading symbols from ./ceshi1...
(gdb) b 12
Breakpoint 1 at 0x11e6: file ceshi1.c, line 12.
(gdb) b 17
Breakpoint 2 at 0x11fa: file ceshi1.c, line 17.
(gdb) b 19
Breakpoint 3 at 0x1206: file ceshi1.c, line 19.
(gdb) r
Starting program: /home/xin/game/1.4.4-vscode-gdb/ceshi1 

Breakpoint 1, main () at ceshi1.c:12
12          if(a == 7)
(gdb) n

Breakpoint 2, main () at ceshi1.c:17
17          add(&a);
(gdb) n
a=1

Breakpoint 3, main () at ceshi1.c:19
19          int b = 10;
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/xin/game/1.4.4-vscode-gdb/ceshi1 

Breakpoint 1, main () at ceshi1.c:12
12          if(a == 7)
(gdb) n

Breakpoint 2, main () at ceshi1.c:17
17          add(&a);
(gdb) s
add (a=0x555555555260 <__libc_csu_init>) at ceshi1.c:4
4       {
(gdb) n
5           (*a)++;
(gdb) n
6           printf("a=%d\n",*a);
(gdb) n
a=1
7       }

3:return 和 finish 命令

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

4:until 命令

实际调试时,还有一个 until 命令(简写为 u)可以指定程序运行到某一行停下来。如果两个之间有很多的代码,我们不想一行一行移动过去,那我们可以使用这个命令。

5:Jump 命令

jump <location>

该命令会带一个参数,即要跳转到的代码位置,可以是源代码的行号:
(gdb) jump 555         跳转到源代码的第 555 行的位置
可以是相对当前代码位置的偏移量:
(gdb) jump +10         跳转到距当前代码下 10 行的位置
也可以是代码所处的内存地址:
(gdb) jump *0x12345678         跳转到位于该地址的代码处

11:set args 和 show args 命令

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

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

(gdb) set args ../redis.conf
(gdb) show args
Argument list to give program being debugged when it is started is "../redis.conf ".

如果单个命令行参数之间含有空格,可以使用引号将参数包裹起来。 

(gdb) set args "999 xx" "hu jj"
(gdb) show args
Argument list to give program being debugged when it is started is ""999 xx" "hu jj""

如果想清除掉已经设置好的命令行参数,使用 set args 不加任何参数即可 。

(gdb) set args
(gdb) show args
Argument list to give program being debugged when it is started is ""

12:tbreak 命令

        tbreak 命令也是添加一个断点,第一个字母“t”的意思是 temporarily(临时的),也就是说这个命令加的断点是临时的,所谓临时断点,就是一旦该断点触发一次后就会自动删除。添加断点的方法与上面介绍的 break 命令一模一样。

13:watch 命令

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

形式一:整型变量
int i;
watch i

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

形式三:watch 一个数组或内存区间
char buf[128];
watch buf

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

注意:当设置的观察点是一个局部变量时,局部变量无效后,观察点也会失效。在观察点失效时 GDB 可能会提示如下信息:
Watchpoint 2 deleted because the program has left the block in which its expression is valid.

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

(gdb) info watch
Num Type Disp Enb Address What
3 hw watchpoint keep y mem
(gdb) delete 3

14:display 命令

        display 命令监视的变量或者内存地址,每次程序中断下来都会自动输出这些变量或内存的值。例如,假设程序有一些全局变量,每次断点停下来我都希望 GDB 可以自动输出这些变量的最新值,那么使用“display 变量名”设置即可。可以使用 info display 查看当前已经自动添加了哪些值,使用 delete
display 清除全部需要自动输出的变量,使用 delete diaplay 编号 删除某个自动输出的变量。

五:调试技巧

1:将printf打印的结果显示完整

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

2:多线程下禁止线程切换

        假设现在有 5 个线程,除了主线程,工作线程都是一样的函数流程,我们将所有的代码直接叫做:

void thread_proc(void* arg)
{
    //代码行 1
    //代码行 2
    //代码行 3
    //代码行 4
    //代码行 5
    //代码行 6
    //代码行 7
    //代码行 8
    //代码行 9
    //代码行 10
    //代码行 11
    //代码行 12
    //代码行 13
    //代码行 14
    //代码行 15
}

        假设 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,这里就不介绍了。

3:条件断点

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

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

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

 对于条件断点

如果我们想让i == 5000触发if的条件判断,如果是修改程序的话,我们会发现十分麻烦,要重新编译等等。那我们使用条件断点就很简单了。

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;
}

        有了条件断点就不需要这么麻烦了,添加条件断点的命令是 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.

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

4:使用GDB调试多进程程序

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

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

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

        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".

https://github.com/0voice

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值