WOW64からのシステムコール概要

もうちょっと実装よりに、Win7(RC)x64 で以下のコードを用いて違いを確認する。

int _tmain()
{
  __debugbreak();
  FlushProcessWriteBuffers();
  return 0;
}

FlushProcessWriteBuffersは、引数なし戻り値なしのとても単純なAPIなので、処理の概要を理解するのに適している。
Visual StudioではWOW64を追いかけることができないため、デバッガにはWinDbgを使う。前にも書いたとおり、WOW64の仕組みは完全にユーザーモードで完結しているので、カーネルデバッグなどは特に必要ない。

x64(ネイティブ)

x64としてビルドすると、FlushProcessWriteBuffersはntdll!NtFlushProcessWriteBuffersを指しており、直ちにsyscallを発行する。

ntdll!NtFlushProcessWriteBuffers:
00000000`778df140 4c8bd1          mov     r10,rcx
00000000`778df143 b8c4000000      mov     eax,0C4h  ; eax = システムコール番号
00000000`778df148 0f05            syscall
00000000`778df14a c3              ret

;0:000> r
;rax=00000000cccccccc rbx=0000000000000000 rcx=0000000000000001
;rdx=0000000000462580 rsi=0000000000000000 rdi=000000000014fb70
;rip=000000007778f140 rsp=000000000014fb48 rbp=0000000000000000
; r8=00000000004626a0  r9=00000000000000fe r10=00000000000000e0
;r11=000000000014f4a8 r12=0000000000000000 r13=0000000000000000
;r14=0000000000000000 r15=0000000000000000
;iopl=0         nv up ei pl nz na po nc
;cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206

コメント部分は、WinDbgのrコマンドでレジスタの状態をダンプしたもの。どこでダンプを出したものかはeip/ripの値をみればおk。


このときロードされるモジュールは以下のとおりで、これについて特筆すべきことはない。

         Base Address      Size                Image Path
ModLoad: 00000001`3fac0000 00000001`3facd000   pj00.exe
ModLoad: 00000000`77740000 00000000`778e8000   ntdll.dll
ModLoad: 00000000`77520000 00000000`7763e000   C:\Windows\system32\kernel32.dll
ModLoad: 000007fe`fd7e0000 000007fe`fd849000   C:\Windows\system32\KERNELBASE.dll
ModLoad: 00000000`74730000 00000000`748e0000   C:\Windows\system32\MSVCR100D.dll

x86(WOW64)

次にx86としてビルドして実行する。ロードされるモジュールは以下のとおりで、wow64が構築されていることがわかる。

         Base Address      Size                Image Path
ModLoad: 00000000`00f70000 00000000`00f8b000   pj00.exe
ModLoad: 00000000`77740000 00000000`778e8000   ntdll.dll
ModLoad: 00000000`77920000 00000000`77aa0000   ntdll32.dll
ModLoad: 00000000`75430000 00000000`7546f000   C:\Windows\SYSTEM32\wow64.dll
ModLoad: 00000000`753d0000 00000000`7542c000   C:\Windows\SYSTEM32\wow64win.dll
ModLoad: 00000000`753c0000 00000000`753c8000   C:\Windows\SYSTEM32\wow64cpu.dll
ModLoad: 00000000`763a0000 00000000`764a0000   C:\Windows\syswow64\kernel32.dll
ModLoad: 00000000`76270000 00000000`762b4000   C:\Windows\syswow64\KERNELBASE.dll
ModLoad: 00000000`70010000 00000000`70171000   C:\Windows\SysWOW64\MSVCR100D.dll


さて、システムコールのシーケンスを見ていく。
WOW64におけるFlushProcessWriteBuffersは32ビットntdll.dllのNtFlushProcessWriteBuffersを指している。

ntdll32!NtFlushProcessWriteBuffers:
77940b2c b8c4000000      mov     eax,0C4h             ; eax = システムコール番号
77940b31 33c9            xor     ecx,ecx              ; ecx = 0
77940b33 8d542404        lea     edx,[esp+4]          ; edx = スタックポインタの場所
77940b37 64ff15c0000000  call    dword ptr fs:[0C0h]  ; fs:0053:000000c0=00000000
77940b3e 83c404          add     esp,4
77940b41 c3              ret

;0:000:x86> r
;eax=000000c4 ebx=7efde000 ecx=00000000 edx=002cf724 esi=002cf724 edi=002cf7f0
;eip=77940b37 esp=002cf720 ebp=002cf7f0 iopl=0         nv up ei pl zr na pe nc
;cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246

eaxにシステムコール番号を入れて、fs:[0C0h]へ飛ぶ。これは次に示すwow64cpu!X86SwitchTo64BitModeを指している。

wow64cpu!X86SwitchTo64BitMode:
753c2320 ea1e273c753300  jmp     0033:753C271E

;0:000:x86> r
;eax=000000c4 ebx=7efde000 ecx=00000000 edx=002cf724 esi=002cf724 edi=002cf7f0
;eip=753c2320 esp=002cf71c ebp=002cf7f0 iopl=0         nv up ei pl zr na pe nc
;cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246

ここでもさらに0033:753C271Eというセグメント間ジャンプを行う。これは以下のwow64cpu!CpupReturnFromSimulatedCodeを指している。


64ビットモードへの切り替え

; アセンブリにはコメントで(このときの)実効アドレスをつけている
wow64cpu!CpupReturnFromSimulatedCode:
00000000`753c271e 67448b0424         mov     r8d,dword ptr [esp]       ; ds:00000000`002cf71c=77940b3e
00000000`753c2723 458985bc000000     mov     dword ptr [r13+0BCh],r8d  ; ds:00000000`0009fddc=7793fa62
00000000`753c272a 4189a5c8000000     mov     dword ptr [r13+0C8h],esp  ; ds:00000000`0009fde8=002cf354
00000000`753c2731 498ba42480140000   mov     rsp,qword ptr [r12+1480h] ; ds:00000000`7efdc480=000000000009e580
00000000`753c2739 4983a4248014000000 and     qword ptr [r12+1480h],0   ; ds:00000000`7efdc480=000000000009e580
00000000`753c2742 448bda             mov     r11d,edx
wow64cpu!TurboDispatchJumpAddressStart:
00000000`753c2745 41ff24cf           jmp     qword ptr [r15+rcx*8]     ; ds:00000000`753c2450={wow64cpu!TurboDispatchJumpAddressEnd (00000000`753c2749)}

;0:000> r
;rax=00000000000000c4 rbx=000000007efde000 rcx=0000000000000000
;rdx=00000000002cf724 rsi=00000000002cf724 rdi=00000000002cf7f0
;rip=00000000753c271e rsp=00000000002cf71c rbp=00000000002cf7f0
; r8=000000000000002b  r9=000000007793fa62 r10=0000000000000000
;r11=000000000009dc90 r12=000000007efdb000 r13=000000000009fd20
;r14=000000000009e5f0 r15=00000000753c2450
;iopl=0         nv up ei pl zr na po nc
;cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246

先のジャンプによってCSレジスタの値が0x23から0x33へ変わり、プロセッサが64ビットモードになっている。この関数の仕事は、

  1. [r13+0BCh]にリターンアドレスを保存
  2. [r13+0C8h]にespを保存
  3. [r12+1480h]からx64スタックポインタを復元
  4. [r12+1480h]を0クリア
  5. r11にedxの値を保存
  6. ジャンプ

ジャンプアドレスはr15を基点とし、rcxを添え字とした関数配列へのジャンプであることがわかるので、ためしにr15の値でシンボルの調査してみる(dpsコマンドは dump pointer_size symbol って感じの意味で、ddsにするとDWORDサイズで解釈したりする。この辺はWinDbgのリファレンスをどうぞー)。

0:000> dps 753c2450 753c2450+100
00000000`753c2450  00000000`753c2749 wow64cpu!TurboDispatchJumpAddressEnd
00000000`753c2458  00000000`753c2d8d wow64cpu!Thunk0Arg
00000000`753c2460  00000000`753c2bb3 wow64cpu!Thunk0ArgReloadState
00000000`753c2468  00000000`753c2d3d wow64cpu!Thunk1ArgSp
00000000`753c2470  00000000`753c2d4e wow64cpu!Thunk1ArgNSp
00000000`753c2478  00000000`753c2d4a wow64cpu!Thunk2ArgNSpNSp
00000000`753c2480  00000000`753c2c66 wow64cpu!Thunk2ArgNSpNSpReloadState
00000000`753c2488  00000000`753c2d57 wow64cpu!Thunk2ArgSpNSp
00000000`753c2490  00000000`753c2d39 wow64cpu!Thunk2ArgSpSp
00000000`753c2498  00000000`753c2d28 wow64cpu!Thunk2ArgNSpSp
00000000`753c24a0  00000000`753c2d46 wow64cpu!Thunk3ArgNSpNSpNSp
00000000`753c24a8  00000000`753c2d35 wow64cpu!Thunk3ArgSpSpSp
00000000`753c24b0  00000000`753c2d60 wow64cpu!Thunk3ArgSpNSpNSp
00000000`753c24b8  00000000`753c2ba8 wow64cpu!Thunk3ArgSpNSpNSpReloadState
00000000`753c24c0  00000000`753c2d71 wow64cpu!Thunk3ArgSpSpNSp
00000000`753c24c8  00000000`753c2d24 wow64cpu!Thunk3ArgNSpSpNSp
00000000`753c24d0  00000000`753c2d53 wow64cpu!Thunk3ArgSpNSpSp
00000000`753c24d8  00000000`753c2d42 wow64cpu!Thunk4ArgNSpNSpNSpNSp
00000000`753c24e0  00000000`753c2d6d wow64cpu!Thunk4ArgSpSpNSpNSp
00000000`753c24e8  00000000`753c2ae2 wow64cpu!Thunk4ArgSpSpNSpNSpReloadState
00000000`753c24f0  00000000`753c2d7e wow64cpu!Thunk4ArgSpNSpNSpNSp
00000000`753c24f8  00000000`753c2ba4 wow64cpu!Thunk4ArgSpNSpNSpNSpReloadState
00000000`753c2500  00000000`753c2d20 wow64cpu!Thunk4ArgNSpSpNSpNSp
00000000`753c2508  00000000`753c2d31 wow64cpu!Thunk4ArgSpSpSpNSp
00000000`753c2510  00000000`753c27b5 wow64cpu!QuerySystemTime
00000000`753c2518  00000000`753c277a wow64cpu!GetCurrentProcessorNumber
00000000`753c2520  00000000`753c297f wow64cpu!ReadWriteFile
00000000`753c2528  00000000`753c28c5 wow64cpu!DeviceIoctlFile
00000000`753c2530  00000000`753c2a30 wow64cpu!RemoveIoCompletion
00000000`753c2538  00000000`753c27f3 wow64cpu!WaitForMultipleObjects
00000000`753c2540  00000000`753c27fa wow64cpu!WaitForMultipleObjects32
00000000`753c2548  00000000`753c2779 wow64cpu!ThunkNone
00000000`753c2550  6666cccc`cccccccc  ; ここでおわりみたい


引数の数に応じた関数へのジャンプになるようだ。たとえばNtFlushKeyではecxに3を設定しており、wow64cpu!Thunk1ArgSpへジャンプすることがわかる。

; NtFlushKey(HANDLE KeyHandle)
ntdll32!NtFlushKey:
77940b10 b8c3000000      mov     eax,0C3h
77940b15 b903000000      mov     ecx,3        ; wow64cpu!Thunk1ArgSp
77940b1a 8d542404        lea     edx,[esp+4]
77940b1e 64ff15c0000000  call    dword ptr fs:[0C0h]
77940b25 83c404          add     esp,4
77940b28 c20400          ret     4


シンボル名のSpは非ポインタ型、NSpはポインタ形を意味している。

; NtAlertResumeThread(HANDLE ThreadHandle, PULONG SuspendCount);
ntdll32!NtAlertResumeThread:
77940278 b869000000      mov     eax,69h
7794027d b907000000      mov     ecx,7        ; wow64cpu!Thunk2ArgSpNSp
77940282 8d542404        lea     edx,[esp+4]
77940286 64ff15c0000000  call    dword ptr fs:[0C0h]
7794028d 83c404          add     esp,4
77940290 c20800          ret     8


専用の関数が用意されているものもある。

; NtGetCurrentProcessorNumber(void);
ntdll32!NtGetCurrentProcessorNumber:
77940bd8 b8cb000000      mov     eax,0CBh
77940bdd b919000000      mov     ecx,19h      ; wow64cpu!GetCurrentProcessorNumber
77940be2 8d542404        lea     edx,[esp+4]
77940be6 64ff15c0000000  call    dword ptr fs:[0C0h]
77940bed 83c404          add     esp,4
77940bf0 c3              ret

また、ReloadStateは待機系の関数(ZwDelayExecutionやZwWaitForSingleObjectなど)につくようだ。それと引数の数と無関係な関数を使うものもある。詳細は調べてない。


とりあえずNtFlushProcessWriteBuffersではwow64cpu!TurboDispatchJumpAddressEndへジャンプするということ。

ネイティブシステムコールの呼び出し

wow64cpu!TurboDispatchJumpAddressStart:
00000000`753c2745 41ff24cf        jmp     qword ptr [r15+rcx*8]    ; ds:00000000`753c2450={wow64cpu!TurboDispatchJumpAddressEnd (00000000`753c2749)}
wow64cpu!TurboDispatchJumpAddressEnd:
00000000`753c2749 4189b5a4000000  mov     dword ptr [r13+0A4h],esi ; ds:00000000`0009fdc4=007e0000
00000000`753c2750 4189bda0000000  mov     dword ptr [r13+0A0h],edi ; ds:00000000`0009fdc0=007e4fe8
00000000`753c2757 41899da8000000  mov     dword ptr [r13+0A8h],ebx ; ds:00000000`0009fdc8=007e4fe0
00000000`753c275e 4189adb8000000  mov     dword ptr [r13+0B8h],ebp ; ds:00000000`0009fdd8=002cf388
00000000`753c2765 8bc8            mov     ecx,eax                  ; ecx = システムコール番号
00000000`753c2767 ff1513e9ffff    call    qword ptr [wow64cpu!_imp_Wow64SystemServiceEx (00000000`753c1080)] ; ds:00000000`753c1080={wow64!Wow64SystemServiceEx (00000000`7543ceb0)}
00000000`753c276d 418985b4000000  mov     dword ptr [r13+0B4h],eax ; ds:00000000`0009fdd4=00000000
00000000`753c2774 e998feffff      jmp     wow64cpu!CpuSimulate+0x61 (00000000`753c2611)

ここでの仕事はWOW64用のレジスタを保存し、wow64!Wow64SystemServiceExを呼び出すこと。
wow64!Wow64SystemServiceExは少し大きめなので要点のみを。全体はこちら

  • ecxに含まれるシステムコール番号が wow64!ServiceTables[index].Fields3 の値より大きければエラーSTATUS_INVALID_SYSTEM_SERVICEを返す。
    • indexは32ビットシステムコールの呼び出し元のスタック位置に応じて 0-3 になる。今回は0。
  • wow64!ServiceTables[index].Fields1[system_call_number] の関数を呼び出す。今回はwow64!whNtFlushProcessWriteBuffers。
    • wow64!pfnWow64LogSystemServiceが非NULLなら、上記呼び出しの前後でこの関数も呼び出す。今回はNULLのため呼び出さない。
  • wow64!ServiceTables[index].Fields1[system_call_number] の関数内では rcx レジスタを起点としてスタックから引数を回収する。
  • wow64!ServiceTables[index].Fields1[system_call_number] の戻り値を eax に入れて戻る。


結果、
[r13+0A0h]にediを保存
[r13+0A4h]にesiを保存
[r13+0A8h]にebxを保存
[r13+0ACh]-----------*1
[r13+0B0h]-----------*2
[r13+0B4h]にeaxを保存(システムコールの戻り値)
[r13+0B8h]にebpを保存
[r13+0BCh]にリターンアドレスを保存(64ビット)
[r13+0C0h]にリターンアドレスを保存(64ビット)
[r13+0C4h]-----------
[r13+0C8h]にespを保存
このようになり、wow64cpu!CpuSimulate+0x61へと移動する。

;0:000> r
;rax=0000000000000000 rbx=000000007efde000 rcx=00007549153a0000
;rdx=000000000009dd38 rsi=00000000002cf724 rdi=00000000002cf7f0
;rip=00000000753c2611 rsp=000000000009e580 rbp=00000000002cf7f0
; r8=000000000009dc88  r9=00000000002cf7f0 r10=0000000000000000
;r11=0000000000000306 r12=000000007efdb000 r13=000000000009fd20
;r14=000000000009e5f0 r15=00000000753c2450
;iopl=0         nv up ei pl nz na po nc
;cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000206


00000000`753c2611 4183a5d002000001 and     dword ptr [r13+2D0h],1
00000000`753c2619 0f84af000000    je      wow64cpu!CpuSimulate+0x11e (00000000`753c26ce)
; ...
00000000`753c26ce 418bbda0000000  mov     edi,dword ptr [r13+0A0h]  ; ds:00000000`0009fdc0=002cf7f0
00000000`753c26d5 418bb5a4000000  mov     esi,dword ptr [r13+0A4h]  ; ds:00000000`0009fdc4=002cf724
00000000`753c26dc 418b9da8000000  mov     ebx,dword ptr [r13+0A8h]  ; ds:00000000`0009fdc8=7efde000
00000000`753c26e3 418badb8000000  mov     ebp,dword ptr [r13+0B8h]  ; ds:00000000`0009fdd8=002cf7f0
00000000`753c26ea 418b85b4000000  mov     eax,dword ptr [r13+0B4h]  ; ds:00000000`0009fdd4=00000000
00000000`753c26f1 4989a42480140000 mov     qword ptr [r12+1480h],rsp; ds:00000000`7efdc480=0000000000000000
00000000`753c26f9 41c7460423000000 mov     dword ptr [r14+4],23h    ; ds:00000000`0009e5f4=00000023
00000000`753c2701 41b82b000000    mov     r8d,2Bh
00000000`753c2707 418ed0          mov     ss,r8w
00000000`753c270a 418ba5c8000000  mov     esp,dword ptr [r13+0C8h]  ; 
00000000`753c2711 458b8dbc000000  mov     r9d,dword ptr [r13+0BCh]  ; ds:00000000`0009fddc=77940b3e
00000000`753c2718 45890e          mov     dword ptr [r14],r9d       ; ds:00000000`0009e5f0=7793fa62
00000000`753c271b 41ff2e          jmp     fword ptr [r14]           ; ds:00000000`0009e5f0=0023 77940b3e

wow64cpu!CpuSimulate+0x61では、冒頭で[r13+2D0h]のフラグを確認しているが、これは例外関連の処理らしい(分岐のフローを見ると iretq 命令で終わっている)。今回の流れでは関係ないので省略した。

WOW64用のレジスタを復元させ、[r12+1480h]にrspを保存する。最後に 0023h:[r13+0BCh] なジャンプを行い、CSを23hにして32ビット版ntdll.dllへと戻っていく。

ntdll32!NtFlushProcessWriteBuffers:
77940b2c b8c4000000      mov     eax,0C4h            
77940b31 33c9            xor     ecx,ecx             
77940b33 8d542404        lea     edx,[esp+4]         
77940b37 64ff15c0000000  call    dword ptr fs:[0C0h] ; callで行って
; ↓ここに戻ってくる 
77940b3e 83c404          add     esp,4               ; jmpで帰ってきたのでスタックの調整を行う
77940b41 c3              ret                        


以上、お疲れ様でした。

わかんない、見てない、そのうち調べる(?)ところ

  • 引数ありの場合の処理
  • wow64!ServiceTables[index] の index が0以外になるタイミング
  • NtGetCurrentProcessorが専用関数になっている理由
  • WOW64の初期化処理。より具体的には
    • qword ptr gs:[30h](00000000'7efdb000)は何者?

今回ではいきなり[r13+*]や[r12+1480h]にコンテキストを保存しているけど、当然、事前にr13やr12には値が入ってる。この初期化は、ざっと見た感じだと前述のwow64cpu!CpuSimulate関数で行われているらしくて、

0:000> k
Child-SP          RetAddr           Call Site
00000000`0013e6a0 00000000`7543d07e wow64cpu!CpuSimulate        <- 初期化処理中のここ!
00000000`0013e760 00000000`7543c549 wow64!RunCpuSimulation+0xa
00000000`0013e7b0 00000000`77783d59 wow64!Wow64LdrpInitialize+0x429
00000000`0013ed00 00000000`77775c83 ntdll!LdrpInitializeProcess+0x17e2
00000000`0013f200 00000000`7776403e ntdll! ?? ::FNODOBFM::`string'+0x28c40
00000000`0013f270 00000000`00000000 ntdll!LdrInitializeThunk+0xe


そのうえ中では結局はどっちも gs:[30h] を基にしてるみたい。

00000000`753c25f4 4c8d742470      lea     r14,[rsp+70h]                   ; r14の初期化
00000000`753c25f9 654c8b242530000000 mov   r12,qword ptr gs:[30h]         ; r12の初期化
                                                                          ; r15の初期化
00000000`753c2602 4c8d3d47feffff  lea     r15,[wow64cpu!CpupSaveLegacyFloatingPointState+0x60 (00000000`753c2450)]  
00000000`753c2609 4d8bac2488140000 mov     r13,qword ptr [r12+1488h]      ; r13の初期化
;
; この後はすぐにx86モードに遷移してしまうので、以後64ビットレジスタは変更されないと思われる
;

r13やr12っていたけど、結局はどっちも gs:[30h] を基にしてるってことだとすると
「じゃあ、gs:[30h] ってナニよ?」
って事になるし、それは誰もわからない。ヤバイ。誰にも分からないなんて凄すぎる。


ついき。WOW64は32bitと64bit両方のPEB,TEBをもってます。それぞれ、PEB64,TEB64という構造体で、gs:30hはTEB64構造体を採ってきます。

*1:例外処理でedxのために使われる。

*2:例外処理でecxのために使われる。