Nim (2)
技評の所で [入門]関数プログラミング―質の高いコードをすばやく直感的に書ける! なんてのをやってた。何これ、TV屋風に言うと再放送じゃん。でも、見ごたえ、もとえ、読み応えがあるな。
んな訳で、オイラーも、プログラミングHaskell本をパラパラしてみたんだ。 ふと、巻末の参考文献に目が行った。興味深いのが載ってた。
暗号解読 -- ロゼッタストーンから量子暗号まで、サイモン・シン著 ってやつ。 2001年に新潮社から出たとな。面白そうだな。果たして内容は?
ぐぐってみたぞ。いろいろ出てきた。
亡くなった叔父の暗号日記をTwitterで公開→約4時間で見事解読される こういうのもね。釣られて読んでみた。寄り道だな。
生々しい、暗号解読の実例。平文は果たして日本語? アイヌ語かも知れない。ソーシャル ハッキングとして、叔父さんの年齢は? なんて質問が有ったりして。。。
そのうちに、ある人が、和文モールスの ト(短点)とツー(長点)をひっくり返しているんじゃ ないって提案。それによって解読を心みると、見事に文章になった。 見事なひらめきだな。
これって一種の換字式暗号になるんかな。和文モールスは知らないので、欧文モールスで やってみる。文字Aのモールスは、ト・ツー。ひっくり返すと、ツー・ト。これを文字に デコードするとNになる。(逆にNはAになる)
叔父さんをプロファイリング。和文モールスって、今じゃアマチュア無線ぐらいにしか、 残っていない世界遺産。年の頃は60代以上か。昔、通信兵をやってた? 船に乗って通信士を やってた? 自衛隊に居た?
暗号の歴史に触れたページがあった。 暗号と暗号史の連載が面白かったよ。
いかん、いかん。道草を喰った。
サイモン・シン「暗号解読 ロゼッタストーンから量子暗号まで」新潮社 青木薫訳
C言語実験室
暗号解読をリクエストしに図書館へ行った。そしたら、 コンピュータのしくみがよくわかる! C言語プログラミングなるほど実験室 なんて本が置いてあったので、借りてきた。Windows上のVisual Studio 2013とかの環境を 使って、いろいろ実験してみましょって趣旨。
オイラーはVS2013なんて、持ってないぞ。で、fedoraです。tccです。ソースは丁寧なコメントが、 Shift_JIS語で書かれているので、nkf -w とかしてあげればよい。
ちょいと実験
[sakae@fedora chap05]$ tcc z.c -lm [sakae@fedora chap05]$ ./a.out 素数の一覧 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, 素数p = 29 素数q = 47 鍵n = 1363 鍵e = 3 鍵d = 859 [sakae@fedora chap05]$ tcc -run z.c -lm Segmentation fault (コアダンプ)
何の実験してるやら? 合わせて読みたい、
コンパイラの最適化についてすべてのプログラマが知っておくべきこと
この本のコラムに載ってた、OS種類によって異なる改行の表現って所で、オイラーの古い 知識が是正されたよ。いわく、最近のMAC OS X では、改行の扱いがLFなんですって。
これが何処に効いてくるかと言うと、以前に調べた、rustのtomlの仕様。改行コードは、 Windows用のCRLFと、Unix用のLFです。古い知識のおいらは、MACはCRだから、MAC無視 されてるよって思った訳。よく考えれば、MACのUnix族だから、改行コードはLFだわな。
Nim言語実験室
ちょいと古い、 nimrodが、記録として残っていたぞ。それはさておき、 目標を、今までやってきた、血圧表示アプリに置き、使えそうな要素技術(って言う表現で良かったかな)の 実験をやっておく。
proc idiv(x,y : int) : int = return x div y echo( idiv(8,2) ) echo( 15.idiv(3) )
自分で書いた整数の割り算。第一変数を外に出せるよ。これを利用すると、簡単に、チェーン化 出来るので、使い道がありそう。
qiitaに載ってた実例
from strutils import split, parseInt from math import sum let inputStr = readLine(stdin) # 一行入力(string) inputStrSeq = readLine(stdin).split(':') # ':'でsplit(seq[string]) inputIntSeq = readLine(stdin).split().map(parseInt) # 空白でsplitしてintに変 換(seq[int]) echo("inputStr : ", inputStr) # 引数を連結して出力 echo("inputStrSeq : ", inputStrSeq) echo("sum of inputIntSeq : ", sum(inputIntSeq)) discard """ sample.txt hoge fuga foo:bar:baz 1 1 2 3 5 8 cat sample.txt | nim c -r stdio.nim inputStr : hoge fuga inputStrSeq : @[foo, bar, baz] sum of inputIntSeq : 20 """
discardってのは無視するって命令。これを使って、複数行のコメントを書ける。他にも、 値を返す関数は、その値をちゃんと受け取らないと怒られるんだけど、このdiscardを使うと、 取り合えず、怒りを静められるよ。debug中に便利かな。
parseとtcc
上の例を見ると、parseIntってのは、文字列を数値に直してくれるんだな。そして、その 関数がstrutilsに有ると読み取れる。群落の法則で、多分浮動小数点への変換も有るだろうと 予想が付く。もっとほかに無いの?
import strutils var x : BiggestInt = 23.BiggestInt x = parseBiggestInt("1000000000000000000000000000000000000000000000000001") echo(x)
期待を持って実行すると、
Traceback (most recent call last) parse.nim(6) parse strutils.nim(430) parseBiggestInt parseutils.nim(218) parseBiggestInt parseutils.nim(203) rawParseInt Error: unhandled exception: over- or underflow [OverflowError] Error: execution of an external program failed Process nim-run exited abnormally with code 1
見事に予想を裏切られた。BiggestIntは、int64の事だったのね。 そうそう、23.BiggestIntってのがあるけど、これは、BiggestInt(23)の事。この型変換関数の 第一引数を前に出し、更にカッコを省略するっていう技を使ったもの。括弧の省略は、Rubyや Pythonでお馴染みなんで、取り入れたんかな。
で、これらを実験してた時、
import strutils echo( parseInt("123") )
こんな、一番簡単そうなやつを、tccをバックエンドにしたやつでコンパイル実行すると
[sakae@fedora t]$ nimhs parse.nim Error: unhandled exception: overflow [OverflowError] Error: execution of an external program failed
見事に落ちた。これは何? tccを緩やかに使ってみると、エラーの詳細ももう少し分かるだろう。
[sakae@fedora t]$ nim --cc:tcc c parse.nim [sakae@fedora t]$ ./parse Traceback (most recent call last) parse.nim(2) parse strutils.nim(421) parseInt parseutils.nim(230) parseInt Error: unhandled exception: overflow [OverflowError]
もう、lib/pure/parseutils.nimを見ろと良いヒントが表示されましたよ。
参考に見てく。
proc parseInt*(s: string, number: var int, start = 0): int {. rtl, extern: "npuParseInt", noSideEffect.} = ## parses an integer starting at `start` and stores the value into `number`. ## Result is the number of processed chars or 0 if there is no integer. ## `EOverflow` is raised if an overflow occurs. var res: BiggestInt result = parseBiggestInt(s, res, start) if (sizeof(int) <= 4) and ((res < low(int)) or (res > high(int))): raise newException(OverflowError, "overflow") else: number = int(res)
230行目って、raise文の所。parseIntと言いながら、中では、parseBiggestIntを使っているのね。 なお、関数名の後ろに米印が付いているのは、モジュールから公開されてるよって事を意味してる。 goでの公開関数は、先頭文字が大文字だったな。こういうルールをいちいち覚えるのは、惚け 防止にうってつけ。
if文の前に、
echo("Debug " & $res) echo(sizeof(int), " ", low(int), " ", high(int))
こんなdebug印字を入れて実行してみる。モジュールもソースになってるってのは利点だなあ。 ああ、アンパサントは、文字列の結合ね。文字列の結合なんで、resも文字列が期待される。 数値を文字列にするには、変数名の前に、ダラー文字を置けばよい。それを忘れると、
lib/pure/parseutils.nim(228, 16) Error: type mismatch: got (string, BiggestInt) but expected one of: system.&(x: string, y: string): string system.&(x: char, y: string): string system.&(x: seq[T], y: T): seq[T] system.&(x: string, y: char): string system.&(x: char, y: char): string system.&(x: T, y: seq[T]): seq[T] system.&(x: seq[T], y: seq[T]): seq[T]
こういう小言を言われるけど、分かり易いやつだなあ。
[sakae@fedora t]$ nimhs parse.nim Debug 123 4 -2147483648 2147483647 Error: unhandled exception: overflow [OverflowError] Error: execution of an external program failed
はて、これからどうする? そりゃ、実験コードを組み立てるに決まってる。
let res = 123.BiggestInt if (sizeof(int) <= 4) and ((res < low(int)) or (res > high(int))): echo("overflow") else: echo("OK")
そして、走らせる。
[sakae@fedora t]$ nim --cc:tcc c -r tc.nim overflow
分離成功。で、BiggestIntを外すと、ちゃんとOKになる。コンパイルしてtc.cを眺めると
NIM_EXTERNC N_NOINLINE(void, tcInit)(void) { res_88004 = IL64(123); : LOC5 = (res_88004 < IL64(-2147483648)); if (LOC5) goto LA6; LOC5 = (IL64(2147483647) < res_88004); LA6: ;
このIL64は、nimbase.hに宣言されてるマクロだ。
/* --------------- how int64 constants should be declared: ----------- */ #if defined(__GNUC__) || defined(__LCC__) || \ defined(__POCC__) || defined(__DMC__) || defined(_MSC_VER) # define IL64(x) x##LL #else /* works only without LL */ # define IL64(x) ((NI64)x) #endif
tccの場合は(NI64)xに展開されるんだな。そしてそのNI64は、long long intとな。tccには、 そんなの無いよって事で、調査終了って事にしましょ。まてまてと囁く声がある。
どんなコードを吐くかアセンブラを眺めればいいんだろうけど、tccではアセンブラ出力 出来ない。gdbにかけて逆アセンブラって手を思い付いた。
[sakae@fedora nimcache]$ tcc -g -I/home/sakae/.nim/lib parse.c parseutils.c strutils.c system.c tcc: error: undefined symbol 'dlclose' tcc: error: undefined symbol 'dlopen' tcc: error: undefined symbol 'dlsym'
これってリナちゃん特製なやつをリンクしないと駄目? 最後に、-ldl を追加。
[sakae@fedora nimcache]$ gdb -q a.out BFD: /home/sakae/t/nimcache/a.out: セクション .text.__x86.get_pc_thunk.bx にグループ情報がありません Reading symbols from a.out...done. (gdb) b tcInit Breakpoint 1 at 0x8048424: file tc.c, line 71. (gdb) run Starting program: /home/sakae/t/nimcache/a.out Missing separate debuginfos, use: debuginfo-install glibc-2.20-8.fc21.i686 Breakpoint 1, tcInit () at tc.c:71 71 res_88003 = IL64(31); (gdb) n 75 LOC3 = 0;
tccでコンパイルしたやつは、debug情報が十分に付いていないのだな。
(gdb) disassemble tcInit Dump of assembler code for function tcInit: 0x0804841b <+0>: push %ebp 0x0804841c <+1>: mov %esp,%ebp 0x0804841e <+3>: sub $0x10,%esp 0x08048424 <+9>: mov $0x1f,%eax 0x08048429 <+14>: mov $0x0,%ecx 0x0804842e <+19>: mov %eax,0x804e740 0x08048434 <+25>: mov %ecx,0x804e744 => 0x0804843a <+31>: mov $0x0,%eax
0x1fってのは、31の事で、それを、0x804e740に保存。上位は0でそれをecx経由で0x804e744 に保存してる。これだけ見ると、64ビットのオペレーションをやってるぽいな。
後は、暇にまけせて、解析してみるか。ifの条件を簡略化したのを巻末に挙げておく。
Nimとprocess
nimから他のプログラムを起動して、それとやり取りするにはどうする? nim付属の例に載ってるかと思ったら、無し。しょうがないので、ロゼッタ・ストーンに 刻まれていないか調べてみたよ。
import osproc let (output, exitCode2) = execCmdEx "ls -l" echo(output)
ふむ、こんな風にすると、外部の力を借りて、結果を取り込めるんだな。それはそれで、 いいんだけど、外部のアプリに情報を伝えるにはどうする?
これはもう、ヒントになるosprocを読むしかありません。で、execCmdExを見ます。
proc execCmdEx*(command: string, options: set[ProcessOption] = {poStdErrToStdOut, poUsePath}) : tuple[output: TaintedString,exitCode: int] {.tags: [ExecIOEffect, ReadIOEffect], gcsafe.} =
難解な入出力を整理してみた。入力は、コマンド文字列とオプション。このオプションには、 省略時の設定(poStdErrToStdOut, poUsePath)が集合型で定義されてる。
出力は、タプル型で、プロセスの結果と終了コードになる。最後の.tagsは、コードの追跡 とかにコンパイラーが使う補助情報だ。ユーザーは気にする必要は無い。
これだけの情報を理解した上で、定義の本体を読み取り、自前のコードに落としてみた。 車輪の再発明じゃなくて、プロセスに指示を与えるようにした。
import osproc, streams let text = "plot '-' w l\n1 3\n2 4\n3 2\nend\n" var pid = startCmd("tr 'a-z' 'n-za-m'", {poStdErrToStdOut, poUsePath}) var outp = outputStream(pid) var inp = inputStream(pid) inp.write(text) inp.close() echo(text) var result = (TaintedString"", -1) var line = newStringOfCap(120).TaintedString while true: if outp.readLine(line): result[0].string.add(line.string) result[0].string.add("\n") else: result[1] = peekExitCode(pid) if result[1] != -1: break close(pid) echo(result[0])
上の例は、unix側のtrコマンドを使って、textに指定してる文字列を暗号化する 積もりのコード例だ。世間ではROT13なんて呼ばれる、シーザー暗号の一種。
unixでは、どう暗号化、復号化されるか、一応、見とく。
[sakae@fedora t]$ echo abc | tr 'a-z' 'n-za-m' nop [sakae@fedora t]$ echo nop | tr 'a-z' 'n-za-m' abc
[sakae@fedora t]$ ./exec plot '-' w l 1 3 2 4 3 2 end cybg '-' j y 1 3 2 4 3 2 raq
今度は、起動するプロセスを gnuplot -p に変更して、グラフが書けるか確認。 結果はちゃんと表示されましたよ。やれやれ。と、すんなりグラフが 書けたような涼しい顔をしてるけど、実はすんなりとは行かなかったのさ。
(たぶん)パイプを伝わってgnuplotに行くはずの命令には、グラフ書き終了のendが入って いるので、ちゃんと動くと思ってたんだ。
でも違った。ここでパイプからの入力が終了しましたって言う目印(EOF)を送ってあげないと、 gnuplot側は行動を起さないんだ。
お前、考えが甘いぞ。gnuplotの気持ちになれば、入力終了まで、ループして文字列を 取り込むだろうに。endが来たから、グラフを書きましょ、なんて面倒な事はしないよな。
で、思い出したのが、上記のコード。goだと、パイプをクローズする事により、その目印が 送られる。多分nimも一緒でしょ。喋る言語は違っても、やる事は一緒って事だもの。
文字化けしたファイル、ディレクトリを消す
先の実験室のファイルをDLし、展開した所、文字化けしちゃった。いつもは、そんな事も あろうかと、tmpを作ってその中で展開するんだけど、魔が差した。
そういう時は、inodeを調べて消すんだよって事は覚えていたんだけど、、詳細を 思い出せなかったので、おばあちゃんの知恵袋に聞いてみた。
[sakae@fedora ~]$ ls -i 663524 ?+????\?[???t?@?C?? 658948 godev 656775 rust 658714 t 663625 D 665441 nim 664585 src 131440 z [sakae@fedora ~]$ find . -inum 663524 -exec rm -rf {} \; find: ‘./\304+\356\246?\342\\\374[\342\357\342t\342@\342C\342\357’: そのよう なファイルやディレクトリはありません [sakae@fedora ~]$ ls D godev nim rust src t z
いきなり消すんじゃなくて、mv するのも一つの手
find . -inum 99999 -ok mv '{}' OBAKE \;
変更してよければyで応答。これで、OBAKEに変更出来る。後は、煮るなり焼くなり、 お好きなように。何せ、化ですから。
My Homework
NIM_EXTERNC N_NOINLINE(void, tcInit)(void) { res_88003 = IL64(31); { if (!(res_88003 < IL64(-2147483648))) goto LA3; printf("%s\012", (((NimStringDesc*) &TMP5))->data); } goto LA1; LA3: ; { printf("%s\012", (((NimStringDesc*) &TMP6))->data); } LA1: ; }
Dump of assembler code for function tcInit: 0x0804841b <+0>: push %ebp 0x0804841c <+1>: mov %esp,%ebp 0x0804841e <+3>: sub $0x4,%esp 0x08048424 <+9>: mov $0x1f,%eax 0x08048429 <+14>: mov $0x0,%ecx 0x0804842e <+19>: mov %eax,0x804e6a0 0x08048434 <+25>: mov %ecx,0x804e6a4 0x0804843a <+31>: mov 0x804e6a0,%eax 0x08048440 <+37>: mov 0x804e6a4,%ecx 0x08048446 <+43>: mov $0x80000000,%edx 0x0804844b <+48>: mov %eax,-0x4(%ebp) 0x0804844e <+51>: mov $0x0,%eax 0x08048453 <+56>: cmp %eax,%ecx 0x08048455 <+58>: jg 0x8048471 <tcInit+86> 0x0804845b <+64>: jne 0x804846c <tcInit+81> 0x08048461 <+70>: mov -0x4(%ebp),%eax 0x08048464 <+73>: cmp %edx,%eax 0x08048466 <+75>: jae 0x8048471 <tcInit+86> 0x0804846c <+81>: jmp 0x8048476 <tcInit+91> 0x08048471 <+86>: jmp 0x804848f <tcInit+116> => 0x08048476 <+91>: mov $0x804e454,%eax 0x0804847b <+96>: push %eax 0x0804847c <+97>: mov $0x804e46c,%eax 0x08048481 <+102>: push %eax 0x08048482 <+103>: call 0x804d380 <printf> 0x08048487 <+108>: add $0x8,%esp 0x0804848a <+111>: jmp 0x80484a3 <tcInit+136> 0x0804848f <+116>: mov $0x804e468,%eax 0x08048494 <+121>: push %eax 0x08048495 <+122>: mov $0x804e470,%eax 0x0804849a <+127>: push %eax 0x0804849b <+128>: call 0x804d380 <printf> 0x080484a0 <+133>: add $0x8,%esp 0x080484a3 <+136>: leave 0x080484a4 <+137>: ret End of assembler dump.
上記は、ifの条件を満足して、overflowを表示しようとした所だ。さあ、目が汚れるぞ。 C語実験室にもアセンブラが出てくるけど、理解しなくてもいいよってなってるから、無視 してもいいけど、tccの虫を捕まえた事は確かだな。
参考まで、gccでコンパイルした正解を挙げておく。
tcInit: .LFB8: .cfi_startproc pushl %ebp .cfi_def_cfa_offset 8 .cfi_offset 5, -8 movl %esp, %ebp .cfi_def_cfa_register 5 subl $8, %esp movl $31, res_88003 movl $0, res_88003+4 movl res_88003, %eax movl res_88003+4, %edx cmpl $-1, %edx jl .L9 cmpl $-1, %edx jg .L13 cmpl $-2147483648, %eax jb .L9 .L13: nop .L11: subl $12, %esp pushl $TMP6+8 ;; OK call puts addl $16, %esp jmp .L8 .L9: subl $12, %esp pushl $TMP5+8 ;; overflow call puts addl $16, %esp .L12: nop .L8: leave .cfi_restore 5 .cfi_def_cfa 4, 4 ret .cfi_endproc .LFE8: .size tcInit, .-tcInit