Open4

WasmLinux: vfork → execveの実装(setjmp/longjmp)

okuokuokuoku

Asyncifyを使った setjmp / longjmp はEmscriptenの作者によって既に考察されている。

https://github.com/kripken/talks/blob/991fb1e4b6d7e4b0ea6b3e462d5643f11d422771/jmp.c

で、このコードから引かれているblogにAsyncifyの細かい挙動は述べられている:

https://kripken.github.io/blog/wasm/2019/07/16/asyncify.html

ただし、 jmp.c では超巨大な jmp_buf が作成できることを仮定している。通常のアーキテクチャでは jmp_buf はプロセッサのステートを格納すれば十分でそれは固定長になるが、Asyncifyでは完全なスタックトレースを格納する必要があるので固定長ではなくなってしまう。

別案としては、EmscriptenのAPIを実装してしまう方法がある。実はLLVMにはsetjmpのコードを "setjmpの呼出しまで" と "setjmp以降の呼出し" にsplitしてくれる専用の最適化passがあり、Emscriptenはこれを使うことで通常のJavaScript例外やWasmネイティブの例外機構でsetjmp/longjmpを実現している。

https://llvm.org/doxygen/WebAssemblyLowerEmscriptenEHSjLj_8cpp.html

... と、ここまで書いて気付いたけど、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) まで実行する関数とする。実際には setjmprun_to_execve はダミーのimportにしておいて、wasm2cの出力をコンパイルするときに置き換える。

これが機能するためには WebAssemblyのツールチェーンがC関数を2つ以上のWasm関数に分割しない という仮定が必要になる。常識的なコードでは問題ないが非常に複雑なコードやC++では不味いかもしれない。

okuokuokuoku

上手くいった

https://github.com/okuoku/lkl-wasm/commit/18873bbd2b292d04f9e230b752f2bc6d2c8c5a63

上手くいかないのはユーザランドスレッドを作ったときにカーネルコンテキストを設定するのを忘れていた。LKLはカーネルコンテキストが無いときは初期スレッドである host0 からカーネルスレッドをcloneするので一見うまくsyscallできるが、 EBADF になったり、 exit でシステム全体が死んだりする。

これでBusyboxの動作自体はできるようになったはず。 ...まぁファイルシステムが無いから何もできないけど。。