GNU ld の挙動で気が付いたことがあったのでメモ。
例によって少し前置きが長くなるが、端的にいえば、system call 等の他人が提供する関数呼び出しをフックして、処理を横取りしたくなることがたまにある。
例えば、system call の write(2) をフックしたいなら、以下のようなプログラムを書くことになる。
1 #include <dlfcn.h> /* これが必要 */
2 #include <unistd.h> /* 今回hookしたい write(2)が定義されている */
3 #include <stdio.h>
4 #include <errno.h>
5
↓ hook したい関数、この場合はwrite(2) と同じようにする。
6 ssize_t (*org)(int fd, const void *buf, size_t count) = 0;
7
↓main()実行の前に呼ばれる初期化ルーチン
8 __attribute__((constructor))
9 static void init_hook()
10 {
11 org = (ssize_t(*)(int, const void *, size_t))dlsym(RTLD_NEXT, "write");
12 printf("%s: constructor called! %p\n", __func__, org);
13 }
14
↓main()から抜けた後に呼ばれる終了ルーチン
15 __attribute__((destructor))
16 static void fini_hook()
17 {
18 printf("%s: destructor called!\n", __func__);
19 }
20
↓write(2)の処理を横取りするルーチン
21 ssize_t write(int fd, const void *buf, size_t count)
22 {
23 int ret;
24
25 /*
26 * Do what you like to do here
27 */
28 printf("%s: hook!\n", __func__);
29 ret = (int)org(fd, buf, count);
30 /*
31 * Do what you like to do here
32 */
33 return ret;
34
35 }
これを、hook.c として保存しておき、hook.so という名前の shared object を作成する。この際、初期化ルーチンの中で、オリジナルの write(2)のエントリを探すのに使っている dlsym(3)のために、-ldl オプション(と -D_GNU_SOURCE)が必要になる。
$gcc -D_GNU_SOURCE -fPIC -shared -ldl hook.c -o hook.so
ぐぐると日本語の解説もいくつか出てくるので参照してほしいのだが、上記のようにして shared object 作り、プログラムの実行時に LD_PRELOAD 環境変数で読み混ませてやれば目的を達することができる。
…はずだった。これまでは。
しかし、上記を(少なくとも) Ubuntu 12.10 上で作成して、実行してみると
$ env LD_PRELOAD=./hook.so cat /tmp/foo
cat: symbol lookup error: ./hook.so: undefined symbol: dlsym
…と、エラーになってしまう。
オリジナルの write(2)を探すのに使っていて、-ldl でちゃんと指定しているはずの dlsym()が見つからないとか言っている。
なんで?
…ということで確認してみると、当然リンクされているはずの libdl.so がリンクされていない!
$ ldd hook.so
linux-vdso.so.1 => (0x00007fff8edba000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f06d3745000)
/lib64/ld-linux-x86-64.so.2 (0x00007f06d3d1a000)
どういうことなの?
…と、ぐぐっていて出てきた以下のトピックを読んで初めて知ったのだが、
http://os.inf.tu-dresden.de/pipermail/l4-hackers/2011/005078.html
実は、少なくとも現行の Ubuntu ではリンカ (GNU ld) の挙動が変更されており、以下のように、--no-as-needed というオプションを指定しないといけないことが分かった。
(脱線するが、L4のMLって、まだ普通に生きているのだなぁ...と、遠い目になったのはひみつ)
$ gcc -D_GNU_SOURCE -Wl,'--no-as-needed' -fPIC -shared -ldl hook.c -o hook.so
$ ldd hook.so
linux-vdso.so.1 => (0x00007fff5494d000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f2d72be2000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f2d72823000)
/lib64/ld-linux-x86-64.so.2 (0x00007f2d72ffc000)
赤字部分のように、今度は意図の通り、リンクされている。
現行のデフォルトの挙動は'--no-as-needed'の逆の'--as-needed'で、これは、リンカ(ld)に -ldl 等と渡された shared object の一覧の中から、プログラムの中で実際に使われているシンボルが入っている shared object のみ実際にリンクするという意味になるようだ。
でも、ちゃんと使っているじゃん?…と思うかもしれない。
これについては、ソースレベルでは(まだ)未確認なのだが、今回の場合は、_init ルーチンの中で dlsym() を使っており、'--as-needed' の場合に探索される範囲から外れているから…というのが真相らしい。
さて、ここでもうひとつ注意がある。'--no-as-needed' をコマンドラインのどの順番で指定するかには意味がある。この例では -ldl の前に指定する必要がある。
実際、-ldl の後ろに指定して shared object を作ってみると
$ gcc -g -Wall -D_GNU_SOURCE -fPIC -shared -ldl -Wl,'--no-as-needed' hook.c -o hook.so
$ ldd hook.so
linux-vdso.so.1 => (0x00007fffea666000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fb722dc8000)
/lib64/ld-linux-x86-64.so.2 (0x00007fb72339d000)
の通り、また libdl.so がリンクされなってしまうので注意のこと。
ちなみに、以下は man ld からの引用なのだが、赤字のところが --no-as-neededの場合は逆になる…ということのようだ。
# そんなの気がつかねーよ...orz
LD(1) GNU Development Tools LD(1)
NAME
ld - The GNU linker
SYNOPSIS
ld [options] objfile ...
DESCRIPTION
(snip)
--as-needed
--no-as-needed
This option affects ELF DT_NEEDED tags for dynamic libraries
mentioned
on the command line after the --as-needed option.
Normally the linker will add a DT_NEEDED tag for each dynamic
library mentioned on the command line, regardless of whether the
library is actually needed or not. --as-needed causes a DT_NEEDED
tag to only be emitted for a library that satisfies an undefined
symbol reference from a regular object file or, if the library is
not found in the DT_NEEDED lists of other libraries linked up to
that point, an undefined symbol reference from another dynamic
library. --no-as-needed restores the default behaviour.
最後に、今回作った hook.so を実際に使ってみるとどうなるかも載せておく。
write(2) を使うできるだけ単純なプログラムということで、cat を使って
$ cat /tmp/bar
FOO
というファイルを表示してみると、
$ env LD_PRELOAD=./hook.so cat /tmp/bar
init_hook: constructor called! 0x7fd48f0228f0
write: hook!
FOO
$
ということで、冒頭のサンプルに書いてある通り、実際の write(2)が走る前に "write: hook!" と自分で追加した処理が実行されていることがわかる。