Linux環境下C++調試的三板斧
調試解決程序的漏洞,是程序員最基本的技能之一。用慣了圖形化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 |
輸出當前指定變量的值 | 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 <//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:
<//www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<//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行,按回車鍵繼續看餘下的。
(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文件
- 首先打開/etc/profile文件,一般都可以在文件中找到這句語句:ulimit -S -c 0 > /dev/null 2>&1,根據上面的例子,我們只要把那個0 改為 unlimited 就ok了。然後保存退出。通過source /etc/profile 使當期設置生效。或者想配置只針對某一用戶有效,則修改此用戶的/.bashrc或者/.bash_profile文件:
limit -c unlimited
- 第二種方法可以通過修改/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文件進行分別命名。
- /proc/sys/kernel/core_uses_pid可以控制core文件的文件名中是否添加pid作為擴展。文件內容為1,表示添加pid作為擴展名,生成的core文件格式為core.xxxx;為0則表示生成的core文件同一命名為core。可通過以下命令修改此文件:
echo "1" > /proc/sys/kernel/core_uses_pid
- 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調試
第一種方法(推薦):
- 啟動gdb,進入core文件,命令格式:gdb [exec file] [core file],用法示例:
gdb ./test core.xxxxx
- 在進入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
第二種方法:
- 啟動gdb,進入core文件,命令格式:
gdb -c [core file] //或 gdb --core=[core file]
- 在進入gdb後,指定core文件對應的符號表,命令格式:
(gdb) file [exec file]
- 查找段錯誤位置:where或者bt。用法示例:
bt
3.打印文本
這是作為新手感覺最簡單好用的方式。有很多的用法,例如:
- 添一條printf語句打印文本,能打印出來說明錯誤在printf語句之後,不能打印說明錯誤在printf語句之前。
- 打印某個變量的值看看變量是否初始化(之前遇到的Segment fault就是用這個解決的)