ローファイ日記

出てくるコード片、ぼくが書いたものは断りがない場合 MIT License としています http://udzura.mit-license.org/

Cレベルのライブラリコールを“Hijack”してみる その2 - libmruby.aを組み込む

少し間が空いたんですが

udzura.hatenablog.jp

こちらの続きです。

今回は 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 の間のダミーの時刻を返す、というふうになっていることがわかる。

なお、RubyKernel#sleep*1どうやら影響されるので、外部コマンドでsleepしているのはご愛嬌である…。


こんな感じの実装が、多分、 Franck さんがRubyKaigiで解説していたアイデアの具体的実装なのではないだろうかと思う。

speakerdeck.com

とりあえず、特定の関数にべったりと書いてみたが、これをその他のライブラリ関数に適用することはできるのだろうか。ということでその3に…続く?

おまけ

github.com

PoCとなるコードをこのリポジトリにあげてみました。適当なLinux環境でチェックアウトして rake すると dest/ 配下に.soファイルができます。

*1:ちゃんと追っていないけど