* セキュリティを考えなくてもOSECPUの設計に至る -(by [[K]], 2013.08.02) ** (0) はじめに -OSECPUのバイトコードは、「第三世代OSASK」として[[K]]が数年前から設計していたものでした。これがOSECPUになる際に、実は大きな仕様変更をすることなく、そのまま流用できています。つまり[[K]]は知らないうちにセキュアな設計をしていたのです。 -ではどうやってこの仕様に決めていたのか、その話を書きたいと思います。 ** (1) 仕様の理由(セキュアに関係ある部分) -なぜJITコンパイル方式? --CPUに依存しない互換性を提供したかったから。Javaがうらやましかったから真似してみた。 --(セキュアなOSを考えると、アプリを実行する前に、悪い命令が混ざっていないかをチェックすることは重要である。JITコンパイルはすべての命令を実行前にチェックできるので、これはとても理にかなっていた。) -なぜ整数レジスタとポインタレジスタを分離したのか? --ページングもセグメンテーションもない環境下で、(多少時間がかかってもいいから)メモリ上のある構造体を移動することを考えよう。幅広いCPUに対応するのならこのような可能性も検討しておくべきだ。・・・この時、この構造体を指していたポインタはすべて修正されなければいけない。だからメモリ内のデータのうち、どれがポインタであるかはOSが完全に把握することにした(だからmalloc時に型を明示する)。 --ではレジスタはどうか。もし汎用レジスタ方式を採用していたら、その値は整数値なのかポインタなのかをどうにかして判断できなければいけない。そのために各レジスタがポインタを扱っているのかどうかを反映するフラグレジスタを作り、それを制御する命令を付けたモデルを検討したが、結局のところ、汎用レジスタ方式をやめるほうが単純化できると分かった。 --汎用レジスタ方式をやめれば、整数レジスタとポインタレジスタのビット幅も独立に決められる。さらにポインタレジスタの値を直接設定させなければ、ポインタレジスタのビット数に依存しないでプログラムが書けることもわかった。これはアドレス幅に無関係にアプリが書けるということなので、互換性の面で非常に良い。 --(ポインタレジスタを分離していたからこそ、ポインタレジスタ側にたくさんの属性情報を追加することができた。型情報やアクセスリミット、死活情報などである。命令セットをほとんど変えることなく、これらをサポートできた。) -なぜメモリアクセス時に型チェックをするのか? --エンディアンが違うシステムでも同一の動作結果を保証するにはこれがいいと思ったから。 --またポインタレジスタの値をメモリに書いて、それを整数レジスタで読み取ることを許せば、結局は整数レジスタでのポインタ操作を許していることと同じになり、構造体移動の際には整数値まで補正しなければいけなくなる危険性がある。 -なぜラベル命令で無限ループ対策という仕組みを導入するのか? --これは[[page0055]]の(3)の事を指している。 --最初はこうだ。まずJITコンパイラでは、バイトコードの1命令が複数の実CPUの命令に翻訳されるということがよく起こる。これで普通に割り込みを許せば、命令が中途半端にしか実行されない状態で割り込まれるかもしれないということを意味する。これは気持ち悪いし、もしかしたら深刻なバグの原因になるかもしれない。 --じゃあどうするか。とりあえず、そんなものはすべてCLI/STIでサンドイッチにして、途中で割り込みが生じないようにしておくしかない(x86の場合)。・・・しかしこの方法だとIOPLによっては実行できないし、翻訳コードはCLIとSTIばかりになって、実行速度が下がってしまう。これはよくない。 --というか、「割り込めない期間がたまにある」ではなくて「割り込みできるタイミングがたまにある」のなら、アプリにとって都合のよいタイミングで割り込み要求がきていないかを自発的にチェックすればいいではないか。しかもこの方法ならCPUに割り込み機能がなくても割り込みが実現できる。 --しかし何からのミスで割り込みチェックを忘れてしまうと割り込みに対応できなくなってしまう。割り込みチェックがあまりにも長い間隔なのもダメだ。ということで、アプリがちゃんと適正な間隔で割り込みチェックしていることを確認するために、分岐回数カウンタというアイデアが生まれた。無限ループ対策はそれを利用しているだけに過ぎない。 -なぜセキュリティをOFFにできる、という設計なのか? --このページの説明からも明らかなように、セキュリティは全くの後付けである。だからOFFにしてもいいものであって、それゆえにOFFにできなければならない。 --これはOFFにしなければいけないという意味ではない。選択できなければいけないという意味である。OFFにしたらアプリにばれて好き放題されるので、事実上できない、なんていうことは許されない。 ** (2) 仕様の理由(セキュアに関係ない部分) -なぜレジスタ数がこんなに多いのか? --x86が8本しか汎用レジスタがなく、しかもそのうちの一つはESPで、実質7本しかない。これで長い間プログラミングをしていたら、いつの間にかレジスタがたくさんほしいと熱望するようになった。一時は256本にしようとさえ思ったほどだ。 --64にした最大の動機は、hh4エンコードで1バイトで表現できる本数が64だったから、というだけに過ぎない。でも今でも64本というのはなかなか程よい本数だと思っている。 -なぜ「reg0=reg1+reg2;」のような三項演算形式にしたのか? --ARMの影響。x86で計算値を生成するたびに、元の値が必ず破壊されていくのが嫌だった。そのたびに破壊前の値をPUSHしたり別のレジスタにMOVしたりしていた。ARMがうらやましかった。 -なぜフラグレジスタがないのか? --MIPSの仕様がかっこいいと思ったから。 -なぜスタックがないのか? --レジスタが十分に豊富なら、スタックがなくても困らないはずだと直感的に思った。スタックを扱わなくてよければ、それだけ命令セットは単純になる。 --スタックには、整数値もポインタ値も積んだりおろしたりするだろう。ということは、「メモリ内データの型管理情報」を頻繁に更新する必要が出てくる。これは遅そうだ。こんなことになるくらいなら、スタックなんかなくてもいいじゃないか。それとも整数専用スタックとポインタ専用スタックを作るのか?またそうやって命令の種類が増えて、命令セットが複雑になるのか?それでいいのか? -なぜスタックマシンにしなかったのか?Javaのまねで出発したのに。 --レジスタマシンが好きだから。スタックマシンはレジスタマシンよりも機能密度が高いという説があるらしいが、それと対決してやろうと思った。 --レジスタマシンならJITコンパイラがレジスタ割り付けに悩まなくて済む。 ** (3) 考察 -セキュリティを考えなくても既にセキュアな仕様になっていたとは、どういうことなのでしょうか。それはこの設計が予想以上に汎用的だったということではないでしょうか。・・・つまり設計がよかったのです。 -よい設計は十分にシンプルで、多くの問題を同時に解決するものです。複雑にすれば複数の問題を解決できるのは当然であって、それはもはやよい設計ではありません。また複雑なのにひとつの問題すら満足に解決できないものもありますが、そんなのは論外です。 * こめんと欄 #comment