SystemTapでBrainf*ckを実装してみる

Brainfuckとは < > + - , . [ ] の8命令だけからなるプログラミング言語です。チューリング完全なので、頑張ればどんなプログラムでも書けるハズですが、なにしろ8命令しか無いので書くのはとても大変です。一方、コンパイラインタプリタを書くのはとても簡単です。

何となく、LinuxカーネルレベルでBrainfuckに対応させてみます。つまり、Brainf*ckのソースを記述したファイルを読み込むと、それを実行した結果が読み出されるようにカーネルを改変してみます。
といってもパッチ書いたりCでカーネルモジュール書いたりはしません。


カーネルを弄るのに使うのは、カーネルデバッグ用言語SystemTap。(←詳しくはキーワードリンク参照)
本来はデバッグ用ですが、ライブにカーネルの動作を書き換えるという、まともじゃない使い方もできてしまうのです。

(ちなみに、ゆの in SystemTap - ゆの対応カーネルですよ! - Okiraku Programming と同じ手法です。)


以下はFedora 9で試しました(ubuntu等でも出来るはず)。


なお、SystemTap自身は安全に使えるよう様々なリミッターがかかっていますが、今回それを外しています。
場合によってはカーネルがお亡くなりになったりする可能性があります。壊しては困る環境では実行しないこと。

動かし方

以下の方法であらかじめSystemTapカーネルデバッグ情報をインストールしておきます。

# yum install kernel-devel systemtap
# debuginfo-install kernel


先にbrainfuck用のスクリプトを用意しておきます。ファイル名は「〜.bf」としてください(拡張子固定)。
何でも良いですが、ソース/出力が長過ぎる(1000byte以上)のはちょっとまずいので、HelloWolrd

>+++++++++[<++++++++>-]<.>+++++++[<++++>-]<+.+++++++..+++.
[-]>++++++++[<++++>-]<.>+++++++++++[<+++++>-]<.>++++++++[<+++>-]<.
+++.------.--------.[-]>++++++++[<++++>-]<+.[-]++++++++++.

とかFizzBuzz

++++++[->++++> >+>+>-<<<<<]>[<++++> >+++>++++> >+++>+
++++>+++++> > > > > >++> >++<<<<<<<<<<<<<<-]<++++>+++
>-->+++>-> >--->++> > >+++++[->++>++<<]<<<<<<<<<<[->-
[> > > > > > >]>[<+++>.>.> > > >..> > >+<]<<<<<-[> > > >]>[<+
++++>.>.>..> > >+<]> > > >+<-[<<<]<[[-<<+> >]> > >+>+<<<<<
<[-> >+>+>-<<<<]<]>>[[-]<]>[> > >[>.<<.<<<]<[.<<<<]>]>.<<<<
<<<<<<<]

あたりなら良いでしょう。

なお入力を受け取るスクリプトは、入力文字列をスクリプトの後ろに「 : 」で区切って書いておく必要があります。例えば

+[,.] :Hello,World!

とかです。


ちなみにQuineを動かそうとしても、出力が変わらないのでよく分からないと思います。


こんなもんが本当に動くプログラムなのか?と疑問な方は、色んな人が(もっとまともな方法で)Brainfuck インタプリタコンパイラを作っていますから、適当に検索して試してみると良いかもです。



さて、今回使うSystemTapインタプリタを実行しましょう。

本エントリの末尾のスクリプトを「brainfuck.stp」というファイルに保存し、rootで

# chmod 755 brainfuck.stp
# ./brainfuck.stp

として実行します。実行すると、Pass 1: ... という表示が出てきます。しばらくして "Pass 5: starting run."と出たら準備完了です。


別ウィンドウにコンソールを開き、用意したBrainf*ckのソースファイルをcatで読んでみましょう。catじゃなくてもlessでもemacsでも大丈夫です。(tacだと特殊な読み方をするためダメなようですが。)
すると、ソースの代わりに実行結果が読み出されているはずです。

% cat hello.bf
Hello World!


brainfuck.stpを実行していた端末で Ctrl-C を叩くと元に戻ります。

% cat hello.bf
>+++++++++[<++ .....


(なおあまりに重いソースを実行すると、しばらくマシンが固まったようになります。無限ループしても多分1分くらいでSystemTapの安全機構(MAXACTIONS)が働いて強制停止するはずですが、回復しなくなったら諦めて強制リブート…orz)

動作原理

このSystemTapスクリプトを実行すると、システムコールを監視し、名前の末尾が".bf"のファイルをopenシステムコールで開いた際、開いたプロセスとファイルディスクリプタを記憶します(probe syscall.open.return の部分。.returnはシステムコールから復帰する時を表します)。


そのプロセスがそのディスクリプタに対してreadシステムコールを呼び出すと、readから復帰する瞬間にファイルから読み込んだ内容をBrainfuckソースと見なして実行します(probe syscall.read.return の部分)。そして、実行結果でreadに渡されたバッファ($buf。bufはカーネル内の第2引数の変数名です)を上書きします。なおユーザが渡したバッファ長を超えないようにしています。そして、readシステムコールの返り値(長さ)もその大きさに書き換えます。
readするたびにバッファの中身を評価するので、まとめて全体をreadしないと動きません(1byteずつ読まれたりすると実行結果を返す領域もないですし)。


Brainf*ckインタプリタの部分(function eval_bf)は一命令ずつインタプリタ方式で実行しているだけです。
メモリはSystemTapには配列が無いので連想配列で実現しています。
出力は文字列として一文字ずつconcatしています(遅そう)。
ただし、入力を受け取るのは大変なので、スクリプトの後ろに「 : 」で区切って書くことにしています。


当該ディスクリプタをcloseするとbrainf*ck実行対象から外します。(probe syscall.closeの部分)(もしそのディスクリプタをdup2で閉じたら呼ばれないってこと? あな恐ろしや…)


スクリプト中の %{ 〜 %} の部分は Embedded Cで、SystemTapスクリプトのみではできなかった部分を実装しています。
具体的には、バッファへの上書きや、ASCIIコードと文字の相互変換を実装しています。
後者くらいは標準であっても良い気がするのですが。。。

制限について

stapコマンド(SystemTap実行コマンド)のオプションとして -g -DMAXACTION=99999999 -DMAXSTRINGLEN=1024 -DSTP_NO_OVERLOAD を指定していますが、これはSystemTapの安全措置を緩めるものです。Brainf*ckは実行ステップがかなり掛かるので、こうしないと動きません。

  • g は埋め込みCを使ったりカーネル関数の返り値をSystemTapから書き換えたりといった危険なことが出来るようにします。
  • DMAXACTION= は一つのprobeの中で実行できるステップ数の上限を増やしています。
  • DMAXSTRINGLEN=1024 は文字列の長さ上限。この長さを超えるスクリプトは動きません。(1024以上に増やそうとしてもエラーになる模様?)
  • DSTP_NO_OVERLOAD はprobe内で時間がかかりすぎても強制停止しないようにします。

スクリプト

#/bin/sh
//usr/bin/stap -vg -DMAXACTION=99999999 -DMAXSTRINGLEN=1024 -DSTP_NO_OVERLOAD $0 ; exit

global target_fd

probe syscall.open.return {
  if ($return<0) next  # error
  filename = user_string($filename)
  len = strlen(filename)
  ext = substr(filename, len-3,len-1)
  if (ext == ".bf") {
    printf("%s(%d) - opening brainf*ck : %d %s\n",
	   execname(), pid(), $return, filename)
    target_fd[pid(),$return]  = 1
  }
}

probe syscall.close {
  if (!target_fd[pid(), fd]) next
  printf("%s(%d) - script closed : %d\n", execname(), pid(), fd)
  delete target_fd[pid(),fd]
}

probe syscall.read.return {
  if ($return <= 0) next
  if (!target_fd[pid(), $fd]) next
  script = user_string_n($buf, $return)
  if (strlen(script) < $return) {
    printf("** failed to load script : %d/%d bytes\n", strlen(script), $return)
    next
  }
  printf("** script loaded : %d bytes\n", $return)
  result = eval_bf(script)
  len = strlen(result)
  $return = $count<len?$count:len
  if (overwrite($buf, $return, result))
    printf("%s(%d) - overwrite read : %d -> %dbyte\n",
	   execname(), pid(), $fd, $return)
  else
    printf("%s(%d) - overwrite failed! : %d -> %dbyte\n",
	   execname(), pid(), $fd, $return)
}

function overwrite:long(buf:long, size:long, result:string) %{
  void *buf = (void*)(long)THIS->buf;
  THIS->__retvalue = 0;
  if (access_ok(VERIFY_WRITE,buf,THIS->size)) {
    memcpy(buf, (void*)(long)THIS->result, THIS->size);
    THIS->__retvalue = 1;
  }
%}

function ord:long(c:string) %{
  THIS->__retvalue = THIS->c[0];
%}

function chr:string(c:long) %{
  THIS->__retvalue[0] = THIS->c;
  THIS->__retvalue[1] = 0;
%}

global mem[1000000]
global stack

function eval_bf:string(script:string) {
  script = tokenize(script, ":")
  input = tokenize("", ":")
  len = strlen(script)
  pos = 0  # inst pointer
  ptr = 0  # memory pointer
  inp = 0  # input ptr
  sp  = 0  # stack pointer for [] loop
  output = "" # output string
  while (pos < len) {
    inst = substr(script, pos++, 1)
    # printf("inst:%d/%d -> %s\t", pos-1, len, inst)
    if      (inst == ">") ptr++
    else if (inst == "<") ptr--
    else if (inst == "+") {mem[ptr]++ if(mem[ptr] == 256) mem[ptr] =   0}
    else if (inst == "-") {mem[ptr]-- if(mem[ptr] ==  -1) mem[ptr] = 255}
    else if (inst == ".") output .= chr(mem[ptr])
    else if (inst == ",") mem[ptr] = ord(substr(input,inp++,1))
    else if (inst == "[") {
      if (mem[ptr]) {
	stack[sp++] = pos-1; # push [ position
	# printf("sp:%d -> %d ", sp-1, stack[sp-1])
      } else { # jump to corresponding ]
	skip_lv = 0
	while(pos < len && skip_lv >= 0) {
	  inst = substr(script, pos++, 1)
	  if (inst == "[") skip_lv++
	  if (inst == "]") skip_lv--
	}
      }
    }
    else if (inst == "]") {
      # printf("sp:%d -> %d ", sp-1, stack[sp-1])
      if (sp < 0) {
	printf("invalid ] at pos %d\n", pos)
	break
      }
      pos = stack[--sp] # pop [ position
    }
    # printf("mem:%d -> %d\n", ptr, mem[ptr])
  }
  delete mem
  delete stack
  return output;
}