WasmLinux: vfork → execveの実装(setjmp/longjmp)
Asyncifyを使った setjmp
/ longjmp
はEmscriptenの作者によって既に考察されている。
で、このコードから引かれているblogにAsyncifyの細かい挙動は述べられている:
ただし、 jmp.c
では超巨大な jmp_buf
が作成できることを仮定している。通常のアーキテクチャでは jmp_buf
はプロセッサのステートを格納すれば十分でそれは固定長になるが、Asyncifyでは完全なスタックトレースを格納する必要があるので固定長ではなくなってしまう。
別案としては、EmscriptenのAPIを実装してしまう方法がある。実はLLVMにはsetjmpのコードを "setjmp
の呼出しまで" と "setjmp以降の呼出し" にsplitしてくれる専用の最適化passがあり、Emscriptenはこれを使うことで通常のJavaScript例外やWasmネイティブの例外機構でsetjmp/longjmpを実現している。
... と、ここまで書いて気付いたけど、wasm2cの出力にホストの setjmp
を埋め込めば良いじゃん。つまり、gcc拡張であるstatement as expressionを使って:
#define vfork() ({int r; char jb[sizeof(jmp_buf)]; r = setjmp(jb); if(r) r = run_to_execve(&jb, &r); r;})
のようなマクロにしてしまう。
ここで、 run_to_execve
は実際の execve
(2) まで実行する関数とする。実際には setjmp
と run_to_execve
はダミーのimportにしておいて、wasm2cの出力をコンパイルするときに置き換える。
これが機能するためには WebAssemblyのツールチェーンがC関数を2つ以上のWasm関数に分割しない という仮定が必要になる。常識的なコードでは問題ないが非常に複雑なコードやC++では不味いかもしれない。
Musl側を実装
これで vfork
を使うとwasm2cの出力には
/* import: 'wasmlinux_hooks' 'vfork' */
u32 w2c_wasmlinux__hooks_vfork(struct w2c_wasmlinux__hooks*);
が含まれるようになるので、これをマクロに置き換えてやれば良い。
ラインタイム側も実装
... してみたけど、親プロセスではなくカーネルにシグナルが飛んで死んでしまう。というか、これ新しいユーザープロセスのグローバル変数を初期化してないな。。
上手くいった
上手くいかないのはユーザランドスレッドを作ったときにカーネルコンテキストを設定するのを忘れていた。LKLはカーネルコンテキストが無いときは初期スレッドである host0
からカーネルスレッドをcloneするので一見うまくsyscallできるが、 EBADF
になったり、 exit
でシステム全体が死んだりする。
これでBusyboxの動作自体はできるようになったはず。 ...まぁファイルシステムが無いから何もできないけど。。