ソース隠蔽手法

昨日の続き。隠蔽といってもアセンブラレベルでは見れてしまうので頑張れば解析可能なわけだが。Rubyアセンブラは高レベルなので読みやすいというのが難点か。
ともあれ、このようなファイルを作る。名前はiseqloader.rbとしよう。

require 'fiddle'

class RubyVM
  class InstructionSequence
    addr = Fiddle.dlopen(nil)['rb_iseq_load']
    fn   = Fiddle::Function.new(
             addr, [Fiddle::TYPE_VOIDP] * 3, Fiddle::TYPE_VOIDP)

    define_singleton_method(:load) do |dat, par=nil, opt=nil|
      fn.call(Fiddle.dlwrap(dat), par, opt).to_value
    end
  end
end

alias old_require_relative require_relative

def require_relative(filename)
  ext = File.extname(filename)
  base = File.basename(filename)

  if ext.empty?
    rbname = filename + ".rb"
    rbcname = filename + ".rbc"
  elsif ext == ".rb"
    rbname = filename
    rbcname = filename + "c"
  else
    old_require_relative(filename)
    return
  end

  if File.exists?(rbname)
    if !File.exists?(rbcname) or File.mtime(rbname) > File.mtime(rbcname)
      open(rbcname, "wb") do |fh|
        Marshal.dump(RubyVM::InstructionSequence.compile_file(rbname).to_a, fh)
      end
    end
  end

  if File.exists?(rbcname)
    open(rbcname, "rb") do |fh|
      RubyVM::InstructionSequence.load(Marshal.load(fh)).eval
    end
  else
    old_require_relative(filename)
  end
end

このコードをrequireするとrequire_relativeが置き換えられて、実行時にrbと同名のrbcファイルというコンパイル済みファイルを生成し、そっちを読んで実行するようになる。
rbのほうが新しければrbcを再生成するし、無ければ存在するrbcを読んで実行する。rbもrbcも無ければ通常のrequire_relativeが実行される。
例えばこのようなファイルを作る。rbctest.rbとしよう。

class RBCTest
  def initialize(v)
    @v = v
  end
  def v
    @v
  end
end

んで、メインのファイルをtest.rbとして作る。

require_relative 'iseqloader'
require_relative 'rbctest'

p RBCTest.new(10).v #=> 10

test.rbを実行するとrbctest.rbcが生成されて、10が表示される。このあと、rbctest.rbを削除してtest.rbを実行してもエラーにならずに10が表示される。
問題点はrequire_relativeと違って同じファイルを2回以上読み込んでしまうところで、このへんは絶対パスを生成して配列にでも入れておいて同じファイルを読まないようにチェックを入れる必要があるだろう。
開発中はiseqloaderのrequireをはずしておかないとエラーの箇所がわからないだろうが、これを入れて動作するならそのままrbファイルを削除しても動くはずである。その状態でocraでexe化してやれば生のソースを含まないexeの完成、となる。かもしれない。rbcファイルはocraのexeには入ってくれないだろうけど、まあ、問題は無い。か?
誰かきちんと作ってgemにでもしてくれないかしらん。