Linux環境下C++除錯的三板斧

xiuzhublog發表於2020-09-24

除錯解決程式的漏洞,是程式設計師最基本的技能之一。用慣了圖形化IDE,在目前使用gtest框架進行單元測試,需要通過xshell遠端連線Linux虛擬機器進行C++程式碼的除錯時,覺得很不適應。經過幾天查資料,和實踐,找到了幾種簡單方便的除錯方法。

1.gdb

GDB 是GNU開源組織釋出的一個強大的UNIX下的程式除錯工具。它沒有圖形化的介面,但是並沒有想象的那麼難用,甚至在某些方面比圖形化的除錯工具做得更好。

常用命令列表

命令 解釋 簡寫
file 裝入想要除錯的可執行檔案
list 列出產生執行檔案原始碼的一部分 l
next 執行一行原始碼但不進入函式內部 n
step 執行一行原始碼而且進入函式內部 s
run run執行當前被除錯的程式 r
continue 繼續執行程式 c
quit 終止GDB q
print 輸出當前指定變數的值 p
break 在程式碼裡設定斷點 b
info break 檢視設定斷點的資訊i ib
delete 刪除設定的斷點 $1600
watch 監視一個變數的值,一旦值有變化,程式停 wa
help GDB中的幫助命令 h

常用用法示例

1.生成可執行檔案

gcc -g test.c -o test

注意必須使用-g引數,這樣編譯時會加入除錯資訊,否則無法除錯執行檔案。

2.啟動gdb

gdb test 
gdb -q test //表示不列印gdb版本資訊,介面較為乾淨;

兩種命令的資訊如下:

root@ubuntu:/home/eit/c_test# gdb test
GNU gdb (Ubuntu 7.7-0ubuntu3) 7.7
Copyright (C) 2014 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 test...done.
(gdb) q

root@ubuntu:/home/eit/c_test# gdb -q test
Reading symbols from test...done.
(gdb) 

3.檢視原始檔

list(簡寫 l): 檢視源程式程式碼,預設顯示10行,按Enter鍵繼續看餘下的。

(gdb) list 
9	#define MAX_SIZE
10	
11	int main()
12	{
13	    int i,fd,size1 ,size2 ,len;
14	    char *buf = "helo!I'm liujiangyong ";
15	    char buf_r[15];
16	    len = strlen(buf);
17	    fd = open("/home/hello.txt",O_CREAT | O_TRUNC | O_RDWR,0666);
18	    if (fd<0)
(gdb) 
19	        {
20	            perror("open :");
21	            exit(1);
22	        }
23	    else
24	        {
25	        printf("open file:hello.txt %d\n",fd);
26	        }
27	    size1 = write(fd,buf,len);
28	    if (fd<0)
(gdb) 
29	    {
30	        printf("writre erro;");
31	
32	    }
33	    else
34	    {
35	        printf("寫入的長度:%d\n寫入文字內容:%s\n",size1,buf);
36	
37	    }
38	    lseek(fd,0,SEEK_SET);
(gdb) 
39	    size2 = read(fd,buf_r,12);
40	    if (size2 <0)
41	    {
42	        printf("read  erro\n");
43	    }
44	    else
45	    {
46	        printf("讀取長度:%d\n 文字內容是:%s\n",size2,buf_r);
47	    }
48	    close(fd);    
(gdb) 
49	
50	
51	}
(gdb) 
Line number 52 out of range; write.c has 51 lines.
(gdb) 

4.執行程式

run(簡寫 r) :執行程式直到遇到 結束或者遇到斷點等待下一個命令;

(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/eit/c_test/test 
open file:hello.txt 3
寫入的長度:22
寫入文字內容:helo!I'm liujiangyong 
讀取長度:12
 文字內容是:helo!I'm liu
[Inferior 1 (process 19987) exited normally]
(gdb) 

5.設定斷點

break(簡寫 b) :格式 b 行號,在某行設定斷點;

(gdb) b 5
Breakpoint 3 at 0x400836: file write.c, line 5.
(gdb) b 26 
Breakpoint 4 at 0x4008a6: file write.c, line 26.
(gdb) b 30
Breakpoint 5 at 0x4008c6: file write.c, line 30.
(gdb) info breakpoints 
Num     Type           Disp Enb Address            What
3       breakpoint     keep y   0x0000000000400836 in main at write.c:5
4       breakpoint     keep y   0x00000000004008a6 in main at write.c:26
5       breakpoint     keep y   0x00000000004008c6 in main at write.c:30
(gdb) 

6.單步執行

使用 continue、step、next命令

(gdb) r
Starting program: /home/eit/c_test/test 

Breakpoint 3, main () at write.c:12
12	{
(gdb) n
14	    char *buf = "helo!I'm liujiangyong ";
(gdb) 
16	    len = strlen(buf);
(gdb) 
17	    fd = open("/home/hello.txt",O_CREAT | O_TRUNC | O_RDWR,0666);
(gdb) s
open64 () at ../sysdeps/unix/syscall-template.S:81
81	../sysdeps/unix/syscall-template.S: No such file or directory.
(gdb) 
main () at write.c:18
18	    if (fd<0)
(gdb) 
25	        printf("open file:hello.txt %d\n",fd);
(gdb) 
__printf (format=0x400a26 "open file:hello.txt %d\n") at printf.c:28
28	printf.c: No such file or directory.
(gdb) c
Continuing.
open file:hello.txt 3

Breakpoint 4, main () at write.c:27
27	    size1 = write(fd,buf,len);
(gdb) 
Continuing.
寫入的長度:22
寫入文字內容:helo!I'm liujiangyong 
讀取長度:12
 文字內容是:helo!I'm liu
[Inferior 1 (process 20737) exited normally]
(gdb) 

6.檢視變數

使用print、whatis命令

main () at write.c:28
28	    if (fd<0)
(gdb) 
35	        printf("寫入的長度:%d\n寫入文字內容:%s\n",size1,buf);
(gdb) print fd
$10 = 3
(gdb) whatis fd
type = int
(gdb) 


7.退出gdb

用quit命令退出gdb:

(gdb) r
Starting program: /home/eit/c_test/test 
open file:hello.txt 3
寫入的長度:22
寫入文字內容:helo!I'm liujiangyong 
讀取長度:12
 文字內容是:helo!I'm liu
[Inferior 1 (process 20815) exited normally]
(gdb) q
root@ubuntu:/home/eit/c_test# 


8.

到此基本的使用流程就結束了,但是gdb的功能和技巧遠不止於此,還需要以後多多探索。

2. core dump

關於Segment fault

這個錯誤在除錯過程中出現的頻率還是比較高的,造成這個問題的原因也有很多,我認為這個錯誤本質上來說就是程式訪問了非法的地址.

這個錯誤與普通錯誤的明顯區別是它不會告訴你是哪一行、哪個函式、哪個變數出錯了,所以除錯起來就更為棘手。

在這種情況下依然可以使用gdb進行除錯,但是由於不知道錯誤的位置,所以需要一步一步地執行。如果錯誤出現在程式碼的前幾行,或者程式碼比較短小,這種方法當然可以使用。但是如果有成千上萬行程式碼甚至數十萬上百萬行程式碼,再使用gdb純粹是跟自己過不去。

什麼是core dump

當gdb無法解決上述問題時,可以考慮core檔案

當程式執行的過程中異常終止或崩潰,作業系統會將程式當時的記憶體狀態記錄下來,儲存在一個檔案中,這種行為就叫做Core Dump(中文有的翻譯成“核心轉儲”)。我們可以認為 core dump 是“記憶體快照”,但實際上,除了記憶體資訊之外,還有些關鍵的程式執行狀態也會同時 dump 下來,例如暫存器資訊(包括程式指標、棧指標等)、記憶體管理資訊、其他處理器和作業系統狀態和資訊。

core檔案不僅可以幫助我們快速找出錯誤,還對某些無法重現的錯誤很有幫助,例如指標異常。

使用

開啟或關閉core檔案的生成

1.檢視core檔案是否開啟:

ulimit -c  # 如果為 0 表示coredump開關處於關閉狀態

2.開啟core檔案生成:

ulimit -c 1024         # 1024個blocks,一般1block=512bytes
ulimit -c unlimited    # 取消大小限制

3.檢查core檔案的選項是否開啟:

ulimit -a  # 顯示當前所有limit資訊
命令引數      描述                                          例子
-H    設定硬資源限制,一旦設定不能增加。                      ulimit – Hs 64;限制硬資源,執行緒棧大小為 64K。
-S    設定軟資源限制,設定後可以增加,但是不能超過硬資源設定。  ulimit – Sn 32;限制軟資源,32 個檔案描述符。
-a    顯示當前所有的 limit 資訊                             ulimit – a;顯示當前所有的 limit 資訊
-c    最大的 core 檔案的大小, 以 blocks 為單位              ulimit – c unlimited; 對生成的 core 檔案的大小不進行限制
-d    程式最大的資料段的大小,以 Kbytes 為單位                ulimit -d unlimited;對程式的資料段大小不進行限制
-f    程式可以建立檔案的最大值,以 blocks 為單位              ulimit – f 2048;限制程式可以建立的最大檔案大小為 2048 blocks
-l    最大可加鎖記憶體大小,以 Kbytes 為單位                   ulimit – l 32;限制最大可加鎖記憶體大小為 32 Kbytes
-m    最大記憶體大小,以 Kbytes 為單位                         ulimit – m unlimited;對最大記憶體不進行限制
-n    可以開啟最大檔案描述符的數量                           ulimit – n 128;限制最大可以使用 128 個檔案描述符
-p    管道緩衝區的大小,以 Kbytes 為單位                     ulimit – p 512;限制管道緩衝區的大小為 512 Kbytes
-s    執行緒棧大小,以 Kbytes 為單位                          ulimit – s 512;限制執行緒棧的大小為 512 Kbytes
-t    最大的 CPU 佔用時間,以秒為單位                        ulimit – t unlimited;對最大的 CPU 佔用時間不進行限制
-u    使用者最大可用的程式數                                  ulimit – u 64;限制使用者最多可以使用 64 個程式
-v    程式最大可用的虛擬記憶體,以 Kbytes 為單位               ulimit – v 200000;限制最大可用的虛擬記憶體為 200000 Kbytes

4.永久配置core:

以上配置只對當前會話起作用,下次重新登陸後,還是得重新配置。要想配置永久生效,得在/etc/profile或者/etc/security/limits.conf檔案中進行配置。

有兩種方法可以永久開啟:如果為了防止core檔案重複覆蓋而給檔名加了pid、時間戳之類的,儲存空間可能會很快被佔滿。因為除錯的時候只執行一次程式就解決bug的情況只佔少數,大多數時候需要多次除錯,就會生成很多core檔案

  1. 首先開啟/etc/profile檔案,一般都可以在檔案中找到這句語句:ulimit -S -c 0 > /dev/null 2>&1,根據上面的例子,我們只要把那個0 改為 unlimited 就ok了。然後儲存退出。通過source /etc/profile 使當期設定生效。或者想配置只針對某一使用者有效,則修改此使用者的/.bashrc或者/.bash_profile檔案:
limit -c unlimited
  1. 第二種方法可以通過修改/etc/security/limits.conf檔案來設定,首先以root許可權登陸,然後開啟/etc/security/limits.conf檔案,進行配置:
#vim /etc/security/limits.conf
<domain>    <type>    <item>        <value>
 
*          soft       core         unlimited
 

設定core檔案的檔名和儲存位置

檔名

預設情況下,核心在coredump時所產生的core檔案放在與該程式相同的目錄中,並且檔名固定為core。很顯然,如果有多個程式產生core檔案,或者同一個程式多次崩潰,就會重複覆蓋同一個core檔案,因此我們有必要對不同程式生成的core檔案進行分別命名。

  1. /proc/sys/kernel/core_uses_pid可以控制core檔案的檔名中是否新增pid作為擴充套件。檔案內容為1,表示新增pid作為副檔名,生成的core檔案格式為core.xxxx;為0則表示生成的core檔案同一命名為core。可通過以下命令修改此檔案:
echo "1" > /proc/sys/kernel/core_uses_pid
  1. proc/sys/kernel/core_pattern可以控制core檔案儲存位置和檔名格式,可通過以下命令修改此檔案:
echo "/corefile/core-%e-%p-%t" > core_pattern # 可以將core檔案統一生成到/corefile目錄下,產生的檔名為core-命令名-pid-時間戳

引數列表:

%% - 單個%字元
%p - 新增pid
%u - 新增當前uid
%g - 新增當前gid
%s - 新增導致產生core的訊號
%t - 新增core檔案生成時的unix時間
%h - 新增主機名
%e - 新增程式檔名 

儲存位置

core檔案預設的儲存位置與對應的可執行程式在同一目錄下,檔名是core,可以通過下面的命令看到core檔案的存在位置:

cat  /proc/sys/kernel/core_pattern  # 預設值是|/usr/share/apport/apport %p %s %c %P

注意:這裡是指在程式當前工作目錄的下建立。通常與程式在相同的路徑下。但如果程式中呼叫了chdir函式,則有可能改變了當前工作目錄。這時core檔案建立在chdir指定的路徑下。有好多程式崩潰了,我們卻找不到core檔案放在什麼位置。和chdir函式就有關係。當然程式崩潰了不一定都產生 core檔案。

更改coredump檔案的儲存位置:

echo “/data/coredump/core”> /proc/sys/kernel/core_pattern  # 把core檔案生成到/data/coredump/core目錄下

注意,這裡當前使用者必須具有對/proc/sys/kernel/core_pattern的寫許可權。

用GDB除錯coredump:

其實分析coredump的工具有很多,現在大部分類unix系統都提供了分析coredump檔案的工具,不過,我們經常用到的工具是gdb。 這裡我們以程式為例子來說明如何進行定位,使用gdb除錯core檔案來查詢程式中出現段錯誤的位置時,要注意的是可執行程式在編譯的時候需要加上-g編譯命令選項。

1.cpp檔案編譯執行

檔案如下:

#include<stdio.h>
 
void core_test1(){
	int i=0;
	scanf("%d",i);
	printf("%d\n",i);
}
void core_test2(){
	char *ptr = "my name is hello world";
	*ptr = 0;
}
 
int main(){
	core_test1();
	return 0;
}

編譯:

g++ -g core.cpp -o test 

執行:

./test
 
12
Segmentation fault (core dumped)   # 可以看到,當輸入12的時候,系統提示段錯誤並且core dumped

2.判斷是否為core檔案

在類unix系統下,coredump檔案本身主要的格式也是ELF格式,可以通過簡單的file命令進行快速判斷:

file core.xxxxx
 
輸出:
core.11691: ELF 64-bit LSB core file x86-64, version 1 (SYSV), SVR4-style, from './test'

3.使用GDB除錯

第一種方法(推薦):

  1. 啟動gdb,進入core檔案,命令格式:gdb [exec file] [core file],用法示例:
gdb ./test core.xxxxx
  1. 在進入gdb後,查詢段錯誤位置:where或者bt,用法示例:
bt
#0  0x00007f205b7afde5 in _IO_vfscanf_internal (s=<optimized out>, format=<optimized out>, argptr=argptr@entry=0x7ffdf417be88, 
    errp=errp@entry=0x0) at vfscanf.c:1902
#1  0x00007f205b7ba87b in __scanf (format=<optimized out>) at scanf.c:33
#2  0x0000000000400589 in core_test1 () at core.cpp:5
#3  0x00000000004005bf in main () at core.cpp:15

第二種方法:

  1. 啟動gdb,進入core檔案,命令格式:
gdb -c [core file]  //或 gdb --core=[core file]
  1. 在進入gdb後,指定core檔案對應的符號表,命令格式:
(gdb) file [exec file]
  1. 查詢段錯誤位置:where或者bt。用法示例:
 bt

3.列印文字

這是作為新手感覺最簡單好用的方式。有很多的用法,例如:

  • 添一條printf語句列印文字,能列印出來說明錯誤在printf語句之後,不能列印說明錯誤在printf語句之前。
  • 列印某個變數的值看看變數是否初始化(之前遇到的Segment fault就是用這個解決的)

相關文章