fork on Windows

Win32サブシステムへの接続をのぞけば、動いた、ような、気がする。多分。
Device Drive Kitのヘッダファイルにも無い呼び出しとかがあったので(バージョンによるのか?)、検索して適当にヘッダファイルをでっち上げ、先日の日記のようにGetProcAddressでアドレスを取得。構造体等の定義もヘッダに入れないといけないので面倒でした。
あとは、WINDOWS NT/2000 NATIVE API REFERENCEの本の例を若干変更。コンパイルでエラーになる部分に明治的なキャストを入れるとか。これでコンパイルは通ったけど、実行時エラー。さんざん悩んだあげく、#pragma optimize("y", off)の場所を移動すると、動いた(ような気がする)。で、これが何なのかだが…

forkの定義がどうなっているかだけど(ものはこの辺にある)、

  • ハンドルの継承を設定
  • ZwCreateProcessでプロセス生成
  • Thread生成のための情報を今のプロセスから取得:ここで、生成したスレッドが動き出す場所を指定
  • ZwCreateThreadで先に生成したプロセス上にスレッドを生成
  • 今のスレッドのexception handlerを新しく生成したスレッドにコピー(よくわからん)
  • Win32サブシステムに接続を通知
  • 生成したスレッドを起動

という感じ。本によると。
で、「生成したスレッドが動き出す場所」というのが、こう指定されている:

   __declspec(naked) int child()
{
    typedef BOOL (WINAPI *CsrpConnectToServer)(PWSTR);

    CsrpConnectToServer(0x77F8F65D)(L"\\Windows");
    __asm mov   eax, 0
    __asm mov   esp, ebp
    __asm pop   ebp
    __asm ret
}


#pragma optimize("y", off)  // disable frame pointer omission

int fork()
{
...
    context.Eip = ULONG(child);
...
    NT::ZwCreateThread(&hThread, THREAD_ALL_ACCESS, &oa,
                       hProcess, &cid, &context, &stack, TRUE);
....
}

fork()の中のcontext.Eip = ULONG(child)で、生成したスレッドがchildのアドレスから実行を開始することを指定している(インストラクションポインタの設定)。
で、childの方を見てみる。CsrpConnectToServerというのが、強制的にWin32サブシステムに接続する関数。これはntdll.dllから公開されていないので、アドレスを直書きしている。で、このアドレスがWindowsのバージョンによって異なると :-( (あ、先日の日記で変数のアドレスって書いたけど、それは間違いでした。)
で、このままだとエラーになったので、ここはとりあえずコメントアウト
その下で、謎のアセンブリコードがあるけど、これは、関数からのリターンを指定しているみたい。fork()関数の中からgoto的に飛ぶので?(親プロセスのメモリ・スタックの状態のままで、インストラクションポインタがchildを指すようになるので)、fork()関数からのリターンになる様子。
eaxに0を指定することで、fork()の返り値を0にして、ebpをpopすることで、スタックポインタを戻している。で、これが動作するためには、「フレームポインタを使わない」という最適化をoffにする必要がある。それが、#pragma optimize("y", off)の正体。

とそこまではいいんだけど、僕の環境(Visual Studio 2005 / VISTA)では、#pragmaをchildの前に持っていかないと動かなかった。__declspec(naked)というのが謎なんだけど、以前の環境では、これがあると#pragmaは不要だったのかも知れない。

まー、何となく動いているという状態ではあるのだが、これからどうしようかなぁ。Win32サブシステムと接続しなくても、共有メモリが実現できれば並列計算はできるけど… I/Oが使えないのは寂しいが…