少し間が空いたんですが
こちらの続きです。
今回は LD_PRELOAD するshared objectにlibmruby.aを組み込み、mrubyで置き換え対象の処理を記述できるようにする。
組み込み用の libmruby.a を作成する
git clone --depth=1 https://github.com/mruby/mruby vim mruby/build_config.rb # 後述する通り、共有ライブラリにするため -fPIC オプションを渡すよう書き換え # libmruby.a を単体で作る場合は、生成ファイルのフルパスを指定すると良い bash -c 'cd mruby && rake `pwd`/build/host/lib/libmruby.a'
-fPIC
はこの場所に書く。
MRuby::Build.new do |conf| #... # C compiler settings # conf.cc do |cc| # cc.command = ENV['CC'] || 'gcc' # cc.flags = [ENV['CFLAGS'] || %w()] # cc.include_paths = ["#{root}/include"] # cc.defines = %w(DISABLE_GEMS) # cc.option_include_path = '-I%s' # cc.option_define = '-D%s' # cc.compile_options = "%{flags} -MMD -o %{outfile} -c %{infile}" # end conf.cc.flags << '-fPIC' #... end
これだけでシュッと作成可能。この場合、mrubyバイナリなどの作成もスキップする。
clock_gettime関数をmruby連携させる
続いて、代替用のclock_gettime関数のC実装を書く。 全体はGistにアップした。
ポイントをいくつかメモしておく。
- どこからやってくるか不確定なポインタの先のデータをラップして操作するようなmrubyのオブジェクトは、通常のオブジェクトに後付けで
DATA_PTR(self)
にセットしてあげて、その状態で更新してあげるとできた。GCのタイミングによっては全てが破壊されるので mrb_arena_save/restore で保護してあげると良さそう。 - 今回はtimespec構造体を操作できると嬉しいので、対応するメソッドを実装しておく。
static mrb_value mrb_timespec_register(mrb_state *mrb, mrb_value tsrb, struct timespec *tp) { void *data = DATA_PTR(tsrb); if (data) { mrb_raise(mrb, E_RUNTIME_ERROR, "Register data failed"); } DATA_PTR(tsrb) = tp; return tsrb; } static struct timespec *mrb_timespec_unregister(mrb_state *mrb, mrb_value tsrb) { struct timespec *tp = DATA_PTR(tsrb); DATA_PTR(tsrb) = NULL; return tp; } static mrb_value mrb_timespec_get_sec(mrb_state *mrb, mrb_value self) { struct timespec *tp = DATA_PTR(self); if (!tp) { mrb_raise(mrb, E_RUNTIME_ERROR, "Core data not yet set"); } return mrb_fixnum_value(tp->tv_sec); } static mrb_value mrb_timespec_set_sec(mrb_state *mrb, mrb_value self) { struct timespec *tp = DATA_PTR(self); if (!tp) { mrb_raise(mrb, E_RUNTIME_ERROR, "Core data not yet set"); } mrb_int newsec; mrb_get_args(mrb, "i", &newsec); tp->tv_sec = newsec; return mrb_fixnum_value(tp->tv_sec); } /* ...Doing same for tv_nsec */ //... int clock_gettime(clockid_t clk_id, struct timespec *tp) { //... mrb_value mrb_tp = mrb_obj_new(mrb, mrb_class_get(mrb, "Timespec"), 0, NULL); tp->tv_sec = 0; tp->tv_nsec = 0; mrb_timespec_register(mrb, mrb_tp, tp); mrb_value ret = mrb_funcall(mrb, klass, "clock_gettime", 2, mrb_clk_id, mrb_tp); tp = mrb_timespec_unregister(mrb, mrb_tp); //... }
- 元の関数定義を呼び出したい時が結構曲者で、今回は
mrb_state->ud
に元の関数定義のテーブルを格納して対応した。関数呼び出しごとにmrb_open()
するのでスレッド安全性などの問題は起きない…はず。
struct mrb_simulacre_super_func_table { int (*clock_gettime)(clockid_t, struct timespec *); clockid_t clock_gettime_arg1; struct timespec *clock_gettime_arg2; }; static mrb_value mrb_simulacre_super(mrb_state *mrb, mrb_value self) { struct mrb_simulacre_super_func_table *table; if (mrb->ud == NULL) { mrb_raise(mrb, E_RUNTIME_ERROR, "userdata not set"); } table = (struct mrb_simulacre_super_func_table *)mrb->ud; // ここは、ちゃんとmrubyのオブジェクトから取得できそう。比較実装したい table->clock_gettime(table->clock_gettime_arg1, table->clock_gettime_arg2); return mrb_true_value(); } //... int clock_gettime(clockid_t clk_id, struct timespec *tp) { struct mrb_simulacre_super_func_table table; int (*super)(clockid_t, struct timespec *); super = dlsym(RTLD_NEXT, "clock_gettime"); table.clock_gettime = super; table.clock_gettime_arg1 = clk_id; table.clock_gettime_arg2 = tp; //... }
clock_gettime
のmruby側実装の読み込ませ方。- 以下のようなmrubyの実装を
mrblib
なりの下に作成。
module Simulacre def self.clock_gettime(clk_id, ts) if rand(2) == 0 Simulacre.super else ts.sec = rand(10000) end return true end end class Timespec # 空のクラス end
- libmruby.a の生成の過程で
mruby/build/host/bin/mrbc
ができているはず。mrbc
コマンドを用いると、mrubyのコードをコンパイルしたバイナリ表現が、Cのソースコードの形で手に入るので出力し、それを適切な場所に貼る。 - このバイナリは
mrb_load_irep(mrb, libs);
というふうに直接mrb_stateに読み込ませることができる。 - 詳細は この辺の実装など 。
// $ mruby/build/host/bin/mrbc -Blibs -o- mrblib/*.rb /* dumped in little endian order. use `mrbc -E` option for big endian CPU. */ #include <stdint.h> extern const uint8_t libs[]; const uint8_t #if defined __GNUC__ __attribute__((aligned(4))) #elif defined _MSC_VER __declspec(align(4)) #endif libs[] = { 0x45,0x54,0x49,0x52,0x30,0x30,0x30,0x33,0x25,0xf6,0x00,0x00,0x01,0x84,0x4d,0x41, 0x54,0x5a,0x30,0x30,0x30,0x30,0x49,0x52,0x45,0x50,0x00,0x00,0x01,0x42,0x30,0x30, //...... };
コンパイルとテスト
そんな感じでいい感じに作った src/mrb_clock_gettime.c
をコンパイルする。
gcc -g -c -fPIC -I./mruby/include src/mrb_clock_gettime.c -o src/mrb_clock_gettime.o gcc -g -shared -o libjack.so src/mrb_clock_gettime.o mruby/build/host/lib/libmruby.a file libjack.so #=> libjack.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=e9fbbbf40755006ea73d85bf31217d13ee462894, not stripped
なんと、コンパイルが通った。
これを読み込ませてみましょう……
$ LD_PRELOAD=./libjack.so ruby -e "100.times { t = Time.now; p [t.to_s, t.to_i]; system 'sleep 1' }" ["2017-04-04 15:43:16 +0900", 1491288196] ["2017-04-04 15:43:17 +0900", 1491288197] ["2017-04-04 15:43:18 +0900", 1491288198] ["1970-01-01 11:02:33 +0900", 7353] ["2017-04-04 15:43:21 +0900", 1491288201] ["1970-01-01 11:20:17 +0900", 8417] ["1970-01-01 09:02:38 +0900", 158] ["1970-01-01 11:19:58 +0900", 8398] ["1970-01-01 11:37:21 +0900", 9441] ["1970-01-01 10:00:20 +0900", 3620] ["2017-04-04 15:43:27 +0900", 1491288207] ["2017-04-04 15:43:28 +0900", 1491288208] ["1970-01-01 09:36:50 +0900", 2210] ["2017-04-04 15:43:30 +0900", 1491288210] ["1970-01-01 09:06:24 +0900", 384] ...
mrubyでの実装の通り、ランダムで1/2は正しい時刻を、1/2はエポックタイム 0 ~ 9999 の間のダミーの時刻を返す、というふうになっていることがわかる。
なお、Rubyの Kernel#sleep
も*1どうやら影響されるので、外部コマンドでsleepしているのはご愛嬌である…。
こんな感じの実装が、多分、 Franck さんがRubyKaigiで解説していたアイデアの具体的実装なのではないだろうかと思う。
とりあえず、特定の関数にべったりと書いてみたが、これをその他のライブラリ関数に適用することはできるのだろうか。ということでその3に…続く?
おまけ
PoCとなるコードをこのリポジトリにあげてみました。適当なLinux環境でチェックアウトして rake
すると dest/
配下に.soファイルができます。
*1:ちゃんと追っていないけど