作者:edelweiss 日期:2020年3月3日
參考書目:ISBN 978-7-121-14048-8 DEBUG HACKS 中文版
1. 熱身準(zhǔn)備
1 調(diào)試是什么
2 Debug hacks 的地圖
3 調(diào)試的心得
2. 調(diào)試前的必知必會
4 獲取進程的內(nèi)核轉(zhuǎn)儲
獲取內(nèi)核轉(zhuǎn)儲 (core dump) 的最大好處是陋气,它能保存問題發(fā)生時的狀態(tài)。只要有問題發(fā)生時程序的可執(zhí)行文件和內(nèi)核轉(zhuǎn)儲引润,就可以知道進程當(dāng)時的狀態(tài)巩趁。比如,在不清楚 bug 復(fù)現(xiàn)方法的情況下淳附,或是 bug 極其罕見议慰,又或者 bug 只在特定機器上發(fā)生的情況等,只要獲取內(nèi)核轉(zhuǎn)儲奴曙,那么即使手頭沒有復(fù)現(xiàn)環(huán)境别凹,也能夠進行調(diào)試。
啟用內(nèi)核轉(zhuǎn)儲
大多數(shù) Linux 發(fā)行版默認(rèn)情況下關(guān)閉了內(nèi)核轉(zhuǎn)儲功能洽糟。用 ulimit 命令可以查看當(dāng)前的內(nèi)核轉(zhuǎn)儲功能是否有效炉菲。
-c 選項 表示內(nèi)核轉(zhuǎn)儲文件的大小限制。上例中限制為 0 坤溃,表示內(nèi)核轉(zhuǎn)儲無效拍霜。按照以下方式執(zhí)行 ulimit 命令,即可開啟內(nèi)核轉(zhuǎn)儲浇雹。
hanhan@ubuntu:~/DEBUG$ ulimit -c
0
hanhan@ubuntu:~/DEBUG$ ulimit -c unlimited #這個命令的意思是不限制內(nèi)核轉(zhuǎn)儲的空間大小
hanhan@ubuntu:~/DEBUG$ ulimit -c
unlimited
在設(shè)置成無限制之后沉御,發(fā)生問題時進程的內(nèi)存就可以全部轉(zhuǎn)儲到內(nèi)核轉(zhuǎn)儲文件中。在調(diào)試大量消耗內(nèi)存的進程時昭灵,可能會希望設(shè)置內(nèi)核轉(zhuǎn)儲文件的上限吠裆,這是直接在參數(shù)中指定大小即可。例如烂完,下面的命令設(shè)置上限為 1 GB试疙。
$ ulimit -c 1073741824
樣例程序
segfault.c
#include <stdio.h>
int main(void)
{
int *a = NULL;
*a = 0x1;
return 0;
}
使用 gcc -g 編譯并加入調(diào)試信息。
dongyu@During:~/DEBUG$ gcc -g segfault.c
在開啟內(nèi)核轉(zhuǎn)儲之后抠蚣,執(zhí)行上面的程序祝旷,確認(rèn)能否生成內(nèi)核轉(zhuǎn)儲。
dongyu@During:~/DEBUG$ ./a.out
Segmentation fault (core dumped)
dongyu@During:~/DEBUG$ ls
a.out core segfault.c
當(dāng)前目錄下已經(jīng)生成了 core 文件,要用 GDB 調(diào)試生成的內(nèi)核轉(zhuǎn)儲文件怀跛,應(yīng)當(dāng)使用以下方式啟動 GDB距贷。
dongyu@During:~/DEBUG$ gdb -c core ./a.out
...
[New LWP 19862]
Core was generated by `./a.out'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x000056464918d60a in main () at segfault.c:6
6 *a = 0x1;
(gdb)
segfault.c 的第六行收到了 SIGSEGV 信號。用 GDB list 命令可以查看附近的源代碼。
(gdb) l 5
1 #include <stdio.h>
2
3 int main(void)
4 {
5 int *a = NULL;
6 *a = 0x1;
7 return 0;
8 }
(gdb)
由于指針變量的值為 NULL ,所以在解引用空指針的時候收到了信號膛堤。
這是個最簡單的例子,但調(diào)試復(fù)雜程序時阁最,從內(nèi)核轉(zhuǎn)儲入手十分有效。程序越復(fù)雜骇两,就越難判斷接收信號的時候程序作了什么操作速种。此外,如果 bug 很難復(fù)現(xiàn)低千,那么單靠跟蹤源代碼會很難確定原因配阵。在這些情況下,內(nèi)核轉(zhuǎn)儲能將問題發(fā)生時的狀態(tài)原原本本地保存下來示血,有助于確定 bug 原因闸餐。
在專用目錄中生成內(nèi)核轉(zhuǎn)儲
在使用大型文件系統(tǒng)時,會希望將內(nèi)核轉(zhuǎn)儲放在固定的位置矾芙。默認(rèn)情況下會在當(dāng)前目錄中生成,但可能難以弄清文件到低在哪里生成近上。此外剔宪,大量生成的內(nèi)核轉(zhuǎn)儲文件可能會給系統(tǒng)的磁盤空間造成壓力。這種情況下可以準(zhǔn)備內(nèi)核轉(zhuǎn)儲專用分區(qū)壹无,并在該分區(qū)中設(shè)置內(nèi)核轉(zhuǎn)儲文件葱绒,這樣就方便很多。轉(zhuǎn)儲保存位置的完整路徑可以通過 sysctl 變量 kernel.core_pattern 設(shè)置斗锭。假設(shè)在 /etc/sysctl.conf 中這樣設(shè)置地淀。
/etc/sysctl.conf
1 # DEBUG HACKS
2 kernel.core_pattern = /var/core/%t-%e-%p-%c.core
3 kernel.core_uses_pid = 0
dongyu@During:~/DEBUG$ sudo sysctl -p
在該狀態(tài)下 sudo 執(zhí)行剛才的 a.out 程序,就會在 /var/core下生成內(nèi)核轉(zhuǎn)儲文件岖是。
dongyu@During:~/DEBUG$ sudo ./a.out
Segmentation fault
dongyu@During:~/DEBUG$ ls /var/core/
1583486733-a.out-20519-18446744073709551615.core
dongyu@During:~/DEBUG$ sudo ./a.out
Segmentation fault
dongyu@During:~/DEBUG$ ls
a.out segfault.c
dongyu@During:~/DEBUG$ ls /var/core/
1583486733-a.out-20519-18446744073709551615.core
dongyu@During:~/DEBUG$
kernel.core_pattern 中可以設(shè)置的格式符
格式符 | 說明 |
---|---|
%% | %本身 |
%p | 被轉(zhuǎn)儲的進程 ID (PID) |
%u | 被轉(zhuǎn)儲的進程的真實用戶 ID (read UID) |
%g | 被轉(zhuǎn)儲進程的真實組 ID (real GID) |
%s | 引發(fā)轉(zhuǎn)儲的信號編號 |
%t | 轉(zhuǎn)儲時刻 (從 1970年 1 月 1 日 0:00 開始的秒數(shù)) |
%h | 主機名 (同 uname (2) 返回的 nodename) |
%e | 可執(zhí)行文件名 |
%c | 轉(zhuǎn)儲文件的大小上限 (內(nèi)核版本 2.6.24 以后可以使用) |
上例中設(shè)置了 kernel.core_uses_pid=0, 是因為我們改變了文件中 PID 的位置帮毁。如果設(shè)置該值為 1,文件名末尾就會添加.PID豺撑。
使用用戶編寫的程序自動壓縮內(nèi)核轉(zhuǎn)儲文件
啟動整個系統(tǒng)的內(nèi)核轉(zhuǎn)儲功能
利用內(nèi)核轉(zhuǎn)儲掩碼排除共享內(nèi)存
5 GDB 的基本使用方法一
GDB 的基本使用流程
(1) 帶著調(diào)試選項編譯烈疚、構(gòu)建調(diào)試對象
(2) 啟動調(diào)試器 (GDB)
(2-1) 設(shè)置斷點
(2-2)顯示棧幀
(2-3)顯示值
(2-4)繼續(xù)執(zhí)行
準(zhǔn)備
uname.c
1 #include <stdio.h>
2 #include <stdlib.h>
3
4
5 int main(int argc, char* argv[])
6 {
7 int a = 10;
8 int b = 20;
9 int c = a + b;
10
11 printf("c = %d\n", c);
12
13 for (int i = 1; i < argc; i++ )
14 {
15 printf("%s\n", argv[i]);
16 }
17
18 return 0;
19 }
~
通過 gcc 的 -g 選項生成調(diào)試信息
$ gcc -wall -O2 -g 源文件
如果用 configure 腳本生成 makefile, 可以這樣用。
$ ./configure CFLAGS="-Wall -02 -g"
本例實際操作
dongyu@During:~/DEBUG$ gcc -o uname -Wall -g -O2 uname.c
dongyu@During:~/DEBUG$ ls
uname uname.c
啟動
dongyu@During:~/DEBUG$ gdb uname
...
(gdb)
設(shè)置斷點
可以在函數(shù)名和行號上設(shè)置斷點聪轿。程序運行后爷肝,到達(dá)斷點就會自動暫停運行。此時可以查看該時刻的變量值、顯示棧幀灯抛、重新設(shè)置斷點或重新運行等金赦。斷點命令 (break) 可以簡寫為 b 。
格式:
break 函數(shù)名
break 行號
break 文件名:行號
break 文件名:函數(shù)名
break + 偏移量
break *地址
(gdb) break main
Breakpoint 1 at 0x5a0: file uname.c, line 6.
(gdb) break 9
Breakpoint 2 at 0x5c1: file uname.c, line 9.
設(shè)置好的斷點可以通過 info break 確認(rèn)
(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep y 0x00000000000005a0 in main at uname.c:6
2 breakpoint keep y 0x00000000000005c1 in main at uname.c:9
(gdb)
運行
用 run 命令開始運行对嚼。不加參數(shù)只執(zhí)行 run, 就會執(zhí)行到設(shè)置了斷點的位置暫停下來夹抗。可以簡寫為 run 猪半。
在 run 命令后面加上可執(zhí)行程序的參數(shù)
(gdb) run hello world #參數(shù) 1 hello 參數(shù) 2 world
經(jīng)常用到的一個操作是在 main() 上設(shè)置斷點兔朦,然后執(zhí)行到 main()函數(shù)。start 命令能達(dá)到同樣的效果磨确。
顯示棧幀
backtrace 命令可以在遇到斷點而暫停時顯示棧幀沽甥。該命令簡寫為 bt 。此外乏奥,backtrace 的別名還有 where 和 info stack (簡寫為 info s)摆舟。
格式:
backtrace:顯示所有棧幀。
backtrace N:只顯示開頭 N 個棧幀邓了。
backtrace full N :只顯示最后 N 個 棧幀恨诱。
顯示變量
print 命令可以顯示變量。print 可以簡寫為 p骗炉。
(gdb) print argc
$1 = 3
(gdb) print argv
$2 = (char **) 0x7fffffffe578
(gdb) print argv
$3 = (char **) 0x7fffffffe578
(gdb) print argv[0]
$4 = 0x7fffffffe7bf "/home/dongyu/DEBUG/uname"
(gdb) print argv[1]
$5 = 0x7fffffffe7d8 "hello"
(gdb)
顯示寄存器
info registers 可以顯示寄存器照宝,簡寫為 info reg。
在寄存器名之前添加 $句葵,即可顯示各個寄存器的內(nèi)容厕鹃。
格式:
p/格式 變量名
顯示寄存器可使用的格式
格式 | 說明 |
---|---|
x | 顯示為十六進制數(shù) |
d | 顯示為十進制數(shù) |
u | 顯示為無符號十進制數(shù) |
o | 顯示為八進制數(shù) |
t | 顯示為二進制數(shù), t 的由來是 two |
a | 地址 |
c | 顯示為字符 (ASCII) |
f | 浮點小數(shù) |
s | 顯示為字符串 |
i | 顯示為機器語言 (僅在顯示內(nèi)存的 X 命令中可用) |
單步執(zhí)行
單步執(zhí)行的意思是根據(jù)源代碼一行一行地執(zhí)行乍丈。
執(zhí)行源代碼中一行的命令為 next (簡寫為 n)剂碴。執(zhí)行時如果遇到函數(shù)調(diào)用,可能想執(zhí)行到函數(shù)內(nèi)部轻专,此時可以使用 step (簡寫為 p ) 命令忆矛。
next 命令和 step 命令都是執(zhí)行源代碼中的一行。如果要逐條執(zhí)行匯編指令请垛,可以分別使用 nexti 和 stepi 命令催训。
nexti 命令不會進入函數(shù)內(nèi)部執(zhí)行,而 stepi 命令會宗收。
繼續(xù)運行
調(diào)試時瞳腌,可以使用 continue (簡寫為c) 命令繼續(xù)運行程序。程序會在遇到斷點后再次暫停運行镜雨。如果沒有遇到斷點嫂侍,就會一直運行到結(jié)束儿捧。
格式:
continue
continue 次數(shù)
指定次數(shù)可以忽略斷點。例如挑宠,continue 5 則 5 次遇到斷點不停止菲盾,第6次遇到斷點是才暫停執(zhí)行。
被調(diào)試的程序通常為以下幾種情況之一:
- 可以正常結(jié)束各淀。
- 由于某種原因異常結(jié)束 (發(fā)生內(nèi)核轉(zhuǎn)儲懒鉴、非法訪問等)
- 無法結(jié)束 (死循環(huán)等)
- 被掛起 (停止響應(yīng)、死鎖等)
除了正常結(jié)束之外碎浇,其他情況都需要從頭開始繼續(xù)執(zhí)行临谱,以尋找問題的原因 (調(diào)試)。
監(jiān)視點
大型軟件或大量使用指針的程序中奴璃,很難弄清楚變量在什么地方被改變悉默。要想找到變量在何處被改變,可以使用 watch 命令 (監(jiān)視點, watchpoint)苟穆。
格式:
watch <表達(dá)式>
<表達(dá)式> 發(fā)生變化時暫停運行抄课。
此處 <表達(dá)式> 的意思是常量或變量等。
格式:
awatch <表達(dá)式>
<表達(dá)式> 被訪問雳旅、改變時暫停運行跟磨。
格式:
reatch <表達(dá)式>
<表達(dá)式> 被訪問時暫停運行。
刪除斷點和監(jiān)視點
用 delete (簡寫為d)命令刪除斷點和監(jiān)視斷點攒盈。
格式:
delete <編號>
刪除 <編號> 指示的斷點或監(jiān)視點抵拘。
其他斷點
硬件斷點 (hbreak), 適用于 ROM 空間等無法修改的內(nèi)存區(qū)域中的程序。在有些架構(gòu)中無法使用型豁。
臨時斷點 (tbreak)和臨時硬件斷點 (thbreak), 與斷點 (硬件斷點)相同仑濒,都會在運行到該處時暫停,不同之處就是臨時斷點 (臨時硬件斷點)會在此時被刪除偷遗,所以在只需要停止一次時用起來很方便。
遺憾的是并沒有臨時監(jiān)視點驼壶。
改變變量的值
格式:
set variable <變量>=<表達(dá)式>
該功能可以在運行時隨意修改變量的值氏豌,因此無需修改源代碼就能確認(rèn)各種值的情況。
生成內(nèi)核轉(zhuǎn)儲文件
使用 generate-core-file 可將調(diào)試中的進程生成內(nèi)核轉(zhuǎn)儲文件热凹。
有了內(nèi)核轉(zhuǎn)儲文件和調(diào)試對象泵喘,以后就能查看生成轉(zhuǎn)儲文件時的運行歷史 (寄存器值、內(nèi)存值等)般妙。
6 GDB 的基本使用方法二
attach 到進程
要調(diào)試守護進程 (daemon process )等已經(jīng)啟動的進程纪铺,或者調(diào)試陷入死循環(huán)而無法返回控制臺的進程時,可以使用 attach 命令碟渺。
格式:
attach pid
執(zhí)行這一命令就可以 attach 到進程 ID 為 pid的進程上鲜锚。
條件斷點
有一種斷點只在特定條件下中斷。
格式:
break 斷點 if 條件
這條命令將測試給定的條件,如果為真則暫停運行旺隙。
刪除斷點觸發(fā)條件
格式:
condition 斷點編號
添加斷點觸發(fā)條件
格式:
condition 斷點編號 條件
反復(fù)執(zhí)行
格式:
igore 斷點編號 次數(shù)
在編號指定的斷點蔬捷、監(jiān)視點(watchpoint)或捕獲點(catchpoint)忽略指定的次數(shù)。
continue 命令與 ignore 命令一樣凰兑,也可以指定次數(shù)罕容,達(dá)到指定次數(shù)前,執(zhí)行到斷點時不暫停稿饰,二者的意義是相同的锦秒。
刪除斷點和禁用斷點
用 clear 命令刪除已定義的斷點。
只想臨時禁用斷點的話喉镰,可以使用disable 命令旅择。將禁用的斷點重新啟用,則可使用 enable 命令侣姆。
斷點命令
斷點命令 (commands)可以定義在斷點中斷后自動執(zhí)行的命令生真。
7 GDB 的基本使用方法三
值的歷史
通過 print 命令顯示過的值會記錄在內(nèi)部的值歷史中捺宗。這些值可以在其他表達(dá)式中使用蚜厉。
變量 | 說明 |
---|---|
$ | 值歷史的最后一個值 |
$n | 值歷史的第 n 個值 |
$$ | 值歷史的倒數(shù)第 2 個值 |
$$n | 值歷史的倒數(shù)第 n 個值 |
$_ | x 命令顯示過的最后的地址 |
$__ | x 命令顯示過的最后的地址的值 |
$_exitcode | 調(diào)試中的程序的返回代碼 |
$bpnum | 最后設(shè)置的斷點編號 |
變量
可以隨意定義變量。變量以$開頭昼牛,有英文字母和數(shù)字組成术瓮。
命令歷史
可以將命令歷史保存到文件中。保存命令歷史后贰健,就能在其他調(diào)試會話中重復(fù)利用這些命令(通過箭頭查找以前的命令),十分方便。默認(rèn)命令歷史文件位于./.gdb_history。
初始化文件
Linux 環(huán)境下的初始化文件為.gdbinit载庭。如果存在.gdbinit 文件,GDB 就會在啟動之前將其作為命令文件運行。
命令定義
利用 define 命令可以自定義命令,還可以使用 document 命令給自定義命令添加說明。用 "help 命令名" 可以查看定義的命令匕争。
8 intel 架構(gòu)的基本知識
作為調(diào)試的基本知識,這里簡單介紹一下 CPU 架構(gòu)艘蹋。
字節(jié)序
所謂 Endian,就是多字節(jié)數(shù)據(jù)在內(nèi)存中的排列方式票灰。
例如 0x12345678 這個數(shù)據(jù)女阀,地位數(shù)據(jù)排在內(nèi)存低地址宅荤,高位數(shù)據(jù)排在內(nèi)存高地址,這就叫做小端浸策。
0003 | 0002 | 0001 | 0000 |
---|---|---|---|
0x12 | 0x34 | 0x56 | 0x78 |
32 位環(huán)境中的寄存器
通用寄存器有 8 種冯键,分別是 EAX、EBX庸汗、ECX惫确、ESI、EDI蚯舱、EBP改化、ESP,用于邏輯運算枉昏、數(shù)學(xué)運算陈肛、地址計算、內(nèi)存指針等兄裂。
ESP 寄存器用于保存棧指針
某些命令使用特定的寄存器句旱。例如,字符串將 EXC晰奖、ESI 和 EDI 寄存器作為操作數(shù)使用谈撒。
通用寄存器的主要用途
寄存器 | 用途 |
---|---|
EAX | 操作數(shù)的運算、結(jié)果 |
EBX | 指向 DS 端中數(shù)據(jù)的指針 (主要端寄存器的用途見表 2-8) |
ECX | 字符串操作或循環(huán)的計數(shù)器 |
EDX | 輸入輸出指針 |
ESI | 指向 DS 寄存器所指示的段中某個數(shù)據(jù)的指針畅涂,或者是字符串操作中字符串的復(fù)制源 (source) |
EDI | 指向 ES 寄存器所指示的端中某個數(shù)據(jù)的指針港华,或者是字符串操作中字符串的復(fù)制目的地 (destination) |
ESP | 棧指針 (SS 段) |
EBP | 指向棧上數(shù)據(jù)的指針 (SS 段) |
CS | 代碼段 |
DS | 數(shù)據(jù)段 |
SS | 堆棧段 |
ES | 數(shù)據(jù)段 |
FS | 數(shù)據(jù)段 |
GS | 數(shù)據(jù)段 |
程序代碼放在代碼段中,數(shù)據(jù)放在數(shù)據(jù)段中午衰,程序所用的棧放在堆棧段中立宜。
但是,通用寄存器的用途并不限于上面所述臊岸,也可以用于一般用途橙数,所以上表只能作為參考。寄存器的結(jié)構(gòu)見下圖帅戒。
EFLAGS 寄存器中包含狀態(tài)標(biāo)志 (status flag)灯帮、控制標(biāo)志 (confrol flag)、系統(tǒng)標(biāo)志 (system flag) 等逻住。
EIP (Instruction Pointer)寄存器是 32 位指令指針
其他寄存器有控制寄存器 (CR0~CR4)钟哥、GDTR、IDTR瞎访、TR腻贰、LDTR、調(diào)試寄存器 ( DR0/DR1/DR2/DR3/DR6/DR7 )扒秸、內(nèi)存類型范圍寄存器 MTPP播演、MSR(Model Specific Register)寄存器冀瓦、機器檢查寄存器 (Machine Check Register)、性能監(jiān)視計數(shù)器 (Performance Monitoring Counter)等写烤。
64 位環(huán)境中的寄存器
地址
CPU 可以通過內(nèi)存總線訪問到的地址稱為物理地址翼闽。
平坦模型 (flat model)中,內(nèi)存可看做單一洲炊、平坦的連續(xù)地址空間感局,稱為線性地址空間。Linux 采用這種內(nèi)存模型选浑。
分段式內(nèi)存模型 (segment model) 中蓝厌,將內(nèi)存看做被稱為“段”(segment)的獨立地址空間的集合。通過段選擇器和偏移量組成的邏輯地址來訪問段內(nèi)地址古徒。首先用段選擇器識別出要訪問的段拓提,然后通過偏移量找到該段的地址空間中的內(nèi)存 (圖 2-10)。32位模式下最多能指定 16383 個段隧膘,各段的最大大小為 2的32次方 字節(jié)代态。
64 位模式采用了平坦模型,因此可以使用 64 位線性地址疹吃。不能使用分段式內(nèi)存模型蹦疑。
數(shù)據(jù)類型
基本數(shù)據(jù)類型包括字節(jié) (8 比特)、字(16 比特)萨驶、雙字(32 比特)歉摧、四字(64比特)、和雙四字(128比特)腔呜。
9 調(diào)試時必須的棧知識
棧 (strack) 是程序存放數(shù)據(jù)的內(nèi)存區(qū)域之一叁温,其特征是 LIFO (Last In First Out)式數(shù)據(jù)結(jié)構(gòu),即后放進去的數(shù)據(jù)先被取出核畴。向棧中存儲數(shù)據(jù)的操作稱為 PUSH, 從棧中取出數(shù)據(jù)的操作稱為 POP 膝但。在保存動態(tài)分配的自動變量時需要使用棧。此外在函數(shù)調(diào)用時谤草,棧還用于傳遞函數(shù)參數(shù)跟束,以及用于保存返回地址和返回值。
示例程序:這個程序?qū)⒚钚袇?shù)傳遞過來的數(shù)字作為終值丑孩,計算 0 到終值之前所有正數(shù)的總和冀宴。
#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
#define MAX (1UL << 20)
typedef unsigned long long u64;
typedef unsigned int u32;
u32 max_addend = MAX;
u64 sum_till_MAX(u32 n)
{
u64 sum;
n++;
sum = n;
if (n < max_addend)
sum += sum_till_MAX(n);
return sum;
}
int main(int argc, char** argv)
{
u64 sum = 0;
if ((argc == 2) && isdigit(*(argv[1])))
max_addend = strtoul(argv[1], NULL, 0);
if (max_addend > MAX || max_addend == 0){
fprintf(stderr, "Invalid number is specified\n");
return 1;
}
sum = sum_till_MAX(0);
printf("sum(0..%lu) = %llu\n", max_addend, sum);
return 0;
}