下篇見調試技巧2。
Debug Hacks
作者爲吉岡弘隆、大和一洋、大巖尚宏、安部東洋、吉田俊輔,有中文版《Debug
Hacks中文版—深入調試的技術和工具》。這本書涉及了很多調試技巧,對調試器使用、內核調試方法、常見錯誤的原因,還介紹了systemtap、strace、ltrace等一大堆工具,非常值得一讀。
話說我聽說過的各程序設計課程似乎都沒有強調過調試的重要性,把調試當作單獨一節課來上(就算有估計也上不好),很多人都只會printf調試法,breakpoint都很少用,就不提conditional
breakpoint、watchpoint、reverse
execution之類的了。也看到過很多同學在調試上浪費了很長很長的時間。
下面是篇review,也包含了一些我自己整理的一些調試技巧。
折騰工具
繼續牢騷幾句,我接觸過的人當中感覺最執着與折騰工具的人只有兩個,ppwwyyxx和xiaq,他們是少有的能把折騰工具當作正經工作來做的人。
很久以前我還會到處在網上搜索好的實用工具,尤其是那些CLI程序,比如renameutils、xsel、recode、the_silver_searcher,查閱文檔定製自己的配置文件。但這麼做花費的時間太多。後來就想我可以搜索一些善於折騰的人的配置文件,關注他們修改了哪些地方,我的配置只要取衆家之所長就可以了。
先厚顏自薦一下我的配置。下面的用戶列表就是我找到的在GitHub上把dotfiles配置地井井有條的人(如果GitHub支持按照項目的大小排序,列表蒐集就能省很多麻煩了):
1 | alejandrogomez bhj craigbarnes dotvim hamaco joedicastro laurentb ok100 pyx roylez sjl trapd00r vodik w0ng |
有了上述的dotfiles,其他人的dotfiles大多都不願看了。但是五嶽歸來不看山,黃山歸來不看嶽,ppwwyyxx的dotfiles感覺與之前諸位相比更勝一籌。
無關的話到此結束,下面是正文:
gdb
記錄歷史
把下面幾行添加到~/.gdbinit中吧,gdb啓動時會自動讀取裏面的命令並執行:
1 | set history save on |
我習慣在~/.history堆放各個歷史文件。有了歷史,使用readline的reverse-search-history (C-r)就能輕鬆喚起之前輸入過的命令。
修改任意內存地址的值
1 | set {int}0x83040 = 4 |
顯示intel風格的彙編指令
1 | set disassembly-flavor intel |
斷點在function prologue前
先說一下function prologue吧,每個函數最前面一般有三四行指令用來保存舊的幀指針(rbp),並騰出一部分棧空間(通常用於儲存局部變量、爲當前函數調用其他函數騰出空間存放參數,有時候還會存儲字面字符串,當有nested function時也會用於保存當前的棧指針)。
在x86-64環境下典型的funcition prologue長成這樣:
1 | push rbp |
可能還會有and指令用於對齊rsp。如果編譯時加上-fomit-frame-pointer(Visual
Studio中文版似乎譯作“省略框架指針”),那麼生成的指令就會避免使用rbp,function
prologue就會簡化成下面一行:
1 | sub rsp, 0x10 |
設置斷點時如果使用了b *func的格式,也就是說在函數名前加上*,gdb就會在執行function
prologue前停下,而b func則是在執行function
prologue後停下。參考下面的會話:
1 | % gdb a.out |
Checkpoint
gdb可以爲被調試的程序創建一個快照,即保存程序運行時的狀態,等待以後恢復。這個是非常方便的一個功能,特別適合需要探測接下來會發生什麼但又不想離開當前狀態時使用。
ch是創建快照,d c ID是刪除指定編號的快照,i ch是查看所有快照,restart ID是切換到指定編號的快照,詳細說明可以在shell裏鍵入info '(gdb) Checkpoint/Restart'查看。
1 | % gdb ./a.out |
上面的會話中先用ch創建了一個快照,緊接着a被修改爲了3,隨後用restart 1恢復到編號爲1的快照,繼續運行程序可以發現a仍然爲原來的值0。
以色列的Haifa Linux
club有一次講座講gdb,講稿值得一看:http://haifux.org/lectures/210/gdb_-_customize_it.html
逆向技術
Long Le的peda很不錯,感覺比http://reverse.put.as的https://github.com/gdbinit/Gdbinit好用。
gcc
Mudflap
使用了compile-time
instrumentation(CTI)的工具。編譯時加上-fmudflap -lmudflap選項即可,會在很多不安全代碼生成的指令前加上判斷合法性的指令。
1 | % echo 'int main() { int z[1]; z[1] = 2; }' | cc -xc - -fmudflap -lmudflap |
第一行用-xc -讓cc從標準輸入讀源代碼,並當作C來編譯。接來下執行./a.out,可以看到運行時程序報錯了。
使用MUDFLAP_OPTIONS環境變量可以控制Mudflap的運行期行爲,具體參見Mudflap Pointer
Debugging。
AddressSanitizer
和Mudflap類似的工具,clang和gcc可以加上選項-fsanitize=address使用,比如:
1 | clang -fsanitize=address a.c |
如果想在出錯的地方斷點停下來,可以用gdb打開,輸入b __asan_report_store1回車,再輸入r回車運行程序。
-ftrapv
這個選項是調試有符號整型溢出問題的利器。在i386環境下,gcc會把int32_t運算編譯成call __addvsi3,__addvsi3函數會在運行時檢查32位有符號加法運算是否產生溢出,如果是則調用abort函數中止程序。減法、乘法和取反運算也有類似的運行時函數檢查溢出,另外也有64位版本的__addvdi3等函數。但不存在對無符號整型的溢出檢測函數。比如下面這些代碼均會觸發trap:
1 | int a = INT_MAX; a++; |
這段代碼來自gcc項目目錄的libgcc/libgcc2.c:
1 |
|
但注意在x86-64環境下-ftrapv只檢查64位溢出。考慮下面這段代碼:
1 |
|
在x86-64下用gcc編譯運行,輸出barrier後才會執行abort使程序中止,因爲int32_t的溢出不會觸發trap。
clang也有-ftrapv,在x86-64環境下對於int32_t的溢出也能觸發trap。
_FORTIFY_SOURCE
gets、strcpy這類函數容易造成stack
mashing。gcc編譯時如果指定了-D_FORTIFY_SOURCE=1,生成的彙編程序中這些不安全的函數調用會被替代爲libc.so中名字類似__gets_chk的一類安全函數,會在運行期檢查是否產生了緩衝區溢出。比如,下面的代碼會在運行時報錯:
1 |
|
Gentoo
Portage從gcc-4.3.3-r1開始默認開啓_FORTIFY_SOURCE標誌了,好多發行版都開啓了,測試發現Arch
Linux的gcc似乎沒有。shell裏執行下面代碼就可以看到Gentoo裏是怎麼定義_FORTIFY_SOURCE的了:
1 | echo -e '#undef __OPTIMIZE__\nmain() { printf("%d\\n", _FORTIFY_SOURCE); }' | cpp |
也就是當優化等級在-O1或以上時_FORTIFY_SOURCE會生效,名字爲__$func_chk模式的函數會被使用。這種做法造成了一些麻煩,比如suricata
git
tree裏的src/suricata.c使用了#ifdef _FORTIFY_SOURCE,會造成編譯無法通過。
-fstack-protector
-fstack-protector -fstack-protector-all gcc 4.8.1 -fstack-protector-strong
https://securityblog.redhat.com/2013/10/23/debugging-stack-protector-failures/
開啓Stack-Smashing Protector (SSP)。我的理解是在儲存的幀指針(rbp)前寫入一個magic number,函數返回的時候檢查下這個magic number是否被改動,如果是就可能產生stack smashing了。這個方法的footprint最小,但是保護力度也比較弱。
IA32
1 | function prologue |
x86-64
1 | function prologue: |
execinfo.h
提供了int backtrace (void **buffer, int size)、char ** backtrace_symbols (void *const *buffer, int size)在程序運行時查看函數調用棧。參見http://www.gnu.org/software/libc/manual/html_node/Backtraces.html。
Misc
Valgrind
一系列調試和profiling工具的套件,其中的Memcheck是一個使用了dynamic
binary instrumentation(DBI)的工具,
在程序指令間插入自己的指令檢查validity和addressablity。另外Memcheck替換了標準的malloc,這樣就可以檢測出off-by-one
error、double free、內存泄漏等許多問題。
Memcheck引入的footprint極小,無需重編譯程序,也沒有繁瑣的配置。比如原來是用./a.out執行程序,需要Memcheck時就換成valgrind ./a.out。
在程序訪問某一內存地址時Memcheck會檢查是否有越界之類的錯誤,Memcheck能診斷出大量但不是全部的訪問錯誤,比如下面這樣有問題的代碼就沒法檢查出來:
1 | int main() |
因爲a[1992]的地址在棧上,允許訪問。
Valgrind啓動時會讀取~/.valgrindrc,對於memcheck我配置了下面這幾行:
1 | --memcheck:leak-check=yes |
valgrind --vgdb-error=0 --vgdb=yes很強大,可以在進程遇到錯誤時讓gdb調試。
strace
記錄程序執行的系統調用和收到的信號,和valgrind類似,使用非常簡單:
1 | strace ./a.out |
有一些選項可以attach到現有進程上去(-p)、記錄時刻(-t)、統計系統調用使用次數(-c)、過濾特定的系統調用(-e)等。
帶上-c選項可以統計系統調用的使用次數:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28% strace -c ls
chap04 chap05 chap06 chap07 chap08 chap09 chap10 chap11 chap12 chap13 chap14 chap15 chap16 chap17
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
0.00 0.000000 0 5 read
0.00 0.000000 0 1 write
0.00 0.000000 0 7 open
0.00 0.000000 0 10 close
0.00 0.000000 0 8 fstat
0.00 0.000000 0 20 mmap
0.00 0.000000 0 12 mprotect
0.00 0.000000 0 2 munmap
0.00 0.000000 0 3 brk
0.00 0.000000 0 2 rt_sigaction
0.00 0.000000 0 1 rt_sigprocmask
0.00 0.000000 0 2 ioctl
0.00 0.000000 0 1 1 access
0.00 0.000000 0 1 execve
0.00 0.000000 0 1 fcntl
0.00 0.000000 0 2 getdents
0.00 0.000000 0 1 getrlimit
0.00 0.000000 0 1 arch_prctl
0.00 0.000000 0 2 1 futex
0.00 0.000000 0 1 set_tid_address
0.00 0.000000 0 1 openat
0.00 0.000000 0 1 set_robust_list
------ ----------- ----------- --------- --------- ----------------
100.00 0.000000 85 2 total
-e選項只跟蹤指定系統調用:
1 | % strace -e read,open ls |
使用strace還可以做一些很可怕的事,比如有root權限的情況下嗅探sshd以得到其他嘗試SSH登錄的用戶的密碼:SSHD
password sniffing。
-p很有用,比如調試CGI
wrapperfcgiwrap,觀察它的輸出:
1 | strace -s200 -p$(pidof -s fcgiwrap) -e write |
ltrace
記錄程序調用的動態庫中的函數。名字和strace很像,使用方式和很多命令行選項也如出一轍。
查看echo test
1 | % ltrace echo test |
Ltrace
Internals描述了ltrace的實現機制。
SystemTap
SystemTap提供了一套底層工具用於trace/probe。用戶編寫SystemTap
script語言的程序,SystemTap將其翻譯爲C代碼,再編譯成臨時的內核模塊。內核模塊加載時SystemTap
script腳本裏的hook就會在特定event發生時執行。當SystemTap腳本停止運行時,相應的hook就被刪除,移除臨時的內核模塊。這一整套流程都是通過一個簡單的CLI程序stap驅動的。
SystemTap使用前的配置過程比較複雜,需要特製的內核,開啓CONFIG_KPROBES=y、CONFIG_DEBUG_INFO=y等諸多內核編譯選項。
比如如下的簡單腳本就能顯示各進程調用net/socket.c內函數的情況:
1 | probe kernel.function("*@net/socket.c").call { |
perf
1 | perf record -e probe_a:main -e probe_a:main_1 /home/ray/tmp/a |
可執行文件不能在tmpfs分區。
1 | A=~/tmp; cc -xc <(echo 'main(){}') -Wl,-rpath,$A -o a && sudo perf probe -d '*' || :; sudo perf probe -x $A/libc.so.6 malloc && sudo perf record -e probe_libc:malloc -aR ./a && sudo perf report -n |
其他
書裏還介紹了很多神奇的玩意兒,比如kaho,用於讀取被編譯器優化掉的變量;livepatch,運行時動態修改變量、替換函數等。這兩個工具我在網上檢索了下,感覺是個proof
of
concept的東西,也沒有更新了。不夠這些思路很奇特,想到了並試圖去解決調試時常受困擾的問題,很棒。