Skip to content

Commit

Permalink
update
Browse files Browse the repository at this point in the history
  • Loading branch information
gigigigi committed Jul 16, 2015
1 parent 5e53d0d commit 5b44572
Show file tree
Hide file tree
Showing 8 changed files with 325 additions and 0 deletions.
7 changes: 7 additions & 0 deletions SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,11 @@
* [準備開發裝置驅動程式](linux_device_driver_programming/03.md)
* [開發驅動程式的第一步 ](linux_device_driver_programming/04.md)
* [開發 driver 需要的基礎知識 ](linux_device_driver_programming/05.md)
* [fcamel ](fcamel/README.md)
* [[]ld, ld.so 和 ldconfig 的行為](fcamel/01.md)
* [[]解決 undefined symbol / reference](fcamel/02.md)
* [[] 列出用到的 shared library](fcamel/03.md)
* [[]用 LD_PRELOAD 替換動態連結的函式庫](fcamel/04.md)
* [[] 用 strace 和 ltrace 找出用到的 system call 和 library call](fcamel/05.md)
* [[] 加速 linking time](fcamel/[]__linking_time.md)

71 changes: 71 additions & 0 deletions fcamel/01.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# ld, ld.so 和 ldconfig 的行為


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

**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 為例, 對應如下:<br />
```
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 應該會更彈性。<br />

讀出 soname:<br />
```
$ readelf -d libjpeg.so | grep SONAME
0x000000000000000e (SONAME) Library soname: [libjpeg.so.62]
```

**static 和 dynamic linker**

- ld (ld.bfd) 是 static linker。Google 開發的 gold 是取代 ld.bfs 的 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 中間的部份行為如下:<br />

- gcc 透過 -lm 的指示告知 ld 要連結 libm.so

- ld 會找到某處的 libm.so 指向 /lib/x86_64-linux-gnu/libm.so.6, 確認要用到的 symbol 都有, 沒有 link error

- 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 觀察:<br />

```
$ 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 後, 它做的事如下:<br />

- 讀出 /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。

- 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 裡沒提到這點。<br />

結論是別隨便手動更新 soname 的檔案, 執行 ldconfig 後可能會出問題。<br />
裝套件後, 系統工具會自動跑 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, 之後再來翻翻。
55 changes: 55 additions & 0 deletions fcamel/02.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#解決 undefined symbol / reference


基本觀念

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

- 編譯 .c / .cpp 為 .o (object file) 時, 需要提供 header 檔 (用到 gcc 參數 -I)。事實上, 在編譯單一檔案時, gcc/g++ 根本不在意真正的 symbol 是否存在, 反正有宣告它就信了, 所以有引對 header 即可。這也是可分散編譯的原因 (如 distcc ), 程式之間在編譯成 .o 檔時, 並沒有相依性。

- 用 linker (ld 或 gold) 將 *.o 連結成 dynamic library 或執行檔時, 需要提供要連結的 library (用到 gcc 參數 -L 指定目錄位置, 用 -l 指定要連什麼函式庫)。不同於前一步, 此時 symbol 一定要在。

- 執行的時候, 會再動態開啟 shared library 讀出 symbol。換句話說, 前一個步驟只是檢查是否有。檢查通過也連結成 executable 或 shared library 後, 若執行時對應的檔案不見了, 仍會在執行期間找不到 symbol。若位置沒設好, 可能需要用 LIB_LIBRARY_PATH 指定動態函式的位置, 但不建議這麼做, 最好在執行 linker 時就指定好位置。原因見《Why LD_LIBRARY_PATH is bad》。<br />


明白這點後, 就看 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 --search、locate 或是 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 實際連到的檔案。
參考資料

《Linux Tutorial - Static, Shared Dynamic and Loadable Linux Libraries》
17 changes: 17 additions & 0 deletions fcamel/03.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# [] 列出用到的 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, 不過執行速度好像有慢一些, 不太確定。之後再比較看看兩者適合的使用時機。

參考資料
- 《Linux Tutorial - Static, Shared Dynamic and Loadable Linux Libraries》
68 changes: 68 additions & 0 deletions fcamel/04.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# 用 LD_PRELOAD 替換動態連結的函式庫

換掉動態連結的函式庫有不少用途, 比方說像 Scott 提的「用來驗證是否執行到特定函式」, 或如 jserv 提的「在沒有原始碼的情況下修補執行檔的行為」。最近又看到這個東西, 記一下筆記以免忘了。

來看程式碼和執行效果。
- mylib.c

```
#include <stddef.h>
#include <stdio.h>
int putchar(int c) {
printf("call putchar()\n");
return c;
}
void *memset(void *s, int c, size_t n) {
printf("call memset()\n");
return s;
}
```
- main.c

```
#include <string.h>
#include <stdio.h>
int main(void) {
char s[10];
memset(s, 0, 0);
putchar('X');
putchar('\n');
return 0;
}
```

- 執行結果

```
$ gcc -Wall -fpic -shared -o libmylib.so mylib.c
$ gcc -o main main.c
# 沒用 LD_PRELOAD 的結果
$ ./main
X
# 用 LD_PRELOAD 後的結果, 記得加 "./", 不然會找不到 libmylib.so
$ LD_PRELOAD=./libmylib.so ./main
call putchar()
call putchar()
$ strings main | grep memset
```

可以看出 putchar 有被換成自己編的版本, 但 memset 沒有。用 strings 查看 binary 檔 main 會發覺並沒有呼叫 memset 的跡象。猜測是 compiler 發覺 memset 實際上沒任何作用, 所以沒呼叫。

修改 main.c, 將 memset(s, 0, 0) 換成 memset(s, 0, 1) 再來看看:
```
$ gcc -o main main.c
$ strings main | grep memset
memset
$ LD_PRELOAD=./libmylib.so ./main
call memset()
call putchar()
call putchar()
```

結果顯示有換到 memset, 表示之前是完全沒呼叫 memset, 才會以為沒換到。printf 也是如此, 只輸出字串時, 不會呼叫 printf, 而是用 puts。

- 參考資料: 《Modifying a Dynamic Library Without Changing the Source Code | Linux Journal》

73 changes: 73 additions & 0 deletions fcamel/05.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# 用 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 / 在不更動原始程式碼的前提下,修改動態程式庫》。

**備註**
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]

總共也才九節, 遊戲竟然自成一節......。
1 change: 1 addition & 0 deletions fcamel/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# fcamel
33 changes: 33 additions & 0 deletions fcamel/[]__linking_time.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# [] 加速 linking time

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

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

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

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

```
#!/bin/bash
LD=/usr/bin/ld
GNULD=${LD}.bfd
GOLD=${LD}.gold
current=$(\ls -l $LD | awk '{print $NF}')
if [ $current = $GNULD ]; then
sudo ln -fs ${GOLD} ${LD}
echo "switch ld to ${GOLD}"
else
sudo ln -fs ${GNULD} ${LD}
echo "switch ld to ${GNULD}"
fi
```

另外 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 時就沒有找到上面提的參數說明。

0 comments on commit 5b44572

Please sign in to comment.