手把手教你學(xué)會(huì)gdb,適應(yīng)Linux調(diào)試環(huán)境

在前文 基于vscode 打造Linux C++編碼環(huán)境 一期中,講解了如何基于vscode搭建Linux c++的編碼環(huán)境,但是還沒(méi)有講解如何基于vscod搭建調(diào)試環(huán)境。本期,主要有兩個(gè)任務(wù):

  • 講解常用的gcc編譯選項(xiàng)
  • 講解常用的gdb編譯指令


常用gcc編譯選項(xiàng)

深入了解C++系列中,我經(jīng)常使用如下的格式進(jìn)行編譯、執(zhí)行demo:

$ g++ -g -O0 main.cc -o main && ./main

下面,我們來(lái)看看常用的gcc編譯選項(xiàng)有哪些。

選項(xiàng) 作用
-E 生成預(yù)處理文件
-S 生成匯編文件
-c 生成可目標(biāo)文件
-o 指定生成文件的文件名
-On 指定代碼優(yōu)化等級(jí)
-g 用于gdb調(diào)試、objdump
-Wall 顯示代碼中的所有warning行為
-w 禁止顯示代碼中的warning行為
-Werror 將代碼中的warning行為視為為error
-D 設(shè)置預(yù)定義宏
-l 鏈接(link)指定的函數(shù)庫(kù)
-std=c++11 指定編譯代碼的C++標(biāo)準(zhǔn)為C++11

對(duì)于這些編譯選項(xiàng),簡(jiǎn)單的解釋下。

-E、 -S-c 三個(gè)選項(xiàng)直接對(duì)應(yīng)著編譯的前三個(gè)基本階段

預(yù)編譯處理(.i)

將源文件main.cc 經(jīng)過(guò)預(yù)處理后,生成文件預(yù)處理所得文件main.i

g++ -E main.cc -o main.i
編譯、優(yōu)化程序(.s)

main.i 文件翻譯成一個(gè)匯編文件 main.s ;

g++ -S main.i  -o main.s
匯編程序(.o)

運(yùn)行匯編器,將 main.s 翻譯成一個(gè)可重定位目標(biāo)文件 main.o ;

 g++ -c main.s -o main.o
鏈接程序(.elf)

運(yùn)行鏈接器,將 main.o 中使用到的目標(biāo)文件組合起來(lái),并創(chuàng)建一個(gè)可執(zhí)行的文件 main 。由于main.cc代碼沒(méi)有額外的依賴,因此可以直接輸出main文件。

 g++ main.o -o main

實(shí)際上,一步就能完成上面所有的操作:

g++ main.cc -o main
定義宏 -D

比如,對(duì)于下面的一段demo,如果定義了宏DEBUG,則輸出hello cpp

int main(int argc, char const *argv[]) {
#ifdef DEBUG
  std::cout<<"Hello Cpp" <<std::endl;
#endif
  return 0;
}

下面在gcc編譯時(shí)基于-D選項(xiàng)設(shè)置DEBUG宏,來(lái)控制程序執(zhí)行。

$ g++ -DDEBUG main.cc -o main && ./main
Hello Cpp

對(duì)于GCC的編譯選項(xiàng),沒(méi)有必要全部記住,記住常用的即可,其他用到了再去官網(wǎng)查詢:

https://gcc.gnu.org/onlinedocs/gcc/Invoking-GCC.html

常用gdb指令

本期主要講解下我常用的gdb指令、以及怎么去學(xué)習(xí)gdb。希望能通過(guò)本期博客,能幫助你擺脫對(duì)gdb恐懼,并熟悉下gdb的常用指令,對(duì)于沒(méi)有講解到的指令,在本期之后,可以去官方網(wǎng)站自行學(xué)習(xí),那里有著詳細(xì)且為全面的介紹:

https://sourceware.org/gdb/current/onlinedocs/gdb/

為了方便后面基于gdb調(diào)試REDIS源碼的講解,可以先下載REDIS6.0的源碼,并在編譯代碼的時(shí)候,加上-g -O0選項(xiàng),生成調(diào)試信息。比如,我學(xué)習(xí)REDIS的時(shí)候,編譯指令如下:

$ git clone https://github.com/redis/redis.git  # 下載redis源碼
$ cd redis/src                                  # 進(jìn)入源代碼
$ make FLAGS="-g -O0"  -j 16                    # 編譯
$ ./redis-server                                # 運(yùn)行REDIS服務(wù)器

啟動(dòng)gdb

關(guān)于啟動(dòng)gdb的方式,下面介紹下常用的三種啟動(dòng)gdb方式:

  1. gdb [program]:這種方式最常用,比如使用gdb調(diào)試上面編譯生成的main文件,那么就直接 gdb main。
  2. gdb [program] core:用于調(diào)試導(dǎo)致coredump的錯(cuò)誤,此時(shí)需要在program后面加上因?yàn)閏oredump生成的core文件路徑。
  3. gdb -p [pid]:使用gdb調(diào)試正在運(yùn)行的pid進(jìn)程

gdb program

以如下的main程序?yàn)槔?/p>

// main.cc
#include <iostream>

int main(int argc, char const *argv[])
{
  int cnt =0;
  for(int idx=0; idx < 10; ++idx) { 
    cnt++;
  }
  std::cout<<cnt<<std::endl;

  return 0;
}

編譯指令:

$ g++ -g -O0 main.cc -o main

在終端輸入gdb main,會(huì)從main文件中加載符號(hào)表,便于設(shè)置斷點(diǎn)等信息:

$ gdb main
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04) 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"...
# 以上都是關(guān)于gdb的開源信息,為便于描述,下面的教程中會(huì)省略這部分信息
Reading symbols from main...
(gdb) 

輸入gdb main后,會(huì)首先顯示關(guān)于gdb的一大串的開源信息,而且每次啟動(dòng)都會(huì)顯示。因此,在后文的講解中,每次啟動(dòng)gdb會(huì)省略掉這部分信息。

attach pid

如果某個(gè)程序正在運(yùn)行出現(xiàn)故障,比如服務(wù)器程序,無(wú)法被中止,如何使用gdb來(lái)調(diào)試它?

比如,此刻我電腦正在運(yùn)行REDIS服務(wù)器程序,其pid是1607:

  • 我先以root權(quán)限啟動(dòng)gdb
  • 再使用attach pid命令來(lái)調(diào)試正在運(yùn)行的REDIS服務(wù)器程序

示例如下:

$ sudo gdb                          # 先以root權(quán)限啟動(dòng)gdb
# ...關(guān)于gdb的開源聲明省略
(gdb) attach 1607                   # 再使當(dāng)前gdb環(huán)境去調(diào)試redis服務(wù)器
Attaching to process 1607
[New LWP 1608]
[New LWP 1609]
[New LWP 1610]
[New LWP 1611]
[New LWP 1612]
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
0x00007f2d694925ce in epoll_wait (epfd=5, events=0x7f2d68ede980, maxevents=10128, timeout=100)
    at ../sysdeps/unix/sysv/linux/epoll_wait.c:30
30      in ../sysdeps/unix/sysv/linux/epoll_wait.c
(gdb) 

當(dāng)使用attach命令調(diào)試完服務(wù)器程序,可以使用detach指令退出。

(gdb) detach        
Detaching from program: /home/szza/redis-6.0.5/redis-6.0.5/src/redis-server, process 1607
[Inferior 1 (process 1607) detached]

gdb -p pid

當(dāng)然,也可以直接使用gdb -p pid指令,來(lái)調(diào)試正在運(yùn)行的REDIS服務(wù)器程序,其效果和attach一致:

$ sudo gdb -p 1607              # 也要使用root權(quán)限
Attaching to process 1607
[New LWP 1608]
[New LWP 1609]
[New LWP 1610]
[New LWP 1611]
[New LWP 1612]
[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--
0x00007f2d694925ce in epoll_wait (epfd=5, events=0x7f2d68ede980, maxevents=10128, timeout=100)
    at ../sysdeps/unix/sysv/linux/epoll_wait.c:30
30      in ../sysdeps/unix/sysv/linux/epoll_wait.c

毫無(wú)疑問(wèn),這也是可以由detach命令,退出調(diào)試環(huán)境:

(gdb) detach 
Detaching from program: /home/szza/redis-6.0.5/redis-6.0.5/src/redis-server, process 1607
[Inferior 1 (process 1607) detached]

其他啟動(dòng)gdb的方式,可以參考官方文檔:

https://sourceware.org/gdb/current/onlinedocs/gdb/Invoking-GDB.html#Invoking-GDB

運(yùn)行程序

run

run 指令,簡(jiǎn)寫是r,在啟動(dòng)gdb環(huán)境之后,用于運(yùn)行待調(diào)試的程序。比如啟動(dòng)REDIS程序:

$ gdb redis-server       # 先啟動(dòng) gdb 環(huán)境
#...
Reading symbols from redis-server...
(gdb) r                  # 再啟動(dòng)redis服務(wù)器
# ---------------- 下面是redis的啟動(dòng)信息,暫時(shí)不用管 --------------- #
Starting program: /home/szza/redis-6.0.5/redis-6.0.5/src/redis-server 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
1845:C 27 Mar 2021 20:42:02.143 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
1845:C 27 Mar 2021 20:42:02.143 # Redis version=6.0.5, bits=64, commit=00000000, modified=0, pid=1845, just started
1845:C 27 Mar 2021 20:42:02.143 # Warning: no config file specified, using the default config. In order to specify a config file use /home/szza/redis-6.0.5/redis-6.0.5/src/redis-server /path/to/redis.conf
                _._                                                  
           _.-``__ ''-._                                             
      _.-``    `.  `_.  ''-._           Redis 6.0.5 (00000000/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._                                   
 (    '      ,       .-`  | `,    )     Running in standalone mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 6379
 |    `-._   `._    /     _.-'    |     PID: 1845
  `-._    `-._  `-./  _.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |           http://redis.io        
  `-._    `-._`-.__.-'_.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |                                  
  `-._    `-._`-.__.-'_.-'    _.-'                                   
      `-._    `-.__.-'    _.-'                                       
          `-._        _.-'                                           
              `-.__.-'                                               

1845:M 27 Mar 2021 20:42:02.146 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
1845:M 27 Mar 2021 20:42:02.146 # Server initialized
1845:M 27 Mar 2021 20:42:02.146 # 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 0x7ffffe7a0700 (LWP 1849)]
[New Thread 0x7ffffdf90700 (LWP 1850)]
[New Thread 0x7ffffd780700 (LWP 1851)]
[New Thread 0x7ffffcf70700 (LWP 1852)]
[New Thread 0x7ffffc760700 (LWP 1853)]
-----1----1845:M 27 Mar 2021 20:42:02.151 * Loading RDB produced by version 6.0.5
1845:M 27 Mar 2021 20:42:02.151 * RDB age 839 seconds
1845:M 27 Mar 2021 20:42:02.151 * RDB memory usage when created 0.77 Mb
1845:M 27 Mar 2021 20:42:02.152 * DB loaded from disk: 0.000 seconds
1845:M 27 Mar 2021 20:42:02.152 * Ready to accept connections

set args

如果待調(diào)試的程序需要輸入?yún)?shù),那么在啟動(dòng)gdb環(huán)境后、運(yùn)行待調(diào)試程序前,使用set args指令來(lái)設(shè)置程序所需的輸入?yún)?shù)。

比如在啟動(dòng)REDIS的哨兵服務(wù)器時(shí),需要設(shè)置哨兵模式下的配置文件路徑:

$ gdb redis-server                                                           # 啟動(dòng) gdb 環(huán)境
(gdb) set args  /home/szza/redis-6.0.5/redis-6.0.5/sentinel.conf --sentinel  # 設(shè)置輸入?yún)?shù)
(gdb) r                                                                      # 運(yùn)行

退出gdb

退出gdb調(diào)試界面命令是:quit,簡(jiǎn)寫q

如果程序正在運(yùn)行,你嘗試去退出,會(huì)有個(gè)提示,是否真的要退出,防止你不小心將gdb調(diào)試終止:

(gdb) quit
A debugging session is active.

        Inferior 1 [process 1660] will be killed.

Quit anyway? (y or n) 

斷點(diǎn)

break

break指令,簡(jiǎn)寫是b,用于在指定的地方加上斷點(diǎn),當(dāng)程序運(yùn)行至斷點(diǎn)處就會(huì)暫停,便于調(diào)試。break指令如下:

  • breakbreak后面沒(méi)有任何參數(shù),那么就在當(dāng)前棧幀的下一個(gè)指令處加上斷點(diǎn)

  • break line:在當(dāng)前運(yùn)行程序的line行處加斷點(diǎn)。如果想在其他文件的某行添加斷點(diǎn),可以使用break filename:line指令。

  • break function:在當(dāng)前運(yùn)行程序的function處加上斷點(diǎn)。

    對(duì)于C++程序,可能會(huì)存在重載,甚至不同類存在同名函數(shù),那么可以更加具體的設(shè)置:

    • break filename:function:在filename文件的 function 處加上斷點(diǎn)
    • break filename:function(ArgsType...):在filename文件的function(args)處加上斷點(diǎn),其參數(shù)類型ArgsType...
    • break class:function:在類classfunction處加上斷點(diǎn),當(dāng)然這里的函數(shù)可以加上具體參數(shù)類型

下面以REDIS程序?yàn)槔菔鞠聨追N打斷點(diǎn)的方法。

在指令setCommand位置處加上斷點(diǎn):

# 方式1
(gdb) break t_string.c:99
Breakpoint 1 at 0x7c6e9: file t_string.c, line 99.
# 方式2
(gdb) break setCommand 
Note: breakpoint 1 also set at pc 0x7c6e9.
Breakpoint 2 at 0x7c6e9: file t_string.c, line 99.
# 方式3
(gdb) break t_string.c:setCommand 
Note: breakpoint 1 also set at pc 0x7c6e9.
Breakpoint 3 at 0x7c6e9: file t_string.c, line 99.

當(dāng)redis服務(wù)接收到客戶端的 SET指令時(shí),就會(huì)在該斷點(diǎn)位置處停止:

Thread 1 "redis-server" hit Breakpoint 3, setCommand (c=0x8042e22 <dictGenCaseHashFunction+47>) at t_string.c:99
99      void setCommand(client *c) {

關(guān)于break指令能指定位置,可以參考:

https://sourceware.org/gdb/current/onlinedocs/gdb/Specify-Location.html#Specify-Location

break … if cond

但是如果只想在滿足某個(gè)條件時(shí),才觸發(fā)斷點(diǎn),怎么辦?

可以考慮使用break … if cond命令,其中...是上述break后的參數(shù)。

比如,以上面的main.cc程序?yàn)槔?dāng)cnt > 3的時(shí)候停止程序:

(gdb) break 7 if cnt > 3
Breakpoint 1 at 0x80011d0: file main.cc, line 7.

當(dāng)程序運(yùn)行到cnt >3時(shí)就會(huì)停止:

Breakpoint 1, main (argc=1, argv=0x7ffffffedfb8) at main.cc:7
7           cnt++;
(gdb) print cnt     # 顯示 cnt 的值
$1 = 4
by the way

break … if cond指令有時(shí)候不會(huì)生效,比如:

(gdb) break main if cnt > 3
Breakpoint 2 at 0x80011a9: file main.cc, line 4.

整個(gè)程序運(yùn)行結(jié)束,也不會(huì)觸發(fā)。我猜測(cè),條件斷點(diǎn)需要在cnt每次產(chǎn)生值改變的位置加上判斷條件,而這個(gè)位置剛好是第7行。

關(guān)于斷點(diǎn)指令的更多信息,參考官方文檔:

https://sourceware.org/gdb/current/onlinedocs/gdb/Set-Breaks.html#Set-Breaks

info b

查看斷點(diǎn)信息,可以使用info breakpoints指令,簡(jiǎn)寫是info b

仍然以上面的REDIS程序?yàn)槔?/p>

(gdb) info b    
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x000000000007c6e9 in setCommand at t_string.c:99
2       breakpoint     keep y   0x000000000007c6e9 in setCommand at t_string.c:99
3       breakpoint     keep y   0x000000000007c6e9 in setCommand at t_string.c:99

disable 、enable 、delete

  • disable n1 n2 n3 ...:臨時(shí)關(guān)閉編號(hào)為n1n2、...的斷點(diǎn)
  • enable n1 n2 n3 ...:開啟被disable指令關(guān)閉的斷點(diǎn) n1n2、...
  • delete n1 n2 n3 ...:直接刪除斷點(diǎn)n1 n2 n3 ...

如果disableenable、delete后面沒(méi)有指定具體的參數(shù),則是關(guān)閉、開啟、刪除所有的斷點(diǎn)。

下面是以REDIS為例的斷點(diǎn)設(shè)置(觀察Enb下的標(biāo)識(shí),Y表示開啟,N表示關(guān)閉):

(gdb) disable 1
(gdb) info b
Num     Type           Disp Enb Address            What
1       breakpoint     keep n   0x000000000007c6e9 in setCommand at t_string.c:99
2       breakpoint     keep y   0x000000000007c6e9 in setCommand at t_string.c:99
3       breakpoint     keep y   0x000000000007c6e9 in setCommand at t_string.c:99
(gdb) enable 1
(gdb) info b
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x000000000007c6e9 in setCommand at t_string.c:99
2       breakpoint     keep y   0x000000000007c6e9 in setCommand at t_string.c:99
3       breakpoint     keep y   0x000000000007c6e9 in setCommand at t_string.c:99
(gdb) delete 1
(gdb) info b
Num     Type           Disp Enb Address            What
2       breakpoint     keep y   0x000000000007c6e9 in setCommand at t_string.c:99
3       breakpoint     keep y   0x000000000007c6e9 in setCommand at t_string.c:99
(gdb) disable 2 3
(gdb) info b
Num     Type           Disp Enb Address            What
2       breakpoint     keep n   0x000000000007c6e9 in setCommand at t_string.c:99
3       breakpoint     keep n   0x000000000007c6e9 in setCommand at t_string.c:99
(gdb) enable 2 3
(gdb) info b
Num     Type           Disp Enb Address            What
2       breakpoint     keep y   0x000000000007c6e9 in setCommand at t_string.c:99
3       breakpoint     keep y   0x000000000007c6e9 in setCommand at t_string.c:99

關(guān)于disable、enable的其余指令,可以參考:

https://sourceware.org/gdb/current/onlinedocs/gdb/Disabling.html#Disabling

執(zhí)行流程

僅僅有斷點(diǎn)還不行,還是需要進(jìn)一步控制程序的執(zhí)行流程。主要有以下三種:

  • next
  • step
  • continue

continue

continue指令,簡(jiǎn)寫是c,用于恢復(fù)被break指令中斷的程序,使其繼續(xù)向下運(yùn)行。

step [count]

step [count]指令,簡(jiǎn)寫是s,是逐步執(zhí)行count個(gè)步驟,而不是count個(gè)語(yǔ)句、函數(shù)。當(dāng)不寫count時(shí),默認(rèn)就執(zhí)行一步。

step指令,用于配合break指令一起使用:當(dāng)在某個(gè)函數(shù)起始處觸發(fā)斷點(diǎn),想要進(jìn)入該函數(shù)體,則可以使用step指令。而step count則是一次性執(zhí)行count步,避免繁瑣的中間行為,比如避免C++中的構(gòu)造函數(shù)等。

比如對(duì)于下面的C++程序:

int main(int argc, char const *argv[])
{
  std::unordered_map<int, int> map;
  map.insert({1,1});    // 第 6 行
  return 0;
}

想要在gdb中查看insert函數(shù)的原型,而忽略中間的{1,1}的構(gòu)造過(guò)程:

(gdb) break 6
Breakpoint 1 at 0x1298: file main.cc, line 6.
(gdb) r
Starting program: /home/szza/stl/c++/demo/main 

Breakpoint 1, main (argc=1, argv=0x7ffffffedfb8) at main.cc:6
6         map.insert({1,1});
(gdb) s 9       # 一次性執(zhí)行9步
std::unordered_map<int, int, std::hash<int>, std::equal_to<int>, std::allocator<std::pair<int const, int> > >::insert (this=0x7ffffffede64, __x=...)
    at /usr/include/c++/9/bits/unordered_map.h:585
585           insert(value_type&& __x)
(gdb) s         # 直接進(jìn)入insert函數(shù)體
586           { return _M_h.insert(std::move(__x)); }

這樣可以忽略中間構(gòu)造std::pair<int, int>{1,1}的行為,直接進(jìn)入insert函數(shù)中,使得調(diào)試更加清晰明了。

next [count]

next指令,簡(jiǎn)寫是n,next指令是逐函數(shù)執(zhí)行,即當(dāng)停在斷點(diǎn)觸發(fā)的函數(shù)處:

  • step指令是逐步執(zhí)行,下一步是會(huì)進(jìn)入函數(shù)體中
  • next指令會(huì)直接執(zhí)行完整個(gè)函數(shù),然后進(jìn)入下一行

對(duì)于 step [count]中的演示demo,如果是next指令,會(huì)直接執(zhí)行完map.insert函數(shù),進(jìn)入下一行:

(gdb) r
Starting program: /home/szza/stl/c++/demo/main 

Breakpoint 1, main (argc=1, argv=0x7ffffffedfb8) at main.cc:6
6         map.insert({1,1});
(gdb) n     # 直接進(jìn)入下一行
7         return 0;
(gdb) 

step、next合理的使用,控制調(diào)試的進(jìn)度,使得調(diào)試更加方便。

set step-mode

如果某個(gè)函數(shù)、語(yǔ)句沒(méi)有包含debug信息,gdb默認(rèn)就會(huì)跳過(guò)這個(gè)函數(shù)、語(yǔ)句。但是,可以通過(guò)設(shè)置step-mode選項(xiàng)是否跳過(guò):

  • set step-mode on:不跳過(guò)沒(méi)有調(diào)試信息的函數(shù)、語(yǔ)句
  • set step-mode off:默認(rèn)行為,跳過(guò)

可以通過(guò)show step-mmode來(lái)查看:

(gdb) show step-mode 
Mode of the step operation is off.

finish

finish指令,簡(jiǎn)寫fin,用于將當(dāng)前函數(shù)剩下的部分執(zhí)行完畢,并且顯示輸出結(jié)果。

int countSum(int from, int to) {
  int sum =0;
  
  for (int from = 0; from < to; from++) 
  {
    sum += from;
  }
  sum+=1;
  sum+=2;
  sum+=3;
  sum+=4;
  sum+=5;
  sum+=6;
  sum+=7;   

  return sum;   // 第16行
}

int main(int argc, char const *argv[]) {
 
  countSum(0, 10);
  return 0;
}

countSum函數(shù)處添加斷點(diǎn),當(dāng)該斷點(diǎn)觸發(fā),執(zhí)行step指令進(jìn)入countSum函數(shù)。此時(shí),直接執(zhí)行finish指令,gdb會(huì)直接返回countSum的結(jié)果,然后進(jìn)入下一行:

(gdb) break countSum(int, int) 
Breakpoint 1 at 0x1129: file main.cc, line 1.
(gdb) r
Starting program: /home/szza/stl/c++/demo/main 

Breakpoint 1, countSum (from=0, to=134222333) at main.cc:1
1       int countSum(int from, int to) {
(gdb) s                     // 進(jìn)入函數(shù)體
2         int sum =0;
(gdb) finish 
Run till exit from #0  countSum (from=0, to=10) at main.cc:2
main (argc=1, argv=0x7ffffffedfb8) at main.cc:22
22        return 0;
Value returned is $1 = 73   // 直接執(zhí)行完,并返回結(jié)果

finish指令默認(rèn)會(huì)顯示函數(shù)的返回結(jié)果,也可以設(shè)置為不顯示。不過(guò)既然是調(diào)試,那么肯定是提供越多信息越好。

  • set print finish [on|off]:控制finish返回結(jié)果是否顯示
  • show print finish:輸出finish的返回結(jié)果是否顯示
(gdb) show print finish
Printing of return value after `finish' is on.

return

return,指令與finish不同:

  • finish會(huì)把這個(gè)函數(shù)剩余的部分,正常運(yùn)行完后在返回;
  • return指令,是直接在函數(shù)的當(dāng)前位置返回,不管你執(zhí)行到什么位置。

很好理解,就是finish把函數(shù)完整地執(zhí)行完畢后返回,return是函數(shù)執(zhí)行到某個(gè)位置,強(qiáng)行的返回,而不管函數(shù)的后續(xù)。

until [location]

until指令,簡(jiǎn)寫u,可以用于直接跳出循環(huán)體。

比如上面的countSum函數(shù),進(jìn)入后,如果不想一直next單步執(zhí)行,就執(zhí)行until指令,會(huì)直接跳出for循環(huán)。

until

until指令,不加上參數(shù),沒(méi)有遇到循環(huán)體時(shí)功能類似于next,遇到了可以直接跳出循環(huán)體

(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/szza/stl/c++/demo/main 

Breakpoint 1, countSum (from=0, to=134222333) at main.cc:1
1       int countSum(int from, int to) {
(gdb) until             # 進(jìn)入函數(shù)
2         int sum =0;
(gdb) until             # 遇到循環(huán)體
4         for (int from = 0; from < to; from++) 
(gdb) until             # 直接執(zhí)行完循環(huán)體
6           sum += from;
(gdb) until
4         for (int from = 0; from < to; from++) 
(gdb) until             # 執(zhí)行完循環(huán)體
8         sum+=1;
(gdb) until
9         sum+=2;
(gdb) until
10        sum+=3;
(gdb) 
until location

until location指令中的location格式和break location的格式一樣,可以是行數(shù)、函數(shù)名。 可以直接運(yùn)行到指定行數(shù)。

以上面的countSum為例:

(gdb) break countSum(int, int) 
Breakpoint 1 at 0x1129: file main.cc, line 1.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/szza/stl/c++/demo/main 

Breakpoint 1, countSum (from=0, to=134222333) at main.cc:1
1       int countSum(int from, int to) {
(gdb) s                              # 進(jìn)入函數(shù)體
2         int sum =0;       
(gdb) until main.cc:16               # 一直執(zhí)行到 return sum; 語(yǔ)句
countSum (from=0, to=10) at main.cc:16
16        return sum;
(gdb) n                              # 下一條就是函數(shù)返回了
17      }

會(huì)發(fā)現(xiàn),直接運(yùn)行到指定的位置:countSum函數(shù)的return語(yǔ)句處。

進(jìn)一步,將main函數(shù)修改如下:

int main(int argc, char const *argv[]) {
 
  countSum(0, 10);
  countSum(10, 20);
  return 0;
}

如果我在執(zhí)行countSum(0,10)函數(shù)時(shí),突然想執(zhí)行完當(dāng)前函數(shù),然后跳到轉(zhuǎn)countSum(10,20)函數(shù)中,行不行呢?

當(dāng)然是可以,可以借助until location指令實(shí)現(xiàn)。

(gdb) break countSum(int, int)                          # 先在countSum函數(shù)處加上斷點(diǎn)
Breakpoint 1 at 0x1129: file main.cc, line 1.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/szza/stl/c++/demo/main 

Breakpoint 1, countSum (from=0, to=10) at main.cc:1     # countSum(0, 10)第一次觸發(fā)
1       int countSum(int from, int to) {
(gdb) s                                                 # 進(jìn)入函數(shù)體
2         int sum =0;       
(gdb) until main.cc:22                                  # 直接執(zhí)行完當(dāng)前函數(shù),并跳轉(zhuǎn)到 countSum(10, 20)
main (argc=1, argv=0x7ffffffedfb8) at main.cc:22
22        countSum(10, 20);
(gdb) s

Breakpoint 1, countSum (from=0, to=20) at main.cc:1     # 直接執(zhí)行到countSum(10, 20)
1       int countSum(int from, int to) {
(gdb) s
2         int sum =0;
(gdb)

通過(guò)until指令,可以很好的控制函數(shù)的指令流程。更多指令可以參考:

https://sourceware.org/gdb/current/onlinedocs/gdb/Continuing-and-Stepping.html#Continuing-and-Stepping

顯示

查看程序中運(yùn)行時(shí)的變量的值,有兩種方式:

  • print指令:手動(dòng)輸出
  • display指令:自動(dòng)顯示

下面分別講解。

print

print指令,簡(jiǎn)寫p,其格式如下兩種。

  • print [[options] --] expr
  • print [[options] --] /f expr
print [[options] --] expr

print [[options] --] expr,其中expr可以是表達(dá)式、變量。其中,輸出的變量,要么是全局變量、static變量,要么就是在當(dāng)前作用域內(nèi)可見(jiàn)的局部變量。

在多數(shù)情況下,print指令輸出的結(jié)果就符合要求,但是有時(shí)候?yàn)榱双@得更好的顯示,可以提供 options 選項(xiàng),獲得更好的輸出。

比如,對(duì)于下面的代碼,

int main(int argc, char const *argv[]) {
  
  std::vector<int> vec{1,2,3};
  return 0;
}

想要在gdb中顯示vec的內(nèi)容:

23        std::vector<int> vec{1,2,3};
(gdb) n
24        return 0;
(gdb) print vec             # 直接輸出
$1 = std::vector of length 3, capacity 3 = {1, 2, 3}
(gdb) set print array on    # 開啟數(shù)組顯示
(gdb) print vec             # 有更好的輸出顯示
$2 = std::vector of length 3, capacity 3 = {
  1,
  2,
  3
}
(gdb) 

對(duì)于printoption選項(xiàng)設(shè)置,具體可以參考:

https://sourceware.org/gdb/current/onlinedocs/gdb/Data.html#Data
print [[options] --] /f expr

print [[options] --] /f expr,其中/fexpr是輸出格式:

    x  按十六進(jìn)制格式顯示變量
    d  按十進(jìn)制格式顯示變量
    u  按十六進(jìn)制格式顯示無(wú)符號(hào)整型
    o  按八進(jìn)制格式顯示變量
    t  按二進(jìn)制格式顯示變量
    a  按十六進(jìn)制格式顯示變量
    c  按字符格式顯示變量
    f  按浮點(diǎn)數(shù)格式顯示變量
    s  按字符串顯示
    z  與'x'格式一樣,該值被視為整數(shù)并被打印為十六進(jìn)制,但是前導(dǎo)零被打印出來(lái)以便將該值填充為整數(shù)類型的大小
    r  'r'是'raw'的縮寫,按照python的Pretty-printer風(fēng)格進(jìn)行打印

以上面的countSum函數(shù)為例,按照不同格式顯示返回值sum

(gdb) print sum 
$4 = 73
(gdb) print/a sum
$5 = 0x49
(gdb) print/c sum
$7 = 73 'I'
(gdb) p/x $pc       # 當(dāng)前指令指向的地址
$23 = 0x807c6fa

順便說(shuō)下,$pc表示當(dāng)前指令地址,因此print/x $pc是以16進(jìn)制顯示當(dāng)前指令的地址。

關(guān)于輸出流格式信息,原文參考:

https://sourceware.org/gdb/current/onlinedocs/gdb/Output-Formats.html#Output-Formats

display

print指令,是手動(dòng)輸出表達(dá)、變量的值。display可以讓指定的表達(dá)式、變量在每次的單步執(zhí)行中自動(dòng)顯示。主要有以下三種使用方式:

display   expr
display/f expr
display/f addr
display /f expr

display /f expr 的使用,和print的格式基本一致。

比如,在countSum函數(shù)中,想要觀察變量的sum值,由于是在一個(gè)循環(huán)體中,一直使用print指令查看sum變量的值,不免過(guò)于麻煩。此時(shí),使用display指令來(lái)查看,使得gdb在運(yùn)行每條語(yǔ)句的時(shí)候都會(huì)顯示一次sum的值。

效果如下:

Breakpoint 1, countSum (from=0, to=134222349) at main.cc:1
1       int countSum(int from, int to) {
(gdb) s
2         int sum =0;
(gdb) n
4         for (int from = 0; from < to; from++) 
(gdb) display sum   # display 指令
1: sum = 0
(gdb) n             # 每條指令都會(huì)顯示 sum 的值
6           sum += from;
1: sum = 0          # 每條指令都會(huì)顯示 sum 的值
(gdb) 
4         for (int from = 0; from < to; from++) 
1: sum = 0          # 每條指令都會(huì)顯示 sum 的值
(gdb) 
6           sum += from;
1: sum = 0
(gdb) 
4         for (int from = 0; from < to; from++) 
1: sum = 1
...
display /f addr

當(dāng)自動(dòng)顯示的是地址時(shí),可以使用/i格式描述符,查看地址 addr的匯編代碼,$pc指向的當(dāng)前指令的地址。

因此display /i &pc這條指令,可以查看當(dāng)前指令對(duì)應(yīng)的匯編代碼。

Breakpoint 1, countSum (from=0, to=134222349) at main.cc:1
1       int countSum(int from, int to) {
(gdb) display sum       # 設(shè)置自動(dòng)顯示 sum 變量
1: sum = 134222272
(gdb) display /i $pc    # 設(shè)置顯示當(dāng)前代碼的匯編
2: x/i $pc
=> 0x8001129 <countSum(int, int)>:      endbr64 
(gdb) n                 # 每一步都會(huì)顯示上面的兩個(gè)設(shè)置
2         int sum =0;
1: sum = 134222272
2: x/i $pc
=> 0x8001137 <countSum(int, int)+14>:   movl   $0x0,-0x8(%rbp)

關(guān)于輸出顯示的指令的輸出顯示信息,可以參考:

https://sourceware.org/gdb/current/onlinedocs/gdb/Data.html#Data

棧幀

backtrace

backtrace指令,簡(jiǎn)寫bt,可以在break指令設(shè)置的斷點(diǎn)觸發(fā)時(shí),查看程序是怎么執(zhí)行到此斷點(diǎn)處的,追蹤下棧幀信息。

比如,在REDIS程序中,setCommand函數(shù)處的斷點(diǎn)觸發(fā)時(shí),想要看看REEDIS是怎么從main函數(shù)執(zhí)行到setCommand函數(shù)的,可以使用bt指令來(lái)追蹤下棧幀軌跡:

Thread 1 "redis-server" hit Breakpoint 1, setCommand (c=0x8042e22 <dictGenCaseHashFunction+47>)
    at t_string.c:99
99      void setCommand(client *c) {
(gdb) bt
#0  setCommand (c=0x7fffff11c680) at t_string.c:101
#1  0x000000000804a765 in call (c=0x7fffff11c680, flags=15) at server.c:3301
#2  0x000000000804b73c in processCommand (c=0x7fffff11c680) at server.c:3695
#3  0x000000000805e24f in processCommandAndResetClient (c=0x7fffff11c680) at networking.c:2057
#4  0x000000000805e4ae in processInputBuffer (c=0x7fffff11c680) at networking.c:2169
#5  0x000000000805e874 in readQueryFromClient (conn=0x7fffff015140) at networking.c:2275
#6  0x000000000810888b in callHandler (conn=0x7fffff015140, handler=0x805e52e <readQueryFromClient>) at connhelpers.h:79
#7  0x0000000008108f57 in connSocketEventHandler (el=0x7fffff00b480, fd=8, clientData=0x7fffff015140, mask=1) at connection.c:330
#8  0x0000000008040cad in aeProcessEvents (eventLoop=0x7fffff00b480, flags=27) at ae.c:497
#9  0x0000000008040eeb in aeMain (eventLoop=0x7fffff00b480) at ae.c:558
#10 0x000000000804fac3 in main (argc=1, argv=0x7ffffffedf48) at server.c:5236
(gdb) 

bt指令的輸出信息可以看出整個(gè)調(diào)用鏈,是如何從main函數(shù)執(zhí)行到setCommand函數(shù)的,這對(duì)于理清項(xiàng)目框架至關(guān)重要,尤其是大量使用回調(diào)函數(shù)的項(xiàng)目中,比如REDIS、Libuv。

frame N

frame指令,簡(jiǎn)寫f,frame N表示跳轉(zhuǎn)到編號(hào)為N的棧幀中,不加參數(shù)的frame 指令,可以顯示當(dāng)前棧幀的基本信息。

上面的bt指令,可以詳細(xì)地看到從main函數(shù)運(yùn)行到setCommnad函數(shù)的調(diào)用過(guò)程。但是,如果我想看看其中某一個(gè)棧幀的調(diào)用過(guò)程,那怎么辦?

比如,現(xiàn)在我就想知道REDIS是怎么處理客戶端的請(qǐng)求的,想去processInputBuffer函數(shù)所在棧幀,那么就如下操作:

(gdb) frame 5
#5  0x000000000805e874 in readQueryFromClient (conn=0x7fffff015140) at networking.c:2275
2275         processInputBuffer(c);
(gdb) s                             # 進(jìn)入 processInputBuffer 函數(shù)
101         robj *expire = NULL;                

frame NN 是調(diào)用 processInputBuffer 函數(shù)的棧幀,即 processInputBuffer 函數(shù)的上一個(gè)棧幀,由于 processInputBuffer 函數(shù)是在 readQueryFromClient 函數(shù)中被調(diào)用,因此要查看processInputBuffer函數(shù),需要進(jìn)入readQueryFromClient所處的棧幀,因此 N=5。

info frame

info frame指令,簡(jiǎn)寫info f,會(huì)顯示當(dāng)前棧幀的詳細(xì)信息,比如:當(dāng)前調(diào)用函數(shù)的地址,被調(diào)用函數(shù)的地址,源碼語(yǔ)言、函數(shù)參數(shù)地址及值、局部變量的地址等等。

比如,當(dāng)前執(zhí)行到setCommand函數(shù)中,那么info f就可以查看當(dāng)前的棧幀:

(gdb) info frame
 Stack level 0, frame at 0x7ffffffedae0:    # 當(dāng)前函數(shù)棧幀地址  
 rip = 0x807c6fa in setCommand (t_string.c:101); saved rip = 0x804a765
 called by frame at 0x7ffffffedb60          # 當(dāng)前函數(shù)在哪里被調(diào)用的
 source language c.                         # c 語(yǔ)言寫的
 Arglist at 0x7ffffffeda78, 
 args: c=0x7fffff11c680                     # 函數(shù)參數(shù)
 Locals at 0x7ffffffeda78, Previous frame's sp is 0x7ffffffedae0
 Saved registers:
  rbx at 0x7ffffffedac8, rbp at 0x7ffffffedad0, rip at 0x7ffffffedad8

info args

info args指令,可以獲取當(dāng)前棧幀函數(shù) setCommand 的參數(shù)名及其值。

setCommand 的原型是 setCommand(client *c) ,其參數(shù)是指針類型,因此獲得參數(shù)c值后,可以打印參數(shù)c指向的數(shù)據(jù)。比如,現(xiàn)在想看看 setCommand 的參數(shù)c中的字段c->argv的第一個(gè)字符串是不是set

(gdb) info args 
c = 0x7fffff11c680                       # 和 info frame 顯示的地址一致
(gdb) print (const char*)((client*)0x7fffff11c680)->argv->ptr
$16 = 0x7fffff134d93 "set"               # 確實(shí)是set

info locals

打印出當(dāng)前函數(shù)中所有局部變量及其值。

(gdb) info locals 
j = 0
expire = 0x7fffff009031
unit = -75072
flags = 32767

關(guān)于棧幀的更多信息,可以參考:

https://sourceware.org/gdb/current/onlinedocs/gdb/Stack.html#Stack

補(bǔ)充

shell

如果想要在gdb環(huán)境中,執(zhí)行Linux命令,可以在指令前加上shell即可,比如clear命令,在gdb下執(zhí)行為:

(gdb) shell clear

空行

在gdb下,直接回車,即輸入一個(gè)空行,相當(dāng)于重復(fù)執(zhí)行上一條指令。

比如,在 setCommand 函數(shù)觸發(fā)時(shí):

Thread 1 "redis-server" hit Breakpoint 1, setCommand (c=0x8042e22 <dictGenCaseHashFunction+47>)
    at t_string.c:99
99      void setCommand(client *c) {
(gdb) s
101         robj *expire = NULL;                //* 超時(shí)時(shí)間
(gdb) n
102         int unit = UNIT_SECONDS;            //* 超時(shí)的時(shí)間單位
(gdb)           # 空行就是重復(fù)執(zhí)行 next
103         int flags = OBJ_SET_NO_FLAGS;       //* set 指令的類型
(gdb)           # 空行就是重復(fù)執(zhí)行 next
107         for (j = 3; j < c->argc; j++) {
(gdb) 

到此,常用的GDB指令基本講解完畢,如果能跟著走一遍,已經(jīng)能完成大部分的調(diào)試任務(wù)。更多的GDB指令,以及某些指令更深入的使用,比如print指令的輸出格式,可以去官方文檔學(xué)習(xí)。

如果熟悉了gcc編譯、gdb調(diào)試,基本就可以卸載vscode里面的code runner插件,也免去了每次task.json等文件的繁瑣配置,可以盡情地享受命令行帶來(lái)的便捷、愉快。

此外,之后會(huì)準(zhǔn)備技術(shù)直播 《基于vscode使用gdb帶你理清REDIS-6.0框架》系列,用gdb去理清REDIS服務(wù)器框架。gdb配合vscode效果奇佳,在直播中可以更好展示,請(qǐng)敬請(qǐng)期待。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容