* Kの開発メモ #0001 -(by [[K]], 2013.03.12) -ここは川合の開発の進捗などをレポートするところです。 --当初はその予定だったが、今ではむしろ主たる目的は[[K]]の備忘録になってしまっている(苦笑)。 ** 2013.03.12 Tue -諸事情により、このプロジェクトを早期に立ち上げる必要が出てきた。ということで本気出す。 -以下のページは内容が難しすぎるので、読まないでください or 内容がOSECPUと関係ないので読まないでください or 内容が既に古くて、正しくありません。 --[[page0001]], [[page0002]], [[page0004]] -ええとそれで、freeしたメモリを再利用できてしまう問題はどうなったんだっけ? --よし、こうしよう → [[page0006]] -基本的な考え方として、たとえばGCみたいに、「メモリなんてどんどんアロケートして使えばいいんだよー、free()はシステムが自動でやってくれるんだよー」みたいなのは嫌だ。それはプログラマが楽できるけど、同時にバカになっていくような気がする。 -僕としては、「プログラマのやることを減らす」方向ではなくて、「プログラマのバグ発見のお手伝いをする」方向でやりたい。 -関数呼び出しについても考え始めました。 → [[page0007]] ** 2013.03.16 Sat -やっぱり最初からJITしようかなと思案中・・・。 -インタプリタだと作りやすいけど、すごく遅くなってそれで可能性を見限られたら残念かなーと。 ** 2013.03.19 Tue -クラックするプログラムの多くは、まずみんなシェルを取ってルートになることを目指すパターンが多い。そんなにルートになりたいんだったら、ルートにしてやればいいじゃないかと急に思いつく。え・・・と思うかもしれないけど、何が言いたいのかというと、ルートにさせない方法を講じるのも大事だけど、ルートになっても大したことが出来なければいいんじゃないかなーと。 -いやもちろん、これは出発点だ。本当にルートになってやりたい放題されたら終わってしまう。・・・僕の言いたいことは、結局はパスワードさえわかれば何でもできますみたいなOSだとパスワードが狙われるだけなので、パスワードが分かっても悪いことはできません、みたいなことを目指すべきなんじゃないかなと。というか最初のログイン時以外にはパスワードは要求されない、というのはどうか?それ以降はどれほどたくさんパスワードを持っていても何もできない、と。 -もう少し考えた! -唐突だけど、ブラウザのプラグインみたいなものを考えてみる。もし自作のブラウザにプラグインを付けられるようにしようと思ったらどうするだろうか?なんらかの独自の言語処理系を付けるだろうか。まあその路線で考えてみよう。 -そうすると問題が出てくる。プラグインが悪さをしたらどうしよう、ということだ。ということで悪さできないように念入りに設計する。何をやってもOSや他のアプリやブラウザそのものや他のプラグインの迷惑にはならないようになっている、と。それが無事にできるのであればそれは素晴らしい。 -しかしきっとどこかでミスる。・・・そしてそこがセキュリティホールになる。まあこのセキュリティホールをつついたところで、せいぜい破壊できるのはブラウザや他のプラグインくらいにはとどまると思うが、しかしそれでも十分に残念だ。 -これを防ぐにはどうしたらいいだろう。そう、独自の言語処理系を作らせなければいい。既存の枯れた言語処理系を使わせればいい。・・・そもそもどうしてプラグインのために独自の言語処理系を作ることになったんだ?それは、ブラウザの子プロセスではどうしていけないのか?そう、子プロセスだとなんでも好き放題できてしまうから駄目なんだ。セキュアなプログラムが阻止すべき最初のことは「任意のプログラムが実行できてしまうこと」なくらいだから。 -しかし僕は逆で行く。子プロセスでいいんだ。子プロセスがいいんだ。それでも十分にセキュアなんだ。どんなに悪さをしようと思っても、必ず失敗するんだ。僕はそういう仕組みを作りたい。 -僕は独自言語を作ることそのものは否定しない。プログラム言語はどんどん提案されるべきだし、プラグインに向いたプログラミング言語というのものだってきっとあるだろう。そのことを否定したいんじゃない。むしろその言語は独自ブラウザのプラグイン用の独自言語ではなくて、他の場面でも自由に使えるべきなんだ。テキストエディタや表計算のマクロを書いたり、普通にゲームを作ったり・・・。そうなればセキュリティはすぐに枯れる。 -つまり何が言いたいのかと言えば、「プラグイン機構は子プロセスで十分」なOSが現れれば、セキュアな世界はずっと広がる、ということなんだ!子プロセスというか、ただ関数をロードして実行するくらいのものしか考えていないので、DLLというべきかもしれない。 -親プロセスは以下のことを制御できれば、プラグイン機構を子プロセスで代用できる。 --関数ポインタを渡せる(親プロセスの機能を呼び出すため)。 --渡していないオブジェクトには絶対にアクセスできない。 --システムコールが使えないか、もしくは使えるんだけど制限がかかっている。どんな制限にするかは親プロセスが自由に設定できる。 ---使ってもよいシステムコール、呼び出し回数、アクセス権など --タイムアウト的なものがあり、いつまでも帰ってこないようなら打ち切らせる。 ---これは実時間などではなく、分岐命令の実行回数を使いたい --使用できるリソース量も上限がある。 ---メモリ、ファイル、など -ディレクトリパスについて。 --フルパスを許さない。これを許してしまえばどこへでも行けてしまう。 --相対パスについて、..で親に戻る処理はAPIで行い、APIはどこまでなら戻れるのかを正確に把握している。 -システムコールについて。 --システムコールするにはハンドルが必須。まずハンドルがもらえなければシステムコールはすべてできないということになる。そしてハンドルごとに何がどこまで許されるのかという設定が細かく設定されていて、それによって子プロセスは制約を受けている。 #br -とりあえず、JITできるプログラムを作ってみた。まだNOPと整数レジスタへの即値ロードと加算演算しか作ってないけど。 --(今夜アップロード予定) --http://osecpu.osask.jp/download/osecpu000a.zip ---x86アセンブラとか第二世代OSASKとかいろいろ余計なものが入ってムダにサイズが大きくなっています。どうもすみません。肝心のソースは140行程度でC言語しか使っていません(サンプルアプリはnaskのDB命令で作っていますが)。 ** 2013.03.21 Thu -(1) neriさんがJavaやC++の例外(try-catch)について書いてくれています。 --http://nerry.hatenablog.com/entry/2013/03/20/192613 -僕は[[page0009]]の(9)でmallocやfopenなどのエラー処理について書いたばかりだったので、この記事は興味深く読みました。もちろん僕もJavaは何年も前からかじってはいるので、例外処理のことは知っていますが、でも改めて自分の考えたエラー処理の仕方は例外処理と比べてどうなのかなと比較してみたくなったのです。 -「例外」は結局、プログラマにエラー処理を忘れさせないための仕組みだったのだと思います。それでも忘れたい人は何もしないcatch節を書いて握りつぶすわけですが、とにかく例外は「これはエラーが起きるかもしれないんだ」ということを意識させる仕組みだと思います。つまり注意力が乏しいから思い出させようというわけですね。それと同時に例外処理はプログラム的には少々ややこしくもなっています。例外処理を持つ処理系は例外処理のためにランタイムルーチンが複雑になっています。 -一方で僕が今回考えた方法はどうでしょうか。まず出発点として、僕はfopen()して結果がNULLだったら「fopen-errorとか表示してexit(1)する」みたいな処理を毎回書いているという事実があります。毎回毎回書くから、だんだんバカらしくなってきます。だったらエラー処理付きのfopenがあればいいじゃないかと思いました。このmy_fopenはfopenに失敗したら規定のエラーメッセージを出力してexit(1)するという関数です。 -このmy_fopenを使っている限り、僕はこいつがもしかしたらNULLを返すんじゃないかということを完全に忘れていられます。もちろんときには「エラーになったからって勝手に終了しないでくれよー、その場合はやるべき後処理があるんだ」ということだってあります。そういうときはmy_fopenを使うのをやめて、fopenに戻ればいいだけです。 -これで思ったのは、実はmy_fopenこそデフォルトで、fopenのほうがオプショナルであるほうが自然じゃないかということでした。そして第二世代OSASKではAPIをそのように設計して、エラー処理はAPIに任せるのを基本にしてみました。するとどうでしょう、プログラムはとても書きやすくなり、それでいてエラー処理を忘れているというわけでもなく(十分ではないかもしれないが、とにかく最低限のエラー処理はできている)、そしてプログラムは単純で短くなったのです。・・・僕は「これだ!」と思い、今回のOSECPUでもこの思想を採用しました。 -そもそもみんなそんなにエラー処理をしたいのでしょうか?理想はエラー処理を忘れてもそれなりになんとかなることなんじゃないでしょうか?ある関数を使うときに、全ての起こりうるエラーについて熟知しなければ使ってはいけないというのが果たしてよいことなんでしょうか?どんなエラーが起こるのかを完全に知っているのはむしろfopenのほうなのですから、基本的にはエラー処理もfopenに任せるほうがうまくやってくれるのではないでしょうか?もちろん自分でやりたいときはありますし、自分でやったほうがうまく行くことだってあります。でもデフォルトがどちらであるべきかといえば、断然OSECPUのやりかたのほうがうまいのではないかと思うのです。 -僕のやっていることは、昔のBASICの「ON ERROR GOTO」命令を思い出せます。つまりかつて全ての命令はデフォルトではエラー時にメッセージを出して終了だったのです。そしてエラーハンドラを書いたときだけ、そちらに制御が移るわけです。今の常識的なエラー処理の考え方は違っていて(これには例外によるプログラミングスタイルも含みます)、全てのプログラムにエラー処理コードを要求するのです。それで書き忘れると誤動作するのです。 -僕のやっていることは、昔のBASICの「ON ERROR GOTO」命令を思い出させます。つまりかつて全ての命令はデフォルトではエラー時にメッセージを出して終了だったのです。そしてエラーハンドラを書いたときだけ、そちらに制御が移るわけです。今の常識的なエラー処理の考え方は違っていて(これには例外によるプログラミングスタイルも含みます)、全てのプログラムにエラー処理コードを要求するのです。それで書き忘れると誤動作するのです。 -結局僕は何も新しいことを考えたわけではなくて、昔のやり方に戻っただけなのです。 #br -(2) 現状のOSECPUは実レジスタを仮想レジスタに一つも割り当てていません。そのせいでたぶん性能が出ていないと思います。これはいつか改善すべきポイントですが、しかしこれをやるとコードが複雑になってOSECPUの「教材向け」の側面が弱くなってしまうので、当面は先送りにしようと思います。すみません。 -JITにするかしないかは多分10倍くらいの性能差があると思います。実レジスタを仮想レジスタに割り当てるようにすれば、おそらくさらに2倍くらいは性能が変わると思います。・・・つまり何を言いたいのかというと、単純化を目指すとはいっても10倍も性能が変わるインタプリタ方式を検討するのかといえば、それはさすがないないかなといいたいわけです。一方で、2倍くらいの性能差であれば、OSECPUでは単純化のほうを選ぶこともありうる、というわけです。 #br -(3) OSECPUの開発に費やせる当面の時間がどのくらいあるかを考えてみた。これはセキュリティキャンプ関連ということで会社の時間も投入しているので、時間は結構ある(セキュアなOSの自作ブームを起こしたい・・・小さいものでいいので)。ある程度のコミュニティを作るためには、やはりゲームを作れる環境になっていなければいけない。あとさすがにDB命令やバイナリエディタでゲームを作ってねというのはひどすぎるので、簡単な言語処理系も作る必要があるだろう。まあ1ヶ月くらいでできるところまでやってみるか。 #br -(4) 今日はもう少し作って、条件分岐ができるようになった。これで0から1万までの和を求めてみた。さすがはJITC(=JustInTimeCompiler方式のこと)、一瞬で終わってしまった。性能を測りたいので、0から1万までの和を100万回繰り返してみた。Core i7-2600 @ 3.40GHz では、23.56秒だった。速いなー。・・・osecpu.cの行数は362行になりました。 --(今夜アップロード予定) --http://osecpu.osask.jp/download/osecpu001a.zip -アセンブラなしでプログラミングするのがつらくなってきたのでプリプロセッサでこんなのを作りました。 #include "osecpu_asm.h" /* 0から1万までの和を100万回繰り返す */ OSECPU_HEADER(1); #define sum R00 #define i R01 #define j R02 #define const0 R30 #define const1 R31 #define const10001 R32 #define const1000000 R33 LOADINT(const0, 0); LOADINT(const1, 1); LOADINT(const10001, 10001); LOADINT(const1000000, 1000000); FOR(0, j, const0); /* label:0, conter:j, init:const0 */ COPYINT(sum, const0); FOR(1, i, const0); ADDINT2(sum, i); NEXT(1, i, const1, const10001); /* label:1, conter:i, step:const1, end:const10001 */ NEXT(0, j, const1, const1000000); #if 0 /* 以下のように書いてもよい */ COPYINT(j, const0); LABEL(0); COPYINT(sum, const0); COPYINT(i, const0); LABEL(1); ADDINT2(sum, i); ADDINT2(i, const1); CMPJNE(i, const10001, 1); /* if (i != const10001) goto label_1; */ ADDINT2(j, const1); CMPJNE(j, const1000000, 0); /* if (j != const1000000) goto label_0; */ #endif -さらにもう少しだけ改良して、フラグレジスタ+条件ジャンプ命令を持つ実CPUに有利な仕様を追加した。さっきのプログラムが23.06秒になった。うーん、0.5秒しか変わらなかったか・・・Core i7は工夫しなくても相当に速いんだなあ。 --(今夜アップロード予定) --http://osecpu.osask.jp/download/osecpu002a.zip -次はいよいよメモリアクセスかな。これはいろいろ鬼門。・・・いきなりメモリアクセスに行かないで、練習が必要かもしれない。 ** 2013.03.22 Fri -アドレスレジスタって64本も必要かな?さすがにそんなには使いこなせない予感。・・・じゃあ半分の32本でいいかな。 -pregにはポインタの型も記録されているが、高速危険モードではこの情報を参照しない。つまりこの情報はあくまでもセキュリティチェック用。チェック以外では参照しなくてもいいように必要な情報は命令バイトコード内に含むようにする。 -ポインタの比較にあっては、同型かつ同域であることを確認できれば、それが何型であるかは関与しない。 ** 2013.03.23 Sat -早くメモリアクセスをできるようになりたい。そうすればneriさんのCLEに追いつける(実際には浮動小数点演算ができないから追いついてないけど、でもCLEはまだメモリアクセスできなさそうだから、一勝一敗ってことで)。 ** 2013.03.24 Sun -上記訂正。CLEはARM版やx64版も既にあるんだった。というかWin版もLinux版もあるし。これに追いついたといえそうな代替の機能としては、やっぱり実用アプリとかゲームくらいは作って見せないとダメかな。 --まあもちろんまともに対抗してFPU対応とかARM対応とかDOS(16bitモード用)対応とかも面白そうなんですが、OSECPUとしてはまずはセキュリティなので、そちらがどうしても優先されます。 -CONDN(Rxx); /* 05 [Rxx] 2バイトプリフィクス */ この命令は失敗だった予感・・。こんな命令なくても全然困らない。XOR(Rb,Ra,-1);すればRbに反転結果が入るわけだし。 -R3C~R3Eの一時定数レジスタの考え方はわれながら面白い。そういう保存しなくてもいいレジスタっていうのはもっといろいろ規定してもよさそうな気がする。普通の整数レジスタやポインタレジスタにもそれぞれ4本くらいはほしい。 --じゃあ、R28~R2FとP1C~P1Fを割り当てよう。 -/* 0D: 0D [imm8] テストコンディションプリフィクス */ これも失敗の予感。ラベル命令を6バイトにしてしまえばよかったんだ。 ** 2013.03.25 Mon -(1) neriさんがゼロレジスタについて書いてくれています。 --http://nerry.hatenablog.com/entry/2013/03/22/190030 -これを読んで、ゼロレジスタのことを思い出しました。今まですっかり忘れていました。 -それでOSECPUにゼロレジスタを導入するかどうか悩んでいます。 --メリット:命令を3個削減できる。命令フォーマットの種類を減らせる。 --デメリット:COPYINTの命令長が長くなる。 -いろいろ考えた末、osecpu.cのコードが短くなるようなら採用、長くなるようなら不採用ということにします。 --で、短くなったので採用。 #br -(2) neriさんがバイトコードのフロントエンド・バックエンドの話を書いてくれています。 --http://nerry.hatenablog.com/entry/2013/03/23/203638 -これについてOSECPUの考え方を書いておきます。OSECPUには内部中間コードみたいなものは現状持っていませんし、今後も持たない予定です。これはそういう方式に否定的だというわけじゃなくて、バックエンド的なコード体系をそのままOSECPUに採用したいということです。 -そしてこのOSECPUの上にバイトコード変換プログラムを乗っけて、理想(?)のバイトコードを利用可能にします。 --というかそれくらい割り切っておかないと、バイトコード体系を凝りたくなってしまって、開発が遅れてしまいそうなのです、僕の性格では(笑)。 -つまりneriさんはすでに上位層まで作っているということです。はあー、追いつくまでの先は遠い・・・。 #br -(3) とりあえず今日もがんばった。メモリアクセスつけた。それでまあいかさまだけど本番用ではないAPIも付けた。それで、テキストファイルを読み込んで適当に加工して、テキストファイルを出力するというフィルタを書いた。このフィルタがあるとアプリを作りやすくなるから、一応開発ツールを作れたことになる。 --003a: 命令を追加した後でosecpu.cを少し整理。411行になる。ちなみに002aは422行だった。 --004a: バイトコードのリビジョンを0002にアップ。osecpu.cは388行になる。 --005a: メモリアクセス命令を突貫工事で追加。仮のAPIも付けた。osecpu.cは570行。cas2nasというツールも作る。 --006a: cas2nasの存在を前提にアプリを書き直し。すっきりした。 --(今夜アップロード予定) --http://osecpu.osask.jp/download/osecpu003a.zip --http://osecpu.osask.jp/download/osecpu004a.zip --http://osecpu.osask.jp/download/osecpu005a.zip --http://osecpu.osask.jp/download/osecpu006a.zip -もう少し詳しい説明: --cas2nasというのは、 DB(1,2,3); DB(4,5,6); DB(7,8,9); --というファイルを DB 1,2,3 DB 4,5,6 DB 7,8,9 --の3行に書き換えるだけのツールです。本当に大したことはないんだけど、今までこれがなかったせいで、アプリを作るのが超大変でした。 /* 006aのosecpu_asm.hの一部 */ #define DDBE(i) DB(((i)>>24)&0xff,((i)>>16)&0xff,((i)>>8)&0xff,(i)&0xff) #define LABEL(imm) DB(0x01,0x00); DDBE(imm) #define LOADINT(reg, imm) DB(0x02,reg); DDBE(imm) #define LOADLBL(preg, imm) DB(0x03,preg); DDBE(imm) /* 005aのosecpu_asm.hの一部 */ #define LABEL(imm) DB 0x01,0x00,((imm)>>24)&0xff,((imm)>>16)&0xff,((imm)>>8)&0xff,(imm)&0xff #define LOADINT(reg, imm) DB 0x02,reg,((imm)>>24)&0xff,((imm)>>16)&0xff,((imm)>>8)&0xff,(imm)&0xff #define LOADLBL(preg, imm) DB 0x03,preg,((imm)>>24)&0xff,((imm)>>16)&0xff,((imm)>>8)&0xff,(imm)&0xff --005aがひどいことになっていたのは、アセンブラでは一行にDBを一つしか置けないせいです(セミコロンで切ってマルチステートメントができない)。だからcas2nasで改行形式に展開したわけです。 --ちなみにこんな簡単な処理しかしてないのに、cas2nas00a.oseはなんと317バイトもあります。これはひどい!!本当に気にしないとここまでひどくなるんですね・・・。 -(4) プログラムを書いて思ったこと: --レジスタがたくさんあるっていいなー。x86でスタックを使ってレジスタをやりくりしていたのが嘘みたいだ。ローカル変数のためにメモリを使う場面が想像できない。 --3項形式は、あまり使わないなーと実感。でも僕は3項形式が好きだ!フロントエンドでは2項形式をサポートすると思う。まあでもそういうことはOSECPUじゃなくて第三世代OSASKでやるかもしれない。OSECPUは教材なので、あまり本気を出してはいけない(笑)。 ** こめんと欄 #comment