2017年3月7日 星期二

除錯技巧:在 Ubuntu 上找出第三方函式庫的程式碼

這篇藉由一個小例子說明如何使用 gdb, locate, apt-file, apt-get 找出問題原因。藉由取得第三方函式庫的程式碼,可以減少瞎猜的時間。

問題

我發現某個程式會不預期的結束。但它不是 crash,沒有 core dump 可看。

用 gdb 找出怎麼結束的

先用 gdb attach 程式,繼續操作。程式結束時 gdb 顯示是呼叫 exit() 結束的。

於是再執行一次,這次用 gdb 在 exit 設中斷點再繼續執行。

取得的 backtrace 如下:

#0  __GI_exit (...) at exit.c:104
#1  0x00007fdd27f60408 in _XDefaultError (...) at ../../src/XlibInt.c:1414
#2  0x00007fdd27f6054b in _XError (...) at ../../src/XlibInt.c:1463
#3  0x00007fdd27f5d5e7 in handle_error ...) at ../../src/xcb_io.c:213
#4  0x00007fdd27f5e687 in _XReply (...) at ../../src/xcb_io.c:699
#5  0x00007fdd27f45346 in XGetWindowProperty (...) at ../../src/GetProp.c:69
#6  0x00007fdd2825db30 in XmuClientWindow () from /usr/lib/x86_64-linux-gnu/libXmu.so.6
...

看 backtrace 沒什麼頭緒,都是第三方函式庫的程式。照 XmuClientWindow() 的說明,它可能會失敗,但它不該直接呼叫 exit()。先找 XmuClientWindow 的原始碼,看看有什麼線索。

用 Ubuntu 的 package 系統找出原始碼

首先用 apt-file 找出 libXmu.so.6 在那個套件 (第一次執行需先跑 apt-file update 更新索引):

$ apt-file search /usr/lib/x86_64-linux-gnu/libXmu.so.6
libxmu6: /usr/lib/x86_64-linux-gnu/libXmu.so.6
libxmu6: /usr/lib/x86_64-linux-gnu/libXmu.so.6.2.0
libxmu6-dbg: /usr/lib/debug/usr/lib/x86_64-linux-gnu/libXmu.so.6.2.0

然後用 apt-get source 取得原始碼:

$ apt-get source libxmu6

在 src/ClientWin.c 裡找到 XmuClientWindow [*1]:

Window
XmuClientWindow(Display *dpy, Window win)
{
    ...
    XGetWindowProperty(dpy, win, WM_STATE, 0, 0, False, AnyPropertyType,
               &type, &format, &nitems, &after, &data);
    ....
}

沒什麼有用資訊,繼續找 XGetWindowProperty 的程式碼。從 backtrace 裡知道它在 ../../src/GetProp.c 裡,但我不知道 GetProp.c 是什麼套件的程式。用 Google 搜尋 GetProp.c 也許會有線索,不過這裡我用另一個作法。

從 header 找出原始碼

src/ClientWin.c 引入的 header 不多,只有這些:

#include <X11/Xlib.h>
#include <X11/Xatom.h>

#include <X11/Xmu/WinUtil.h>

先用 locate 查出完整路徑:

$ locate X11/Xlib.h
/usr/include/X11/Xlib.h

再用 apt-file 找套件名稱:

$ apt-file search /usr/include/X11/Xlib.h
libx11-dev: /usr/include/X11/Xlib.h

取得原始碼:

$ apt-get source libx11-dev

附帶一提,可透過 gcc -E 展開 #include 驗證 XGetWindowProperty 確實出自 X11/Xlib.h:

$ echo "#include " | gcc - -E | grep XGetWindowProperty
extern int XGetWindowProperty(

header 太多的話,直接編譯原始檔比較快:

$ gcc -E src/ClientWin.c > t
得到的資訊如下:
1674 # 1303 "/usr/include/X11/Xlib.h" 3 4
...
3060 extern int XGetWindowProperty(

找到目標後,往回找第一個 header 路徑就是來源了。如果找不到的話,可能是少指定 include header 的來源目錄。可以從 make 產生的結果得知完整的編譯訊息:

$ ./configure
$ make V=1 | grep ClientWin.c
/bin/bash ../libtool  --tag=CC   --mode=compile gcc -std=gnu99 -DHAVE_CONFIG_H \
-I. -I..  -I../include -I../include/X11/Xmu < ... 略 ...> -c -o ClientWin.lo ClientWin.c
...

找出 -I 的部份,和 -E 一樣加在 gcc 的參數上展開 include。

關於如何讓 make 顯示編譯時的參數 (cflags),參見這裡

繼續追踪程式碼

在 src/GetProp.c 裡發現:

 69     if (!_XReply (dpy, (xReply *) &reply, 0, xFalse)) {

這和前面的 backtrace 一致:

#0  __GI_exit (...) at exit.c:104
#1  0x00007fdd27f60408 in _XDefaultError (...) at ../../src/XlibInt.c:1414
#2  0x00007fdd27f6054b in _XError (...) at ../../src/XlibInt.c:1463
#3  0x00007fdd27f5d5e7 in handle_error ...) at ../../src/xcb_io.c:213
#4  0x00007fdd27f5e687 in _XReply (...) at ../../src/xcb_io.c:699
#5  0x00007fdd27f45346 in XGetWindowProperty (...) at ../../src/GetProp.c:69
#6  0x00007fdd2825db30 in XmuClientWindow () from /usr/lib/x86_64-linux-gnu/libXmu.so.6

表示尋找的方向正確。再繼續看 src/xcb_io.c 和 src/XlibInt.c 內相關函式,得知:

// src/XlibInt.c
1463     rtn_val = (*_XErrorFunction)(dpy, (XErrorEvent *)&event); /* upcall */

對照 backtrace 得知執行 _XErrorFunction 後,是呼叫 _XDefaultError。_XDefaultError 程式如下:

1409 int _XDefaultError(
1410     Display *dpy,
1411     XErrorEvent *event)
1412 {
1413     if (_XPrintDefaultError (dpy, event, stderr) == 0) return 0;
1414     exit(1);
1415     /*NOTREACHED*/
1416 }

確認是這裡呼叫 exit()。所以換掉 _XErrorFunction 可以解決問題。

在程式碼裡搜尋 _XErrorFunction 發現可用 XSetErrorHandler() 設值。這樣至少不會造成程式結束了。

備註

1. 可用 grep 尋找,我是用 gj,找起來更有效率。

在 Fedora 下裝 id-utils

Fedora 似乎因為執行檔撞名,而沒有提供 id-utils 的套件 ,但這是使用 gj 的必要套件,只好自己編。從官網抓好 tarball ,解開來編譯 (./configure && make)就是了。 但編譯後會遇到錯誤: ./stdio.h:10...