GDB 調試技巧:多線程案例分析(保姆級)

在軟件開發的複雜世界裏,高效的調試工具是解決問題的關鍵利器。今天,我們將深入探討強大的調試工具 ——GDB(GNU Debugger)。GDB 爲開發者提供了一種深入程序內部運行機制、查找錯誤和優化性能的有效途徑。讓我們一同開啓 GDB 的調試之旅,解鎖代碼中的奧祕。

一、GDB 調試器

GDB(GNU 項目調試器)可以讓您瞭解程序在執行時 “內部” 究竟在幹些什麼,以及在程序發生崩潰的瞬間正在做什麼。

GDB 做以下 4 件主要的事情來幫助您捕獲程序中的 bug:

1.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_server hello_server.c

那麼如何判斷 hello_server 是否帶有調試信息呢?我們使用 gdb 來調試一下這個程序,gdb 會顯示正確讀取到該程序的調試信息,在打開的 Linux Shell 終端輸入 gdb hello_server 查看顯示結果即可:

$ gdb ./hello_server
GNU gdb (Ubuntu 8.2-0ubuntu1) 8.2
Copyright (C) 2018 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"...
Reading symbols from ./hello_server...done.
(gdb)

Gdb 加載成功以後,會顯示如下信息:

Reading symbols from ./hello_server...done.

即讀取符號文件完畢,說明該程序含有調試信息。我們不加 -g 選項再試試:gcc -g -o hello_server2 hello_server.c

Reading symbols from ./hello_server2...(no debugging symbols found)...done.

順便提一下,除了不加 -g 選項,也可以使用 Linux 的 strip 命令移除掉某個程序中的調試信息,我們這裏對 hello_server 使用 strip 命令試試:

$ strip hello_server
##使用 strip 命令之前
-rwxrwxrwx 1 root root 18928 Nov  2 11:30 hello_server
##使用 strip 命令之後
-rwxrwxrwx 1 root root 14320 Nov  2 11:32 hello_server

在實際生成調試程序時,一般不僅要加上 -g 選項,也建議關閉編譯器的程序優化選項。編譯器的程序優化選項一般有五個級別,從 O0 ~ O4 ( 注意第一個 O0 ,是字母 O 加上數字 0 ), O0 表示不優化,從 O1 ~ O4 優化級別越來越高,O4 最高。這樣做的目的是爲了調試的時候,符號文件顯示的調試變量等能與源代碼完全對應起來。

1.2 啓動 GDB 調試

GDB 調試主要有三種方式:

⑴直接調試目標程序

比如上一小節的

gdb ./hello_server

⑵ 附加進程

在某些情況下,一個程序已經啓動了,我們想調試這個程序,但是又不想重啓這個程序。比如調試 redis。

lqf@ubuntu:~$ ps -ef | grep redis
lqf        2411   2397  0 11:38 pts/1    00:00:00 redis-server *:6379

得到 redis 進程 PID 爲 2411,然後使用 gdb attach 2411,如果不是 root 權限需要加上 sudo,即是 sudo gdb attach 2411。

$ sudo gdb attach 2411
GNU gdb (Ubuntu 8.2-0ubuntu1) 8.2
For help, type "help".
Type "apropos word" to search for commands related to "word"...
attach: No such file or directory.
Attaching to process 2411
[New LWP 2415]
[New LWP 2416]
[New LWP 2417]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
0x00007f7867bf810f in epoll_wait (epfd=5, events=0x7f78676cd900, 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)

當用 gdb attach 上目標進程後,調試器會暫停下來,此時可以使用 continue 命令讓程序繼續運行,或者加上相應的斷點再繼續運行程序。

當調試完程序想結束此次調試時,而且不對當前進程 redis 有任何影響,也就是說想讓這個程序繼續運行,可以在 GDB 的命令行界面輸入 detach 命令讓程序與 GDB 調試器分離,這樣 redis 就可以繼續運行了:

⑶調試 core 文件

有時候,服務器程序運行一段時間後會突然崩潰,這並不是我們希望看到的,需要解決這個問題。只要程序在崩潰的時候有 core 文件產生,就可以使用這個 core 文件來定位崩潰的原因。當然,Linux 系統默認是不開啓程序崩潰產生 core 文件這一機制的,我們可以使用 ulimit -c 命令來查看系統是否開啓了這一機制。

$ ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 11036
max locked memory       (kbytes, -l) 16384
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 11036
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

發現 core file size 那一行默認是 0,表示關閉生成 core 文件,可以使用 “ulimit 選項名 設置值” 來修改。例如,可以將 core 文件生成改成具體某個值(最大允許的字節數),這裏我們使用 ulimit -c unlimited(unlimited 是 -c 選項值)直接修改成不限制大小。將 ulimit -c unlimited 放入 / etc/profile 中,然後執行 source /etc/profile 即可立即生效。 即是:

$ ulimit -a
core file size          (blocks, -c) unlimited
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 11036
max locked memory       (kbytes, -l) 16384
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 11036
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

範例測試:

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

編譯運行:

$ gcc -g -o core_dump core_dump.c 
$ ./core_dump
Segmentation fault

用 ll 命令查看當前目錄

有產生 core 文件,使用 gdb 進行調試

$ gdb core_dump core

提示出錯位置。

⑷Core Dump 的核心轉儲文件目錄和命名規則

系統默認 corefile 是生成在程序的執行目錄下或者程序啓動調用了 chdir 之後的目錄, 我們可以通過設置生成 corefile 的格式來控制它,讓其生成在固定的目錄下(對應的是自己的目錄,不要直接寫我的目錄),並讓每次啓動後自動生效。

(1)在 / etc/sysctl.conf 寫入 corefile 文件生成的目錄。

kernel.core_pattern=/home/lqf/core_dump/core-%e-%p-%t

比如:

2)創建應對的生成目錄

mkdir /home/lqf/core_dump

(3)然後執行生效

sudo sysctl -p /etc/sysctl.conf

其中:/home/lqf/core_dump/ 對應自己要存放的目錄,core-%e-%p-%t 文件格式

關於格式的的控制有如下幾個參數:

%%:相當於%
%p:相當於<pid>
%u:相當於<uid>
%g:相當於<gid>
%s:相當於導致dump的信號的數字
%t:相當於dump的時間
%e:相當於執行文件的名稱
%h:相當於hostname

(4)可以使用 cat 去查看路徑是否生效

cat  /proc/sys/kernel/core_pattern

生效則顯示:

再次測試

(1)先把原來的 core 文件先刪除

(2)然後執行./core_dump 程序

$ ./core_dump
Segmentation fault (core dumped)

(3)查看 / home/lqf/core_dump

lqf@ubuntu:~/core_dump$ ll
total 124
drwxrwxr-x  2 lqf lqf   4096 Nov  4 11:06 ./
drwxr-xr-x 26 lqf lqf   4096 Nov  4 10:58 ../
rw-------  1 lqf lqf 385024 Nov  4 11:05 core-core_dump-2725-1572836737

(4)在 (3) 中可以看到生成的 corefile,我們使用新生成的 corefile(注意引用完整的路徑)進行調試

$ gdb ./core_dump /home/lqf/core_dump/core-core_dump-2725-1572836737
GNU gdb (Ubuntu 8.2-0ubuntu1) 8.2
Reading symbols from ./core_dump...done.
[New LWP 2725]
Core was generated by `./core_dump'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0  main () at core_dump.c:7
7     *(char *)0 = 0;
(gdb)

1.3 常用簡介

1.4 常用命令實戰

①下載源碼並解壓

wget http://download.redis.io/releases/redis-4.0.11.tar.gz
tar zxvf redis-4.0.11.tar.gz

②進入 redis 源碼目錄並編譯,注意編譯時要生成調試符號並且關閉編譯器優化選項。

cd redis-4.0.11
make CFLAGS="-g -O0" -j 2

由於 redis 是純 C 項目,使用的編譯器是 gcc,因而這裏設置編譯器的選項時使用的是 CFLAGS 選項;如果項目使用的語言是 C++,那麼使用的編譯器一般是 g++,相對應的編譯器選項是 CXXFLAGS。這點請讀者注意區別。

另外,這裏 makefile 使用了 -j 選項,其值是 2,表示開啓 2 個進程同時編譯,加快編譯速度。

編譯成功後,會在 src 目錄下生成多個可執行程序,其中 redis-server 和 redis-cli 是需要調試的程序。 進入 src 目錄,使用 GDB 啓動 redis-server 這個程序:

cd src
gdb ./redis-server

⑴run 命令

默認情況下, gdb filename 命令只是附加的一個調試文件,並沒有啓動這個程序,需要輸入 run 命令(簡寫爲 r)啓動這個程序:

(gdb) r
Starting program: /home/lqf/0voice/gdb/redis-4.0.11/src/redis-server 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
7231:C 02 Nov 14:29:39.087 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
7231:C 02 Nov 14:29:39.088 # Redis version=4.0.11, bits=64, commit=00000000, modified=0, pid=7231, just started
7231:C 02 Nov 14:29:39.088 # Warning: no config file specified, using the default config. In order to specify a config file use /home/lqf/0voice/gdb/redis-4.0.11/src/redis-server /path/to/redis.conf
7231:M 02 Nov 14:29:39.090 * Increased maximum number of open files to 10032 (it was originally set to 1024).
[New Thread 0x7ffff69ff700 (LWP 7235)]
[New Thread 0x7ffff61fe700 (LWP 7238)]
[New Thread 0x7ffff59fd700 (LWP 7239)]
               _._  
          _.-``__ ''-._                                             
      _.-``    `.  `_.  ''-._           Redis 4.0.11 (00000000/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._                                   
 (    '      ,       .-`  | `,    )     Running in standalone mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 6379
 |    `-._   `._    /     _.-'    |     PID: 7231
  `-._    `-._  `-./  _.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |           http://redis.io        
  `-._    `-._`-.__.-'_.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |                                  
  `-._    `-._`-.__.-'_.-'    _.-'                                   
      `-._    `-.__.-'    _.-'                                       
          `-._        _.-'                                           
              `-.__.-'                                               

7231:M 02 Nov 14:29:39.099 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
7231:M 02 Nov 14:29:39.099 # Server initialized
7231:M 02 Nov 14:29:39.100 # 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.
7231:M 02 Nov 14:29:39.100 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled.
7231:M 02 Nov 14:29:39.100 * Ready to accept connections

這就是 redis-server 啓動界面,假設程序已經啓動,再次輸入 run 命令則是重啓程序。我們在 GDB 界面按 Ctrl + C 快捷鍵讓 GDB 中斷下來,再次輸入 r 命令,GDB 會詢問我們是否重啓程序,輸入 yes 確認重啓。

^C
Thread 1 "redis-server" received signal SIGINT, Interrupt.
0x00007ffff7d3410f in epoll_wait (epfd=5, events=0x7ffff6ad7380, 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) r
The program being debugged has been started already.
Start it from the beginning? (y or n) yes
Starting program: /home/lqf/0voice/gdb/redis-4.0.11/src/redis-server

⑵continue 命令

當 GDB 觸發斷點或者使用 Ctrl + C 命令中斷下來後,想讓程序繼續運行,只要輸入 continue 命令即可(簡寫爲 c)。當然,如果 continue 命令繼續觸發斷點,GDB 就會再次中斷下來。

^C
Thread 1 "redis-server" received signal SIGINT, Interrupt.
0x00007ffff7d3410f in epoll_wait (epfd=5, events=0x7ffff6ad7380, 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.

⑶break 命令

break 命令(簡寫爲 b)即我們添加斷點的命令,可以使用以下方式添加斷點:

這三種方式都是我們常用的添加斷點的方式。

在 redis main() 函數處添加一個斷點:

(gdb) b main
Breakpoint 1 at 0x3d308: file server.c, line 3709.

設置斷點後重啓程序

(gdb) r
Starting program: /home/lqf/0voice/gdb/redis-4.0.11/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=0x7fffffffe468) at server.c:3709
3709	int main(int argc, char **argv) {(gdb)}

redis-server 默認端口號是 6379,綁定端口是需要調用 bind 函數,通過文件搜索可以找到相應位置文件,在 anet.c 441 行。

使用 break 命令在這個地方加一個斷點:

(gdb) b anet.c:441
Breakpoint 2 at 0x555555585909: file anet.c, line 441.

由於程序綁定端口號是 redis-server 啓動時初始化的,爲了能觸發這個斷點,再次使用 run 命令重啓下這個程序,GDB 第一次會觸發 main() 函數處的斷點,輸入 continue 命令繼續運行,接着觸發 anet.c:441 處的斷點:

(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/lqf/0voice/gdb/redis-4.0.11/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=0x7fffffffe468) at server.c:3709
3709	int main(int argc, char **argv) {
(gdb) c
Continuing.
7379:C 02 Nov 14:49:04.618 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
7379:C 02 Nov 14:49:04.618 # Redis version=4.0.11, bits=64, commit=00000000, modified=0, pid=7379, just started
7379:C 02 Nov 14:49:04.619 # Warning: no config file specified, using the default config. In order to specify a config file use /home/lqf/0voice/gdb/redis-4.0.11/src/redis-server
/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) {

anet.c:441 對應的代碼:

現在斷點停在第 441 行,所以當前文件就是 anet.c,可以直接使用 “break 行號” 添加斷點。例如,可以在第 444 行、450 行、452 行分別加一個斷點,看看這個函數執行完畢後走哪個 return 語句退出,則可以執行:

(gdb) b 444
Breakpoint 3 at 0x555555585955: file anet.c, line 444.
(gdb) b 450
Breakpoint 4 at 0x5555555859a3: file anet.c, line 450.
(gdb) b 452
Breakpoint 5 at 0x5555555859aa: file anet.c, line 452.

添加好這三個斷點以後,我們使用 continue 命令繼續運行程序,發現程序運行到第 452 行中斷下來(即觸發 Breakpoint 5):

(gdb) c
Continuing.

Breakpoint 5, anetListen (err=0x5555558d4930 <server+560> "", s=6, sa=0x5555558d99a0, len=28, backlog=511)
  at anet.c:452
452	    return ANET_OK;

說明 redis-server 綁定端口號並設置偵聽(listen)成功,我們可以再打開一個 SSH 窗口,驗證一下,發現 6379 端口確實已經處於偵聽狀態了。

lqf@ubuntu:~$ lsof -i:6379
COMMAND    PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
redis-ser 7379  lqf    6u  IPv6  63292      0t0  TCP *:6379 (LISTEN)

⑷backtrace 與 frame 命令

backtrace 命令(簡寫爲 bt)用來查看當前調用堆棧。接上,redis-server 現在中斷在 anet.c:452 行,可以通過 backtrace 命令來查看當前的調用堆棧:

(gdb) bt
#0  anetListen (err=0x5555558d4930 <server+560> "", s=6, sa=0x5555558d99a0, len=28, backlog=511) at anet.c:452
#1  0x0000555555585bb5 in _anetTcpServer (err=0x5555558d4930 <server+560> "", port=6379, bindaddr=0x0, af=10, 
    backlog=511) at anet.c:487
#2  0x0000555555585ca6 in anetTcp6Server (err=0x5555558d4930 <server+560> "", port=6379, bindaddr=0x0, backlog=511)
    at anet.c:510
#3  0x000055555558bca1 in listenToPort (port=6379, fds=0x5555558d4864 <server+356>, 
    count=0x5555558d48a4 <server+420>) at server.c:1728
#4  0x000055555558c323 in initServer () at server.c:1852
#5  0x00005555555919a1 in main (argc=1, argv=0x7fffffffe468) at server.c:3862

這裏一共有 6 層堆棧,最頂層是 main() 函數,最底層是斷點所在的 anetListen() 函數,堆棧編號分別是 #0 ~ #5 ,如果想切換到其他堆棧處,可以使用 frame 命令(簡寫爲 f),該命令的使用方法是 “frame 堆棧編號(編號不加 #)”。在這裏依次切換至堆棧頂部,然後再切換回 #0 練習一下:

(gdb) f 1
#1  0x0000555555585bb5 in _anetTcpServer (err=0x5555558d4930 <server+560> "", port=6379, bindaddr=0x0, af=10, 
    backlog=511) at anet.c:487
487	        if (anetListen(err,s,p->ai_addr,p->ai_addrlen,backlog) == ANET_ERR) s = ANET_ERR;
(gdb) f 2
#2  0x0000555555585ca6 in anetTcp6Server (err=0x5555558d4930 <server+560> "", port=6379, bindaddr=0x0, backlog=511)
    at anet.c:510
510	    return _anetTcpServer(err, port, bindaddr, AF_INET6, backlog);
(gdb) f 3
#3  0x000055555558bca1 in listenToPort (port=6379, fds=0x5555558d4864 <server+356>, 
    count=0x5555558d48a4 <server+420>) at server.c:1728
1728	            fds[*count] = anetTcp6Server(server.neterr,port,NULL,
(gdb) f 4
#4  0x000055555558c323 in initServer () at server.c:1852
1852	        listenToPort(server.port,server.ipfd,&server.ipfd_count) == C_ERR)
(gdb) f 5
#5  0x00005555555919a1 in main (argc=1, argv=0x7fffffffe468) at server.c:3862
3862	    initServer();
(gdb)

⑸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 斷點編號” 重新啓用。

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

使用 disable 1 以後,第一個斷點的 Enb 一欄的值由 y 變成 n,重啓程序也不會再次觸發

(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/lqf/0voice/gdb/redis-4.0.11/src/redis-server 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
7425:C 02 Nov 15:04:21.165 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
7425:C 02 Nov 15:04:21.166 # Redis version=4.0.11, bits=64, commit=00000000, modified=0, pid=7425, just started
7425:C 02 Nov 15:04:21.166 # Warning: no config file specified, using the default config. In order to specify a config file use /home/lqf/0voice/gdb/redis-4.0.11/src/redis-server /path/to/redis.conf
7425:M 02 Nov 15:04:21.168 * 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) {
(gdb)

如果 disable 命令和 enable 命令不加斷點編號,則分別表示禁用和啓用所有斷點:

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

使用 “delete 編號” 可以刪除某個斷點,如 delete 2 3 則表示要刪除的斷點 2 和斷點 3:

(gdb) delete 2 3
(gdb) info b
Num     Type           Disp Enb Address            What
1       breakpoint     keep n   0x0000555555591308 in main at server.c:3709
4       breakpoint     keep n   0x00005555555859a3 in anetListen at anet.c:450
5       breakpoint     keep n   0x00005555555859aa in anetListen at anet.c:452

同樣的道理,如果輸入 delete 不加命令號,則表示刪除所有斷點。

⑹list 命令

(gdb) list

如果不帶任何參數的話,該命令會接着打印上次 list 命令打印出代碼後面的代碼。如果是第一次執行 list 命令則會顯示當前正在執行代碼位置附近的代碼。

(gdb) list -

如果參數是一個減號的話,則和前面剛好相反,會打印上次 list 命令打印出代碼前面的代碼。

(gdb) list LOATION

list 命令還可以帶一個代碼位置作爲參數,顧名思義,這樣的話就會打印出該代碼位置附近的代碼。這個代碼位置的定義和在 break 命令中定義的相同,可以是一個行號:

也可以是一個函數名:

list 命令還可以指定要顯示代碼的具體範圍:

(gdb) list FIRST,LAST

這裏 FIRST 和 LAST 都是具體的代碼位置,此時該命令將顯示 FIRST 到 LAST 之間的代碼。可以不指定 FIRST 或者 LAST 參數,這樣的話就將顯示 LAST 之前或者 FIRST 之後的代碼。注意,即使只指定一個參數也要帶逗號,否則就編程前面的命令,顯示代碼位置附近的代碼了。

list 命令默認只會打印出 10 行源代碼,如果覺得不夠,可以使用如下命令修改:

(gdb) set listsize COUNT

這樣的話,下次 list 命令就會顯示 COUNT 行源代碼了。如果想查看這個參數當前被設置成多少,可以使用如下命令:

(gdb) show listsize

還有一個非常有用的命令,如果你想看程序中一共定義了哪些函數,可以使用下面的命令:

(gdb) info functions

這個命令會顯示程序中所有函數的名詞,參數格式,返回值類型以及函數處於哪個代碼文件中。

list 命令(簡寫爲 l)可以查看當前斷點處的代碼。使用 frame 命令切換到剛纔的堆棧 #3 處,然後輸入 list 命令看下效果:

(gdb) c
Continuing.

Breakpoint 5, anetListen (err=0x5555558d4930 <server+560> "", s=6, sa=0x5555558d99a0, len=28, backlog=511)
    at anet.c:452
452	    return ANET_OK;
(gdb) bt
#0  anetListen (err=0x5555558d4930 <server+560> "", s=6, sa=0x5555558d99a0, len=28, backlog=511) at anet.c:452
#1  0x0000555555585bb5 in _anetTcpServer (err=0x5555558d4930 <server+560> "", port=6379, bindaddr=0x0, af=10, 
    backlog=511) at anet.c:487
#2  0x0000555555585ca6 in anetTcp6Server (err=0x5555558d4930 <server+560> "", port=6379, bindaddr=0x0, backlog=511)
    at anet.c:510
#3  0x000055555558bca1 in listenToPort (port=6379, fds=0x5555558d4864 <server+356>, 
    count=0x5555558d48a4 <server+420>) at server.c:1728
#4  0x000055555558c323 in initServer () at server.c:1852
#5  0x00005555555919a1 in main (argc=1, argv=0x7fffffffe468) at server.c:3862
(gdb) f 4
#4  0x000055555558c323 in initServer () at server.c:1852
1852	        listenToPort(server.port,server.ipfd,&server.ipfd_count) == C_ERR)
(gdb) l
1847	    }
1848	    server.db = zmalloc(sizeof(redisDb)*server.dbnum);
1849	
1850	    /* Open the TCP listening socket for the user commands. */
1851	    if (server.port != 0 &&
1852	        listenToPort(server.port,server.ipfd,&server.ipfd_count) == C_ERR)
1853	        exit(1);
1854	
1855	    /* Open the listening Unix domain socket. */
1856	    if (server.unixsocket != NULL) {
(gdb)

斷點停在第 1852 行,輸入 list 命令以後,會顯示第 1852 行前後的 10 行代碼,再次輸入 list 命令試一下:

(gdb) l
1857	        unlink(server.unixsocket); /* don't care if this fails */
1858	        server.sofd = anetUnixServer(server.neterr,server.unixsocket,
1859	            server.unixsocketperm, server.tcp_backlog);
1860	        if (server.sofd == ANET_ERR) {
1861	            serverLog(LL_WARNING, "Opening Unix socket: %s", server.neterr);
1862	            exit(1);
1863	        }
1864	        anetNonBlock(NULL,server.sofd);
1865	    }
1866	
(gdb) l
1867	    /* Abort if there are no listening sockets at all. */
1868	    if (server.ipfd_count == 0 && server.sofd < 0) {
1869	        serverLog(LL_WARNING, "Configured to not listen anywhere, exiting.");
1870	        exit(1);
1871	    }
1872	
1873	    /* Create the Redis databases, and initialize other internal state. */
1874	    for (j = 0; j < server.dbnum; j++) {
1875	        server.db[j].dict = dictCreate(&dbDictType,NULL);
1876	        server.db[j].expires = dictCreate(&keyptrDictType,NULL);

代碼繼續往後顯示 10 行,也就是說,第一次輸入 list 命令會顯示斷點處前後的代碼,繼續輸入 list 指令會以遞增行號的形式繼續顯示剩下的代碼行,一直到文件結束爲止。當然 list 指令還可以往前和往後顯示代碼,命令分別是 “list + (加號)” 和 “list - (減號)”:

(gdb) list -
1857            unlink(server.unixsocket); /* don't care if this fails */
1858            server.sofd = anetUnixServer(server.neterr,server.unixsocket,
1859                server.unixsocketperm, server.tcp_backlog);
1860            if (server.sofd == ANET_ERR) {
1861                serverLog(LL_WARNING, "Opening Unix socket: %s", server.neterr);
1862                exit(1);
1863            }
1864            anetNonBlock(NULL,server.sofd);
1865        }
1866
(gdb) l -
1847        }
1848        server.db = zmalloc(sizeof(redisDb)*server.dbnum);
1849
1850        /* Open the TCP listening socket for the user commands. */
1851        if (server.port != 0 &&
1852            listenToPort(server.port,server.ipfd,&server.ipfd_count) == C_ERR)
1853            exit(1);
1854
1855        /* Open the listening Unix domain socket. */
1856        if (server.unixsocket != NULL) {

可以在試試 list +15 和 list -15。

⑹print 和 ptype 命令

通過 print 命令(簡寫爲 p)我們可以在調試過程中方便地查看變量的值,也可以修改當前內存中的變量值。切換當前斷點到堆棧 #4 ,然後打印以下三個變量。

(gdb) f 4
#4  0x000055555558c323 in initServer () at server.c:1852
1852	        listenToPort(server.port,server.ipfd,&server.ipfd_count) == C_ERR)
(gdb) l
1847	    }
1848	    server.db = zmalloc(sizeof(redisDb)*server.dbnum);
1849	
1850	    /* Open the TCP listening socket for the user commands. */
1851	    if (server.port != 0 &&
1852	        listenToPort(server.port,server.ipfd,&server.ipfd_count) == C_ERR)
1853	        exit(1);
1854	
1855	    /* Open the listening Unix domain socket. */
1856	    if (server.unixsocket != NULL) {
(gdb) p server.port
$1 = 6379
(gdb) p server.ipfd
$2 = {0 <repeats 16 times>}
(gdb) p server.ipfd_count 
$3 = 0
(gdb)

這裏使用 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 試試:

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

總結起來,利用 print 命令,我們不僅可以查看程序運行過程中的各個變量的狀態值,也可以通過臨時修改變量的值來控制程序的行爲。

⑺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

可以看到,對於一個複合數據類型的變量,ptype 不僅列出了這個變量的類型( 這裏是一個名叫 redisServer 的結構體),而且詳細地列出了每個成員變量的字段名,方便我們去查看每個變量的類型定義。

⑻info 和 thread 命令

在前面使用 info break 命令查看當前斷點時介紹過,info 命令是一個複合指令,還可以用來查看當前進程的所有線程運行情況。下面以 redis-server 進程爲例來演示一下,使用 delete 命令刪掉所有斷點,然後使用 run 命令重啓一下 redis-server,等程序正常啓動後,我們按快捷鍵 Ctrl+C 中斷程序,然後使用 info thread 命令來查看當前進程有哪些線程,分別中斷在何處:

(gdb) delete 
Delete all breakpoints? (y or n) y
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/lqf/0voice/gdb/redis-4.0.11/src/redis-server 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
7653:C 02 Nov 15:33:27.959 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
7653:C 02 Nov 15:33:27.959 # Redis version=4.0.11, bits=64, commit=00000000, modified=0, pid=7653, just started
7653:C 02 Nov 15:33:27.959 # Warning: no config file specified, using the default config. In order to specify a config file use /home/lqf/0voice/gdb/redis-4.0.11/src/redis-server /path/to/redis.conf
7653:M 02 Nov 15:33:27.962 * Increased maximum number of open files to 10032 (it was originally set to 1024).
[New Thread 0x7ffff69ff700 (LWP 7654)]
[New Thread 0x7ffff61fe700 (LWP 7655)]
[New Thread 0x7ffff59fd700 (LWP 7656)]
7653:M 02 Nov 15:33:27.965 * Ready to accept connections
^C
Thread 1 "redis-server" received signal SIGINT, Interrupt.
0x00007ffff7d3410f in epoll_wait (epfd=5, events=0x7ffff6ad7380, 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) info threads 
  Id   Target Id                                       Frame 
* 1    Thread 0x7ffff7c17bc0 (LWP 7653) "redis-server" 0x00007ffff7d3410f in epoll_wait (epfd=5, 
    events=0x7ffff6ad7380, maxevents=10128, timeout=100) at ../sysdeps/unix/sysv/linux/epoll_wait.c:30
  2    Thread 0x7ffff69ff700 (LWP 7654) "redis-server" futex_wait_cancelable (private=<optimized out>, expected=0, 
    futex_word=0x5555558bf6a8 <bio_newjob_cond+40>) at ../sysdeps/unix/sysv/linux/futex-internal.h:88
  3    Thread 0x7ffff61fe700 (LWP 7655) "redis-server" futex_wait_cancelable (private=<optimized out>, expected=0, 
    futex_word=0x5555558bf6d8 <bio_newjob_cond+88>) at ../sysdeps/unix/sysv/linux/futex-internal.h:88
  4    Thread 0x7ffff59fd700 (LWP 7656) "redis-server" futex_wait_cancelable (private=<optimized out>, expected=0, 
    futex_word=0x5555558bf708 <bio_newjob_cond+136>) at ../sysdeps/unix/sysv/linux/futex-internal.h:88
(gdb)

通過 info thread 的輸出可以知道 redis-server 正常啓動後,一共產生了 4 個線程,包括一個主線程和三個工作線程,線程編號(Id 那一列)分別是 4、3、2、1。

注意 雖然第一欄的名稱叫 Id,但第一欄的數值不是線程的 Id,第三欄括號裏的內容(如 LWP 53065)中,53065 這樣的數值纔是當前線程真正的 Id。Light Weight Process 輕量級進程),即是我們所說的線程。

怎麼知道線程哪個線程是主線程,現在有 4 個線程,也就有 4 個調用堆棧,如果此時輸入 backtrace 命令查看調用堆棧,由於當前 GDB 作用在線程 1,因此 backtrace 命令顯示的一定是線程 1 的調用堆棧:

(gdb) bt
#0  0x00007ffff7d3410f in epoll_wait (epfd=5, events=0x7ffff6ad7380, maxevents=10128, timeout=100)
    at ../sysdeps/unix/sysv/linux/epoll_wait.c:30
#1  0x00005555555838ad in aeApiPoll (eventLoop=0x7ffff6a3c0a0, tvp=0x7fffffffe2c0) at ae_epoll.c:112
#2  0x0000555555584571 in aeProcessEvents (eventLoop=0x7ffff6a3c0a0, flags=11) at ae.c:411
#3  0x0000555555584868 in aeMain (eventLoop=0x7ffff6a3c0a0) at ae.c:501
#4  0x0000555555591aff in main (argc=1, argv=0x7fffffffe468) at server.c:3899

由此可見,堆棧 #4 的 main() 函數也證實了上面的說法,即線程編號爲 1 的線程是主線程。

如何切換到其他線程呢?可以通過 “thread 線程編號” 切換到具體的線程上去。例如,想切換到線程 2 上去,只要輸入 thread 2 即可,然後輸入 bt 就能查看這個線程的調用堆棧了:

(gdb) thread 2
[Switching to thread 2 (Thread 0x7ffff69ff700 (LWP 7654))]
#0  futex_wait_cancelable (private=<optimized out>, expected=0, futex_word=0x5555558bf6a8 <bio_newjob_cond+40>)
    at ../sysdeps/unix/sysv/linux/futex-internal.h:88
88	../sysdeps/unix/sysv/linux/futex-internal.h: No such file or directory.
(gdb) bt
#0  futex_wait_cancelable (private=<optimized out>, expected=0, futex_word=0x5555558bf6a8 <bio_newjob_cond+40>)
    at ../sysdeps/unix/sysv/linux/futex-internal.h:88
#1  __pthread_cond_wait_common (abstime=0x0, mutex=0x5555558bf600 <bio_mutex>, cond=0x5555558bf680 <bio_newjob_cond>)
    at pthread_cond_wait.c:502
#2  __pthread_cond_wait (cond=0x5555558bf680 <bio_newjob_cond>, mutex=0x5555558bf600 <bio_mutex>)
    at pthread_cond_wait.c:655
#3  0x00005555555f2f9c in bioProcessBackgroundJobs (arg=0x0) at bio.c:176
#4  0x00007ffff7e0b164 in start_thread (arg=<optimized out>) at pthread_create.c:486
#5  0x00007ffff7d33def in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95

因此利用 info thread 命令就可以調試多線程程序,當然用 GDB 調試多線程程序還有一個很麻煩的問題,我們將在後面的 GDB 高級調試技巧中介紹。請注意,當把 GDB 當前作用的線程切換到線程 2 上之後,線程 2 前面就被加上了星號:

(gdb) info threads 
  Id   Target Id                                       Frame 
  1    Thread 0x7ffff7c17bc0 (LWP 7653) "redis-server" 0x00007ffff7d3410f in epoll_wait (epfd=5, 
    events=0x7ffff6ad7380, maxevents=10128, timeout=100) at ../sysdeps/unix/sysv/linux/epoll_wait.c:30
* 2    Thread 0x7ffff69ff700 (LWP 7654) "redis-server" futex_wait_cancelable (private=<optimized out>, expected=0, 
    futex_word=0x5555558bf6a8 <bio_newjob_cond+40>) at ../sysdeps/unix/sysv/linux/futex-internal.h:88
  3    Thread 0x7ffff61fe700 (LWP 7655) "redis-server" futex_wait_cancelable (private=<optimized out>, expected=0, 
    futex_word=0x5555558bf6d8 <bio_newjob_cond+88>) at ../sysdeps/unix/sysv/linux/futex-internal.h:88
  4    Thread 0x7ffff59fd700 (LWP 7656) "redis-server" futex_wait_cancelable (private=<optimized out>, expected=0, 
    futex_word=0x5555558bf708 <bio_newjob_cond+136>) at ../sysdeps/unix/sysv/linux/futex-internal.h:88

info 命令還可以用來查看當前函數的參數值,組合命令是 info args,我們找個函數值多一點的堆棧函數來試一下:

(gdb) thread 1
[Switching to thread 1 (Thread 0x7ffff7fec780 (LWP 53062))]
#0  0x00007ffff73ee923 in epoll_wait () from /lib64/libc.so.6
(gdb) bt
#0  0x00007ffff73ee923 in epoll_wait () from /lib64/libc.so.6
#1  0x00000000004265df in aeApiPoll (tvp=0x7fffffffe300, eventLoop=0x7ffff08350a0) at ae_epoll.c:112
#2  aeProcessEvents (eventLoop=eventLoop@entry=0x7ffff08350a0, flags=flags@entry=11) at ae.c:411
#3  0x0000000000426aeb in aeMain (eventLoop=0x7ffff08350a0) at ae.c:501
#4  0x00000000004238ef in main (argc=1, argv=0x7fffffffe648) at server.c:3899
(gdb) f 2
#2  aeProcessEvents (eventLoop=eventLoop@entry=0x7ffff08350a0, flags=flags@entry=11) at ae.c:411
411             numevents = aeApiPoll(eventLoop, tvp);
(gdb) info args
eventLoop = 0x7ffff08350a0
flags = 11
(gdb)

上述代碼片段切回至主線程 1,然後切換到堆棧 #2,堆棧 #2 調用處的函數是 aeProcessEvents() ,一共有兩個參數,使用 info args 命令可以輸出當前兩個函數參數的值,參數 eventLoop 是一個指針類型的參數,對於指針類型的參數,GDB 默認會輸出該變量的指針地址值,如果想輸出該指針指向對象的值,在變量名前面加上 * 解引用即可,這裏使用 p *eventLoop 命令:

(gdb) p *eventLoop
$26 = {maxfd = 11, setsize = 10128, timeEventNextId = 1, lastTime = 1536570672, events = 0x7ffff0871480, fired = 0x7ffff08c2e40, timeEventHead = 0x7ffff0822080,
  stop = 0, apidata = 0x7ffff08704a0, beforesleep = 0x429590 <beforeSleep>, aftersleep = 0x4296d0 <afterSleep>}

如果還要查看其成員值,繼續使用 變量名 -> 字段名 即可,在前面學習 print 命令時已經介紹過了,這裏不再贅述。上面介紹的是 info 命令最常用的三種方法,更多的方法使用 help info 查看。

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

這幾個命令是 GDB 調試程序時最常用的幾個控制流命令,因此放在一起介紹。next 命令(簡寫爲 n)是讓 GDB 調到下一條命令去執行,這裏的下一條命令不一定是代碼的下一行,而是根據程序邏輯跳轉到相應的位置。

①next 命令 舉個例子:

如果當前 GDB 中斷在上述代碼第 6 行,此時輸入 next 命令 GDB 將調到第 11 行,因爲這裏的 if 條件並不滿足。

這裏有一個小技巧,在 GDB 命令行界面如果直接按下回車鍵,默認是將最近一條命令重新執行一遍,因此,當使用 next 命令單步調試時,不必反覆輸入 n 命令,直接回車就可以了。

next 命令用調試的術語叫 “單步步過”(step over),即遇到函數調用直接跳過,不進入函數體內部。而下面的 step 命令(簡寫爲 s)就是 “單步步入”(step into),顧名思義,就是遇到函數調用,進入函數內部。舉個例子,在 redis-server 的 main() 函數中有個叫 spt_init(argc, argv) 的函數調用,當我們停在這一行時,輸入 s 將進入這個函數內部。

// 除去不相關的干擾,代碼有刪除
int main(int argc, char **argv) {
    struct timeval tv;
    int j;
    /* We need to initialize our libraries, and the server configuration. */
    spt_init(argc, argv);
    setlocale(LC_COLLATE,"");
    zmalloc_set_oom_handler(redisOutOfMemoryHandler);
    srand(time(NULL)^getpid());
    gettimeofday(&tv,NULL);
    char hashseed[16];
    getRandomHexChars(hashseed,sizeof(hashseed));
    dictSetHashFunctionSeed((uint8_t*)hashseed);
 server.sentinel_mode = checkForSentinelMode(argc,argv);
    initServerConfig();
    moduleInitModulesSystem();
    //省略部分無關代碼...
 }

②step 命令

使用 b main 命令在 main() 處加一個斷點,然後使用 r 命令重新跑一下程序,會觸發剛纔加在 main() 函數處的斷點,然後使用 n 命令讓程序走到 spt_init(argc, argv) 函數調用處,再輸入 s 命令就可以進入該函數了:

(gdb) b main
Breakpoint 3 at 0x423450: file server.c, line 3704.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /root/redis-4.0.9/src/redis-server
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".

Breakpoint 3, main (argc=1, argv=0x7fffffffe588) at server.c:3704
3704    int main(int argc, char **argv) {
(gdb) n
3736        spt_init(argc, argv);
(gdb) s
spt_init (argc=argc@entry=1, argv=argv@entry=0x7fffffffe588) at setproctitle.c:152
152     void spt_init(int argc, char *argv[]) {
(gdb) l
147
148             return 0;
149     } /* spt_copyargs() */
150
151
152     void spt_init(int argc, char *argv[]) {
153             char **envp = environ;
154             char *base, *end, *nul, *tmp;
155             int i, error;
156
(gdb)

⑽ return 和 finish 命令

實際調試時,我們在某個函數中調試一段時間後,不需要再一步步執行到函數返回處,希望直接執行完當前函數並回到上一層調用處,就可以使用 finish 命令。與 finish 命令類似的還有 return 命令,return 命令的作用是結束執行當前函數,還可以指定該函數的返回值。

這裏需要注意一下二者的區別:finish 命令會執行函數到正常退出該函數;而 return 命令是立即結束執行當前函數並返回,也就是說,如果當前函數還有剩餘的代碼未執行完畢,也不會執行了。

⑾until 命令

實際調試時,還有一個 until 命令(簡寫爲 u)可以指定程序運行到某一行停下來,還是以 redis-server 的代碼爲例:

1812    void initServer(void) {
1813        int j;
1814
1815        signal(SIGHUP, SIG_IGN);
1816        signal(SIGPIPE, SIG_IGN);
1817        setupSignalHandlers();
1818
1819        if (server.syslog_enabled) {
1820            openlog(server.syslog_ident, LOG_PID | LOG_NDELAY | LOG_NOWAIT,
1821                server.syslog_facility);
1822        }
1823
1824        server.pid = getpid();
1825        server.current_client = NULL;
1826        server.clients = listCreate();
1827        server.clients_to_close = listCreate();
1828        server.slaves = listCreate();
1829        server.monitors = listCreate();
1830        server.clients_pending_write = listCreate();
1831        server.slaveseldb = -1; /* Force to emit the first SELECT command. */
1832        server.unblocked_clients = listCreate();
1833        server.ready_keys = listCreate();
1834        server.clients_waiting_acks = listCreate();
1835        server.get_ack_from_slaves = 0;
1836        server.clients_paused = 0;
1837        server.system_memory_size = zmalloc_get_memory_size();
1838
1839        createSharedObjects();
1840        adjustOpenFilesLimit();
1841        server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
1842        if (server.el == NULL) {
1843            serverLog(LL_WARNING,
1844                "Failed creating the event loop. Error message: '%s'",
1845                strerror(errno));
1846            exit(1);
1847        }

這是 redis-server 代碼中 initServer() 函數的一個代碼片段,位於文件 server.c 中,當停在第 1813 行,想直接跳到第 1839 行,可以直接輸入 u 1839,這樣就能快速執行完中間的代碼。當然,也可以先在第 1839 行加一個斷點,然後使用 continue 命令運行到這一行,但是使用 until 命令會更簡便。

b initServer
u 1839

⑿Jump 命令

jump 命令基本用法是:

jump <location>

該命令會帶一個參數,即要跳轉到的代碼位置,可以是源代碼的行號:

(gdb) jump 555 #跳轉到源代碼的第555行的位置

可以是相對當前代碼位置的偏移量:

(gdb) jump +10 #跳轉到距當前代碼下10行的位置

也可以是代碼所處的內存地址:

(gdb) jump *0x12345678 #跳轉到位於該地址的代碼處

注意,在內存地址前面要加 “*”。還有,jump 命令不會改變當前程序調用棧的內容,所以當你從一個函數跳到另一個函數時,當函數運行完返回進行退棧操作時就會發生錯誤,因此最好還是在同一個函數中進行跳轉。

location 可以是程序的行號或者函數的地址,jump 會讓程序執行流跳轉到指定位置執行,當然其行爲也是不可控制的,例如您跳過了某個對象的初始化代碼,直接執行操作該對象的代碼,那麼可能會導致程序崩潰或其他意外行爲。jump 命令可以簡寫成 j,但是不可以簡寫成 jmp,其使用有一個注意事項,即如果 jump 跳轉到的位置後續沒有斷點,那麼 GDB 會執行完跳轉處的代碼會繼續執行。舉個例子:

1 int somefunc()
2 {
3   //代碼A
4   //代碼B
5   //代碼C
6   //代碼D
7   //代碼E
8   //代碼F
9 }

假設我們的斷點初始位置在行號 3 處(代碼 A),這個時候我們使用 jump 6,那麼程序會跳過代碼 B 和 C 的執行,執行完代碼 D( 跳轉點),程序並不會停在代碼 6 處,而是繼續執行後續代碼,因此如果我們想查看執行跳轉處的代碼後的結果,需要在行號 6、7 或 8 處設置斷點。有時候也可以用來測試一些我們想要執行的代碼(正常邏輯不太可能跑到),比如

我們想執行 12 行的代碼。則

b main
jump 12

就會將 else 分支執行。

⒀disassemble 命令

當進行一些高級調試時,我們可能需要查看某段代碼的彙編指令去排查問題,或者是在調試一些沒有調試信息的發佈版程序時,也只能通過反彙編代碼去定位問題,那麼 disassemble 命令就派上用場了。

(gdb) bt
#0  initServer () at server.c:1839
#1  0x00005555555919a1 in main (argc=1, argv=0x7fffffffe468) at server.c:3862
(gdb) disassemble 
Dump of assembler code for function initServer:
   0x000055555558c18a <+0>:	push   %rbp
   0x000055555558c18b <+1>:	mov    %rsp,%rbp
   0x000055555558c18e <+4>:	push   %rbx
   0x000055555558c18f <+5>:	sub    $0x18,%rsp
   0x000055555558c193 <+9>:	mov    $0x1,%esi
   0x000055555558c198 <+14>:	mov    $0x1,%edi
   0x000055555558c19d <+19>:	callq  0x55555557f830 <signal@plt>
   0x000055555558c1a2 <+24>:	mov    $0x1,%esi
   0x000055555558c1a7 <+29>:	mov    $0xd,%edi
   0x000055555558c1ac <+34>:	callq  0x55555557f830 <signal@plt>

⒁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 查看命令行參數是否設置成功。

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

如果單個命令行參數之間含有空格,可以使用引號將參數包裹起來。

(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"".
(gdb)

如果想清除掉已經設置好的命令行參數,使用 set args 不加任何參數即可。

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

⒂ tbreak 命令

tbreak 命令也是添加一個斷點,第一個字母 “t” 的意思是 temporarily(臨時的),也就是說這個命令加的斷點是臨時的,所謂臨時斷點,就是一旦該斷點觸發一次後就會自動刪除。添加斷點的方法與上面介紹的 break 命令一模一樣,這裏不再贅述。

⒃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.c

#include <stdio.h>
#include <string.h>

char mem[8];
char buf[128];

void initBuf(char *pBuf)
{
    int i, j;
    mem[0] = '0';
    mem[1] = '1';
    mem[2] = '2';
    mem[3] = '3';
    mem[4] = '4';
    mem[5] = '5';
    mem[6] = '6';
       mem[7] = '7';
    //ascii table first 32 is not printable
    for (i = 2; i < 8; i++)
    {
        for (j = 0; j < 16; j++)
            pBuf[i * 16 + j] = i * 16 + j;
    }
}

void prtBuf(char *pBuf)
{
    int i, j;
    for (i = 2; i < 8; i++)
    {
        for (j = 0; j < 16; j++)
            printf("%c  ", pBuf[i * 16 + j]);
        printf("\n");
    }
}

int main()
{
    initBuf(buf);
    prtBuf(buf);
    return 0;
}

測試

$ gdb ./watch
GNU gdb (Ubuntu 8.2-0ubuntu1) 8.2

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./watch...done.
(gdb) b main 
Breakpoint 1 at 0x1248: file watch.c, line 39.
(gdb) watch mem
Hardware watchpoint 2: mem
(gdb) c
The program is not being run.
(gdb) r
Starting program: /mnt/hgfs/ubuntu/vip/20191102-valgrind/src/gdb/watch 

Breakpoint 1, main () at watch.c:39
39	    initBuf(buf);
(gdb) c
Continuing.

Hardware watchpoint 2: mem

Old value = "\000\000\000\000\000\000\000"
New value = "0\000\000\000\000\000\000"
initBuf (pBuf=0x555555558060 <buf> "") at watch.c:11
11	    mem[1] = '1';
(gdb) c
Continuing.

Hardware watchpoint 2: mem

Old value = "0\000\000\000\000\000\000"
New value = "01\000\000\000\000\000"
initBuf (pBuf=0x555555558060 <buf> "") at watch.c:12
12	    mem[2] = '2';
(gdb) c
Continuing.

Hardware watchpoint 2: mem

Old value = "01\000\000\000\000\000"
New value = "012\000\000\000\000"
initBuf (pBuf=0x555555558060 <buf> "") at watch.c:13
13	    mem[3] = '3';
(gdb) c
Continuing.

......省略部分

Hardware watchpoint 2: mem

Old value = "012345\000"
New value = "0123456"
initBuf (pBuf=0x555555558060 <buf> "") at watch.c:17
17	    mem[7] = '7';
(gdb)

watch i 的問題:是可以同時去 watch,只是局部變量需要進入到相應的起作用範圍才能 watch。比如 initBuf 函數的 i。問題來了,如果要取消 watch 怎麼辦?先用 info watch 查看 watch 的變量,然後根據編號使用 delete 刪除相應的 watch 變量。

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

⒁display 命令

display 命令監視的變量或者內存地址,每次程序中斷下來都會自動輸出這些變量或內存的值。例如,假設程序有一些全局變量,每次斷點停下來我都希望 GDB 可以自動輸出這些變量的最新值,那麼使用 “display 變量名” 設置即可。

Program received signal SIGINT, Interrupt.
0x00007ffff71e2483 in epoll_wait () from /lib64/libc.so.6
(gdb) display $ebx
1: $ebx = 7988560
(gdb) display /x $ebx
2: /x $ebx = 0x79e550
(gdb) display $eax
3: $eax = -4
(gdb) b main
Breakpoint 8 at 0x4201f0: file server.c, line 4003.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /root/redis-5.0.3/src/redis-server
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".

Breakpoint 8, main (argc=1, argv=0x7fffffffe4e8) at server.c:4003
4003    int main(int argc, char **argv) {
3: $eax = 4325872
2: /x $ebx = 0x0
1: $ebx = 0
(gdb)

上述代碼中,使用 display 命令分別添加了寄存器 ebp 和寄存器 eax,ebp 寄存器分別使用十進制和十六進制兩種形式輸出其值,這樣每次程序中斷下來都會自動把這些值打印出來,可以使用 info display 查看當前已經自動添加了哪些值,使用 delete display 清除全部需要自動輸出的變量,使用 delete diaplay 編號 刪除某個自動輸出的變量。

(gdb) delete display
Delete all auto-display expressions? (y or n) n
(gdb) delete display 3
(gdb) info display
Auto-display expressions now in effect:
Num Enb Expression
2:   y  $ebp
1:   y  $eax

二、 調試技巧

2.1 將 print 打印結果顯示完整

當使用 print 命令打印一個字符串或者字符數組時,如果該字符串太長,print 命令默認顯示不全的,我們可以通過在 GDB 中輸入 set print element 0 命令設置一下,這樣再次使用 print 命令就能完整地顯示該變量的所有字符串了。

2.2 多線程下禁止線程切換

假設現在有 5 個線程,除了主線程,工作線程都是下面這樣的一個函數:

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

爲了能說清楚這個問題,我們把四個工作線程分別叫做 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,這裏就不介紹了。

2.3 條件斷點

在實際調試中,我們一般會用到三種斷點:普通斷點、條件斷點和硬件斷點。

硬件斷點又叫數據斷點,這樣的斷點其實就是前面課程中介紹的用 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)

2.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".
(gdb)

三、GDB TUI——在 GDB 中顯示程序源碼

3.1 開啓 GDB TUI 模式

開啓 GDB TUI 模式有兩個方法。

方法一:使用 gdbtui 命令或者 gdb-tui 命令開啓一個調試。

gdbtui -q 需要調試的程序名

方法二:直接使用 GDB 調試代碼,在需要的時候使用切換鍵 Ctrl + x,然後按 a ,進入常規 GDB 和 GDB TUI 的來回切換。

3.2GDB TUI 模式常用窗口

默認情況下,GDB TUI 模式會顯示 command 窗口和 source 窗口,如上圖所示,還有其他窗口,如下列舉的四個常用的窗口:

可以通過 “layout + 窗口類型” 命令來選擇自己需要的窗口,例如,在 cmd 窗口輸入 layout asm 則可以切換到彙編代碼窗口。

layout 命令還可以用來修改窗口布局,在 cmd 窗口中輸入 help layout,常見的有:

Usage: layout prev | next | <layout_name> 
Layout names are:
   src   : Displays source and command windows.
   asm   : Displays disassembly and command windows.
   split : Displays source, disassembly and command windows.
   regs  : Displays register window. If existing layout
           is source/command or assembly/command, the 
           register window is displayed. If the
           source/assembly/command (split) is displayed, 
           the register window is displayed with 
           the window that has current logical focus.

另外,可以通過 winheight 命令修改各個窗口的大小,如下所示:

(gdb) help winheight
Set the height of a specified window.
Usage: winheight <win_name> [+ | -] <#lines>
Window names are:
src  : the source window
cmd  : the command window
asm  : the disassembly window
regs : the register display

##將代碼窗口的高度擴大 5 行代碼
winheight src + 5
##將代碼窗口的高度減小 4 代碼
winheight src - 4

3.3 常用快捷鍵

C - 代碼 Ctrl 鍵,下面介紹的皆爲組合鍵,比如:

C-x a,即是先按Ctrl + x 兩個鍵,然後再去按a鍵。

單鍵模式對應的快捷鍵:

3.4 窗口焦點切換

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/YYBEqFd1zj3iDLPTCdhgvw