高bit対応について
はじめに
- OSECPU-VMでは、整数演算において32bitまでしか指定できません。命令フォーマット的にはもちろん上限はないのですが、OSECPU-VMにはそこまでをサポートする義務がないのです。つまりそういうアプリがあっても、実行できるかどうかは分からない、OSECPU-VMではそれを保証しない、というわけです。
- これが第三世代OSASKだと話は違ってきて、最低でも256bitまではサポートします。第三世代OSASKはOSECPU-VMと同じ命令フォーマットです。
- この切り分けはなぜかといえば、OSECPU-VMの最大の主眼は高bit演算への対応ではなくて、セキュリティだからです。二兎を追っては一兎も得ないかもしれません。32bitくらいあれば大抵の用途は大丈夫でしょう。移植もしやすいでしょう。そういう切り分けです。
- ということで以下の議論は、OSECPU-VMだけを考えるのなら気にしなくていい話です。
- ちなみにrev2からサポートされた浮動小数点レジスタについても、OSECPU-VMが仕様としてサポートするのは32bitの単精度までです。64bitの倍精度はサポートしてなくてもOSECPU-VMを名乗ることに問題はありません。
- まあ浮動小数点サポートがばっさり削られていても、「整数演算のみのOSECPU-VMです」っていってしまえばそれでOKなのでありますが。
仕組み
- かりに整数演算を256bitまでサポートするOSECPU-VMがあったとします。その場合、内部実装はどうあるべきでしょうか。
- 僕が想定しているのは、256bitのレジスタイメージを常にまともに持つことです。32bitのマシンで実現するのなら、intを8個つなげて一つのレジスタとして扱います。
- それぞれの演算命令にはbitというフィールドがついています。ですからこれより上位のbitについては計算を省略して構いません。256bitのレジスタを用意したけど、下位32bitしか使わないのなら、上位224bitについては何も操作しなくていいのです。ゴミデータが入っているかもしれませんが、どうせ参照されないので心配はいりません(参照しようとするとセキュリティ保護でエラーになります、これはmallocしたけど初期化されていない変数へのアクセスと同じことでセキュリティエラーなのです)。
- この手のセキュリティエラーは、アプリが外部からの攻撃に負けてしまうタイプのセキュリティ問題ではなく、外部のデータの痕跡を盗み見ようとか、もしくは外部のデータに誤って依存してしまうなどのバグを見つけるためのものです。
- この問題を解決する別の方法としては、アプリにメモリを渡すときはすべて事前にゼロクリアするとか、もしくはレジスタもすべてクリアしておくなどの手法がありますが(そしてその手法が一般的なのですが)、OSECPU-VMではこの手法ではなくて、ちゃんと検出できるという方を選びました。
- takeutch-kemecoさんは、twitterで、OSECPU-VMが内部でhh4のような可変長をサポートするというアイデアを紹介していました。僕としてはhh4は外部に対して使うフォーマットであって、内部では使わないつもりでした。オーバーヘッドが大きいからです。だからむしろこのアイデアは新鮮でした。僕はそういう内部仕様のOSECPU-VMを検討していませんが、誰かが将来作ってくれる分には歓迎します。それはそれでおもしろそうです。
- そしてtakeutch-kemecoさんは、レジスタをスタックに退避するときのことも心配していました。これは非常にセンスがいいです。実はこれは僕も相当に悩まされていた問題なのです。
スタック退避問題
- あるレジスタの値を一時的にメモリに保存して、あとでレジスタに戻すという処理は、結構頻出です。この手の処理はたいていスタックを使って行います。でもスタックじゃないときもあります。
- まず最初に思いつくのは、「そのVMがサポートしている最大のbit数のメモリ型」という型を定義して、それを使わせるというものです。この方法ならとりあえずうまくいきますが、しかし実行マシンによってメモリ消費量が異なり、実行途中で状態を保存して、それを他のマシンで実行するということがやりにくくなります。そもそも結局中身を見たら10bitくらいしか使っていないかもしれないのに、毎回256bitを保存するのが果たして賢い方法なのでしょうか。僕は何か間違っている気がするのです。
- ということで、こうします。各関数は、仕様として何bitまでを保存するかを明記するのです。ある関数aは32bitまでしか保存しません。もし33bit以上を使っていた状態でメモリ退避を実行すると、復元時にレジスタ値は「不定」になります。この不定になったレジスタに対して値を参照しようとすると、セキュリティ保護でプログラムは止まるのです。
- これはどういうことかというと、もしレジスタが33bit以上の値を持った状態で関数aを使いたいのであれば、そのレジスタを「呼び出し元が」呼び出し前に自前で保存しなければいけないということです。これはこれで不便かもしれませんが、僕が考えた結論はこれになりました。
- もちろん関数によって保存bit数は64bitだったり16bitだったりするでしょう。それはその関数の使われ方にもよると思います。だからこのビット数をOSECPU-VMの仕様として決めることはありません。OSECPU-VMとしてはそのビット数の決定をライブラリ開発者に任せる代わりに、誤用時に検出できる仕組みを持って支援するというわけです。
脱線: OSECPU-VMの設計方針
- 言語仕様やVMの使用を考えるときに、大きく2つの方針があると思います。
- [1]用心深く使えば性能が良いが、ミスするとバグやセキュリティ問題になる。
- [2]性能が少し犠牲になるが、ミスの心配はない。
- たとえば、メモリをアロケートした時にかならずゼロクリアされるという仕様は、[2]に属します。クリアする必要のないものまでクリアしてしまうが、そのコストはわずかです。それで他のプロセスからの情報漏洩がなくなるのなら安いものだと考えるわけです。
- しかしOSECPU-VMは[1]を選びます。つまりクリアする必要のないものはクリアしないですむ方が良いのです。そのために初期化し忘れていないかどうかを厳重にチェックします。結果的にその方が格段にオーバーヘッドが大きくて遅いわけですが、しかし正しい作法は身につくでしょう。そして信頼できるようになってチェックをオフにすれば、最高速度で動かすことができます。
- 同じことはmalloc-freeモデルの採用にも現れています。ガーベージコレクション方式はまさに[2]です。GCではメモリ解放のタイミングは制御できません。スキャンのコストもあります。malloc-freeモデルなら、うまく書ければ最高の性能が出ます。そして誤用チェックは厳密で、おかげで通常状態では結構重くなってしまいます。しかし正しい作法は身につくし、最終的にチェックをオフにすれば、最高速度が得られるわけです。
- まあVM方式の互換性重視の時点で、性能なんていっても冗談にしかならないわけですが、しかし正しい作法は重要だし、それを学ぶ機会があっていいはずです。世間的には[2]の設計になることが多くて、それはつまりプログラミングがやさしくなっているのですが、その代償は永久に払わなければいけません。つまり少しの性能低下を永遠に負担させられるわけです。開発者のスキルが上がって書き直したとしても回避できません。その言語処理系やVMを利用をやめない限り。
- そしてOSECPU-VMのレジスタ構成に近いCPUを作れば、ほんとにかなり高速に動作するかもしれません。
こめんと欄