使用 GDB 除錯多程式程式

工程師WWW發表於2015-05-13

GDB 是 linux 系統上常用的 c/c++ 除錯工具,功能十分強大。對於較為複雜的系統,比如多程式系統,如何使用 GDB 除錯呢?考慮下面這個三程式系統:

程式
程式

Proc2 是 Proc1 的子程式,Proc3 又是 Proc2 的子程式。如何使用 GDB 除錯 proc2 或者 proc3 呢?

實際上,GDB 沒有對多程式程式除錯提供直接支援。例如,使用GDB除錯某個程式,如果該程式fork了子程式,GDB會繼續除錯該程式,子程式會不受干擾地執行下去。如果你事先在子程式程式碼裡設定了斷點,子程式會收到SIGTRAP訊號並終止。那麼該如何除錯子程式呢?其實我們可以利用GDB的特點或者其他一些輔助手段來達到目的。此外,GDB 也在較新核心上加入一些多程式除錯支援。

接下來我們詳細介紹幾種方法,分別是 follow-fork-mode 方法,attach 子程式方法和 GDB wrapper 方法。

follow-fork-mode

在2.5.60版Linux核心及以後,GDB對使用fork/vfork建立子程式的程式提供了follow-fork-mode選項來支援多程式除錯。

follow-fork-mode的用法為:

set follow-fork-mode [parent|child]

  • parent: fork之後繼續除錯父程式,子程式不受影響。
  • child: fork之後除錯子程式,父程式不受影響。

因此如果需要除錯子程式,在啟動gdb後:

(gdb) set follow-fork-mode child

並在子程式程式碼設定斷點。

此外還有detach-on-fork引數,指示GDB在fork之後是否斷開(detach)某個程式的除錯,或者都交由GDB控制:

set detach-on-fork [on|off]

  • on: 斷開除錯follow-fork-mode指定的程式。
  • off: gdb將控制父程式和子程式。follow-fork-mode指定的程式將被除錯,另一個程式置於暫停(suspended)狀態。

注意,最好使用GDB 6.6或以上版本,如果你使用的是GDB6.4,就只有follow-fork-mode模式。

follow-fork-mode/detach-on-fork的使用還是比較簡單的,但由於其系統核心/gdb版本限制,我們只能在符合要求的系統上才能使用。而且,由於follow-fork-mode的除錯必然是從父程式開始的,對於fork多次,以至於出現孫程式或曾孫程式的系統,例如上圖3程式系統,除錯起來並不方便。

Attach子程式

眾所周知,GDB有附著(attach)到正在執行的程式的功能,即attach <pid>命令。因此我們可以利用該命令attach到子程式然後進行除錯。

例如我們要除錯某個程式RIM_Oracle_Agent.9i,首先得到該程式的pid

[root@tivf09 tianq]# ps -ef|grep RIM_Oracle_Agent.9i
nobody    6722  6721  0 05:57 ?        00:00:00 RIM_Oracle_Agent.9i
root      7541 27816  0 06:10 pts/3    00:00:00 grep -i rim_oracle_agent.9i

通過pstree可以看到,這是一個三程式系統,oserv是RIM_Oracle_prog的父程式,RIM_Oracle_prog又是RIM_Oracle_Agent.9i的父程式。

[root@tivf09 root]# pstree -H 6722
通過 pstree 察看程式
通過 pstree 察看程式

啟動GDB,attach到該程式

用 GDB 連線程式
用 GDB 連線程式

現在就可以除錯了。一個新的問題是,子程式一直在執行,attach上去後都不知道執行到哪裡了。有沒有辦法解決呢?

一個辦法是,在要除錯的子程式初始程式碼中,比如main函式開始處,加入一段特殊程式碼,使子程式在某個條件成立時便迴圈睡眠等待,attach到程式後在該程式碼段後設上斷點,再把成立的條件取消,使程式碼可以繼續執行下去。

至於這段程式碼所採用的條件,看你的偏好了。比如我們可以檢查一個指定的環境變數的值,或者檢查一個特定的檔案存不存在。以檔案為例,其形式可以如下:

void debug_wait(char *tag_file)
{
    while(1)
    {
        if (tag_file存在)
            睡眠一段時間;
        else
            break;
    }
}

當attach到程式後,在該段程式碼之後設上斷點,再把該檔案刪除就OK了。當然你也可以採用其他的條件或形式,只要這個條件可以設定/檢測即可。

Attach程式方法還是很方便的,它能夠應付各種各樣複雜的程式系統,比如孫子/曾孫程式,比如守護程式(daemon process),唯一需要的就是加入一小段程式碼。

GDB wrapper

很多時候,父程式 fork 出子程式,子程式會緊接著呼叫 exec族函式來執行新的程式碼。對於這種情況,我們也可以使用gdb wrapper 方法。它的優點是不用新增額外程式碼。

其基本原理是以gdb呼叫待執行程式碼作為一個新的整體來被exec函式執行,使得待執行程式碼始終處於gdb的控制中,這樣我們自然能夠除錯該子程式程式碼。

還是上面那個例子,RIM_Oracle_prog fork出子程式後將緊接著執行RIM_Oracle_Agent.9i的二進位制程式碼檔案。我們將該檔案重新命名為RIM_Oracle_Agent.9i.binary,並新建一個名為RIM_Oracle_Agent.9i的shell指令碼檔案,其內容如下:

[root@tivf09 bin]# mv RIM_Oracle_Agent.9i RIM_Oracle_Agent.9i.binary
[root@tivf09 bin]# cat RIM_Oracle_Agent.9i
#!/bin/sh
gdb RIM_Oracle_Agent.binary

當fork的子程式執行名為RIM_Oracle_Agent.9i的檔案時,gdb會被首先啟動,使得要除錯的程式碼處於gdb控制之下。

新的問題來了。子程式是在gdb的控制下了,但還是不能除錯:如何與gdb互動呢?我們必須以某種方式啟動gdb,以便能在某個視窗/終端與gdb互動。具體來說,可以使用xterm生成這個視窗。

xterm是X window系統下的模擬終端程式。比如我們在Linux桌面環境GNOME中敲入xterm命令:

xterm
xterm

就會跳出一個終端視窗:

終端
終端

如果你是在一臺遠端linux伺服器上除錯,那麼可以使用VNC(Virtual Network Computing) viewer從本地機器連線到伺服器上使用xterm。在此之前,需要在你的本地機器上安裝VNC viewer,在伺服器上安裝並啟動VNC server。大多數linux發行版都預裝了vnc-server軟體包,所以我們可以直接執行vncserver命令。注意,第一次執行vncserver時會提示輸入密碼,用作VNC viewer從客戶端連線時的密碼。可以在VNC server機器上使用vncpasswd命令修改密碼。

[root@tivf09 root]# vncserver 

New 'tivf09:1 (root)' desktop is tivf09:1

Starting applications specified in /root/.vnc/xstartup
Log file is /root/.vnc/tivf09:1.log

[root@tivf09 root]#
[root@tivf09 root]# ps -ef|grep -i vnc
root     19609     1  0 Jun05 ?        00:08:46 Xvnc :1 -desktop tivf09:1 (root) 
  -httpd /usr/share/vnc/classes -auth /root/.Xauthority -geometry 1024x768 
  -depth 16 -rfbwait 30000 -rfbauth /root/.vnc/passwd -rfbport 5901 -pn
root     19627     1  0 Jun05 ?        00:00:00 vncconfig -iconic
root     12714 10599  0 01:23 pts/0    00:00:00 grep -i vnc
[root@tivf09 root]#

Vncserver是一個Perl指令碼,用來啟動Xvnc(X VNC server)。X client應用,比如xterm,VNC viewer都是和它通訊的。如上所示,我們可以使用的DISPLAY值為tivf09:1。現在就可以從本地機器使用VNC viewer連線過去:

VNC viewer:輸入伺服器
VNC viewer:輸入伺服器

輸入密碼:

VNC viewer:輸入密碼
VNC viewer:輸入密碼

登入成功,介面和伺服器本地桌面上一樣:

VNC viewer
VNC viewer

下面我們來修改RIM_Oracle_Agent.9i指令碼,使它看起來像下面這樣:

#!/bin/sh
export DISPLAY=tivf09:1.0; xterm -e gdb RIM_Oracle_Agent.binary

如果你的程式在exec的時候還傳入了引數,可以改成:

#!/bin/sh
export DISPLAY=tivf09:1.0; xterm -e gdb --args RIM_Oracle_Agent.binary $@

最後加上執行許可權

[root@tivf09 bin]# chmod 755 RIM_Oracle_Agent.9i

現在就可以除錯了。執行啟動子程式的程式:

[root@tivf09 root]# wrimtest -l 9i_linux
Resource Type  : RIM
Resource Label : 9i_linux
Host Name      : tivf09
User Name      : mdstatus
Vendor         : Oracle
Database       : rim
Database Home  : /data/oracle9i/920
Server ID      : rim
Instance Home  : 
Instance Name  : 
Opening Regular Session...

程式停住了。從VNC viewer中可以看到,一個新的gdb xterm視窗在伺服器端開啟了

gdb xterm 視窗
gdb xterm視窗
[root@tivf09 root]# ps -ef|grep gdb
nobody   24312 24311  0 04:30 ?        00:00:00 xterm -e gdb RIM_Oracle_Agent.binary
nobody   24314 24312  0 04:30 pts/2    00:00:00 gdb RIM_Oracle_Agent.binary
root     24326 10599  0 04:30 pts/0    00:00:00 grep gdb

執行的正是要除錯的程式。設定好斷點,開始除錯吧!

注意,下面的錯誤一般是許可權的問題,使用 xhost 命令來修改許可權:

xterm 錯誤
xterm 錯誤
[root@tivf09 bin]# export DISPLAY=tivf09:1.0
[root@tivf09 bin]# xhost +
access control disabled, clients can connect from any host

xhost + 禁止了訪問控制,從任何機器都可以連線過來。考慮到安全問題,你也可以使用xhost + <你的機器名>。

小結

上述三種方法各有特點和優劣,因此適應於不同的場合和環境:

  • follow-fork-mode方法:方便易用,對系統核心和GDB版本有限制,適合於較為簡單的多程式系統
  • attach子程式方法:靈活強大,但需要新增額外程式碼,適合於各種複雜情況,特別是守護程式
  • GDB wrapper方法:專用於fork+exec模式,不用新增額外程式碼,但需要X環境支援(xterm/VNC)。

參考資料

相關文章