/ «2006-07-22 (Sat) ^ 2006-07-31 (Mon)» ?
   西田 亙の本:GNU 開発ツール -- hello.c から a.out が誕生するまで --

Categories Books | Hard | Hardware | Linux | MCU | Misc | Publish | Radio | Repository | Thoughts | Time | UNIX | Writing | プロフィール


2006-07-30 (Sun)

[UNIX] malloc failure (その4)

いよいよ、malloc failureシリーズも最終回。前回作成した malloc_null.c は、ライブラリ中の malloc をハイジャックし、ゼロを呼び出し元に返すだけでしたが、今回は本来の malloc を内部で呼び出し、メモリ割り当てを実行できる wrapper function に挑戦してみましょう。

Wrapper function を実装するためには、malloc ライブラリ関数のエントリアドレスの取得など初期化処理が必要になります。一般のアプリケーションであれば、処理の前後で初期化・終了処理を行うことは簡単ですが、共有オブジェクトで実現するとなると、はてと悩んでしまいます。

実は、GCCにはこのような場合のために、特別な仕掛けが用意されているのです。

__attribute__((constructor)), __attribute__((deconstructor))の活用

まず、次に示す 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 関数によるライブラリ関数のアドレス取得

下準備は以上で整いましたので、いよいよ動的リンクによるアドレス取得に挑戦してみましょう。静的リンクとはことなり、動的リンクでは外部参照シンボルのアドレスは、プロセス起動時まで決まっていません。アドレス解決はダイナミックリンカーローダの役割ですが、今回のようにプログラマが意図的にアドレス解決を行うことができるように、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

ここまでの知識の総括として、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 経由で出力したりと、色々楽しめると思います。

動的リンクと共有ライブラリは、サーバーもしくは組み込みシステムの構築・管理を行う上で避けては通れない重要な部分ですが、これまでその技術背景が語られることはあまりなかったようです。

この機会に「空気と水」の存在を再認識するのも良いでしょう。