秀スクリプトの話

706 Views

November 30, 17

スライド概要

秀スクリプトの話
2017年11月の#ssmjp
http://bit.ly/2AesqgD

profile-image

徳丸本の中の人 OWASP Japanアドバイザリーボード EGセキュアソリューションズ取締役CTO IPA非常勤職員 YouTubeチャンネル: 徳丸浩のウェブセキュリティ講座 https://j.mp/web-sec-study

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

関連スライド

各ページのテキスト
1.

秀スクリプトの話 徳丸 浩

2.

徳丸浩の自己紹介 • 経歴 – 1985年 京セラ株式会社入社 – 1995年 京セラコミュニケーションシステム株式会社(KCCS)に出向・転籍 – 2008年 KCCS退職、HASHコンサルティング株式会社(現社名:EGセキュアソリューションズ株式会社)設立 • 経験したこと – 京セラ入社当時はCAD、計算幾何学、数値シミュレーションなどを担当 – その後、企業向けパッケージソフトの企画・開発・事業化を担当 – 1999年から、携帯電話向けインフラ、プラットフォームの企画・開発を担当 Webアプリケーションのセキュリティ問題に直面、研究、社内展開、寄稿などを開始 – 2004年にKCCS社内ベンチャーとしてWebアプリケーションセキュリティ事業を立ち上げ • 現在 – EGセキュアソリューションズ株式会社 代表 https://www.eg-secure.co.jp/ – 独立行政法人情報処理推進機構 非常勤研究員 https://www.ipa.go.jp/security/ – 著書「体系的に学ぶ 安全なWebアプリケーションの作り方」(2011年3月) 「徳丸浩のWebセキュリティ教室 」(2015年10月) – 技術士(情報工学部門) 2

3.

なぜ秀スクリプトを作ろうと思ったか? • テキストエディタとして普段秀丸を使っているのですが… • なんとなく、秀丸のことを見下されているような気がするわけですよ – 気のせいかもしれないけど • vimは賞賛されるのに、秀丸は賞賛されない、この違いはなんだ… • vim にはVim Scriptがあるが、秀丸には秀丸スクリプトがない… – 秀丸マクロならあります • ないなら作ってしまえ、格好いいやつを… • それが、私の、秀丸愛の証明だ… • (うっそー、と思われたかもしれませんが、これが本当の理由です) 3

4.

言語処理系とわたし • 実は若い頃からコンパイラ作りを趣味としていた • 代表作は Pascal コンパイラ Cabezon (カベソン) – – – – – Cabezon 自体でコンパイラが記述され、自身をコンパイルできる MS-DOS上で動作 MASM 等のアセンブリ言語を生成 自分自身の記述のみを目標においていたので、「実用」は考えていなかった しかし、いくつかの大学で Pascal演習用に使われたらしい • Cabezon使ってましたという方にお会いしたことも… – 東欧でも、「マシンパワーが貧弱でも動くPascalコンパイラ」として教育機関 で使われたらしい(お礼のメールから) – (もう少しちゃんと作っておけば) • お仕事でも、JavaScriptに似たGreenScriptという処理系を作った経験 4

5.
[beta]
秀丸マクロのサンプル
#n=1;
while (#n <= 10) {
call fib #n;
insert str(##return) + "\n";

//
/*
//
//

コメント形式1
#で始まる識別子は数値型変数
コメント形式2 while や if が使える */
関数・サブルーチンは call で呼び出す
関数の戻り値は、##return あるいは $$return

#n = #n + 1;
}
endmacro
fib:
if (##1 <= 2) {
return 1;
}
call fib ##1 - 2;
##temp = ##return;
call fib ##1 - 1;
return ##temp + ##return;

// マクロを停止する。これがないと関数の中身に突っ込む
// 関数 fib の定義
// ##1 は一番目の引数(数値)

// 再帰呼び出しが可能
// 戻り値を覚えておく。## で始まる識別子は数値型ローカル変数
// return 式; で値を返す

5

6.

秀丸マクロのイケテナイ点 • • • • • • • • 全体的に独自仕様が強く、いかにも昔のマクロという感じ 変数に型があり、一文字が $ か # で決まる 関数の戻り値を疑似変数 $$return または ##return で受けとる 変数の宣言は必要ないので、変数名をタイプミスすると、わかりづら いバグの原因となる サブルーチン・関数はただのラベルなので、上から「なだれ込む」 組み込みのサブルーチンは「文」と呼ばれ、ユーザー定義のサブルー チンと呼び出し方が違う 組み込みの関数は、値をダイレクトに受け取れる サブルーチンの呼び出しネストが最大20までと厳しい 6

7.

関数の戻り値を疑似変数 $$return または ##return で受けとる • 以下のように書きたいが書けない while (foo() > 0) { // 色々な処理 } • こうなる(辛い) call foo; while (##return > 0) { // 色々な処理 call foo; } 7

8.

目標を決める • 少なくとも外見上は、今風の言語仕様に見えること • 当初は以下の目標を考えていたが… – 秀丸マクロを生成するトランスパイラ型の処理系 – コンパイラ自体は秀丸マクロで記述 • 秀丸マクロで開発は辛い… • 自分自身が書けると格好良い • なので、以下を目標とする – 秀丸マクロを生成するトランスパイラ型の処理系 – 秀スクリプト自身で自分自身を記述できる 8

9.

ブートストラップ方針 • 一般に言語Aの処理系を言語A自体で開発するためには、「最初はどう するのだ」という、鶏が先か卵が先かというジレンマが生じる – ブートストラップ問題という • 考えられる方法は以下の2つ… A) 秀スクリプトを既存言語のサブセットとして定義し、その既存言語のコンパ イラでコンパイルする B) 最初は秀丸マクロでコンパイラを書き、次に秀スクリプトでコンパイラを書 き直す • B)案はめんどうなので、A)案を採用 • だが、既存言語(以下、親言語と記載)として何を選ぶか? 9

10.

親言語を決める • 秀スクリプトの記述言語(親言語)として何を選択するか? • 求められる条件は下記の通り – 親言語は、秀スクリプトのスーパーセットになるので、親言語を決めることは、 秀スクリプトの言語仕様を決めることに近い – 秀丸マクロを生成することから、秀丸マクロと極端に異なるパラダイムの言語 は選択しにくい – 秀丸マクロは変数に型があるので、親言語も変数に型があるものが望ましい – グローバル変数を多用せざるを得ないのでグローバル変数が使いやすいもの – Windows上のコンソールアプリケーションが開発できるもの • 変数に型があるスクリプト言語ということで、TypeScriptを採用 – それまでTypeScriptもnode.jsも書いたことはなかったw 10

11.

秀スクリプトの言語仕様 • TypeScriptに似た言語仕様を持つ…と言っても高度な機能はないので、 昔のBASIC程度の機能 • 変数、関数の宣言時には型を直接または間接に指定すること var x: number; // 明示的な型指定 var s: string; // 同上 var a = x + 1; // 間接的な型指定(型推論?) function factorial(x: number) : number { … } // 関数定義 • 数値(整数)型と文字列型、これらの配列をサポート • if、while、do … whileの制御構造 • 関数のサポート 11

12.

Demo 秀スクリプトコンパイラをコンパイルしてみる 1. node.js で 2. 秀丸で 12

13.

配列について • 数値型と文字列型配列が可能 var a1 : number[]; // 整数型の配列変数 var a2 : number[] = new Array(); // 同上 // TypeScriptとしても動作させたい時はこの記述とする var a3 : string[]; // 文字列の配列変数 var a4 : string[] = new Array(); // 同上 // TypeScriptとしても動作させたい時はこの記述とする • = new Array() はTypeScriptとの互換性のために用意していて、秀 スクリプトコンパイラは単に読み捨てる • 配列の添字は整数のみで、文字列を添え字とする配列(連想配列)は ない 13

14.

Inside 秀スクリプト(1) 構文解析 • 手書きコンパイラの構文解析は、再帰下降型が主流…かと • EBNF (拡張バッカスナウアーフォーム)をそのままプログラミング • 掛け算と足し算のみの文法の構文解析の例 primary : Identity | number multiply : primay { "*" primary } add : multiply { "+" multiply } function primary() { if (sym == IDENT) { getsym(); return identity; } else if (sym == NUMBER) { getsym(); return number; } else error(); } function multiply() { primary(); while (sym == "*") { getsym(); primary(); } } function add() { multiply(); while (sym == "+") { getsym(); multiply(); } } 14

15.
[beta]
JavaScript(TypeScript)は演算子の優先順位が複雑…
演算子の種類

対応する演算子

メンバ

. []

インスタンスの呼び出し/作成

() new

否定/インクリメント

! ~ - + ++ -- typeof void delete

乗算/除算

*/%

加算/減算

+-

ビットシフト

<< >> >>>

関係

< <= > >= in instanceof

等値

== != === !==

ビット論理積

&

ビット排他的論理和

^

ビット論理和

|

論理積

&&

論理和

||

条件

?:

代入

= += -= *= /= %= <<= >>= >>>= &= ^= |=

コンマ

,
15

16.

演算子の優先順位が複雑だと関数の呼び出しネストも深くなる logicalOrExpression logicalAndExpression bitwiseOrExpression bitwiseXorExpression bitwiseAndExpression equalityExpression relationalExpression shiftExpression additiveExpression multiplicativeExpression unaryExpression primaryExpression これは、 やっておれん 16

17.

式の構文解析に演算子順位構文解析を併用する • 演算子順位構文解析とは、演算子の優先順位を元にテーブル駆動で構 文解析する手法 • 教科書的にはこんな表を使うが… • 秀スクリプトでは2次元データ構造は難しいので1次元にしたい • 簡単にするため、2項演算子のみ演算子順位構文解析を使うことに 17

18.

秀スクリプトの演算子順位表 operators[0] = "!"; opPriority[0] = 2; hidePriority[0] = "5"; operators[1] = "*"; operators[2] = "/"; operators[3] = "%"; opPriority[1] = 3; opPriority[2] = 3; opPriority[3] = 3; hidePriority[1] = "1"; hidePriority[2] = "1"; hidePriority[3] = "1"; operators[4] = "+"; operators[5] = "-"; opPriority[4] = 4; opPriority[5] = 4; hidePriority[4] = "2"; hidePriority[5] = "2"; operators[6] operators[7] operators[8] operators[9] opPriority[6] opPriority[7] opPriority[8] opPriority[9] hidePriority[6] hidePriority[7] hidePriority[8] hidePriority[9] = = = = "<"; "<="; ">"; ">="; = = = = 5; 5; 5; 5; = = = = "3"; "3"; "3"; "3"; operators[10] = "=="; operators[11] = "!="; opPriority[10] = 6; opPriority[11] = 6; hidePriority[10] = "3"; hidePriority[11] = "3"; operators[12] operators[13] operators[14] operators[15] operators[16] opPriority[12] opPriority[13] opPriority[14] opPriority[15] opPriority[16] hidePriority[12] hidePriority[13] hidePriority[14] hidePriority[15] hidePriority[16] = = = = = "&"; "^"; "|"; "&&"; "||"; = = = = = 7; 8; 9; 10; 11; = = = = = "1"; "1"; "1"; "4"; "4"; 18

19.

演算子順位構文解析の様子(初期状態) a * b + c * d スタック(空) 5 4 3 2 1 0 19

20.

演算子順位構文解析の様子(a をプッシュ) a * b + c * d スタック 5 4 3 2 1 0 #a 20

21.

演算子順位構文解析の様子(* b をプッシュ) a * b + c * d スタック 5 4 3 2 #b 1 * 0 #a 21

22.

スタックトップと次の演算子の優先順位を比較 a * b + c * d スタック 5 4 3 2 #b 1 * 0 #a 演算子の優先順位を比較 →乗算の方が優先なので還元処理 22

23.

演算子順位構文解析の様子(還元処理) a * b + c * d スタック 5 4 3 2 1 0 (#a*#b) 23

24.

演算子順位構文解析の様子( + c をプッシュ) a * b + c * d スタック 5 4 3 2 #c 1 + 0 (#a*#b) 24

25.

スタックトップと次の演算子の優先順位を比較 a * b + c * d スタック 5 4 3 2 #c 1 + 0 (#a*#b) 演算子の優先順位を比較 →乗算の方が優先なので還元しない 25

26.

演算子順位構文解析の様子( * d をプッシュ) a * b + c * d スタック 5 4 3 2 1 0 #d * #c + (#a*#b) 式の処理が終わったので、この後は スタックの中身を還元処理していく 26

27.

演算子順位構文解析の様子(スタックトップを還元処理) a * b + c * d スタック 5 4 3 2 1 0 #d * #c + (#a*#b) スタック 還元 5 4 3 2 (#c*#d) 1 + 0 (#a*#b) 27

28.

演算子順位構文解析の様子(スタックトップを還元処理) a * b + c * d スタック 5 4 3 2 (#c*#d) 1 + 0 (#a*#b) スタック 還元 5 4 3 2 1 0 ((#a*#b)+(#c*#d)) 28

29.

生成コードが括弧だらけになるのはちょっといや • 人が見るソースではないので「気にしない」という選択はあり • 僕はいやだった • 秀スクリプト(TypeScript)と秀丸マクロは式の優先順位が異なる • a * b | c * d は、 #a * #b | (#c * #d) とコンパイルするのが望ましい 29

30.

式の意味解析とデータ形式 • 意味解析とコード生成では、式のコンパイル結果をデータとして保持 する必要がある – ツリーなどの形式が一般的だが – 秀スクリプトは構造体的なものがないので苦しい + • 以下の文字列形式とした( a*b+c/d のコード) 2nR#a*#b+#c/#d トップレベル演算子の優先順位 このケースは加算で 2 式の型 n: 数値型 s: 文字列型 N: 数値配列 S: 文字列配列 / * #a #b #c #d 秀丸マクロ形式の式 左辺値または右辺値 L: 左辺値 R: 右辺値 30

31.

秀丸マクロのいやらしいところ 特に注意! 「!」の優先順位が「||」や「&&」と同じなので、例えば if( #a || !#b) if( !#a || #b) この式は、実行時警告が表示される(うざい) のような表記はうまくいきません。このような表記は、 if( #a || (!#b) ) if( (!#a) || #b ) のように記述する必要があります。それと、「||」と 「&&」の優先順位も同じになってしまってるので、例えば if( #a || #b && #c ) のような表記もC言語とは意味が違ってしまいます。このような表記は、 if( #a || ( #b && #c ) ) のように、必要な箇所をカッコで囲むなどして優先順位をはっきりさせるのがお勧めです。 秀丸マクロのヘルプより引用 31

32.
[beta]
2項演算子のコード生成
function genBianryOp(code1: string, op: string, code2: string): string {
var bopPriority = getHidePriority(op);
// 演算子の優先順位
var priority1 = getCodePriority(code1);
var type1 = getCodeType(code1);
code1 = getCodeBody(code1);

第1オペランドの優先順位、型、式の中身

var priority2 = getCodePriority(code2);
var type2 = getCodeType(code2);
第2オペランドの優先順位、型、式の中身
code2 = getCodeBody(code2);
var etype = checkBinOpType(op, type1, type2); // 式の型
if (priority1 > bopPriority || ((priority1 == "4") && (bopPriority == "4")) )
code1 = "(" + code1 + ")";
秀丸の「いやらしいところ」
if (priority2 >= bopPriority)
の対処として括弧をつける
code2 = "(" + code2 + ")";
return bopPriority + etype + "R" + code1 + op + code2;
}

32

33.

関数の前方参照 • JavaScriptやTypeScriptは、関数の前方参照ができる – まだ関数が定義されていない状態で関数を呼び出せる foo(); // 関数の呼び出し…この段階ではまだ関数は定義されていない …. function foo() { … } // 関数の定義 • 秀スクリプトはワンパスコンパイラなので、前方参照は難しい // エラーになる例 function bar() { var x = foo(3); } // この段階では foo()が未定義なのでエラーになる // ... function foo(x: number): number { ... bar(); } // 関数fooの定義 33

34.

関数の前方参照…関数の前方宣言の導入 • TypeScriptと秀スクリプトのつじつまを合わせる形で、関数の前方宣 言をサポートした // アロー演算子を使って関数の前方宣言を行う var foo: (x: number) => number; function bar() { var x = foo(3); } // 前方宣言があるのでエラーにならない // ... foo = function (x: number): number { ... } bar(); // bar()経由でfoo()を呼び出す // 関数fooの定義 34

35.

コード生成の例 • 以下の秀スクリプトは function foo():number { } while (foo() > 0) { } • 以下のようにコンパイルされる goto _end_foo foo: // ここに関数の中身 return 0; _end_foo: goto _LL1 _LL0: // ここにループの中身 _LL1: // continue用ラベル call foo ;#_0=##return; if (#_0>0) goto _LL0 _LL2: // break用ラベル // 関数を呼び出し、一次変数 #_0 に受ける // 秀丸マクロにも while はあるが一次変数を使うためにgotoで 35

36.

TypeScriptと秀スクリプトの両方でコンパイルする工夫 • 秀スクリプトはTypeScriptと互換性があるので、コンパイラはどちら でもコンパイルできるようにしたい TypeScript 秀スクリプト できるだけTypeScriptと秀スクリプトの 共通部分でコンパイラを書く • • • • できるだけTypeScriptと秀スクリプトの共通部分でコンパイラを書く しかし、各処理系固有の記述も現実には必要になる 同一ソースで、どうやって処理を切り替えるか? TypeScriptには条件付きコンパイルはないようだし… 36

37.

TypeScriptと秀スクリプトの両方でコンパイルする工夫 • 以下のようにした if (version() > 0) { // version()は秀丸マクロでは 正の値を返す … // 秀スクリプト固有の処理 } //EOF //EOFは秀スクリプトのEOFであり、これ以降を無視する … // TypeScript 固有の処理 (TypeScript は //EOF を無視する) … // この内容は秀スクリプト処理系は見ないので好きに書ける // version()の定義。TypeScriptは関数の前方参照が可能 // TypeScriptとして動かした場合、version()は 0 を返すように定義 function version() : number { return 0; } 37

38.

サブルーチンのネスト制限問題(1) • 秀丸マクロには、サブルーチンのネスト回数の厳しい制限がある – callされたサブルーチンからさらにサブルーチンを呼ぶこともできます。最高 20回程度重複可能です。 • 20ネストで十分と思うでしょ? 再帰下降型構文解析だと苦しいのよ • 色々試みたがギブアップ 38

39.

サブルーチンのネスト制限問題(2) • 秀シリーズサポートフォーラムでお願いしてみた • ockeghem さん …「callされたサブルーチンからさらにサブルーチンを呼ぶこともできます。最高20回程度重複可 能です。」という制限にひっかかってしまい、関数呼び出しの三重にネストした程度の式でエ ラーになってしまいます。 他ではあまりニーズはないことと思うので恐縮ではあるのですが、この 20回という制限を緩和し てはいただけないでしょうか? • 秀丸担当さん サブルーチンのネストの上限は確かに20回の制限があります。 無制限にするとしたらプロセスをまたぐなどの都合があって面倒になるのですが、単純に上限の 数を増やすだけであれば簡単な修正でできると思います。 別言語からマクロを生成するというのはすごいですね。 三重ネストで20回消費するようなマクロが生成されるということだとしたら、約7倍(140回)以上 あればいいでしょうか。 それでよければ140回か、きりのいいところで200回くらいにしてみようかと思います。 http://www.maruo.co.jp/hidesoft/1/indexg.html より引用 39

40.

秀スクリプトのコンパイル・実行方法 (1) TypeScript+Node.jsとして実行する – hidescript.ts をTypeScriptコンパイラによりコンパイルする。hidescript.jsが生成される – hidescript.js をNode.js上で実行する C:> node hidescript.js foo.hs – foo.mac ができるので、秀丸で実行する ※ 環境変数 hidemacrodirに秀丸マクロ用フォルダ名をセットしておくと、 そのフォルダにfoo.macが書き込まれます。 (2)秀丸マクロとして実行する – – – – – hidescript.mac を hs.mac にリネームして秀丸上でマクロ登録しておく foo.hsを秀丸で開く ショートカットキー等によりhs.macを実行する foo.macのウィンドウが開き、コンパイル結果が挿入、保存される foo.macをショートカット等で実行する 40

41.

テストについて 41

42.

当初、テストはなかった (ぼそっ) 42

43.

,、,,,、,,, _,,;' '" '' ;;,, (rヽ,;''""''゛゛;,ノr) ,; i ___ 、___iヽ゛;, テスト書いてないとかお前それ@t_wadaの前でも同じ事言えんの? ,;'''|ヽ・〉〈・ノ |゙ ';, ,;''"| ▼ |゙゛';, ,;'' ヽ _人_ / ,;'_ /シ、 ヽ ⌒⌒ / リ \ | "r,,`"'''゙´ ,,ミ| | リ、 ,リ | | i ゛r、ノ,,r" i _ | | `ー――-----------v ⌒´ ) (ヽ _____________ ,, _´) (_⌒_______________ ,, ィ T | | | 43

44.

すっ、すまん…儂が悪かった 44

45.

テストを書くにはどうしたらよいか? • 秀スクリプトはNode.js上で動くので、Node.jsに対応したテストシス テムを使えば良いが… • 実機テストとしては秀丸を起動しないといけない • 秀丸上でコンパイル、実行結果などを把握する必要がある • 既存のテストシステムでもやりようはあるのだろうけど、調べるのが 面倒くさい • 作ってしまえ 45

46.

秀スクリプト・テストシステムの概要(1) ディレクトリ構成 hs-test | +-- Test | +-- TestRef | +-- TestResult hs-test.js hs-test.cmd が置かれている テストコード テストの基準データ テスト結果(hs-testが生成する) 起動方法 起動には、同梱の hs-test.cmd を用います。 C>hs-test [テストファイル名 ...] テストファイル名は省略可能で、省略した場合は、Test フォルダ内の全ての .hst ファイルがテストされ ます。 46

47.

秀スクリプト・テストシステムの概要(2) テストファイルの構成 テストコード(拡張子は .hst)は、秀スクリプトのコードに以下のテスト指令を追加したものです。 //status=0 または 1 //message=メッセージ //diff=1 //exec=1 //expected=1 // 秀スクリプトのコード [省略不可; コンパイラのステータス 正常:0 エラー:1] [省略可; コンパイラのエラーメッセージ] [省略可; diff=1 の場合、コンパイル結果をTestRefフォルダ下の .macファイルと比較する] [省略可; diff=1 の場合、秀丸マクロの実行結果をTestRefフォルダ下の .outファイルと比較] [省略可; expected=1 の場合、予定されたエラーであることを示す] テストの例 //message=breakできません //status=1 // if (1) { break; } insertln("done"); quit(); 47

48.

テスト・システムは作って本当によかった • 自分自身をコンパイルできるとは言え、それは正常系が一応動くとい うこと • コンパイラは、異常系の処理(構文エラー、意味的なエラー)の処理 が割合としては多い • テストも、異常系のテストが多い • テストを作成して動かしてみると、多くのバグが見つかった • @t_wada さんに感謝 48

49.

Demo 49

50.

まとめ • TypeScriptに似た文法を持ち、秀丸マクロを生成する「秀スクリプ ト」という言語処理系を作成した • 秀スクリプトは秀スクリプト自身で記述されており、(やろうと思え ば)秀丸上で自分自身をコンパイル可能… – ただし、すごく遅い… Node.js の 3000倍くらい遅い! • 秀スクリプトは、秀丸マクロの制限からデータ構造が弱く、コンパイ ラ開発は苦労した – 昔のBASICでコンパイラを書く気分 – …がそれが楽しいともw • 秀スクリプトが完成した時点で満足してしまって、秀スクリプトの応 用例が少ない…が、今後頑張りますw 50

51.

謝辞 秀スクリプトの開発にあたり、秀丸マクロのサブルーチンのネスト回数 が20回までという制限が厳しく、サポートフォーラムにて制限の緩和を 要望したところ、200回までに緩和いただきました。この仕様緩和で、 秀スクリプトを秀丸上でコンパイルできるようになりました。ご担当い ただいた「秀丸担当」様、サイトー企画様にあつくお礼申し上げます。 ありがとうございました。 51