在前文 基于vscode 打造Linux C++編碼環(huán)境 一期中,講解了如何基于vscode搭建Linux c++的編碼環(huán)境贼穆,但是還沒有講解如何基于vscod搭建調(diào)試環(huán)境题山。本期,主要有兩個(gè)任務(wù):
- 講解常用的gcc編譯選項(xiàng)
- 講解常用的gdb編譯指令
- 本文更好的閱讀體驗(yàn)扮惦,可以點(diǎn)擊: 手把手教你學(xué)會(huì)gdb臀蛛,適應(yīng)Linux調(diào)試環(huán)境
常用gcc編譯選項(xiàng)
在 深入了解C++系列中,我經(jīng)常使用如下的格式進(jìn)行編譯崖蜜、執(zhí)行demo:
$ g++ -g -O0 main.cc -o main && ./main
下面浊仆,我們來看看常用的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ù)庫 |
-std=c++11 | 指定編譯代碼的C++標(biāo)準(zhǔn)為C++11 |
對(duì)于這些編譯選項(xiàng)抡柿,簡單的解釋下。
-E
等恐、 -S
洲劣、-c
三個(gè)選項(xiàng)直接對(duì)應(yīng)著編譯的前三個(gè)基本階段
預(yù)編譯處理(.i)
將源文件main.cc
經(jīng)過預(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)文件組合起來,并創(chuàng)建一個(gè)可執(zhí)行的文件 main
扎即。由于main.cc
代碼沒有額外的依賴吞获,因此可以直接輸出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
宏知市,來控制程序執(zhí)行。
$ g++ -DDEBUG main.cc -o main && ./main
Hello Cpp
對(duì)于GCC的編譯選項(xiàng)蚊荣,沒有必要全部記住初狰,記住常用的即可,其他用到了再去官網(wǎng)查詢:
https://gcc.gnu.org/onlinedocs/gcc/Invoking-GCC.html
常用gdb指令
本期主要講解下我常用的gdb指令互例、以及怎么去學(xué)習(xí)gdb奢入。希望能通過本期博客,能幫助你擺脫對(duì)gdb恐懼媳叨,并熟悉下gdb的常用指令腥光,對(duì)于沒有講解到的指令,在本期之后糊秆,可以去官方網(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方式:
-
gdb [program]
:這種方式最常用莹规,比如使用gdb調(diào)試上面編譯生成的main
文件,那么就直接gdb main
泌神。 -
gdb [program] core
:用于調(diào)試導(dǎo)致coredump
的錯(cuò)誤良漱,此時(shí)需要在program
后面加上因?yàn)閏oredump生成的core文件路徑。 -
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ù)器程序,無法被中止畸冲,如何使用gdb來調(diào)試它嫉髓?
比如,此刻我電腦正在運(yùn)行REDIS服務(wù)器程序,其pid是1607:
- 我先以root權(quán)限啟動(dòng)
gdb
- 再使用
attach pid
命令來調(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
指令,來調(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
毫無疑問漏益,這也是可以由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
指令,簡寫是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
指令來設(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
玉组,簡寫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
指令或舞,簡寫是b
荆姆,用于在指定的地方加上斷點(diǎn),當(dāng)程序運(yùn)行至斷點(diǎn)處就會(huì)暫停映凳,便于調(diào)試胆筒。break
指令如下:
break
:break
后面沒有任何參數(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
:在類class
的function
處加上斷點(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ā)汗侵。我猜測,條件斷點(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
指令雪猪,簡寫是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)為n1
译仗、n2
、...
的斷點(diǎn) -
enable n1 n2 n3 ...
:開啟被disable
指令關(guān)閉的斷點(diǎn)n1
官觅、n2
纵菌、...
-
delete n1 n2 n3 ...
:直接刪除斷點(diǎn)n1 n2 n3 ...
如果disable
、enable
休涤、delete
后面沒有指定具體的參數(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
指令管钳,簡寫是c
钦铁,用于恢復(fù)被break指令中斷的程序,使其繼續(xù)向下運(yùn)行才漆。
step [count]
step [count]
指令牛曹,簡寫是s
,是逐步執(zhí)行count
個(gè)步驟醇滥,而不是count
個(gè)語句黎比、函數(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)造過程:
(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
指令侮攀,簡寫是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ù)颤殴、語句沒有包含debug
信息觅廓,gdb默認(rèn)就會(huì)跳過這個(gè)函數(shù)、語句涵但。但是杈绸,可以通過設(shè)置step-mode
選項(xiàng)是否跳過:
-
set step-mode on
:不跳過沒有調(diào)試信息的函數(shù)、語句 -
set step-mode off
:默認(rèn)行為矮瘟,跳過
可以通過show step-mmode
來查看:
(gdb) show step-mode
Mode of the step operation is off.
finish
finish
指令瞳脓,簡寫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è)置為不顯示琳省。不過既然是調(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
指令,簡寫u
维咸,可以用于直接跳出循環(huán)體剂买。
比如上面的countSum
函數(shù),進(jìn)入后腰湾,如果不想一直next
單步執(zhí)行雷恃,就執(zhí)行until
指令,會(huì)直接跳出for
循環(huán)费坊。
until
until
指令倒槐,不加上參數(shù),沒有遇到循環(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; 語句
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
語句處沼死。
進(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)
通過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
指令蠢沿,簡寫p
,其格式如下兩種匾效。
print [[options] --] expr
print [[options] --] /f expr
print [[options] --] expr
print [[options] --] expr
搏予,其中expr
可以是表達(dá)式、變量弧轧。其中雪侥,輸出的變量碗殷,要么是全局變量、static變量速缨,要么就是在當(dāng)前作用域內(nèi)可見的局部變量锌妻。
在多數(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ì)于print
的option
選項(xiàng)設(shè)置擂橘,具體可以參考:
https://sourceware.org/gdb/current/onlinedocs/gdb/Data.html#Data
print [[options] --] /f expr
print [[options] --] /f expr
晌区,其中/f
是expr
是輸出格式:
x 按十六進(jìn)制格式顯示變量
d 按十進(jìn)制格式顯示變量
u 按十六進(jìn)制格式顯示無符號(hào)整型
o 按八進(jìn)制格式顯示變量
t 按二進(jìn)制格式顯示變量
a 按十六進(jìn)制格式顯示變量
c 按字符格式顯示變量
f 按浮點(diǎn)數(shù)格式顯示變量
s 按字符串顯示
z 與'x'格式一樣,該值被視為整數(shù)并被打印為十六進(jìn)制通贞,但是前導(dǎo)零被打印出來以便將該值填充為整數(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
順便說下昌罩,$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
變量的值啄栓,不免過于麻煩。此時(shí)也祠,使用display
指令來查看昙楚,使得gdb在運(yùn)行每條語句的時(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
指令爆袍,簡寫bt
首繁,可以在break
指令設(shè)置的斷點(diǎn)觸發(fā)時(shí),查看程序是怎么執(zhí)行到此斷點(diǎn)處的陨囊,追蹤下棧幀信息弦疮。
比如,在REDIS程序中蜘醋,setCommand
函數(shù)處的斷點(diǎn)觸發(fā)時(shí)胁塞,想要看看REEDIS是怎么從main
函數(shù)執(zhí)行到setCommand
函數(shù)的,可以使用bt
指令來追蹤下棧幀軌跡:
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
指令斥季,簡寫f
训桶,frame N
表示跳轉(zhuǎn)到編號(hào)為N
的棧幀中,不加參數(shù)的frame
指令酣倾,可以顯示當(dāng)前棧幀的基本信息舵揭。
上面的bt
指令,可以詳細(xì)地看到從main
函數(shù)運(yùn)行到setCommnad
函數(shù)的調(diào)用過程躁锡。但是午绳,如果我想看看其中某一個(gè)棧幀的調(diào)用過程,那怎么辦映之?
比如拦焚,現(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 N
中 N
是調(diào)用 processInputBuffer
函數(shù)的棧幀赎败,即 processInputBuffer
函數(shù)的上一個(gè)棧幀,由于 processInputBuffer
函數(shù)是在 readQueryFromClient
函數(shù)中被調(diào)用蠢甲,因此要查看processInputBuffer
函數(shù)僵刮,需要進(jìn)入readQueryFromClient
所處的棧幀,因此 N=5
。
info frame
info frame
指令搞糕,簡寫info f
勇吊,會(huì)顯示當(dāng)前棧幀的詳細(xì)信息,比如:當(dāng)前調(diào)用函數(shù)的地址寞宫,被調(diào)用函數(shù)的地址萧福,源碼語言、函數(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 語言寫的
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等文件的繁瑣配置顶考,可以盡情地享受命令行帶來的便捷赁还、愉快。
此外驹沿,之后會(huì)準(zhǔn)備技術(shù)直播 《基于vscode使用gdb帶你理清REDIS-6.0框架》系列艘策,用gdb去理清REDIS服務(wù)器框架。gdb配合vscode效果奇佳渊季,在直播中可以更好展示朋蔫,請(qǐng)敬請(qǐng)期待。