やり直しC言語:インターポジショニングによるtime(3)のスタブ化

Posted by Hiraku on 2015-09-13

C言語の本を久しぶりに読み返しているのですが、インターポジショニング(interpose, interposition)なる言葉を初めて知ったのでメモです。

「C標準ライブラリ関数などをローカル定義の関数で上書きできてしまう」というお話です。うっかりやらかすとバグの温床になるのですが、うまく使えばユニットテストのスタブとして便利そう。

time関数についておさらい

例として思いついたので、time(3)を上書きしてみます。

time(3)は現在時刻をUNIX time(time_t型)で取得するC標準ライブラリ付属の関数です。詳細はman 3 timeで。この関数、実行時間によって結果が常に異なるわけで、ユニットテストの時は困り者です。

/* time_ex.c */
#include <stdio.h>
#include <time.h>

int main(void) {
  printf("%ld", time(NULL));
  return 0;
}
$ cc time_ex.c && ./a.out
(現在時刻に相当するunix timeが表示される)

「もし、午前中だったらAを行い、午後だったらBを行う」「2015年の9月1日以後のみ有効になるif文」みたいな、現在時刻をベースに条件分岐するようなロジックがあると、結果が安定しないのでうまくユニットテストが書けません。

time関数をスタブにできればいいわけで、ここでインターポジショニングが使えます。

インターポジショニングによる上書き

まずはペタペタ書いてみます。

さっきのmain関数の前に、普通にtime関数を定義します。

/* time_ex2.c */
#include <stdio.h>
#include <time.h>

time_t time(time_t* tloc) {
  if (tloc == NULL) {
    return 0;
  }
  return *tloc = 0;
}

int main(void) {
  printf("%ld", time(NULL));
  return 0;
}
$ cc time_ex2.c && ./a.out
0

たぶん何度実行しても0が表示されるようになったはず。コンパイル時にも、何のエラーも出ません。これは確かにやらかすと気づきにくいバグになるかも。。

これでスタブにはできましたが、本来やりたかったプログラムを改変しているのとあまり違いがありません。テストしたい時だけ、動的に切り替えたいところです。

この場合、time関数本体のライブラリはダイナミックリンクしてるだけなので、ライブラリのロード順をいじってやると、簡単に上書きできたりします。

スタブのtime関数を切り離す

スタブのtime関数だけ、ダイナミックリンク用のライブラリとしてビルドすればOK。 ただ、OSによってこの辺の仕組みはだいぶ違うようで、コマンドも違います。ここではMac OS Xで試してみます。

/* time_stub.c */
#include <time.h>

time_t time(time_t* tloc) {
  if (tloc == NULL) {
    return 0;
  }
  return *tloc = 0;
}
$ clang -shared -fPIC -o libtime_stub.dylib time_stub.c
$ ls
libtime_stub.dylib time_stub.c

確か最近のMacでは"gcc"コマンドはclangのエイリアスになっているので、clangの代わりにgccコマンドを打っても結果は同じなはず。

あとは、この出来上がったlibtime_stub.dylibを標準ライブラリの前に読み込ませて、実行すればOK。最初のtime_ex.cをビルドしたa.outが同じディレクトリにあるとして、こんな感じです。

$ ./a.out
1442144345
$ DYLD_FORCE_FLAT_NAMESPACE=1 DYLD_INSERT_LIBRARIES=libtime_stub.dylib ./a.out
0

環境変数を使って、先にスタブのライブラリを読み込ませることで、time関数を上書きできました!

もっとtime_stubで遊ぶ

条件が合えば、別にさっきのa.outじゃなくても同じようにtime関数を上書きした状態にできます。

例えば、dateコマンド。実行すると現在時刻を表示してくれるはずですが、libtime_stub.dylibでの上書きが有効っぽい。

$ DYLD_FORCE_FLAT_NAMESPACE=1 DYLD_INSERT_LIBRARIES=libtime_stub.dylib date
1970年 1月 1日 木曜日 09時00分00秒 JST

1970年にタイムスリップしたみたいで楽しい! そろそろ面倒くさくなってきたのでexportして、他のコマンドも試してみます。

$ export DYLD_FORCE_FLAT_NAMESPACE=1 DYLD_INSERT_LIBRARIES=libtime_stub.dylib

$ date
1970年 1月 1日 木曜日 09時00分00秒 JST
$ cal
      1月 1970
日 月 火 水 木 金 土
             1  2  3
 4  5  6  7  8  9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31

$ perl -e 'print time();'
0

$ php -r 'echo date("c");'
1970-01-01T00:00:00+00:00
$ php -r 'echo date_create()->format("c");'
1970-01-01T00:00:00+00:00

$ ruby -e 'require "date"; puts DateTime.now'
2015-09-13T20:48:26+09:00

$ python -c 'import datetime; print datetime.datetime.now()'
2015-09-13 20:57:05.795197

$ node -e 'console.log(Date.now())'
1442145688533

ふむ。。PHPやPerlだとtime(3)を上書きすれば、挙動としても上書きできちゃってるみたいですね。 RubyやPython、Node.jsは影響を受けないみたいです。中身は全然知らないけど、time(3)を使ってないということかな…?

まとめ

  • C言語でも標準関数の上書きができるよ
  • ただ、ライブラリのロードをいじるやり方はOS依存が激しいので、できれば避けたほうがいいとは思う
  • うまく使えば楽しそう。でもバグの原因にもなりやすいので気をつける

C言語も文法の気持ち悪さに慣れてしまえば、すごく奥が深くて楽しいですね。


エキスパートCプログラミング―知られざるCの深層 (Ascii books)



codingの最新記事