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
無駄が省かれた、短いコードだなあ。