Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
こんにちは、プラットフォーム サポートの近藤です。
今回は、最近良く見る x64 コードのデバッグについてお話します。
- x64 呼出規約について
x64 上では、全ての呼び出しが従来の FASTCALL に似た呼出規約が用いられています。ここでは呼出規約の詳細は説明しませんが (興味ある方は記事末尾の参考資料をご覧ください)、x64 アーキテクチャで増えたレジスタを多く使用するようになっています。引数に関しましては、基本的に 4 つ目の引数までが順に rcx、rdx、r8、r9 レジスタで渡され、残りはスタックに保存されます。このように、多くの場合引数がレジスタで渡されているため、デバッグ時には注意が必要です。
では、実際の動きを見てみましょう。
今回は下記テスト プログラムをデバッグしながら進めていきます。
=================================
__declspec(noinline)
void func6(DWORD64 a, DWORD64 b, DWORD64 c, DWORD64 d, DWORD64 e, DWORD64 f)
{
printf("Func6: 0x%x 0x%x 0x%x 0x%x 0x%x 0x%x\n", a, b, c, d, e, f);
return;
}
__declspec(noinline)
void func3(DWORD64 a, DWORD64 b, DWORD64 c)
{
DWORD64 d = 0x40;
DWORD64 e = 0x50;
DWORD64 f = rand();
printf("Func3: 0x%x 0x%x 0x%x\n", a, b, c);
func6(0x10,0x20,0x30,d,e,f);
return;
}
int _tmain(int argc, _TCHAR* argv[])
{
DWORD64 a = 1;
DWORD64 b = 2;
DWORD64 c = rand();
printf("Calling Func3\n");
func3(a, b, c);
return 0;
}
==================================
コンパイラによる最適化を抑えるため、__declspec(noinline) を使用し、引数にもランダムな数字を渡しています。
まずは func3 が呼ばれた瞬間です。
0:000> kb
RetAddr : Args to Child : Call Site
00000001`3f7c10cc : 00000001`3f7c21f0 00000000`00000000 000007ff`fffde000 00000000`000000fe : testProgram!func3
00000001`3f7c1292 : 00000000`00000000 000000dc`b1cca995 00000000`00000000 00000000`00000000 : testProgram!wmain+0x2c
00000000`7789f56d : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : testProgram!__tmainCRTStartup+0x11a
00000000`779d3281 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : kernel32!BaseThreadInitThunk+0xd
00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x1d
func3 には、"0x1" "0x2" "0x29"、の 3 つの引数が渡されていますが、ご覧の通り、'kb' コマンドを実行しても、引数は正常に表示されません。これは引数がスタックではなく、レジスタに保存されているためです。
0:000> r
rax=000000000000000e rbx=0000000000000029 rcx=0000000000000001
rdx=0000000000000002 rsi=0000000000000000 rdi=0000000000000001
rip=000000013f7c1040 rsp=000000000031f898 rbp=0000000000000000
r8=0000000000000029 r9=00000000001771ae r10=0000000000000000
r11=0000000000000246 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
testProgram!func3:
00000001`3f7c1040 48895c2408 mov qword ptr [rsp+8],rbx ss:00000000`0031f8a0={testProgram!`string' (00000001`3f7c21f0)}
次に、func6 を呼んだ瞬間です。
0:000> kb
RetAddr : Args to Child : Call Site
00000001`3f7c1092 : 00000001`3f7c21d8 00000000`00000001 00000000`00000002 00000000`00000029 : testProgram!func6
00000001`3f7c10cc : 00000000`00000029 00000000`00000000 000007ff`fffde000 00000000`000000fe : testProgram!func3+0x52
00000001`3f7c1292 : 00000000`00000000 000000dc`b1cca995 00000000`00000000 00000000`00000000 : testProgram!wmain+0x2c
00000000`7789f56d : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : testProgram!__tmainCRTStartup+0x11a
00000000`779d3281 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : kernel32!BaseThreadInitThunk+0xd
00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x1d
引数として、"0x10" "0x20" "0x30" "0x40" "0x50" "0x4823" を渡しています。レジスタとスタックを見ますと、第 5 と 第 6 引数のみスタックに保存されていることが確認できます。
0:000> r
rax=0000000000000014 rbx=0000000000004823 rcx=0000000000000010
rdx=0000000000000020 rsi=0000000000000000 rdi=0000000000000029
rip=000000013f7c1000 rsp=000000000031f858 rbp=0000000000000000
r8=0000000000000030 r9=0000000000000040 r10=0000000000000000
r11=0000000000000246 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
testProgram!func6:
00000001`3f7c1000 4883ec48 sub rsp,48h
0:000> dps @rsp La
00000000`0031f858 00000001`3f7c1092 testProgram!func3+0x52
00000000`0031f860 00000001`3f7c21d8 testProgram!`string'
00000000`0031f868 00000000`00000001
00000000`0031f870 00000000`00000002
00000000`0031f878 00000000`00000029
00000000`0031f880 00000000`00000050 << 第 5 引数
00000000`0031f888 00000000`00004823 << 第 6 引数
00000000`0031f890 00000000`00000001
00000000`0031f898 00000001`3f7c10cc testProgram!wmain+0x2c
00000000`0031f8a0 00000000`00000029
さて、ここでスタック上の第 5 引数の前に、0x20 バイトの領域があることに気づくと思います。00000000`0031f860 ~ 00000000`0031f880 の領域です。
00000000`0031f858 00000001`3f7c1092 << func3 への戻り値
00000000`0031f860 00000001`3f7c21d8
00000000`0031f868 00000000`00000001
00000000`0031f870 00000000`00000002
00000000`0031f878 00000000`00000029
00000000`0031f880 00000000`00000050 << 第 5 引数
ここは "home space" と呼ばれる空間であり、第 1 から第 4 引数を入れることのできる領域です。この領域は、呼び出した関数が引数を取らなくても用意されます。尚、x64 の場合、'kb' コマンドで表示されるのは関数への引数ではなく、このホームスペースの値となります。
ホーム スペースは呼び出し側が必ず用意しますが、残念ながら、ここの使用は呼び出し先次第であり、必ずしも引数が保存されるわけではありません。デバッグビルドなどの場合は、プロローグコードの最初に渡された引数をこのホーム スペースにコピーしますが、今回の例ではリリース ビルドのため、func3 内にあった printf 関数の引数が残っています。注意していただきたいのは、このコピーを呼び出し先が行うことです。呼び出し元は、必ずレジスタを使用して引数を渡します。
// リリース ビルドの場合
0:000> u testprogram!func6
testProgram!func6:
00000001`3f7c1000 4883ec48 sub rsp,48h
00000001`3f7c1004 488b442478 mov rax,qword ptr [rsp+78h]
00000001`3f7c1009 ba10000000 mov edx,10h
00000001`3f7c100e 488d0d9b110000 lea rcx,[testProgram!`string' (00000001`3f0f21b0)]
00000001`3f7c1015 4889442430 mov qword ptr [rsp+30h],rax
00000001`3f7c101a 448d4a20 lea r9d,[rdx+20h]
00000001`3f7c101e 448d4210 lea r8d,[rdx+10h]
00000001`3f7c1022 48c744242850000000 mov qword ptr [rsp+28h],50h
// デバッグ ビルドの場合
0:000> u testProgram!func6
testProgram!func6:
00000001`3fd31030 4c894c2420 mov qword ptr [rsp+20h],r9
00000001`3fd31035 4c89442418 mov qword ptr [rsp+18h],r8
00000001`3fd3103a 4889542410 mov qword ptr [rsp+10h],rdx
00000001`3fd3103f 48894c2408 mov qword ptr [rsp+8],rcx
00000001`3fd31044 57 push rdi
00000001`3fd31045 4883ec40 sub rsp,40h
00000001`3fd31049 488bfc mov rdi,rsp
00000001`3fd3104c 48b91000000000000000 mov rcx,10h
- 引数の探し方
さて、この時点で、例えば func3 への引数を調べたかったとしましょう。今回はホーム スペースに残っていますが、これはたまたまですので、なかったこととしてデバッグしてみます。
0:000> k
Child-SP RetAddr Call Site
00000000`0031f810 00000001`3f7c1092 testProgram!func6+0x34
00000000`0031f860 00000001`3f7c10cc testProgram!func3+0x52
00000000`0031f8a0 00000001`3f7c1292 testProgram!wmain+0x2c
00000000`0031f8d0 00000000`7789f56d testProgram!__tmainCRTStartup+0x11a
00000000`0031f900 00000000`779d3281 kernel32!BaseThreadInitThunk+0xd
00000000`0031f930 00000000`00000000 ntdll!RtlUserThreadStart+0x1d
まずは func3 が呼ばれる直前のアセンブリを確認します。
0:000> ub 00000001`3f7c10cc
testProgram!wmain+0x6:
00000001`3f7c10a6 ff157c100000 call qword ptr [testProgram!_imp_rand (00000001`3f7c2128)]
00000001`3f7c10ac 488d0d3d110000 lea rcx,[testProgram!`string' (00000001`3f7c21f0)]
00000001`3f7c10b3 4863d8 movsxd rbx,eax
00000001`3f7c10b6 ff157c100000 call qword ptr [testProgram!_imp_printf (00000001`3f7c2138)]
00000001`3f7c10bc ba02000000 mov edx,2
00000001`3f7c10c1 8d4aff lea ecx,[rdx-1]
00000001`3f7c10c4 4c8bc3 mov r8,rbx
00000001`3f7c10c7 e874ffffff call testProgram!func3 (00000001`3f7c1040)
第 1、第 2 引数は "1" と "2" を指定しただけですので、値を直接 ecx と edx に入れていますね。しかし、第 3 引数は rand() の結果を一度 rbx に入れ、それを r8 に入れていることが分かります。
では、func3 の頭をアセンブリを確認します。
0:000> u 00000001`3f7c1040
testProgram!func3:
00000001`3f7c1040 48895c2408 mov qword ptr [rsp+8],rbx
00000001`3f7c1045 57 push rdi
00000001`3f7c1046 4883ec30 sub rsp,30h
00000001`3f7c104a 498bf8 mov rdi,r8
00000001`3f7c104d ff15d5100000 call qword ptr [testProgram!_imp_rand (00000001`3f7c2128)]
00000001`3f7c1053 ba01000000 mov edx,1
00000001`3f7c1058 488d0d79110000 lea rcx,[testProgram!`string' (00000001`3f7c21d8)]
00000001`3f7c105f 448d4201 lea r8d,[rdx+1]
最初に rbx の値を rsp+8 に保存しています。ということは、この時点でのスタックポインタの値が分かれば、rbx に保存されていた第 3 引数の値も分かります。
では、スタック ポインタの値を計算しましょう。まず、x64 呼出では、一つの関数のプロローグ コードとエピローグ コードの間、rsp の値は変わりません。関数開始時に実行されるプロローグコードで、ローカル変数や、関数内から呼ぶ関数の引数用のスタックスペースなど、必要なスタック領域を全て事前に確保するようになっています。アセンブリを見ると、rbx レジスタをスタックに保存した後、push コマンドを実行し、sub コマンドで rsp をずらしていますね。この sub コマンドが、確保処理です。つまり、これ以降、スタックポインタは func3 内では変わりません。
では、func3 実行時のスタック ポインタを確認しましょう。この値は 'k' コマンドで確認することができます。
0:000> k
Child-SP RetAddr Call Site
00000000`0031f810 00000001`3f7c1092 testProgram!func6+0x34
00000000`0031f860 00000001`3f7c10cc testProgram!func3+0x52
00000000`0031f8a0 00000001`3f7c1292 testProgram!wmain+0x2c
00000000`0031f8d0 00000000`7789f56d testProgram!__tmainCRTStartup+0x11a
00000000`0031f900 00000000`779d3281 kernel32!BaseThreadInitThunk+0xd
00000000`0031f930 00000000`00000000 ntdll!RtlUserThreadStart+0x1d
0x00000000`0031f860 ですね。後は、ここからプロローグコードで行われた処理を逆計算すれば、引数の値を確認できます。
プロローグ コードでは 0x30 を引き、さらに push もあったので、rsp の値は +0x38 のはずです。
0:000> ? 00000000`0031f860+30+8
Evaluate expression: 3274904 = 00000000`0031f898
ここから、rsp+8 に保存していたので…
0:000> dps 00000000`0031f898+8 L1
00000000`0031f8a0 00000000`00000029
一致しますね。無事、引数の値を確認することができました。
- 裏技紹介
このように、x64 では引数一つ調べるだけでかなりの工数がかかってしまいます。場合によって、引数を調べるためだけに関数を二つ、三つと追っていく必要があることもあり、大変です。そこで、この作業時間を減らすコマンドを紹介します!
.frame /r < フレーム番号>
このコマンドでは、指定したスタックフレーム時のレジスタの中身を表示します。尚、全てのレジスタ値が表示されますが、信用できる値は非揮発的なレジスタのみです。x64 では rbx、rbp、rdi、rsi、そして r12 ~ r15 ですね。
さて、上記例では、func3 に渡された第 3 引数は、wmain 関数の rbx に保存されていました。そこで、このコマンドの出番です。
まずは wmain のフレーム番号を確認します。
0:000> kn
# Child-SP RetAddr Call Site
00 00000000`0031f810 00000001`3f7c1092 testProgram!func6+0x34
01 00000000`0031f860 00000001`3f7c10cc testProgram!func3+0x52
02 00000000`0031f8a0 00000001`3f7c1292 testProgram!wmain+0x2c
03 00000000`0031f8d0 00000000`7789f56d testProgram!__tmainCRTStartup+0x11a
04 00000000`0031f900 00000000`779d3281 kernel32!BaseThreadInitThunk+0xd
05 00000000`0031f930 00000000`00000000 ntdll!RtlUserThreadStart+0x1d
フレーム 2 ですね。では、フレーム 2 のレジスタの内容を確認しましょう。
0:000> .frame /r 2
02 00000000`0031f8a0 00000001`3f7c1292 testProgram!wmain+0x2c
rax=0000000000004823 rbx=0000000000000029 rcx=000000013f7c21b0
rdx=0000000000000010 rsi=0000000000000000 rdi=0000000000000001
rip=000000013f7c10cc rsp=000000000031f8a0 rbp=0000000000000000
r8=0000000000000020 r9=0000000000000030 r10=0000000000000000
r11=0000000000000246 r12=0000000000000000 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
iopl=0 nv up ei pl nz na pe nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000202
testProgram!wmain+0x2c:
00000001`3f7c10cc 33c0 xor eax,eax
rbx には引数である 0x29 が表示されていますね。簡単に引数を調べることができました。
x64 環境のデバッグを行う際には役立つコマンドですので、是非活用してください。
- 参考資料
Overview of x64 Calling Conventions
https://msdn.microsoft.com/en-us/library/ms235286.aspx
x64 Software Conventions
https://msdn.microsoft.com/en-us/library/7kcdt6fy.aspx
Challenges of Debugging Optimized x64 Code