trace(1)
毎週火曜日はファミブのレディースディ。まあ、おいらには関係無い事だが、たまたま女房の アッシー君を務めた時。DVDのコーナーに足を踏み入れてみた。
韓国に占領されてましたねぇ。日本発のアニメはあちこち(の国)を占領しちゃってるから、 余り目くじら立てる事はないけど。で、コーナーをうろうろしてたら、「天使と悪魔」なんて のが陳列されてたので、女房のレンタルリストに追加して貰った。
確かこのシリーズは、ダン・ブラウンという作者によるものだ。初作は「モナリザの暗号」 だか、「ダビンチ・コード」 たったけかな。友人に借りて全てのシリーズを読んだ記憶がある。懐かしくなって、今度は DVDで見るかとなったのだ。
CERNが出てきて、そこで開発された「反物質」が盗まれて、、、って事で、話は進んで行く 訳だが、CERN == Web(HTML)と連想してしまうおいらは、職業病だろうか? ハードカバーで 2冊分がDVDだと2時間と言うのはあっけないな。やはり、おいらには本が向いていると 思った次第。面白くなって、寝食を忘れて見るのは、本に限る。
暗号解読
本の虫で、「プログラミング Haskell」を読んでいる訳だが、その中にシーザー暗号の作り方と 解読方法の例が出てくる。作り方はともかく、解読方法は、いろいろ試して、文字の出現頻度の 検定を行うとの事だった。これって、ポーの「黄金虫」にもあったような。
この解読方法は理解出来るんだけど、平文がプレーンテキストじゃ無かった場合はどうするんだろう? たとえば、平文を圧縮しちゃうとか、Paintソフトあたりを使ってキャンパスに描いた文字をBITMAPに変換しちゃうとか いわゆるバイナリー化を施してしまう。そして、そのバイナリーデータを暗号にしちゃうのだ。 そうすると、解読には、正しいバイナリーに戻す必要がある。だけど、バイナリーの状態では 出現頻度の検定とか辞書のマッチング等の手は使えない。
バイナリーの解読には、どういう方法が有効なのだろう? 勿論、平文をバイナリーにした時の 解読に必要になりそうな情報(たとえば、ヘッダー情報)は、削除しておくとか、目くらましの 偽情報を混ぜておくとかは、普通にやるだろうから。 こういう事は、 サイモン・シンの本に載っているのだろうか? あるいは、実践ウィルスの 作り方講座あたりが参考になるのだろうか?
debugの友 @oreore_lisp
先ほどから、暗号だとか虫だとかヤバ目な言葉が出てきている。暗号なんて言葉はCIAに傍受 されてそうなんで、もうしない。虫の話なら、ま、問題ないだろう。oreore_lispでdebugした い時、evalなりapplyにBPを置いて調べると思う。受け取った引数がリストだった場合、どんな リストか簡単に見られれば便利だ。
ん?、リストを表示する関数って、oreoreが自前で持ってるよね。そう、print_e だ。こやつを gdbの中で使えればいいんだ。それには、.gdbinit に、登録しておく必要がある。実際に 登録する関数は、sprint_e にした。
define z print sprint_e( $arg0 ) end
ちょっと使い心地を検証してみる。
(gdb) b eval Breakpoint 1 at 0x8049fc6: file oreore_lisp.c, line 549. (gdb) run Starting program: /usr/home/sakae/oreore_lisp/tl lisp> :l base Breakpoint 1, eval (e=0x804c0e8, env=0x0) at oreore_lisp.c:549 warning: Source file is more recent than executable. 549 if (e == NIL || IS_INT(e)) return e; (gdb) z e $1 = 0xc4fdd40 "(setq def (macro (name arg body) (list (quote setq) name (list (quote lambda) arg body))))" (gdb) z top_env $2 = 0xc4fdd40 "((cond *sform*.134517280) (setq *sform*.134517024) (macro *sform*.134517456) (lambda *sform*.134517392) (quote *sform*.134517008) (eval *func* 134520768.000101) (list *func* 134516992.-00001) (acons *"...
よく使うコマンドなので、コマンド名が一文字と言うのは正解だな。でも、人間は欲の塊なので breakしたら、自動的に、"z e"をやって欲しいなあ。gdbには、dispが用意されてて、breakしたら dispで登録しておいた変数を自動表示してくれるけど、コマンドの自動実行はdispに登録できない。
そんな時は、そのものずばりの commandと言うのが用意されている。
(gdb) command Type commands for when breakpoint 1 is hit, one per line. End with a line saying just "end". >z e >end (gdb) c Continuing. Breakpoint 1, eval (e=0x804c1e8, env=0x804c350) at oreore_lisp.c:549 549 if (e == NIL || IS_INT(e)) return e; $5 = 0xc4fdd40 "(list (quote setq) name (list (quote macro) arg body))" (gdb) info breakpoints Num Type Disp Enb Address What 1 breakpoint keep y 0x08049fc6 in eval at oreore_lisp.c:549 breakpoint already hit 11 times z e (gdb) b apply Breakpoint 2 at 0x8049aa8: file oreore_lisp.c, line 499. (gdb) command Type commands for when breakpoint 2 is hit, one per line. End with a line saying just "end". >z func >z args >end (gdb) c Continuing. Breakpoint 2, apply (func=0xc4f62c0, args=0x804c1f0, env=0x804c350) at oreore_lisp.c:499 499 cell fbody = eval(func, env); $8 = 0xc4fdd40 "quote" $9 = 0xc4fdd40 "(setq)"
これで、随分と虫を駆除出来るかな?
さてと、前回、Schemeなら当たり前にtraceが備わっていると書いた。けど、このoreoreは 最小セットなので、traceなんてのは、勿論無い。無いなら作ろうと思うけど、どうやって 作ればいいんだい? そんな時は、先輩はどうやったか調べて技を盗むのがよかろう。
gaucheでtrace
最近、1年ぶりにVerUPされて、0.9になった。 関係者の皆様お疲れ様でした。今回から、Windows版 も、インストーラー付きとなったので、導入はWindowsでも容易なはずだ。
で、traceだが、gaucheには標準で提供されていない。(とまあ、RWHに習って、卓袱台を ひっくり返してみました)でも、世の中には、いろいろなschemeの過不足を補ってくれる ポータブルなコードを提供してくれているありがたいお方がいる。
通称 slib というのがそれだ。 落としてきて、/usr/local/ 以下に展開。rootになって、goshを起動して (use slib)(require 'logical) を 一度行っておくと、普通に使えるようになる。(これは、カタログを作る作業です。) 早速使ってみる。
gosh> (use slib) #<undef> gosh> (require 'trace) #t gosh> (trace fact) #<closure (debug:trace-procedure debug:trace-procedure)> gosh> (fact 5) CALL fact 5 CALL fact 4 CALL fact 3 CALL fact 2 CALL fact 1 RETN fact 1 RETN fact 2 RETN fact 6 RETN fact 24 RETN fact 120 120 gosh> (untrace fact) #<closure fact> gosh> (fact 5) 120
こういうありがたいコードがサーと読めればいいんだけど、汎用的に作ってあるせいか、難読 (勿論、私には、、であるが)である。もっと、簡易なのがないかと探してみる。
と、ggcというパッケージに、traceが含まれている 事が判明。早速、解剖してみる。(trace.scm と trace2.scmの2つが提供されてますが、原理の 勉強という理由で、trace2.scmをちと改変して実験台とします。)
(define-macro (trace proc) `(let ((proc-under-trace ,proc)) (set! ,proc (let ((level 0) (retval #f)) (lambda x (format #t "~a:~a~a~%" level (make-string level #\space) (cons ',proc x)) (set! level (+ level 1)) (set! retval (apply proc-under-trace x)) (format #t "~a->~a~%" (make-string level #\space) retval) (set! level (- level 1)) (when (= level 0) (set! ,proc proc-under-trace)) retval)))))
これがtraceを実現するマクロです。以下、このマクロを使って、factをトレースしてみます。
gosh> fact #<closure fact> gosh> (fact 3) 6 gosh> (trace fact) #<closure #f> gosh> fact #<closure #f>
traceをセットする前は、#<closure fact>となっていましたが、セットした後は#<closure #f> に変化してる事に注目。それでは、実行します。
gosh> (fact 3) 0:(fact 3) 1: (fact 2) 2: (fact 1) 3: (fact 0) ->1 ->1 ->2 ->6 6 gosh> (fact 3) 6
一度実行すると、trace機能はリセットされました。 では次に、マクロの実行でどのように展開されるか見てみます。
gosh> (%macroexpand (trace fact)) (let ((proc-under-trace fact)) (set! fact (let ((level 0) (retval #f)) (lambda x (format #t "~a:~a~a~%" level (make-string level #\space) (cons 'fact x)) (set! level (+ level 1)) (set! retval (apply proc-under-trace x)) (format #t "~a->~a~%" (make-string level #\space) retval) (set! level (- level 1)) (when (= level 0) (set! fact proc-under-trace)) retval))))
これでは、醜いのでemacsに持って行って、整形してみます。
(let ((proc-under-trace fact)) (set! fact (let ((level 0) (retval #f)) (lambda x (format #t "~a:~a~a~%" level (make-string level #\space) (cons 'fact x)) (set! level (+ level 1)) (set! retval (apply proc-under-trace x)) (format #t "~a->~a~%" (make-string level #\space) retval) (set! level (- level 1)) (when (= level 0) (set! fact proc-under-trace)) retval))))
これが、traceを可能にするためのコードです。全体が大きなletになっています。 proc-under-traceと言う変数に、trace前(オリジナルの)fact手続きを保存してます。
次のset!で、新たに(trace機能付きの)factを定義し直しています。で、letで 局所変数を2つ定義。levelは、再帰の深さを。retvalは、trace対象の手続きの戻り値の保存に 使われています。
次のlambdaで、実際のtrace機能付きfactの再定義をしています。factを呼ぶ前の状況を表示。 一回呼ぶと言う事でレベルを増やし、applyで元のfactを呼び出し、結果を保存。そして、結果を 表示。呼ばれた後なので、レベルを減らす。なお、(lambda x ... となっているが、この x は、 再定義した手続きに渡ってくる引数を、リストとして受け取る意味になる。
そして、レベルがゼロだったら、終了なので、保存しておいた本来のfactに挿げ替え。最後の retvalは、再定義したfactからの戻り値。うーん、なかなか巧妙ですねえ。
標準でtraceをサポートしてるscheme
すぐに思いつくのはDrSchemeだ。stepperまで付いてて、 面白いんだけど、独自拡張が過ぎて、slibの作者さんからも見放されている。 私も重すぎるので見放した。RnRSの委員会の人も、どうかねぇと言っている。
しっかりしたマニュアル(Webに公開されてる)と共に使えると言う事で (chez (chez scheme))が出している、無償版のpetiteが お勧め。有償版はコンパイラー付きだけど、無償版はインタープリタのみの提供。軽くて 速いのが嬉しい。また、SWLという repl は、カッコの対応や自動インデントをしてくれる ので、非常に手軽。
Petite Chez Scheme Version 7.4 Copyright (c) 1985-2007 Cadence Research Systems > (define (fact n) (if (= n 1) 1 (* n (fact (- n 1))))) > (fact 3) 6 > (trace fact) (fact) > (fact 3) |(fact 3) | (fact 2) | |(fact 1) | |1 | 2 |6 6
あれ? SICP用に作られたMIT Schemeには 、traceって有ったけかな? 説明書を調べてみたら、有ったよ。debug機能も充実している。
procedure+: trace-both procedure procedure+: trace procedure Equivalent to calling both trace-entry と trace-exit on procedure. trace is the same as trace-both. (trace-both fib) (fib 3) -| [Entering #[compound-procedure 19 fib] -| Args: 3] -| [Entering #[compound-procedure 19 fib] -| Args: 1] -| [1 -| <== #[compound-procedure 19 fib] -| Args: 1] -| [Entering #[compound-procedure 19 fib] -| Args: 2] -| [2 -| <== #[compound-procedure 19 fib] -| Args: 2] -| [3 -| <== #[compound-procedure 19 fib] -| Args: 3] => 3 procedure+: untrace-entry [procedure] Stops tracing the entry of procedure. If procedure is not given, the default is to stop tracing the entry of all entry-traced procedures.
それにしても、traceの有る/無しで、お勧めschemeを上げてみるってのは、作者様に 失礼だぞ!!
gaucheを便利に使うために
とまあ、traceに注目して、超偏見でお勧めschemeを上げた訳だが、traceが備わっているか どうかなんてのは、まあネタと言う事で。(ああ、またちゃぶ台ヒックリ返しちゃったよ。)
実用的には、コードを書く時にサーと書けるかが問題になってくる。先ほど gaucheな人達が集う場所に 行ってみたらgauche用の補完ファイルが紹介されてた。 私は普段gauche(gosh)を使う時はemacsの上から使っているので、補完も普通に使えるから 問題ないんだけど、ちょっとreplを動かしたい時は、shellから素のgoshを起動しちゃう事が ある。そんな時、補完が出来るとマンモスウレp :-) (関係ないけど、あの人もタレント再開で、 マンモスウレp、、最後にpを付けるって、ひょっとしてLisper?)
この補完用辞書に4000余りが登録されてました。巨大なnamespace だなあ。びっくりですよ。 R5RSのScheme仕様書はわずか50ページ程しかないのに、随分と便利な関数が作られたものです。 今後もどんどん成長して行くのだろうか?