顯示具有 strace 標籤的文章。 顯示所有文章
顯示具有 strace 標籤的文章。 顯示所有文章

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 的套件時, 有機會自行找出一線生機。

2012年6月11日 星期一

注意 ptrace 的權限設定

gdb、strace 都是用 ptrace 來監控另一個 process 的情況, 經 wens 提醒關於權限設定的事。

基於安全考量, Ubuntu 10.10 開始, 即使 effective uid 相同, 預設只能對後代 process 用 ptrace。其它情況得用 root 權限執行才可以。在自己的開發機上, 為了方便起見, 可以取消這個限制:

  • 更改目前設定: echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope sysctl
  • 開機後自動生效: 改 /etc/sysctl.d/10-ptrace.conf

2012年1月29日 星期日

ld, ld.so 和 ldconfig 的行為

TLPI ch41 相當值得一看, 從開發者使用 library 的角度說明 library 的生成、靜態連結、動態連結 (載入) 的行為, 內容不多不少, 正好就是我想知道的, 省了看 linker、loader 的時間。

shared library 的名詞介紹

  • soname: 記錄在 shared library header 裡的名稱, 格式為 libX.so.MAJOR。要有同名檔案, 供之後程式載入 shared library 時使用
  • real name: shared library 的檔名, 格式為 libX.so.MAJOR.MINOR.NUMBER
  • linker name: 對 library X 來說, 就是 libX.so, 一般會是 symbolic link 指向最新的 major shared library

以 libjpeg 為例, 對應如下:

libjpeg.so -> libjpeg.so.62.0.0     # linker name
libjpeg.so.62 -> libjpeg.so.62.0.0  # soname
libjpeg.so.62.0.0                   # real name

這是我在 Ubuntu 裝好 package 後的樣子, 照理說 libjpeg.so 指向 libjpeg.so.62 應該會更彈性。

讀出 soname:

$ readelf -d libjpeg.so | grep SONAME
 0x000000000000000e (SONAME)             Library soname: [libjpeg.so.62]

static 和 dynamic linker

  • ld (ld.bfd) 是 static linker。Google 開發的 gold 是取代 ld.bfd 的 static linker。用 gcc 連結 shared library 或 executable 時就是呼叫 ld, 並將需要的參數傳給它。不論連結的是 static library 或 shared library, 都是 static linking。
  • ld 在連結 shared library 或 executable 時, 會將需要的 shared library 的 soname 寫入結果檔裡。注意, 只有 soname 而已, 沒有完整路徑。
  • ld-VERSION.so 是 dynamic (runtime) linker, 執行程式時, 由 runtime linker 載入 executable 開始。若 OS 用的 glibc 版本為 2.13, 就叫 ld-2.13.so。用 ldd 看所有執行檔, 都會找到它 (某個 symbolic link 連到 ld-2.13.so)。

以連結 libm.so 為例, 執行 gcc -lm prog.c -o prog 中間的部份行為如下:

  1. gcc 透過 -lm 的指示告知 ld 要連結 libm.so
  2. ld 會找到某處的 libm.so 指向 /lib/x86_64-linux-gnu/libm.so.6, 確認要用到的 symbol 都有, 沒有 link error
  3. ld 從 libm.so.6 的 header 讀出 soname "libm.so.6", 寫入 "libm.so.6" 到 prog 的 header。

執行 prog 時, ld-2.13.so 會從 prog 讀出 "libm.so.6", 再到預設的路徑上找檔名 "libm.so.6"。注意, static linking 時需要 libm.so, 但之後執行 prog 時用不到它, 因為記錄的 soname 為 "libm.so.6"。

關於 static linking 找檔名的順序, 可用 strace 觀察:

$ strace -e open,execve -f -o gcc.trace gcc -lm prog.c -o prog

在 gcc.trace 裡可看出一二。

ps.

  • 使用 execve 的目的是知道 child process 是那一個程式, 目標是看 ld 開敋的檔案
  • 可由 man 2 exec<TAB> 得知 system call 使用的 exec 函式為 execve。

ldconfig

執行 ldconfig 後, 它做的事如下:

  1. 讀出 /lib, /usr/lib, /etc/ld.so.conf 內的路徑之下的 shared library (ldconfig 會略過 symbolic link), 將結果寫入 /etc/ld.so.cache。之後 ld-2.13.so 會用 ld.so.cache 的記錄來找 shared library。
  2. ldconfig 會自動產生 symbolic link "libX.so.MAJOR" 指向最新版本的 shared library。例如 /lib/libfoo.so.2.0.1 的 soname 是 libbar.so.2, 執行 ldconfig 後, 它會產生 /lib/libbar.so.2 指向 /lib/libfoo.so.2.0.1。

之前困擾我許久的事就是第二步, 而 man ldconfig 裡沒提到這點。

結論是別隨便手動更新 soname 的檔案, 執行 ldconfig 後可能會出問題。裝套件後, 系統工具會自動跑 ldconfig 更新目錄, 可能會蓋掉自己手動更新的同檔名檔案。另外 ldconfig 沒有管 linker name, 若是自己編的 shared library, 要自己產生。

其它

若想連到舊的 major 版本 shared library, 得在 gcc 參數指定舊版檔名。還有可用 rpath 的參數寫入搜尋 shared library 的路徑到 shared library 或 executable 裡。關於這些細節, 還有 static linker 以及 dynamic linker 尋找 shared library 的完整順序, TLPI ch41 講得相當清楚。ch42 描述 dlopen, 之後再來翻翻。

2015-09-05 更新

加上 Scott 補充關於 ld-VERSION.so 和相關工具的說明:

fcamel: ld-2.21.so, ldd, ldconfig 屬於 glibc 是什麼意思啊? 我以為 glibc 是 standard C + POSIX lib

Scott: Linux 下動態連結器是 C Library 的一部分,例如 Android 不用 GLIBC 所以其 Dynamic Linker (/system/bin/linker64) 是 Google 自己寫出來、自己維護的 https://github.com/android/platform_bionic/blob/master/linker/Android.mk

這個知識可用來解釋 Android 動態連結器有許多「特異功能」,例如有註冊 SIGSEGV handler,所以 native code 記憶體存取錯誤會有 backtrace 等。 https://github.com/android/platform_bionic/blob/master/linker/debugger.cpp

至於動態連結器為何是 C Library 的一部分,從「實作 C library 的人」的角度看:凡不是 Kernel,但又是系統「執行期間」需要的低階 Library,就擺在 C Library 內了。

ldd 與 ldconfig 是 GLIBC 作者寫出 ld-2.21.so 動態連結器時「順便」寫的工具,是隨 GLIBC 一起安裝的。系統不用 GLIBC 的話,可能沒有 ldd 與 ldconfig。

2012年1月23日 星期一

用 strace 找出 Ubuntu 如何提示未安裝的指令

在 Ubuntu 下執行指令後, 若沒有安裝指令的話, 會出現提示:

$ apt-rdepends
The program 'apt-rdepends' is currently not installed.  You can install it by typing:
sudo apt-get install apt-rdepends

但若直接用 bash 執行, 卻不會有這效果:

$ bash -c apt-rdepends
bash: apt-rdepends: command not found

以前覺得很好奇, Ubuntu 怎麼做到這件事的, 知道 strace 以後, 追這類原因簡單許多, 只要有輸入和輸出訊息, 就可夾擊出一些線索。

  • 在 terminal 1 輸入 echo $$ 取得該 bash 的 PID
  • 在 terminal 2 輸入 sudo strace -obash.trace -f -s512 -p PID
    • -obash.trace 表示將輸出存到 bash.trace, 訊息很多, 通常都會寫到檔案裡
    • -s512 表示輸出訊息最多到 512, 預設行寬有點短, 之後不方便找輸出的訊息
    • -f 表示一起追蹤 child process, 這點很重要, 沒加 -f 就追不到 bash 使用的其它子程序, 而關鍵就在 bash 叫起的子程序
    • -p PID 表示追踪其它 process, 照理說同一個使用者不用 root 權限應該也能看, 不知為啥不通
  • 在 terminal 1 輸入 apt-rdepends, 因為 strace 有用 "-f", 速度會慢很多。等待指令完成
  • 在 terminal 2 按 Ctrl+C 中斷 strace, 觀察 bash.trace

搜一下 "apt-rdepends" 會看到 bash 在嘗試各種 path 後都找不到檔案:

16520 stat("/home/fcamel/bin/apt-rdepends", 0x...) = -1 ENOENT...
16520 stat("/usr/local/sbin/apt-rdepends", 0x...) = -1 ENOENT...
16520 stat("/usr/local/bin/apt-rdepends", 0x...) = -1 ENOENT...
16520 stat("/usr/sbin/apt-rdepends", 0x...) = -1 ENOENT...
16520 stat("/usr/bin/apt-rdepends", 0x...) = -1 ENOENT...
16520 stat("/sbin/apt-rdepends", 0x...) = -1 ENOENT...
16520 stat("/bin/apt-rdepends", 0x...) = -1 ENOENT...
16520 stat("/usr/games/apt-rdepends", 0x...) = -1 ENOENT...
16520 stat("/home/fcamel/bin/apt-rdepends", 0x...) = -1 ENOENT...
16520 stat("/home/fcamel/bin/apt-rdepends", 0x...) = -1 ENOENT...

之後在別的 child process 呼叫外部程式執行 /usr/lib/command-not-found:

16877 execve("/usr/bin/python", ["/usr/bin/python", "/usr/lib/command-not-found", "--", "apt-rdepends"], [/* 38 vars */]) = 0

若想研究怎麼找出該裝的套件, 可以研究 "/usr/lib/command-not-found"。若想研究 bash 如何判斷在有 terminal 的情況下要多找安裝的指令, 可以自己編含 debug symbol 的 bash, 再用 gdb 找出相關位置, 再讀附近的原始碼。這樣一來, 至少知道需要追的時候該如何進行, 剩下就是增加經驗提昇追程式的速度了。

2012-01-23 更新

wens 提醒, 原來是用 bash 的 hook 做的, 見 /etc/bash.bashrc 了解設定, man bash 在 COMMAND EXECUTION 那節有說明。

2012年1月5日 星期四

熟悉系統工具好處多多

記一下以前很困擾, 現在秒殺的小事。

更新這篇的時候, 忘了函式庫用的 man page 裝在那個 package。以前就會想辦法 google, 運氣好一下會找到, 運氣不好會多找一會兒。

這回我想到新作法:

$ strace -e open man 3 printf > /dev/null
# 發現是讀 /usr/share/man/man3/printf.3.gz

$ dpkg --search /usr/share/man/man3/printf.3.gz
# 找到套件名稱 manpages-dev

$ aptitude show manpages-dev
# 確認描述符合, 收工

2011年12月25日 星期日

善用 strace、debugger 從執行期間找出問題根源

最近被迫在短時間內學會 strace、gdb 這些之前一直用不到的重兵器, 都還不熟練就是了。剛好使用 hgsubversion 時有些困擾, 雖說它和 svn 整合得很好, 無縫接好 pull / push, 但它不會顯示 mercurial 對應到的 svn 版本, 平時看其它和 svn 整合的工具 (如 issue tracking) 會很困擾, 用得都是 svn 版號。

剛剛想到可以學 Strace -- The Sysadmin's Microscope 的做法, 用 strace 找出關聯的程式, 再來看怎麼修正它。

我推測 hg 一定有記錄 svn 相關版本的方式, 不然無法和 svn server 同步資料。於是挑個會讀到 svn 資料的指令來試:

strace -o trace.log -s 512 -e read,open hg svn info

用顯示的 svn 版號 123 來翻 trace.log, 發現這兩行:

open("/path/to/project/.hg/svn/lastpulled", O_RDONLY) = 3
read(3, "123\n", 4096)                 = 5

於是找到 meta data 存在 .hg/svn/ 下。 到該目錄下找到 .hg/svn/rev_map 這個檔案, 裡面存 hg 和 svn 的版號對應表。至少這樣就有足夠的材料寫個 script 來轉換 hg、svn 的版號。

不過若能直接加到 hg 裡, 應該會更方便也較可攜。要做這點相對容易, 可以到 hgsubversion 原始碼目錄下用 ack 找相關程式。

先用 ack rev_map 找到存 meta data 的物件 revmap, 再用 ack revmap 找到 wrappers.py 是換掉 hg 指令的程式。

再來用 pdb 觀察 revmap 如何被使用。先在 wrappers.py 裡設中斷點, 然後執行 pdb /usr/local/bin/hg parents --svn 找出 meta data 如何被讀出來。於是明白可在函式 parents 的部份塞入幾行顯示 svn 版本:

--- a/hgsubversion/wrappers.py        2011-12-25 00:34:39.170606104 +0800
+++ b/hgsubversion/wrappers.py        2011-12-25 00:33:04.161800527 +0800
@@ -57,6 +57,9 @@
         raise hgutil.Abort('No parent svn revision!')
     displayer = cmdutil.show_changeset(ui, repo, opts, buffered=False)
     displayer.show(ha)
+    # patch svn revision
+    print '\033[1;32msvn revision: %d\033[0m' % hashes[ha.node()][0]
+    print
     return 0

這樣打 hg parents --svn 就會多輸出一行 svn 的版號。雖說顯示在 hg log 會更方便, 不過 wrappers.py 裡沒有 log, 之後有再找時間看看怎麼加。

附帶一提, 剛用 pdb 時還不太習慣, 忘了 python 是執行期載入程式, 不能像 gdb 那樣在執行前就指定檔名指定行數設中斷點。而是要在 script 裡直接塞

import pdb; pdb.set_trace()

2011-12-25 更新

如留言裡的討論, 上述的修正沒有實質幫助, 就當作練 debugger 吧。最後覺得另外寫個 shell script 最省事效果也最好, 寫好的東西放在這裡

2010年2月23日 星期二

用 strace 和 ltrace 找出用到的 system call 和 library call

前面提到 host 沒有 call gethostbyaddr, 面惡心善的 Scott 大概是查覺我下載了原始碼, 卻沒有找出確認它的方法。於是在另一篇留言裡說可以用 strace、ltrace 或 gdb 輕易做到這事 (幸好我還沒開始試 profiler 啊...)。

strace 和 ltrace 顧名思議, 它們會列出執行期間用到的 system / library call。若不確定有興趣的函式是那個, 可以用 man page 編號來判別。比方 man gethostbyaddr 顯示被分在  section 3 下, 所以 gethostbyaddr 是 library call [*1]。

分別拿 host 和對照組 getent 試的結果, 可以看到 getent 有打開 /etc/hosts (從 strace 那看到的), 並呼叫 gethostbyaddr;而 host 卻兩者皆無。解開疑惑實乃人生一大痛快之事, 感謝 Scott 的指點。

附上參考用指令:
$ ltrace getent hosts 127.0.0.1 2>&1 | grep gethostby
gethostbyaddr("\177", 4, 2)                      = 0xb7ed0aa0
$ strace getent hosts 127.0.0.1 2>&1 | grep "/etc/hosts"
open("/etc/hosts", O_RDONLY|O_CLOEXEC)  = 3

$ strace host 127.0.0.1 2>&1 | grep "/etc/hosts"
$ ltrace host 127.0.0.1 2>&1 | grep gethostby

另外我好奇之下試了傳說中人人都會寫的 C 版 「Hello, world! 」, 結果發現 compiler 很聰明地用 puts 而非 printf。

參考程式如下:

#include <stdio.h>

int main(void)
{
    printf("hello, world!\n");
    puts("hello, world!\n");
    printf("hello, world! %d\n", 3);
    return 0;
}

執行結果:
$ ltrace ./f > /dev/null
__libc_start_main(0x80483f4, 1, 0xbf812ab4, 0x8048450, 0x8048440 
puts("hello, world!")                            = 14
puts("hello, world!\n")                          = 15
printf("hello, world! %d\n", 3)                  = 16
+++ exited (status 0) +++

接著我用 ltrace 執行 python (ltrace python -c ''), 結果 ltrace 狂噴訊息卻不會停......, 今日閒暇時間用盡這待那天有緣再來研究吧。

2010-02-25 更新

Scott 在留言裡提到也可以用 LD_PRELOAD  抽換動態載入的函式來達到同樣目的 (確認是否有呼叫 gethostbyaddr)。範例程式如下 (稍微修正 Scott 的範例讓它在我的機器能正常 compile):
$ printf '#include <stdio.h>\n#include <assert.h>\nvoid gethostbyaddr(void) { assert(0); }\n' > t.c
$ gcc -fPIC -shared t.c -o t.so
$ LD_PRELOAD=./t.so getent hosts 127.0.0.1
getent: t.c:3: gethostbyaddr: Assertion `0' failed.
Aborted
$ LD_PRELOAD=./t.so host 127.0.0.1
1.0.0.127.in-addr.arpa domain name pointer localhost.

上面的範例透過 LD_PRELOAD 讓程式改用自訂的 gethostbyaddr。如此一來, 抽換掉懷疑的函式再執行指令, 就知道是否有用到了。LD_PRELOAD 的詳細說明可參考 jserv 翻譯的 《Modifying a Dynamic Library Without Changing the Source Code / 在不更動原始程式碼的前提下,修改動態程式庫》

備註
  1. man man 可看到各 section 的含意, 摘錄如下:
    1 Executable programs or shell commands
    2 System calls (functions provided by the kernel)
    3 Library calls (functions within program libraries)
    4 Special files (usually found in /dev)
    5 File formats and conventions eg /etc/passwd
    6 Games
    7 Miscellaneous (including macro packages and conven-
    tions), e.g. man(7), groff(7)
    8 System administration commands (usually only for root)
    9 Kernel routines [Non standard]

    總共也才九節, 遊戲竟然自成一節......。

2012-01-30 更新

見 tag strace 下的新文章, 了解比較詳細的 strace 使用例子。

在 Fedora 下裝 id-utils

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