Categories Books | Hard | Hardware | Linux | MCU | Misc | Publish | Radio | Repository | Thoughts | Time | UNIX | Writing | プロフィール
いよいよ、malloc failureシリーズも最終回。前回作成した malloc_null.c は、ライブラリ中の malloc をハイジャックし、ゼロを呼び出し元に返すだけでしたが、今回は本来の malloc を内部で呼び出し、メモリ割り当てを実行できる wrapper function に挑戦してみましょう。
Wrapper function を実装するためには、malloc ライブラリ関数のエントリアドレスの取得など初期化処理が必要になります。一般のアプリケーションであれば、処理の前後で初期化・終了処理を行うことは簡単ですが、共有オブジェクトで実現するとなると、はてと悩んでしまいます。
実は、GCCにはこのような場合のために、特別な仕掛けが用意されているのです。
まず、次に示す puts_initfini.c を用意してください。
1 #include <stdio.h> // fprintf(), stderr 2 3 void init(void) __attribute__((constructor)); 4 void fini(void) __attribute__((destructor)); 5 6 void init() { 7 fprintf(stderr, "puts_initfini.so: initialized.\n"); 8 } 9 10 void fini() { 11 fprintf(stderr, "puts_initfini.so: finalized.\n"); 12 } 13 14 int puts(const char* msg) { 15 return fprintf(stderr, "puts_initfini.so: %s\n", msg); 16 }
前回作成した puts 関数に加えて、init, fini 関数のふたつが加えられています。いずれも簡単なメッセージを表示するだけのものですが、3・4行のプロトタイプ宣言に注目してください。
3行目では、GCCの拡張機能である __attribute__ 指定子を使い、init 関数をコンストラクターとして宣言しています。このコンストラクターはC++の概念とはことなり、該当モジュールがロードされた際に、初期化関数として自動実行されることを意味しています。
4行目でも同様にして、fini 関数をデストラクターとして宣言しています。この結果、本モジュールがアンロード(unload)される際に、終了処理として fini 関数が自動的に実行されるようになります。
puts_initfini.c を共有オブジェクトしてビルドし、前回使用した puts.c および puts_test.c と併せて実験してみましょう。まず、puts.so, puts_initfini.so 共有オブジェトを作成します。
$ gcc -Wall -fPIC -shared -o puts.so puts.c $ gcc -Wall -fPIC -shared -o puts_initfini.so puts_initfini.c $ file puts.so puts_initfini.so puts.so: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), not stripped puts_initfini.so: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), not stripped
次に、puts_test 実行可能ファイルを作成し、最初に puts.so でテストします。
$ gcc -Wall -o puts_test puts_test.c $ LD_PRELOAD="./puts.so" ./puts_test puts.so: Hello, world!
前回通り、正常に動作しています。次に、今回作成した puts_initfni.so の出番です。
$ LD_PRELOAD="./puts_initfini.so" ./puts_test puts_initfini.so: initialized. puts_initfini.so: Hello, world! puts_initfini.so: finalized.
init, fini 関数が適切に呼び出されていることが確認できます(puts_initfini.c は Linuxだけでなく、*BSDでも動作します)。このテクニックは、共有ライブラリを作成する際によく使われますが、考えれば考えるほど不思議なコードの動きです。どうしてこのような芸当が可能になるかについては、ELFの内部構造を学ぶ必要があります(ヒントは .ctors, .dtors セクションです)。
下準備は以上で整いましたので、いよいよ動的リンクによるアドレス取得に挑戦してみましょう。静的リンクとはことなり、動的リンクでは外部参照シンボルのアドレスは、プロセス起動時まで決まっていません。アドレス解決はダイナミックリンカーローダの役割ですが、今回のようにプログラマが意図的にアドレス解決を行うことができるように、dlsym(Dynamic Linking loader: SYMbol)関数が用意されています。
SYNOPSIS #include <dlfcn.h> void *dlopen(const char *filename, int flag); void *dlsym(void *handle, const char *symbol); dlsym The function dlsym() takes a "handle" of a dynamic library returned by dlopen and the NUL-terminated symbol name, returning the address where that symbol is loaded into memory. If the symbol is not found, in the specified library or any of the libraries that were automatically loaded by dlopen() when that library was loaded, dlsym() returns NULL. (The search performed by dlsym() is breadth first through the dependency tree of these libraries.) Since the value of the symbol could actually be NULL (so that a NULL return from dlsym() need not indicate an error), the correct way to test for an error is to call dlerror() to clear any old error conditions, then call dlsym(), and then call dlerror() again, saving its return value into a variable, and check whether this saved value is not NULL. There are two special pseudo-handles, RTLD_DEFAULT and RTLD_NEXT. The former will find the first occurrence of the desired symbol using the default library search order. The latter will find the next occurrence of a function in the search order after the current library. This allows one to provide a wrapper around a function in another shared library.
man ページに書かれている通り、本来は dlopen 関数と併せて使われますが、Wrapper function を実装する場合は、第一引数に RTLD_NEXT マクロ(Linux, BSD 共に -1L で定義)を指定し、第二引数にシンボル名を指定すると、目的のシンボルアドレスが返されます。
この RTLD_NEXT マクロを指定する点がミソですが、具体的なコードで説明した方が理解が早いかと思います。次の、puts_wrapper.c をご覧ください。
1 #include <stdio.h> // fprintf(), stderr, stdout 2 #include <dlfcn.h> // RTLD_NEXT, dlsym() 3 #include <unistd.h> // _exit() 4 5 static void init(void) __attribute__((constructor)); 6 static void fini(void) __attribute__((destructor)); 7 8 static int (*libc_puts)(const char* msg); 9 10 static void init(void) { 11 libc_puts = dlsym(RTLD_NEXT, "puts"); 12 fprintf(stderr, "puts_wrapper.so: libc_puts = %p\n", libc_puts); 13 if (libc_puts == 0) 14 _exit(1); 15 } 16 17 static void fini(void) { 18 fprintf(stderr, "puts_wrapper.so: terminated.\n"); 19 } 20 21 int puts(const char* msg) { 22 fprintf(stdout, "puts_wrapper.so: "); 23 return (*libc_puts)(msg); 24 }
Name polution を避けるために、今回から global 宣言が不要なシンボルは全て static 宣言を行っています。8行目で関数ポインタ変数 libc_puts を定義し、11行目で dlsym 関数を利用して「ライブラリ関数の puts シンボル」のアドレスを入手しています。
本来であれば、puts_wrapper.c 自身の内部で定義されている puts 関数のアドレスが返されるところですが、RTLD_NEXT マクロが指定されているため、ダイナミックリンカーローダは次のライブラリ(すなわちCライブラリ)の探索を行い、目的のアドレスを得ることができます。
puts ライブラリ関数のエントリアドレスさえ入手してしまえば、後は簡単です。23行で関数ポインタ変数を介して、間接的に puts を呼び出し、仕事は終了です。このあたりは、アドレスを低レベルで自在に操れるC言語の独壇場と言えるでしょう。
ちなみに failmalloc は、GNU C ライブラリ版 malloc の拡張機能であるフック関数を利用して、wrapping を実現しています。*BSD は GNU 開発ツールは採用しているものの、システムの根幹となる基本ライブラリは自前で実装していますので、failmalloc の仕組みは使えません。
それでは実験です。まず Linux の場合ですが、RTLD_NEXT マクロを使用するためには、_GNU_SOURCE マクロを定義しておく必要があります。また、内部で使用している dlsym 関数に対して、-ldl オプション(libdl)を指定する必要があります。
Debian $ gcc -Wall -D_GNU_SOURCE -fPIC -shared -o puts_wrapper.so puts_wrapper.c -ldl Debian $ LD_PRELOAD="./puts_wrapper.so" ./puts_test;echo $? puts_wrapper.so: libc_puts = 0xb7ecb540 puts_wrapper.so: Hello, world! puts_wrapper.so: terminated. 0
puts ライブラリ関数の wrapping に成功しています。次に OpenBSD ですが、こちらは _GNU_SOURCE マクロも -ldl オプションも必要ありません。
OpenBSD $ gcc -Wall -fPIC -shared -o puts_wrapper.so puts_wrapper.c OpenBSD $ LD_PRELOAD="./puts_wrapper.so" ./puts_test;echo $? puts_wrapper.so: libc_puts = 0x53e95f8 puts_wrapper.so: Hello, world! puts_wrapper.so: terminated. 0
Linux と BSD では共有ライブラリのアドレスがことなっていますが、どちらも正常に動作しています。
ここまでの知識の総括として、malloc の wrapper function を作成してみましょう(malloc_wrapper.c)。
1 #include <stdio.h> // fprintf(), stderr 2 #include <dlfcn.h> // RTLD_NEXT, dlsym() 3 #include <unistd.h> // _exit() 4 5 static void init(void) __attribute__((constructor)); 6 static void fini(void) __attribute__((destructor)); 7 8 static void* (*libc_malloc)(size_t); 9 static int calls = 0; 10 static long long allocated = 0LL; 11 12 static void init() { 13 libc_malloc = dlsym(RTLD_NEXT, "malloc"); 14 fprintf(stderr, "malloc_wrapper.so: libc_malloc = %p\n", libc_malloc); 15 if (libc_malloc == 0) 16 _exit(1); 17 } 18 19 static void fini() { 20 fprintf(stderr, "malloc_wrapper.so: %d calls, %lld bytes allocated.\n", \ 21 calls, allocated); 22 } 23 24 void* malloc(size_t size) { 25 void* ptr; 26 27 calls++; 28 ptr = (*libc_malloc)(size); 29 allocated += (long long) size; 30 // fprintf(stderr, "malloc_wrapper.so: malloc(%d)\n", size); 31 return ptr; 32 }
コードについて、説明の必要はないと思います(30行は画面がうるさくなるのでコメントアウト)。calls, allocated 変数を用意し、malloc のコール回数、割り当てた総メモリ容量を最後に出力するようにしています。ビルドは次の通りです。
Debian $ gcc -Wall -D_GNU_SOURCE -fPIC -shared -o malloc_wrapper.so mmalloc_wrapper.c -ldl
早速、ls, awk, perl で実験してみましょう。
Debian $ LD_PRELOAD="./malloc_wrapper.so" ls -F / malloc_wrapper.so: libc_malloc = 0xb7e1ad80 bin/ dev/ initrd.img@ media/ proc/ spoon@ tmp/ vmlinuz@ boot/ etc/ lib/ mnt/ root/ srv/ usr/ cdrom@ home/ lib64/ opt/ sbin/ sys/ var/ malloc_wrapper.so: 189 calls, 157037 bytes allocated.
$ LD_PRELOAD="./malloc_wrapper.so" awk 'BEGIN{print "Hello, world"}' < /dev/null malloc_wrapper.so: libc_malloc = 0xb7ed5d80 Hello, world malloc_wrapper.so: 54 calls, 7114 bytes allocated.
$ LD_PRELOAD="./malloc_wrapper.so" perl -e 'print "Hello, world!\n"' malloc_wrapper.so: libc_malloc = 0xb7ebfd80 Hello, world! malloc_wrapper.so: 652 calls, 251432 bytes allocated.
上手にラッピングできているようです。後は、malloc_wrapper.c を改造し、一定頻度もしくは一定回数呼び出し後に NULL を返したり、メッセージを syslog 経由で出力したりと、色々楽しめると思います。
動的リンクと共有ライブラリは、サーバーもしくは組み込みシステムの構築・管理を行う上で避けては通れない重要な部分ですが、これまでその技術背景が語られることはあまりなかったようです。
この機会に「空気と水」の存在を再認識するのも良いでしょう。