顯示具有 C++ 標籤的文章。 顯示所有文章
顯示具有 C++ 標籤的文章。 顯示所有文章

2014年7月23日 星期三

找出 dangle pointer 的方法

有時候 C++ 程式會莫明地 crash 在呼叫 method 的時候, 像是 obj->method()。檢查 obj 的值有幾種情況:

  • NULL: 一看就知道有問題
  • 某種規律的數字, 像是 0x23232323, 0xCDCDCDCD: 99% 有問題, 還沒猜錯過
  • 特定的位置, 如 0xDEADBEEF: 看了也知道有問題, 這是 debugger 特意覆寫的值
  • 看起來像個正常的指標, 可能是 dangle pointer

需要注意的是, 透過 dangle pointer 呼叫 method, 不一定會 crash。不過呼叫 virtual method 時一定會 crash, 因為 destructor 會重置 virtual method table 內的值 (相關文章見《C++ 執行後掛在 __cxa_pure_virtual》)。

以下是一個測試例子:

$ cat a.cpp
#include 

class Rect {
public:
    Rect(int x, int y, int w, int h)
        : x(x), y(y), w(w), h(h) {}

    int area() const { return w * h; };
    virtual int area2() const { return w * h; };

private:
    int x, y, w, h;
};

int main(void) {
    int w, h;
    scanf("%d%d", &w, &h);
    Rect *r = new Rect(10, 20, w, h);
    printf("area: %d\n", r->area());
    printf("area2: %d\n", r->area2());
    delete r;
    printf("area: %d\n", r->area());
    printf("area2: %d\n", r->area2());
    return 0;
}
$ ./a
30 40
area: 1200
area2: 1200
area: 1200
Segmentation fault (core dumped)

以前我判斷 dangle pointer 的小撇步是印出 pointer 指向的 object 的某些 member field, 若出現很怪的數字, 十之八九是 dangle pointer。不過有時還是會出包, 像上面的例子就是反例。看來用 virtual method 一定會 crash 這點做判斷, 會更準確。

在不改 code 的前提下, 可以設中斷點在懷疑是 dangle pointer 的 object 的 destructor, 再重執行程式。但若有大量同 class 的 object, 在 destructor log object 的記憶體位置會比較方便。此外, Scott 提到可以用 glibc MALLOC_PERTURB_, 看起來滿不錯的。不過我試了一下覺得不太管用, 沒有深入研究。

若使用的 gcc 比較新 (>=4.8, Ubuntu 14.04 有支援) 或用 llvm 的話, 可以考慮用 AddressSanitizer, 重點是執行效率比以往的替代工具快上不少。

2014-07-23 更新

Thinker 提到對於有 virtual method 的 object, 多數平台可以藉由印出 vptr 的值來確認是否已變成 dangle pointer。下面是用這個概念檢查 dangle pointer 的例子:

$ gdb a
...
(gdb) b 22  # break on "delete r"
Breakpoint 1 at 0x400746: file a.cpp, line 22.
(gdb) r
Starting program: /home/fcamel/dev/tmp/a
30 40
area: 1200
area2: 1200

Breakpoint 1, main () at a.cpp:22
22          delete r;
(gdb) p (void**)r[0]
$1 = (void **) 0x400950
(gdb) n
23          printf("area: %d\n", r->area());
(gdb) p (void**)r[0]
$2 = (void **) 0x0
(gdb) p *r
$3 = {_vptr.Rect = 0x0, x = 10, y = 20, w = 30, h = 40}

雖然 vptr (pointer to Virtual Method Table) 在 object 內的位置依實作而定, 在自己的平台實驗一下, 就可以用這招配合 debugger 除錯了。

2013年12月1日 星期日

g++ 最佳化 switch-case 的一個小例子

最近看到一段程式如下:

bool isOkay(Type type) {
  switch (type) {
    case A:
    case C:
    case D:
    case E:
    case G:
    case I:
    case K:
    case L:
    case Y:
      return false;
    default:
      ;
  }
  return true;
}

原本想說 switch-case 和一串 if 沒差太多, 這樣寫應該比 set 慢。畢竟從演算法的角度來看, 前者是線式比對, 複雜度和 type 特例的數量成正比, 而 set 保證是 O(1)。

後來經 command 提醒, 想說搞不好 compiler 有做最佳化, 還是來看產生的組語好了。結果 compiler 真的有做最佳化, 從組語來看, switch 不見得會比較慢。觀察用的程式見這裡, 用 g++ -O2 產生的組語見這裡

結論是:

  • 寫成「一串 if」, 結果的確是一串 cmp 和 je, 複雜度如原本預期。
  • 但是 switch 讓 compiler 知道比較同一個變數, compiler 有機會做最佳化, 變成少數幾個計算和比較。
  • 使用 set 有額外呼叫函式的成本, 對於簡單計算的例子, 是不可忽略的額外成本。

使用 -O2 後, set 的版本有些複雜, 沒辦法藉由「大概看看+腦補推測」搞懂。實測的結果如下:

$ g++ -O2 benchmark_switch_cases_check.cpp -DSWITCH; time ./a.out;
1700000000

real    0m6.398s
user    0m6.396s
sys     0m0.000s
$ g++ -O2 benchmark_switch_cases_check.cpp -DALOTIF; time ./a.out
1700000000

real    0m8.392s
user    0m8.389s
sys     0m0.000s
$ g++ -O2 benchmark_switch_cases_check.cpp -DIF; time ./a.out
1700000000

real    0m8.402s
user    0m8.389s
sys     0m0.008s
$ g++ -O2 benchmark_switch_cases_check.cpp ; time ./a.out
1700000000

real    0m10.092s
user    0m10.089s
sys     0m0.000s

所花時間是竟然是 switch < if < set。

演算法分析可以去掉明顯的壞主意, 但是 compiler 做了太多事, 單就演算法分析不足以推斷出實務上的好壞, 果然還是需要實測來最佳化效能。

2013年11月18日 星期一

為什麼無法轉型 char ** 為 const char ** ?

參見 Why am I getting an error converting a Foo** --> Foo const**?, C++ FAQ。這裡摘錄文中附的例子。

class Foo {
public:
  void modify();  // make some modification to the this object
};

int main()
{
  const Foo x;
  Foo* p;
  Foo const** q = &p;  // q now points to p; this is (fortunately!) an error
  *q = &x;             // p now points to x
  p->modify();         // Ouch: modifies a const Foo!!
  ...
}

C/C++ 的轉型實在是一個我總是以為我搞清楚了, 但其實我從來沒搞清楚的東西 ...。想必過沒多久又會忘了原因吧。

2013年8月25日 星期日

以安裝 ccache 為例, 說明如何使用系統工具除錯

ccache 是什麼?

ccache 藉由暫存編譯過的 object 檔, 可以減少不必要的重新編譯時間。

用法很簡單:

# 安裝
$ sudo aptitude install ccache
# 啟用 ccache
$ export PATH="/usr/lib/ccache:$PATH"
$ gcc ...  # 這時會用到 /usr/lib/ccache/gcc

以我測試的例子來說, 從頭重新編譯一次是半小時左右, 裝了 ccache 後變成一分半。

ccache 官網所言, ccache 有可能重編譯不必要的程式, 但不會用到不對的暫存檔, 這也是使用這類工具時最重要的保證。

不過這篇的重點不在 ccache 的用法, 而是如何「知道如何安裝和使用它」。以前的文章或多或少有提到下文用的工具, 這篇比較有系統地用一個完整的例子使用它們。

已知

  • ccache 替換 gcc 等編譯工具, 檢查編譯條件和檔案沒變時, 直接取得暫存檔作為編譯結果。
  • 粗略地掃過 ccache manual, 得知是用 symbolic link 替換 gcc 為 ccache。

截至目前為止, 我們已得知 ccache 的核心運作原理, 接下來可以動手試看看。

安裝和使用過程

1. 首先確認是否能透過系統套件安裝 ccache:

$ aptitude search ccache
i   ccache                                                                              - Compiler cache for fast recompilation of C/C++ code
p   ccache:i386                                                                         - Compiler cache for fast recompilation of C/C++ code

2. 再來看套件訊息, 確定沒搞錯套件, 還有看看版號是否夠新:

$ aptitude show ccache
Package: ccache
State: installed
Automatically installed: no
Version: 3.1.6-1
...
Description: Compiler cache for fast recompilation of C/C++ code
 ccache is a compiler cache. It speeds up recompilation by caching previous compilations and detecting when the same compilation is being done again. Supported languages are C, C++,
 Objective-C and Objective-C++.
Homepage: http://ccache.samba.org

對照官網版號 3.1.9, 3.1.6-1 還算 OK, 日後有必要再仔細看 release note 決定是否要抓原始檔重編。

3. 開始安裝:

$ sudo aptitude ccache

裝完後沒任何反應 ......, 依先前得知的訊息, 看看 gcc 有沒有被換掉:

$ which gcc
/usr/bin/gcc
$ ls -l /usr/bin/gcc
lrwxrwxrwx 1 root root 7 Nov 14  2012 /usr/bin/gcc -> gcc-4.6*
$ ls -l /usr/bin/gcc-4.6
-rwxr-xr-x 1 root root 353216 Apr 16  2012 /usr/bin/gcc-4.6*

結果 gcc 沒有被換掉 ......。

4. 腦袋卡住了一下下, 接著想到來確認 ccache 裝了什麼, 也許有些線索:

$ dpkg -L ccache
/.
/usr
/usr/share
...
/usr/share/man/man8/update-ccache-symlinks.8.gz
...
/usr/sbin/update-ccache-symlinks
/usr/bin
/usr/bin/ccache
/usr/lib
/usr/lib/ccache

裡面有個可疑的檔案 /usr/sbin/update-ccache-symlinks, 馬上來看看檔案內容。

結果是個 perl script, 從開頭得知重要的資訊:

  6 my $ccache_dir = "/usr/lib/ccache";
  7 my $old_gcc_dir = "/usr/lib/gcc";
  8 my $new_gcc_dir = "/usr/lib/x86_64-linux-gnu/gcc";
  9 my %old_symlinks; # Current compiler names in /usr/lib/ccache
 10 my %new_symlinks; # Compiler names that should be in /usr/lib/ccache
 11 my @standard_names = qw(cc c++);

對照後面的程式碼和這些變數名稱, /usr/lib/ccache 是關鍵的角色:

$ ls -l /usr/lib/ccache/
total 0
...
lrwxrwxrwx 1 root root 16 Aug 24 21:19 g++ -> ../../bin/ccache*
lrwxrwxrwx 1 root root 16 Aug 24 21:19 g++-4.6 -> ../../bin/ccache*
lrwxrwxrwx 1 root root 16 Aug 24 21:19 gcc -> ../../bin/ccache*
lrwxrwxrwx 1 root root 16 Aug 24 21:19 gcc-4.6 -> ../../bin/ccache*
...

Bingo!! 將 /usr/lib/ccache 加到 PATH 前面, 應該會有效果。但是, 要如何快速驗證 ccache 確實有發揮效果呢? 我可不想重編一堆檔案, 過個半小時才發覺「好像沒效」。

依前面的假設, ccache 編譯時會從暫存區取出編譯過的檔案, 所以第一次編譯時, ccache 應該也會寫入編譯過的檔案到暫存區。幸好開檔的 system call 只有一個 open(2), 這時就該 strace 上場了:

$ cd ~/tmp/
$ export  PATH="/usr/lib/ccache:$PATH"
$ strace -eopen -f g++ t.cpp -c > log 2>&1

翻一下 log 會看到

 10 [pid 18133] open("/home/fcamel/.ccache/tmp/t.tmp.fc-vm.18132.ii", O_WRONLY|O_CREAT|O_EXCL|O_TRUNC, 0666) = 4
 11 [pid 18133] open("/home/fcamel/.ccache/tmp/tmp.cpp_stderr.fc-vm.18132", O_WRONLY|O_CREAT|O_EXCL|O_TRUNC, 0666) = 4

由此順便找到暫存目錄在 $HOME/.ccache 下。日後有需要也可以到這裡清清暫存檔。

有興趣的話可以用 strace 追更多 system call, 比對 open 開啟的 file descriptor (fd) 和後續 read、write 操作的 fd, 可以證實更多推測。到此就大功告成了!

小結

相信以 ccache 這樣成熟的套件, 仔細翻翻官網文件、man page 或 google 一下, 就可以找到答案。不過了解如何使用這些系統工具以後, 日後遇到文件不詳細或不易 google 的套件時, 有機會自行找出一線生機。

2013年8月10日 星期六

用 macro 組合變動式參數, 強化 log 函式

程式除錯時常需要加些 log 印出變數內容, 通常會希望附帶印出所在的函式。每次要自己重打一次函式名稱太麻煩了, 可以利用 C 的 macro 避免重覆的動作。

先來看個範例程式:

以及輸出結果:

$ g++ a.cpp -o a; ./a
result: 3
$ g++ -DDEBUG_PRINT a.cpp -o a; ./a
int Calculator::add(int, int) this=0x7ffff03c1d7f a = 2, b = 1
result: 3

有 #define DEBUG_PRINT 的才會真的執行 DebugPrintf, 除了兼顧原本 printf 有的變動式參數外, 順便自動補上函式名稱和目前物件的指標; 反之則完全不會執行到, 不會增加額外負擔。

有需要的話, 可以稍微修改 DebugPrintf, 如同 《trace C/C++ function call 的方法》 記錄檔名和行數。

關於巨集這行:

#define DebugPrintf(format, args...) fprintf(stderr, "%s this=%p " format, __PRETTY_FUNCTION__, this, ##args)

用到的語法包含:

  • C 的 "abc" "def" 會組成 "abcdef"
  • args... 比 __VA_ARGS__ 易於閱讀
  • 只有一個參數的時候, __VA_ARGS__ 會自動移掉前一個 ","

可以參照 Variadic Macros - The C Preprocessor 了解細節。

2013年7月20日 星期六

trace C/C++ function call 的方法

目的

方法一: 使用 gcc 參數 -finstrument-functions

參考資料:

優點:

  • 自動含蓋所有函式。

缺點:

  • 可能增加太多執行負擔, 雖然可以配合 -finstrument-functions-exclude-file-list 去除不想觀察的函式, 對於大專案來說有些不方便。

方法二: 自己寫 logger

使用 C/C++ 或 gcc 提供的語法, 自己寫 logger 沒有想像中的麻煩:

  • 可用 gcc 的 __PRETTY_FUNCTION__ 或標準的 __func__ 取得函式名稱
  • __LINE__ 取得行數
  • __FILE__ 取得檔名

於是可定個巨集:

#define LOG_IT() MyFunctionLogger logger(__FILE__, __LINE__, __PRETTY_FUNCTION__)

然後用 vim 的全域取代:

:% s/^{/&\r  LOG_IT();\r\r/c

這會逐個詢問是否取代下列字串:

void foo()
{
  // blah blah

void foo()
{
  LOG_IT();

  // blah blah

Btw, 由此可知, 函式的左大括號放在行首比放在行尾來得方便。

MyFunctionLogger 只是個 wrapper, 目的是利用 RAII 記錄進入和離開函式, 這樣即使函式中間有任何 return, 都不會漏記離開函式:

class MyFunctionLogger
{
public:
  MyFunctionLogger(const char *file, int line, const char *func)
      : m_file(file), m_line(line), m_func(func)
  {
    MyLogger::Instance()->EnterFunctionCall(m_file, m_line, m_func);
  }

  ~MyFunctionLogger()
  {
    MyLogger::Instance()->LeaveFunctionCall(m_file, m_line, m_func);
  }

private:
  const char *m_file;
  int m_line;
  const char *m_func;
};

MyLogger 可以做的事很多, 比方說:

  • 用 file 做為 tag 分類輸出。
  • 在 EnterFunctionCall() 和 LeaveFunctionCall() 記錄層級, 輸出 function call 時, 視 function call 的深度做對應的縮排。
  • 在 EnterFunctionCall() 和 LeaveFunctionCall() 記錄目前時間, 可在 LeaveFunctionCall() 時計算執行時間, 協助 profiling。

若 MyLogger 會被多個 thread 使用, 別忘了將 MyLogger 寫成 thread-safe, 不過以觀察或除錯的角度來說, 偷懶不作應該也OK吧。

若目標模組是純 C 的程式, 可用 gcc 的 cleanup attribute 做出 RAII 的效果, 達到和 MyFunctionLogger 一樣的效果。

2013年5月30日 星期四

使用 vim script 自動開啟 C/C++ 程式的標頭檔或程式碼

先附上完整 script:

" Open .h if it's a cpp file, and vice versa.
function! OpenComplementFile()
  let f = expand('%')   " (1)
  let suffix = matchstr(f, '\.\a\+$')
  let pattern = suffix . "$"
  if suffix == '.h'
    let suffixes = ['.cpp', '.cc', '.mm', '.m', '.h']
    for suf in suffixes
      let target = substitute(f, pattern, suf, '')   " (2)
      if filereadable(target)
        break
      endif
    endfor
  elseif suffix == '.cpp' || suffix == '.cc' || suffix == '.m' || suffix == '.mm'
    let target = substitute(f, pattern, '.h', '')
    if !filereadable(target)
      let tmp = target
      let target = substitute(tmp, '\v(.+)\..+', 'public/\1.h', '')  " (3)
      if !filereadable(target)
        let target = substitute(tmp, '\v(.+)/(.+)\.(.+)', '\1/public/\2.h', '')
      endif
    endif
  else
    let target = ''
  endif

  if filereadable(target)
    exec 'vsplit ' target
  else
    echo "Complement file not found"
  endif
endfunction
nnoremap <silent> <F4> :call OpenComplementFile()<CR>

在 vimrc 裡放入上述程式, 然後在 normal mode 裡按 F4 會有如下的反應:

  • 若是在 X.h 裡面, 會試著在垂直分開的視窗裡開啟 X.cpp, 若沒有 X.cpp, 則會依序改試 X.cc, X.mm, X.m。
  • 若是在 X.cpp (或 .cc, .mm, .m) 裡, 會先試開 X.h, 若沒有 X.h, 再試 public/X.h。有需要的話, 可以配合自己的習慣往更深的目錄找。

1.

  • 詳見 :h expand。
  • % 表示目前的檔名。
  • 簡單的檔名轉換可以用 expand 就做完, 比方說如下的例子:
"open A_test.X if current file name is A.X
"open A.X if current file name is A_test.X
function OpenCorrespondingFile()
    let d = split(expand("%"), '_test')
    if len(d) == 1
        let name = expand("%:r") . "_test." . expand("%:e")
    else
        let name = d[0] . d[1]
    endif
    exec 'vsplit ' name
endfunction

2.

  • 寫上述 script 的時候, 花最多時間查詢如何使用函式 substitue()。參考 vim 內部的說明 :h substitute() 即可。
  • 注意要加 () 表示查詢函式的文件, 而不是查 Ex command 的 :substitute

3.

  • :h magic 說明 vim 對待特殊字元的方法。在 pattern 前面加上 "\v" 會和其它 script 的用法一致, 比較方便。

2013年3月21日 星期四

和 C 前置處理器相關的小技巧

在 gdb 內查詢 macro

需要在編譯時加上 -g3, info macro 等指令才會有用。

參考資料: http://sourceware.org/gdb/onlinedocs/gdb/Macros.html

列出 include 的內容

$ gcc -E a.c

編譯錯誤指出缺少某個 symbol 時, 可用這招確定到底有沒有引入目標的宣告。有時 #ifdef 太多, 看原始碼不易確認這件事。

列出 define 的內容

比方說若想知道 errno 111 代表什麼意思, 可這麼做:
$ echo "#include <errno.h>" | gcc -dM -E - | grep " 111$"
#define ECONNREFUSED 111
再來 man errno 查詢 ECONNREFUSED 即可得到答案。
另外, 可用以下的方法得知系統預先 define 的東西
gcc -dM -E - < /dev/null
參考資料: http://stackoverflow.com/questions/2224334/gcc-dump-preprocessor-defines

2012年11月22日 星期四

善用 shared library visibility 減少程式之間的衝突

關於 static library 和 shared library 的基本知識:

static library 沒有特別的, 但是 shared library 有不少神奇的功能可用。

想像一個情境, 有四個獨立的專案 A, B, X, Y 四者, 其中 A 用到 B, X 用到 Y。若 A 也想用到 X, 但 B 和 Y 有重覆的 symbol, 得在編譯和連結時動點手腳, 才能過關。

上面情境的示意圖:

A -> B
|
v
X -> Y

假設編譯 B, X, Y 時產生 static library, 在產生執行檔 A 時, ld 會抱怨某些 symbol 衝突 (在 B 和 Y 裡面)。

static library 只是一堆 object file 的集合體, 就像 tar 包覆一堆檔案一般, 只是 static library 裝的是 binary 並且可以加入 index 檔。也因此 static library 通常比 shared library 大包, 因為 static library 不管 (也無法知道) 每個 symbol 最後是否會被用到, 總之就先留著它。

shared library 多了不少功能可用, 其中一個實用的功能是 visibility。可透過編譯 (非連結) 時下參數隱藏不需要的 symbol。以上面的例子來說, 若 A 只用到 X 且不會用到 Y, 那麼, 改用 shared library 的 visibility, 可以避開 B 和 Y 衝突的 symbol, 作法如下所述。

為簡化描述, 以下用單一檔案表示一個專案:

1. 產生 libXY.so

$ g++ -c Y.cpp -fPIC -fvisibility=hidden
$ g++ -c X.cpp -fPIC
$ g++ -shared -o libXY.so X.o Y.o

注意: 編譯 Y.cpp 時多了 -fvisibility=hidden, 表示除非程式內有用 g++ 特有的語法指定 visibility (__attribute__((__visibility__("default"))), 不然預設行為變成:

  • 無論原本 C++ 語意的 scope 為何, shared library 外看不見此 object file 的 symbol
  • shared library 內仍採用 C++ 的語意

對部份平台來說, -fPIC 是編譯 shared library 的必要條件, 先當作編譯 shared library 時需要在編譯時加上 -fPIC 吧。

2. 產生 libB.a

$ g++ -c B.cpp
$ ar rvs libB.a B.o

shared 或 static library 在此無關緊要

3. 產生執行檔 A

$ g++ -c A.cpp
$ g++ A.o libB.a libXY.so -o A

備註:

2011-11-24 更新

Scott 補充有關 PIC 的說明, 我就直接備忘在這裡啦。

相關 key word: "text relocation" [1] "text" 在此指機械碼,跟可執行檔中擺機械碼的 section 稱為 .text section 一樣。"relocation" 指『有參考到某 symbol,故連結器需一併修改的地方』。有加 *-fPIC*,compiler 產出的機械碼就不會有 text relocation,dynamic linker 才願意在執行時載入。

在幾乎全部平台上, -fPIC 或 -fpic 都是必要的。10 年前 i386 Linux 上不加也可以,但現在連 i386 預設也會被 security policy 拒絕。*elfutils* 中有個 eu-findtexrel 可找出 shard library 中沒加 "-fPIC" 編譯的 .o。

運作原理上,若沒加 -fPIC ,compiler 產出的機械碼中每個用到全域變數與函式的地方都會嵌有該 symbol 的位址。即 shared library .text section 中會散佈很多 memory reference。但那些位址在執行期間需被 dynamic linker 修改。加了 -fPIC ,產生出來的機械碼 reference 全域變數與函式的方法就不一樣了 [2]。

在安全性上不希望一頁記憶體既可寫又可執行;從多 process 共用 shared library .text section 的 physical memory pages 角度來看,希望 .text section 保持不變且 read only。 所以後來 dynamic linker 遇到有 text relocation 的 shared library 就拒絕載入了。

[1]: http://www.akkadia.org/drepper/textrelocs.html [2]: http://www.iecc.com/linker/linker10.html

2012年11月15日 星期四

編譯或連結錯誤的檢錯流程 (初版)

開發環境是 Ubuntu。先寫篇草稿, 日後慢慢補完。

找不到 xxx.h

檢查是否有 xxx.h

$ sudo updatedb && locate xxx.h

若 OS 內沒有的話, 看看要裝什麼套件才有

$ sudo apt-file update && apt-file search xxx.h

確定有檔案後, 檢查使用 libxxx 需要用的編譯參數為何

$ pkg-config --cflags xxx

若不確定 pkg-config 參數的名稱, 使用 apt-file search 查到的 package 名稱 "PKG-X", 查詢 PKG-X 包含的檔案

$ dpkg -L PKG-X | grep pkgconfig

比對編譯時用的參數, 是否有含到正確的 include path (參數 -I), 沒有的話, 可能是 makefile 出錯, 檢查產生 makefile 的設定檔是否正確

若 OS 內沒有 xxx.h, 也沒有任何一個套件含有 xxx.h, 可能目前 OS 太舊, 用 Ubuntu Packages Search 查詢 xxx.h, 確認是否新版的 OS 才有 xxx.h

相關文章

2012年10月23日 星期二

查 C/C++ symbol 定義的方法

記一下目前習慣找 SYMBOL 定義的方法, 只是模糊的片段經驗, 日後再補充完整一些

gj

在命令列打: gj SYMBOL classgj SYMBOL struct 找類別定義

找不到的話, 有可能 SYMBOL 是用 code-gen 或巨集產生的。

  • 檢查一下 mkid 時有沒有漏掉 code-gen 的檔案, 再重建索引: $ mkid -m FILE_LIST ( 格式見 Language map - ID database utilities )
  • 可用 $ g++ -E TARGET.cpp -IINCLUDE_DIR 展開檔案, 再回頭看 SYMBOL 是否是巨集產生的。

另外可用 gj SYMBOL void 找函式定義 (針對無傳回值的情況), 或是 gj SYMBOL 再用 ".h" 過濾只留 header 檔。

Btw, 要找 symbol 被使用的地方的話, 就直接 gj SYMBOL

用 gdb

在 gdb 裡用 ptype VAR 會列出 VAR 的型別資訊

2013-12-08 更新

後來依自己使用習慣在 gj 加了一些功能, 像是用 gj -d1 SYMBOL 找定義或宣告, 用 gj -s LITERAL 找可能的 symbol 名稱, 詳見 gj 在 github 上的說明

2012年1月27日 星期五

C99 好用的語法

之前只有用到 C99 的 loop initial declarations (在 for 的初始化部份宣告變數), 看 Scott 提到才知道有其它好東西, 順便來掃一下 C99 的功能

stdbool.h

定義 bool、true、false, 實際上是將 bool 對應到 C99 定義 _Bool

stdint.h

定義了整數範圍、int16_t、int32_t、int64_t 等型別, 再也不用查 short/int/long 等在 32/64 bit OS 上的大小為多少。

designated initializers

可攜又易讀的初始化 (ref.)

// array
int widths[] = { [0 ... 9] = 1, [10 ... 99] = 2, [100] = 3 };
// struct
struct point { int x, y; };
struct point p = { .y = yvalue, .x = xvalue };

像要用建表實作 isspace() 的話, 這樣寫超清楚的:

bool myisspace(int ch) {
    static bool whitespace[256] = {
        [' '] = true, ['\t'] = true, ['\f'] = true,
        ['\n'] = true, ['\r'] = true
    };

    if (ch < 0 || ch >= 256)
        return false;
    return whitespace[ch];
}

其它

像 snprintf、inline、variable-length array (例如 int array[n]) 也很實用。

2012年1月9日 星期一

追踪 glibc 裡的程式

這篇是一堆試誤心得的中間記錄, 使用的版本是 Ubuntu 11.04。

失敗的作法

  • 在 link 時, 用 -L/usr/lib/debug/lib/x86_64-linux-gnu/ 改變 link 到的 libc.so, 但沒有效果。用 strace -e open 觀察 gcc 做的事, 發現是因為 /usr/lib/debug/lib/x86_64-linux-gnu/ 下沒有 libc.so, 而是 libc-2.13.so。之前沒學清楚 -L 和 -l 的細節, 耍笨。
  • man ld.so 得知可用 LD_LIBRARY_PATH 或 LD_PRELOAD 在執行期換掉 libc.so, 但是也沒有效果。用 LD_PRELOAD 換成 debug 版 libc.so 時, 跑 gdb 會 segmentation fault

成功的作法

前置作業

  • $ sudo aptitude install libc6-dbg # 取得有 debug symbol 的 libc.so
  • $ apt-get source libc6-dev # 取得原始碼目錄 eglibc-2.13

執行

  1. $ gcc myprog.c -g -o myprog
  2. $ gdb myprog
  3. $ directory /path/to/eglibc-2.13/stdio-common/
  4. $ start # 跑到 main 就停下來

然後 gdb 會神奇地去找含 debug symbol 的 libc, 後面就可以用 step 進入 glibc 的函式。不知這個行為寫在那裡, 或許可以從 gdb 原始碼找出來吧。

若有進入但說找不到原始碼, 表示沒有告知 gdb 正確的原始碼位置, 到 eglibc-2.13 下找一找, 再回來用 directory 設位置。

另外在用到 sqrt()、log() 時也是如此, 照一樣的編法 gcc myprog.c -g -lm -o myprog, 然後在 start 後, gdb 會去找 debug 版的 libm.so。不過要記得多執行 directory /path/to/eglibc-2.13/math 載入 math 的原始碼, gdb 才能列出原本的程式。用 ldd 觀察 myprog 也驗證原本的執行檔將 libc 和 libm 連到沒有 debug symbol 的版本。

另外試了直接和 debug 版的 libc.so 或 libm.so 編在一起 (gcc myprog.c -g /usr/lib/debug/lib/x86_64-linux-gnu/libc-2.13.so), 但是一跑就會 segmentation fault。

另外有些函式好像是用組語寫的, 看不懂它們的行為, step into sqrt 沒有效果。

結論

要觀察 glibc 的行為, 要做以下的事:

  • 裝 libc6-dbg, 取得含 debug symbol 的 shared lib
  • 用 apt-get source libc6-dev 取得原始碼。由於 glibc 裡有多個 shared lib, 要先 grep 找看看想觀察的程式放在那個目錄下, 跑 gdb 時再用 directory 載入該目錄, 相對路徑才會對。

2012-01-10 更新: 補充觀察 gdb 找 debug lib 的行為

一樣可以用老招 strace -e open 跑 gdb 看出背後發生的事, 以下是沒有裝 libc6-dbg 跑出的訊息:

$ strace -e open -o gdb.trace gdb myprog
然後執行 tail -f gdb.trace | grep libc 觀察行為。

以下是執行 start 以前的訊息:

open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY) = 3
open("/lib/libcrypto.so.0.9.8", O_RDONLY) = 3
open("/usr/share/locale/en_US.UTF-8/LC_MESSAGES/libc.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale/en_US.utf8/LC_MESSAGES/libc.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale/en_US/LC_MESSAGES/libc.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale/en.UTF-8/LC_MESSAGES/libc.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale/en.utf8/LC_MESSAGES/libc.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale/en/LC_MESSAGES/libc.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale-langpack/en_US.UTF-8/LC_MESSAGES/libc.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale-langpack/en_US.utf8/LC_MESSAGES/libc.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale-langpack/en_US/LC_MESSAGES/libc.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale-langpack/en.UTF-8/LC_MESSAGES/libc.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale-langpack/en.utf8/LC_MESSAGES/libc.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale-langpack/en/LC_MESSAGES/libc.mo", O_RDONLY) = -1 ENOENT (No such file or directory)

以下是執行 start 以後的訊息:

open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY) = 6
open("/lib/x86_64-linux-gnu/libc-2.13.so", O_RDONLY) = 7
open("/lib/x86_64-linux-gnu/.debug/libc-2.13.so", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/lib/debug//lib/x86_64-linux-gnu/libc-2.13.so", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/lib/debug/lib/x86_64-linux-gnu/libc-2.13.so", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/lib/x86_64-linux-gnu/libc-2.13.so-gdb.py", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/lib/debug/lib/x86_64-linux-gnu/libc-2.13.so-gdb.py", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/gdb/auto-load/lib/x86_64-linux-gnu/libc-2.13.so-gdb.py", O_RDONLY) = -1 ENOENT (No such file or directory)
可以看出 gdb 不論如何, 都會試著載入 debug 版的函式庫, 來執行目標程式。找不到的時候, 自然就是用沒有 debug symbol 的函式庫。

2012-01-22 更新

《gdb 如何找到 debug symbol》有進一步說明。

2016-02-16 更新

若 Ubuntu 沒有附 X-dbg 的 package,可以自己編原始碼裝進去,詳見 自行編譯含 debug symbol 的套件 (package)

2012年1月5日 星期四

從 C 呼叫 C++ 函式的過程理解程式編譯、連結的原理

今天做了一個錯誤的決定, 想說在一堆 C 程式裡呼叫另一堆 C++ 程式。邊弄邊學, 最後發現什麼都沒改的情況, 改用 g++ link 原本的 C 程式就會爆炸。想想還是撿要用的一小部份程式出來, 另寫 C 的程式比較單純。不過也藉這機會, 才讓我真的搞懂這之中發生什麼事。

先附上要用的範例, 再來慢慢廢話, 沒耐心的人直接玩範例可能就懂了。

原始碼

/* b.h */
#ifndef _B_H_
#define _B_H_

#ifdef __cplusplus
extern "C" {
#endif
int add(int a, int b);
#ifdef __cplusplus
}
#endif
#endif
/* b.cpp */
#include "b.h"

int add(int a, int b) {
    return a + b;
}
/* a.c */
#include <stdio.h>
#include "b.h"

int main(void) {
    printf("%d\n", add(3, 5));
    return 0;
}

編譯方式

$ gcc -c a.c
$ g++ -c b.cpp
$ g++ a.o b.o -o a
$ ./a                    # 輸出 8

說明

《How to mix C and C++  Updated! , C++ FAQ》將 C 和 C++ 互相呼叫的各式注意事項寫得相當清楚, 不愧為 FAQ 啊! 其中在 C 呼叫 C++ 的函式時, 要注意幾點 C++ 有 name mangling, 也就是不管是 class method 還是 function, 名稱 X 編出來都不會是 X, 而 C 通常沒有 name mangling。不過不管 C 是否有 name mangling, 關鍵是讓 C 能用 C 的方式認得 C++ 的 symbol。

先明白編譯、連結時發生了什麼事, 從這裡出發, 可知要讓 C 呼叫到 C++ 的函式, 要從幾個地方下手:

  1. C、C++ 都需要它們各自的 header, 才能編成 object code。編譯時 symbol 仍不用存在
  2. 用 gcc 編 C 程式; 用 g++ 編 C++ 程式。雖說是很自然的事, 對兩者都不熟的我, 一開始還真沒想到。
  3. 連結 object code 時, C 要能找到 C++ 的函式
  4. 既然要連結 C++ 的 object code, 連結時自然要用 g++

回到前面的範例:

  • 寫 b.h 時用 #ifdef __cplusplus 讓 a.c 和 b.cpp 都能 include b.h, 且看到不同的東西。可用 gcc -Eg++ -E 查看前置處理器展開的結果, 見證騙術的最高境界, 同時滿足兩方的需求, 而且還沒有查覺到對方的存在!!
  • 編譯 b.cpp 時, 透過 b.h 加入 extern "C", 這樣才不會做 name mangling。用 nm b.o 查看, 會發現有 name mangling 時是 _Z3addii, 反之則是 add (ps. 經 Scott 提醒, 可用 c++filt demangle symbol)
  • 由於 C 沒有 extern "C" 的語法, b.h 裡面有這段的話會編譯錯誤, 透過 ifdef 避開。編譯 a.c 沒什麼特別的, 騙它有 add 這個 symbol 即可。

連結時很單純, 先前在編 .o 時確保 b.o 裡可以找到 a.o 認得的名稱 add, 再來記得只有 g++ 同時認得 a.o 和 b.o 的格式, 就沒什麼問題了。

另外, 若想用 C++ 的 class method, 得先包成 C++ function 才行。傳遞物件的話, 參考 FAQ 的說明, 有些注意事項, 待要用到時再來細看。

2012-01-30 更正

gcc 和 g++ 都是呼叫 ld, 只是傳的參數不一樣, 我原本的說法「只有 g++ 同時認得 a.o 和 b.o 的格式」並不正確。還沒有仔細研究 ld, 推測 ld 應該沒有區別 object file 是怎麼被產生的。

2012年1月2日 星期一

debug info 和 optimization

要知道程式有沒有編進 debug info, 可用 objdump --source BINARY | less 觀察。若有看到程式碼的內容, 表示有編進去。另外要注意的是, 程式本身和用到的函式庫編法可能不同, 可能程式有加 -g, 但函式庫沒有。造成看得到一些函式內容, 卻看不到另一些的內容。

另一個要注意的點是, 今天聽 wens 說才知道編程式時可以同時用 -O-g。這樣做的好處是, 執行速度快, 接近 production 運作的情況。我自己試用的感覺是, 有些 bug 在 debug mode 可能不方便重制出來, 還有 debug mode 通常跑比較慢, 所以用 -O -g 還滿方便的。但不幸的是, 兩者合用會有一點副作用, 在 man gcc 裡有說明:

GCC allows you to use -g with -O. The shortcuts taken by optimized code may occasionally produce surprising results: some variables you declared may not exist at all; flow of control may briefly move where you did not expect it; some statements may not be executed because they compute constant results or their values were already at hand; some statements may execute in different places because they were moved out of loops.

Nevertheless it proves possible to debug optimized output. This makes it reasonable to use the optimizer for programs that might have bugs.

當程式是用 -O-g 編出來的時候, 用 objdump --source 會看到程式碼怪怪的, gdb 執行順序也會怪怪的, 執行行為和程式碼有時有一點出入, 還有部份的值會印不出來, gdb 輸出 value optimized out。若要觀察的東西就是那些部份, 就得回頭用 -O0 -g 確保沒有開啟最佳化。

2011年12月30日 星期五

C/C++ 檢查和打開 debug 功能的小技巧

打開 debug flag 有助於了解模組的行為。工程師開發時應該都有幫自己除錯留後路, 所以找到他們開發的方式, 可省下自己瞎試的時間。

以前寫 Python 或 Java 時, 都有近乎標準的 logging module 可用, 所以要找到開啟 logging 機制的方式滿簡單的, 看是用那家的 logging module, 針對它的設定檔設 log file, 或在程式開始時塞段程式啟動 logging 即可。不過 C/C++ 好像沒有那麼一致。

留意程式碼會看到輸出訊息的巨集, 像是 P("What the F**k!!");, 然後在某些 debug 相關 header 檔裡會看到類似這樣的東西:

#ifdef DEBUG
#define P(x) ...
#else
#define P(x) ;
#endif

其中 "..." 的部份是該模組用來輸出除錯訊息的機制, 可能是寫到檔案, 或輸出到 stdout/stderr 等。這樣在沒有 define DEBUG 的情況下, 完全不會產生除錯訊息的程式碼, 減少執行負擔。

要確定程式真的有編進除錯訊息的話, 隨便找個程式內有的除錯訊息, 然後用 strings 檢查:

$ strings PROGRAM | grep "What the F**k!!"

看有沒有結果。有的話大概就 ok (找個少見的字串最穩), 不然就是沒編進去, 檢查看看 #define DEBUG 1 有沒有設好, 或是有沒有在 makefile 的 CFLAGS 設好 -DDEBUG

再來, 也可以用 strace 檢查執行期間到底有沒有執行到:

$ strace -e write PROGRAM | grep "What the F**k!!"

不過 write 寫入的字串不見得會一次剛好符合一整串, 這和檔案 flush 機制有關。找子字串, 或用眼睛注意一下比較穩一些。懶得找方法打開 log 檔的話, 用 strace -e write 將就看也還可以, 不過會拖滿執行速度, 且 strace 有時會影響 process 的狀態, 常期用的話還是打開 log 檔最穩。

2011年12月26日 星期一

打開 core dump 和使用 cgdb 檢查程式掛點原因

前置動作

首先, 用 gcc/g++ 編程式時記得加 -g 以加入除錯資訊。

接著參考《產生 core dump 的方法》設成可以產生 core dump。

檢查 backtrace

$ gdb PROGRAM

然後在 gdb 內執行

core core.PROGRAM.PID.TIMESTAMP

接著就能用 bt N 看最底層 N 個 call stack 為何, 也就是所謂的命案現場啦。然後可用 up, do, l 等指令切換 call stack 和列出週圍程式。

在和 gdb 相處一段時間後, 覺得這樣在 stack 之間移動很方便, 但看週圍的程式太辛苦了, 還是會開另一個視窗用 VIM 看完整一些的程式。

看到 jserv 建議使用 cgdb, 試了發現, 人生...啊不, 是視窗變彩色的了!! 不只有彩色的程式碼, 還外加類 VI 的瀏覽方式, 相當順手。

目前有用到的功能如下:

  • 按 i、ESC 在程式碼視窗和 gdb 命令列之間切換。
  • 程式碼視窗裡可用 VI 的部份指令移動行數; 回到命令列後和 gdb 完全相容, 可按上下鍵選用之前的指令。
  • 在程式碼視窗按空白鍵加減 break point。

官網文件有詳細的說明, 之後來掃一遍, 看有什麼好東西可用。

2011年12月24日 星期六

加速 linking time

頻繁修改一點程式又要執行看結果時, 主要的時間都花在 linking。這時才體會到省下 linking time 也是很重要的事。

這篇提到兩個減少 linking time 的做法

  • 使用 ramdisk
  • gold

我本來就有用 SSD, 改將編譯結果全放到 ramdisk 後, 提昇的效果不怎麼明顯。到是用 gold 後減少了一半的 linking 時間。

不過 gold 也不是那麼完美, 這篇提到一些問題。我自己用的時候發覺滿常遇到 ld 可以編, 但 gold 不行。最後的解套方式是寫個小 script 切換 /usr/bin/ld 連到的程式。不常編的東西就暫時換回 ld 連結個一次就好。

另外 jserv 提到 gcc 有命令列參數可直接指定用那個 linker (ref.)。wens 則查到 Debian/Ubuntu changelog 裡有寫 下可用 -B/usr/lib/compat-ld-B/usr/lib/gold-ld for ld.gold

順便 google 一下看怎麼查 changelog, 似乎是看 /usr/share/doc/binutils/changelog.Debian.gz 或用 aptitude changelog PKG, 不過這兩個作法都只有最近的 changelog, 沒有完整記錄, 像查 binutils 時就沒有找到上面提的參數說明。

2011年12月21日 星期三

列出用到的 shared library

本文整理大家的建議和自己實際操作的心得。新手上路, 有錯還請指正。

static library 就是一包 object file, 沒什麼需要提的, static library 沒有記錄其它資訊。所以, 編 shared library 或 executable 時要自行處理好 static library 的相依性, 在前篇有提到一點資訊。

shared library 有兩種, 一種在 linking 時要指定好 shared library 用到的 undefined symbol 放在那些 shared library 裡, 待執行時再載入到記憶體使用; 另一種用 dlopen() 和 dlsym() 載入 (這兩個函式存在 libdl 內)。

前者比較單純, 可用 ldd 透過靜態分析了解用到那些 shared library, 且各自實際指到的檔案。ldd 本身是一個 shell script, 用到 ld.so 事先定義的一些機制 (LD_TRACE_LOADED_OBJECTS) 來讀取資料, man ld.so 裡有相關的說明。其中 LD_LIBRARY_PATH 和 LD_PRELOAD 相當實用, 無法在連結時解決問題時, 至少還有這招可在載入時處理。

若想分析透過 dlopen() 載入的動態函式庫, 有幾個做法

  • 在程式執行中觀察 /proc/PID/maps, 這個檔案記錄 process 用到的各區段記憶體為何, 可從對應到的檔案看出有載入的 shared library。必要時可配合 gdb 在想觀察的部份停住, 再從外部看 /proc/PID/maps這裡man proc 有相關說明。
  • strace 執行程式, 觀察開啟的檔案: strace -f -e open PROGRAM 2>&1 | grep "\.so"

就我自己小試的心得, 看 /proc/PID/maps 最穩, 且方便看各別 process、thread 載入的函式庫, 也不會拖慢觀察目標的執行程式。不過 strace 不需配合 gdb 停在該停的地方, 就「快篩」的角度來看, 也滿有用的, 加上 -f 後方便追蹤 multi-process、multi-thread, 不過執行速度好像有慢一些, 不太確定。之後再比較看看兩者適合的使用時機。

參考資料

2011-12-22 更新

  • 依 wens 和 Scott 的留言更新上面 strace 的例子。
  • strace -e 的參數看來頗有用的, 之後有需求時再來研究。

2011年12月19日 星期一

解決 undefined symbol / reference

C++ 新手上路, 有錯還請幫忙指正。

基本觀念

相較於 script language 或 Java 來說, C/C++ 有完整的「編譯 -> 連結 -> 執行」三個階段, 各階段都可能發生 undefined symbol。在解決惱人的 undefined symbol 前, 得先明白整個編譯流程:

  1. 編譯 .c / .cpp 為 .o (object file) 時, 需要提供 header 檔 (用到 gcc 參數 -I)。事實上, 在編譯單一檔案時, gcc/g++ 根本不在意真正的 symbol 是否存在, 反正有宣告它就信了, 所以有引對 header 即可。這也是可分散編譯的原因 (如 distcc ), 程式之間在編譯成 .o 檔時, 並沒有相依性。
  2. 用 linker (ld 或 gold) 將 *.o 連結成 dynamic library 或執行檔時, 需要提供要連結的 library (用到 gcc 參數 -L 指定目錄位置, 用 -l 指定要連什麼函式庫)。不同於前一步, 此時 symbol 一定要在。
  3. 執行的時候, 會再動態開啟 shared library 讀出 symbol。換句話說, 前一個步驟只是檢查是否有。檢查通過也連結成 executable 或 shared library 後, 若執行時對應的檔案不見了, 仍會在執行期間找不到 symbol。若位置沒設好, 可能需要用 LIB_LIBRARY_PATH 指定動態函式的位置, 但不建議這麼做, 最好在執行 linker 時就指定好位置。原因見《Why LD_LIBRARY_PATH is bad》

明白這點後, 就看 undefined symbol 發生在那個階段, 若是編 object file 時發生, 就是沒和編譯器說 header 檔在那, 記得用 -I 告訴它。若在 linking 時發生, 就要同時設好 -L 和 -l。不過難就難在要去那找 undefined symbol 的出處。

解決問題的流程

首先是判斷 symbol 是不是自己用到的原始碼裡, 可配合 id-utils 找看看 (我是用 gj, 比較方便一點)。或是看有沒有 man page, 有 man page 的話, 裡面會記錄用到的 header 和該怎麼下連結參數。若在專案裡找不到, 再用 Google 搜看看 symbol, 運氣好可能會找到套件名稱, 運氣不好.....目前還不知怎麼處理較好, 目前是四處亂翻看看。如果是網路上找來的程式碼, 別人已附好正確的 include 了, 這時用 apt-file search HEADER_PATH 就能找到套件名稱 ( 記得先跑 apt-file update 更新資料庫 ), 比方說: apt-file search openssl/rsa.h 會得到 libssl-dev: /usr/include/openssl/rsa.h。

在 Ubuntu 上, 通常需要裝 X-dev 以取得 header 檔。若是已經裝好套件了, 可用 dpkg --searchlocate 或是 dpkg -L PKG_NAME 找出 header 位置。

若編譯過但 linking 時出錯, 要做進一步分析, 先看是那一個程式用到 undefined symbol。不管是自己的程式出錯, 或是用到的函式庫出錯, 都可從對應的原始碼找到編譯時用的 header X.h。

  • 先看有沒有 man page, 有的話, 裡面會寫該下什麼參數連結。像 man sqrt 會看到說要 "Link with -lm" (記得裝 manpages-dev)
  • 若 X.h 是自己的, 就在附近找看看原始碼在那, 有沒有編譯到。
  • 若 X.h 放在系統目錄裡, 可用 apt-file search X.h 找出 library 的可能出處 ( 記得先跑 apt-file update 更新資料庫 )。接著可用下列方式之一找出函式庫的可能位置:
    • dpkg --search SUBSTRING_OF_LIBRARY_NAME
    • dpkg -L PKG_NAME | grep lib
    • locate SUBSTRING_OF_LIBRARY_NAME # 記得先跑 updatedb

若知道函式庫的確切名稱, 且有 pkg-config 的資訊的話, 可用 pkg-config --libs LIBRARY_NAME 直接找出 gcc/g++ linking 時該下的參數 (附帶一提, 用 --cflags 找出編譯時用到的參數, 像是 -I 接的)。不然, 用其它方式找到函式庫位置後, 要依 -L-l 的規則寫下參數。記得 -l 後接的名稱不用加 "lib", 像 libm.so 是用 -lm。

實際寫較具規模的專案時, 可能不會用手刻 makefile, 要視自己用的整合工具, 將找到的資訊加入整合工具中。

其它相關資訊

  • 可配合 nm LIBRARY 查看 symbol, man nm 有各狀態說明, U 表示 undefined。若該函式應該要出自該函式庫, 卻標為 U, 表示該函式庫一開始就沒編好, 要重編該函式庫。反之, 若該函式定義在外部函式庫, 則是連結時出錯。
  • nm 只適用 static library 或未 strip 前的 shared library。strip 後的 shared lib 得用 readelf -Ws 來看, 這個情境下沒 nm 簡單易讀。(2014-10-27 更新: 也可用 nm -D)
  • 函式庫有 U 通常是正常的, 編執行檔或 dynamic library 時才要指定連結的位置。換句話說, 若執行檔 X 用到 static library A, 而 A 用到 library B。則編 X 時, 要加上 -lA 和 -lB 的參數。編 X 的部份要知道它用到的函式庫有那些相依性, 而不是 A 自己會搞定自己的相依性, 這點不太直覺 (ref.)。
  • static library 只是一堆 object file 的集合體。之所以會用 ar 和 ranlib 編 static library, 目的是減少連結的檔案以方便管理。在用 readelf -Ws 讀 static library 時, 會列出各個 object file 的內容。讀 dynamic library 時就沒這樣列了 (ref.)。
  • 在 Linux 下 linking 時要注意函式庫的順序, 摘錄 gcc manpage 關於 -l 的說明:
    It makes a difference where in the command you write this option; the linker searches and processes libraries and object files in the order they are specified. Thus, foo.o -lz bar.o searches library z after file foo.o but before bar.o. If bar.o refers to functions in z, those functions may not be loaded.
  • 當 libm.so 和 libm.a 同時存在時, -lm 會連到 libm.so, 官方說明見 man ld--library=namespec 該段 (ref.)。感謝 cmtsij 的說明。
  • 可用 ldd 找出 dynamic library 實際連到的檔案。

參考資料

在 Fedora 下裝 id-utils

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