flisp more

前回使ったhexeditの利用法を、まとめておこうかと思ったけど、人様のまとめを 参照して、お茶を濁す事にする。ああ、詳しくまとめたのは無いっぽい。

バイナリエディタいろいろ

よって、manから抜いてきた。大事なのは、hex/ascii エリアでの入力は、データの 書き換えになるって事。間違って操作したと思ったら、Ctrl-Cで抜けてしまうのが 安全。

       Ctrl-X: save and exit
       Ctrl-C: exit without saving

       Tab:    toggle hex/ascii

       <, > :  go to start/end of the file
       PUp:    page forward
       PDown:  page backward

       Ctrl-S: search forward
       Ctrl-R: search backward

普段はemacsを使ってる人がemacsを忘れてもらっては困る。そういうなら、

Emacsのhexl-modeでバイナリファイルを編集する とか、 27.22 バイナリファイルの編集 を見れ。

逆アセンブラもどき

前回、hexeditを使って、バイナリー・パッチしてみた。その時に変更対象が文字の2だった。 これがそのまま、codeとして使われるとは、ちと気味が悪い。バイナリーの世界では無い と思えるからだ。

だったら、先輩のdisassembleがどうやってるか調べてみろ。そして理解度テストとして、 もどきを書いてみろ。そんな事で書いたのがこちら。

;; (function:vals untrace) first value
(define sc "9000r1e0|316@0c1~c2|31b3[42;];")
(define b2n (table.invert Instructions))
(define i 4)                     ;; skip 4 byte for maxstack(little endian)
(while (< i (length sc))
  (set! c (- (aref sc i) 48))    ;; convert ascii to VM code (48 := '0')
  (princ (hex5 (- i 4)) ":\t")
  (princ (string.char sc i) "\t" c "\t" (get b2n c) "\n")
  (set! i (+ 1 i)))

#t

lispにあるまじきコードだ。手続き型のコードをlispっぽく装飾するために括弧でくくってる って感じ。まあ、お手本にしたのも、同様な書き方だったので、それを真似たと強弁して おこう。

で、コードを説明してナンボ。綺麗に説明出来た時は理解が出来ているって事です。 最初の文字列は、すぐ上のコメントにあるようにして拾ってきたもの。他との関係は? って聞かれると、うーん、と考え中になってしまいますな。で、減点ー10点。

b2nは、ニューモニックと命令コードの辞書をひっくり返した辞書。いわゆる逆引辞書。 命令カウンター i は、4から始まる。最初の4バイトは、スタックの最大使用量を 保持してる。なんのために、こんなデータが必要? 再帰とかしたら、スタックがどんどん 深くなるだろうに! そういう時は、このデータと再帰の回数を乗じて、最大の深さを 見積もってくれって事か?

文字列は、内部ではarrayとして扱われる。一文字をarefで取り出してくると、byte扱いに なり、数値で帰ってくる。そのデータから48(ASCII-codeの0)を引く。すなわち、文字のゼロを数値の0に してるんだ。C語で言うなら、文字ー>数値変換で、n = c - '0' に相当。

アドレスを5桁のHEXで表示。それから、TAB。次のprincで、元の文字とコードと、 それをニューモニックに変換したのを表示。

ここで重大なお知らせ。オペコードによって、オペランドのサイズが、0、1、2、4バイトを 取る。。。。けど、簡略版なので、オペランド相当の所も、ニューモニック表示しちゃってる。 そのせいで、対応コードが無い場合、辞書引きに失敗して落ちる事がある。大事な 事なので、クレームが来る前に告知しましたからね。

disassembleのように、地道に場合分けするか、ニューモニックによってオペランドのサイズを引く表を用意して それを使うなりするのが、正しい逆アセンブラーへの道です。

最後は、命令カウンターを増やす。ここ、incとかのマクロに したかったんだけど、マクロ政策失敗しました。まあ、安倍ちゃんもアベノミクス失敗 するぐらいですから、小さい事にはこだわるな。環境が悪いんですって、反論しておきましょ。

最後の#tは、whileの返り値の数値が表示されるのを防ぎ、いかにも逆アセが成功しま したよって所にユーザーを誘導する為に入れてます。安倍ちゃん得意の目くらまし。

実行するには、このファイル(t.lsp)をロードすれば良い。

> (load "t.lsp")
00000:  r       66      argc
00001:  1       1       dup
00002:  e       53      loadg
00003:  0       0       nop
00004:  |       76      loada0
00005:  3       3       call
00006:  1       1       dup
00007:  6       6       brf
00008:  @       16      not
00009:  0       0       nop
0000a:  c       51      loadv
0000b:  1       1       dup
0000c:  ~       78      loadc00
0000d:  c       51      loadv
0000e:  2       2       pop
0000f:  |       76      loada0
00010:  3       3       call
00011:  1       1       dup
00012:  b       50      loadi8
00013:  3       3       call
00014:  [       43      aref
00015:  4       4       tcall
00016:  2       2       pop
00017:  ;       11      ret
00018:  ]       45      loadt
00019:  ;       11      ret
#t

最後がretで終ってるけど、何処に戻る? それから、このコードはどうやって起動 されてくる? 説明出来るか?何、出来ない? ならば 減点ー30点。

> (compile-thunk (define (hoge x)(* x -129)))
#fn("6000r0c0;" [#fn("7000r1|c0T2;" [-129] hoge)])

> (compile-thunk (define (hoge x)(* x -128)))
#fn("6000r0c0;" [#fn("7000r1|b\xb0T2;" [] hoge)])

> (disassemble (define (hoge x)(* x -128)))
maxstack 7
00000:  argc    1
00002:  loada0
00003:  loadi8  128
00005:  *       2
00007:  ret

小さいデータ(-128から127の範囲)はloadi8を使って、オペランドの中に収納しちゃうけど、大きなデータに なると、外にくくり出して、それを持って来るって設計になってるのね。なかなか工夫 がされてますなあ。

使える関数の探し方

上記の逆アセもどきを最初に書いた時、元になる文字を表示していなかったんだ。 でも、これを表示させて眺めていれば、そのうちに文字を見るだけで、codeの意味が 分かって、暗算ならぬ、脳内逆アセが出来るだろうと思った。

問題になるのは、文字列から文字を切り出す関数が有るかどうか?マニュアルは無いし、 有るのはソースだけって言う、典型的な世界。はてどうやって探す?

ソースツリーを眺めていて、ソース名から連想するんだ。string.cに文字列関係の ビルトイン関数がクラスタを作っているに違いない。開いてみて、ソースの最後の方を チラ見。

static builtinspec_t stringfunc_info[] = {
   :
    { "string.char", fl_string_char },
    { "string.inc", fl_string_inc },
   :
    { NULL, NULL }
};

ユーザーが使う関数名と内部のC語の関数名が列挙されてる。string.charがどうも それっぽい。該当部分を覗いてみる。

value_t fl_string_char(value_t *args, u_int32_t nargs)
{
    argcount("string.char", nargs, 2);
    char *s = tostring(args[0], "string.char");
    size_t len = cv_len((cvalue_t*)ptr(args[0]));
    size_t i = toulong(args[1], "string.char");
    if (i >= len)
        bounds_error("string.char", args[0], args[1]);
    size_t sl = u8_seqlen(&s[i]);
    if (sl > len || i > len-sl)
        bounds_error("string.char", args[0], args[1]);
    return mk_wchar(u8_nextchar(s, &i));
}

ざっと見て、引数は2つ。文字列と、取り出したい文字の位置が必要と想像。 後は、repl上で実験した方が早い。

> (string.char 2 "abc")
type error: string.char: expected string, got 2
#0 (lambda)

> (string.char "abc" 2)
#\c

最初のエラーは親切だな。文字列を期待したのに数値を受け取ったって言ってるから。 まあ、こんな風にして当りを付けて実験ってのが手っ取り早い。

同様にしてnumber->stringとかを見ていると、隠れたオプションが有り、例えばそれを、 16に設定すると、16進数にしてくれる。こういうのを発見するのは楽しいな。

マクロ製作

上で、マクロを定義しようとしたら、盛大なエラーを喰らったと書いた。失地挽回したい。

> (define-macro (inc name)
  `(set! ,name (+ ,name 1)))
     :
  let #fn(map) #.list #fn(copy-list) #fn("8000r2c0|}L3;" [set!]) unwind-protect
  begin #fn("8000r2c0|}L3;" [set!])]) #fn(map) #.car cadr #fn("6000r1c040;" [#fn(gensym)])]))

オイラーが間違ったマクロを定義した? こういう時は、正しいマクロを入力して みる事だ。system.lspからtimeマクロをコピペしてみた。やはり同様のわけわかめな エラー。意味不明。

しょうがないので、system.lspの最後に、

(define-macro (inc name)
  `(set! ,name (+ 1 ,name)))

(define-macro (dec name)
  `(set! ,name (- ,name 1)))

を追加した後、bootstrap.shしたよ。そしたらすんなりと動いた。

> (define foo 10) (define hoge 0)
> (inc hoge)
1
> (dec foo)
9

結論として、repl上からはマクロ定義が出来ないって事だ。やれやれ。マニュアルが 有っても、こういう事は書いてくれるだろうか? 返って、地図無しで探検する度胸が 付いて良いかなあ。

そもそも、define-macroって、シンボルとして登録はしてあるけど、ビルトイン関数では ないんだな。

> cons
#.cons

> define
eval: variable define has no value
#0 (lambda)

> define-macro
eval: variable define-macro has no value
#0 (lambda)

defineもビルトイン関数じゃ無い。コンパイラーが、式定義を解析する時の目印に 使っているからか。その辺の事情は、system.lspに定義してある。

(define (eval x) ((compile-thunk (expand x))))

(define (load-process x) (eval x))

(define (repl)
  (define (prompt)
    (princ "> ") (io.flush *output-stream*)
    (let ((v (trycatch (read)
                       (lambda (e) (begin (io.discardbuffer *input-stream*)
                                          (raise e))))))
      (and (not (io.eof? *input-stream*))
           (let ((V (load-process v)))
             (print V)
             (set! that V)
             #t))))
     :

expandってのはtoplevelで名前解決して、翻訳即実行。何とVMでJITしてますよ。

(if (not (bound? '*syntax-environment*))
    (define *syntax-environment* (table)))

(define (set-syntax! s v) (put! *syntax-environment* s v))
(define (symbol-syntax s) (get *syntax-environment* s #f))

(define-macro (define-macro form . body)
  `(set-syntax! ',(car form)
                (lambda ,(cdr form) ,@body)))

こういうのが有るからbootが必要になって来るんだな。その辺の事情はいずこも同じ。 Gauche:VMの最適化:For 0.8.4 とか Gauche:VM命令セットの変更とビルド で、楽しく読めるよ。

マクロの登録簿が、*syntax-environment* と言う辞書になってて、その辞書を ちょっと引いてみると、

> (symbol-syntax 'define-macro)
#fn("?000s1c0c1|ML2c2c3L1|NL1c4}3133L3;" [set-syntax! quote #fn(nconc) lambda
                                          #fn(copy-list)])

> (symbol-syntax 'inc)
#fn(":000r1c0|c1a|L3L3;" [set! +])

直ぐに逆アセしたくなるな。

> (disassemble (symbol-syntax 'inc))
maxstack 10
00000:  argc    1
00002:  loadv   set!
00004:  loada0
00005:  loadv   +
00007:  load1
00008:  loada0
00009:  list    3
0000b:  list    3
0000d:  ret

VMの起動を紐解く

compile-thunkで色々な拡張コードを眺めている。例えば

> (compile-thunk caar)
#fn("6000r0c0;" [#fn("6000r1|MM;" [] caar)])

> (compile-thunk map)
#fn("6000r0c0;" [#fn(map)])

> (compile-thunk car)
#fn("6000r0c0;" [#.car])

いずれも、頭に呼び出しコードとおぼしき者が付いている。これは何? VMコードを翻訳すると

00000:  66      argc
00001:  0       nop
00002:  51      loadv
00003:  0       nop
00004:  11      ret

何か、臭いを感じませんか?

flispの起動を追っている。ビルトイン関数の登録。

(gdb) bt
#0  cbuiltin (name=0x806c636 "string.char", f=0x805ef50 <fl_string_char>) at cvalues.c:896
#1  0x0805bdce in assign_global_builtins (b=0x8071eb8 <stringfunc_info+56>) at flisp.c:2008
#2  0x0805f9d2 in stringfuncs_init () at string.c:424
#3  0x0805e447 in builtins_init () at builtins.c:485
#4  0x0805d0cd in lisp_init (initial_heapsize=524288) at flisp.c:2333
#5  0x0805d121 in fl_init (initial_heapsize=524288) at flisp.c:2348
#6  0x08061c6a in main (argc=1, argv=0xbfbfec30) at flmain.c:27

ただ今、文字列関係のビルトインを登録中です。そして、flisp.bootのロード。VMの 実行コードROMを、いよいよVMにロードします。

(gdb) bt
#0  fl_load_system_image (sys_image_iostream=679903405) at flisp.c:2357
#1  0x08061dd6 in main (argc=1, argv=0xbfbfec30) at flmain.c:51

そして、ロードが終ると、いよいよVMが動き出します。

51        if (fl_load_system_image(f))
52            return 1;
53
54=>      (void)fl_applyn(1, symbol_value(symbol("__start")),
55                        argv_list(argc, argv));

ここからVMの長い々動作が始まるのです。大河な一滴って所でしょうか。

flisp completions

今更の感がするんだけど、rlwrapを介してflispを使っているんで、補間が出来るように しておく。

まずは補間候補を拾い出す事だな。environmentが何でも知ってるはずなんで、それを 種にして、一行づつ書き出そう。

(let ((aa (environment)))
  (while (not (null? aa))
    (princ (car aa) "\n")
    (set! aa (cdr aa))))

こんな、my.lspを作っておいて、

$ ./flisp my.lsp | sort > ~/.flisp_completions
$ rlwrap -pGreen ./flisp

こんな風に起動。適当な所まで文字を入力してTABを2回叩くと、候補が 出て来るんで、続けて入力すれば良い。

rlwrapのmanを引いたら、goshのマルチライン編集設定が 例に出てて、国際的だなと思ったぞ。AUTHORSには、rlwrapの協力ライブラリー作者の 名は有るけど、自身の名は控えておられる。ソースを読むと出てくるかな。

http://utopia.knoware.nl/~hlub/uck/rlwrap/

goshの名が出てきたので、コンパイル結果をプチ逆アセしてみる。

gosh> (define (hoge x)(+ x x))
hoge
gosh> (disasm hoge)
CLOSURE #<closure hoge>
=== main_code (name=hoge, code=0x80fa3c0, size=3, const=0 stack=0):
args: #f
     0 LREF0                    ; x
     1 LREF-VAL0-NUMADD2(0,0)   ; (+ x x)
     2 RET

無駄が省かれた、短いコードだなあ。

etc

Lispkit Lisp

LispKit Lisp 2 - SECD 機械 in C

micro Scheme コンパイラの作成

YARV Maniacs 【第 13 回】 事前コンパイルへの道