Соглашение о вызовах в x64 — это значительное улучшение по сравнению с ситуацией в x86. Мало кто будет спорить об этом. В конце концов, запоминание различий между __stdcallи того __cdecl, когда использовать каждый из них, какой API по умолчанию соответствует какому соглашению о вызовах и какие конкретные вариации __fastcallJIT-компиляторов используют, когда предоставляется выбор — не лучшее использование времени разработчика и не лучшее с точки зрения отладки производительности.
С учетом вышесказанного соглашение о вызовах x64 часто очень затрудняет получение значений параметров из стека вызовов, если у вас нет личных символов для соответствующего кадра. В двух словах, проблема в том, что соглашение о вызовах x64 позволяет передавать много параметров в энергозависимые регистры, которые затем могут быть изменены вызываемым пользователем. Довольно часто компилятор передает параметры из энергозависимых регистров в предопределенное место в стеке, когда эти энергозависимые регистры должны использоваться для других целей. В других случаях, однако, значения параметров могут исчезнуть без трассировки и сделать восстановление стека исключительно трудным, особенно когда вы имеете дело с файлом дампа, а не с живым процессом, в котором вы можете установить точки останова и исследовать контекст в любой точке ,
Но достаточно сказать: давайте посмотрим на пример, где мы заинтересованы в получении значений параметров из стека. В этом случае у нас есть поток пользовательского интерфейса, который называется WaitForMultipleObjectsAPI, и нас интересуют первые два параметра, передаваемые WaitForMultipleObjects: количество объектов синхронизации, которых ожидает поток, и массив дескрипторов этих объектов. Первая попытка включает в себя kbкоманду, которая принимает предположение о параметрах метода, выводя первые три значения QWORDв RBP+8(сразу после адреса возврата метода, если FPO не использовался):
0:000> kb RetAddr : Args to Child : Call Site 000007f9`346212d2 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!NtWaitForMultipleObjects+0xa 000007f9`368e1292 : 00000000`00000001 000007f6`1cfc9000 00000000`00000001 000000b9`e75faa68 : KERNELBASE!WaitForMultipleObjectsEx+0xe5 000007f6`1d3e1da9 : 00000000`00000001 cccccccc`cccccccc cccccccc`cccccccc cccccccc`cccccccc : KERNEL32!WaitForMultipleObjects+0x12 000007f9`1f710c37 : 000000b9`e5cff3b0 000000b9`e5cfeba0 000000b9`e5cfe2b8 cccccccc`cccccccc : BatteryMeter!CBatteryMeterDlg::OnCPUSelectorChanged+0x159 ...snipped for brevity...
Эти ценности, конечно, полная чушь. Если бы у нас был исходный код для модуля BatteryMeter, мы могли бы проверить его и попытаться определить параметры. Однако без исходного кода мы должны прибегнуть к разборке функции вокруг вызова WaitForMultipleObjects:
0:000> uf BatteryMeter!CBatteryMeterDlg::OnCPUSelectorChanged ...snipped for brevity... 185 000007f6`1d3e1d8d 41b9ffffffff mov r9d,0FFFFFFFFh 185 000007f6`1d3e1d93 41b801000000 mov r8d,1 185 000007f6`1d3e1d99 488d542448 lea rdx,[rsp+48h] 185 000007f6`1d3e1d9e b904000000 mov ecx,4 185 000007f6`1d3e1da3 ff1597df0200 call qword ptr [BatteryMeter!_imp_WaitForMultipleObjects (000007f6`1d40fd40)] ...snipped for brevity...
Обратите внимание, что это действительно сайт вызова: CALLинструкция (длина которой составляет шесть байтов:) ff1597df0200находится в точке 000007f6`1d3e1da3, тогда как адрес возврата для WaitForMultipleObjectsсоставляет в 000007f6`1d3e1da9шесть байтов позже.
Получив сайт вызова, мы можем вызвать порядок параметров в соглашении о вызовах x64. В частности, WaitForMultipleObjectsимеет четыре параметра: количество объектов синхронизации (a DWORD), массив дескрипторов, логическое значение, указывающее, следует ли ждать, пока все объекты не станут сигнальными или какими-либо из них, и, наконец, время ожидания (a DWORD). Эти параметры передаются в ECX, RDX, R8Dи R9Dрегистрах, соответственно. (Вспомните, что RnDэто псевдоним для младших 32 бит 64-битного Rnрегистра.)
На данный момент мы знаем количество объектов в массиве — это константа 4. Кроме того, даже если RDXрегистр, вероятно, был засорен вызываемым объектом, мы все равно можем определить адрес массива, проверив расположение стека RSP+48в рамка звонящего. Чтобы найти значение RSP, мы можем использовать kкоманду:
0:000> k Child-SP RetAddr Call Site 000000b9`e5cfdbd8 000007f9`346212d2 ntdll!NtWaitForMultipleObjects+0xa 000000b9`e5cfdbe0 000007f9`368e1292 KERNELBASE!WaitForMultipleObjectsEx+0xe5 000000b9`e5cfdec0 000007f6`1d3e1da9 KERNEL32!WaitForMultipleObjects+0x12 000000b9`e5cfdf00 000007f9`1f710c37 BatteryMeter!CBatteryMeterDlg::OnCPUSelectorChanged+0x159 ...snipped for brevity...
Теперь мы можем осмотреть сами ручки:
0:000> dq 000000b9`e5cfdf00+48 L4 000000b9`e5cfdf48 00000000`00000118 00000000`00000120 000000b9`e5cfdf58 00000000`00000128 00000000`0000012c
… или даже попросить отладчик распечатать информацию о дескрипторах для массивов в массиве:
0:000> .foreach /pS 1 /ps 1 (h {dq /c 1 000000b9`e5cfdf00+48 L4}) {!handle h f}
Handle 118
Type Thread
Attributes 0
GrantedAccess 0x1fffff:
Delete,ReadControl,WriteDac,WriteOwner,Synch
Terminate,Suspend,Alert,GetContext,SetContext,SetInfo,QueryInfo,SetToken,Impersonate,DirectImpersonate
HandleCount 3
PointerCount 786412
Name <none>
Object Specific Information
Thread Id 1940.1158
Priority 12
Base Priority 0
Start Address 1d3e1fa0 BatteryMeter!CPUSelectorThread
Handle 120
Type Thread
Attributes 0
GrantedAccess 0x1fffff:
Delete,ReadControl,WriteDac,WriteOwner,Synch
Terminate,Suspend,Alert,GetContext,SetContext,SetInfo,QueryInfo,SetToken,Impersonate,DirectImpersonate
HandleCount 3
PointerCount 786416
Name <none>
Object Specific Information
Thread Id 1940.12d0
Priority 10
Base Priority 0
Start Address 1d3e1ff0 BatteryMeter!HardwareChangeDetectorThread
Handle 128
Type Thread
Attributes 0
GrantedAccess 0x1fffff:
Delete,ReadControl,WriteDac,WriteOwner,Synch
Terminate,Suspend,Alert,GetContext,SetContext,SetInfo,QueryInfo,SetToken,Impersonate,DirectImpersonate
HandleCount 3
PointerCount 786420
Name <none>
Object Specific Information
Thread Id 1940.1220
Priority 12
Base Priority 0
Start Address 1d3e2040 BatteryMeter!LocationAwarenessThread
Handle 12c
Type Thread
Attributes 0
GrantedAccess 0x1fffff:
Delete,ReadControl,WriteDac,WriteOwner,Synch
Terminate,Suspend,Alert,GetContext,SetContext,SetInfo,QueryInfo,SetToken,Impersonate,DirectImpersonate
HandleCount 3
PointerCount 786424
Name <none>
Object Specific Information
Thread Id 1940.1814
Priority 10
Base Priority 0
Start Address 1d3e20a0 BatteryMeter!TemperaturePropagationThread
Подводя итог, с некоторыми усилиями мы смогли обнаружить, что WaitForMultipleObjectsфункция была вызвана с массивом из четырех потоков, который мы теперь можем продолжить и проверить. Но это был простой случай, когда параметры не были засорены — уму непостижимо думать, что вам приходится проходить списки разборки каждый раз, когда вы хотите сбросить значения параметров.
Введите CMKD — бесплатное расширение для отладки, которое упрощает анализ 64-битных стеков вызовов. Это расширение выполняет довольно умный анализ структуры стека, областей хранения энергонезависимых регистров и сайтов вызовов функций, чтобы отобразить значения параметров или хотя бы объяснить, откуда они пришли. В нашем конкретном случае это расширение работает блестяще:
0:000> !stack -p -t Call Stack : 44 frames ## Stack-Pointer Return-Address Call-Site ...snipped for brevity... 01 000000b9e5cfdbe0 000007f9368e1292 KERNELBASE!WaitForMultipleObjectsEx+e5 Parameter[0] = 0000000000000004 : rcx saved in current frame into NvReg rbx which is saved by child frames Parameter[1] = 000000b9e5cfdf48 : rdx saved in current frame into NvReg r13 which is saved by child frames Parameter[2] = 0000000000000001 : r8 saved in current frame into stack Parameter[3] = 0000000000000000 : r9 saved in current frame into NvReg r14 which is saved by child frames 02 000000b9e5cfdec0 000007f61d3e1da9 KERNEL32!WaitForMultipleObjects+12 Parameter[0] = 0000000000000004 : rcx setup in parent frame by movb instruction @ 000007f61d3e1d9e from immediate data Parameter[1] = 000000b9e5cfdf48 : rdx setup in parent frame by lea instruction @ 000007f61d3e1d99 from mem @ 000000b9e5cfdf48 Parameter[2] = 0000000000000001 : r8 setup in parent frame by movb instruction @ 000007f61d3e1d93 from immediate data Parameter[3] = 00000000ffffffff : r9 setup in parent frame by movb instruction @ 000007f61d3e1d8d from immediate data 03 000000b9e5cfdf00 000007f91f710c37 BatteryMeter!CBatteryMeterDlg::OnCPUSelectorChanged+159 Parameter[0] = (unknown) : Parameter[1] = (unknown) : Parameter[2] = (unknown) : Parameter[3] = (unknown) : ...snipped for brevity...
В предыдущем выводе выделенные параметры соответствуют тому, что мы ранее обнаружили с помощью ручного труда. CMKD вывел значения параметров и объяснил, откуда они пришли — конкретные MOVB/LEAинструкции, которые инициализировали регистры.
В заключение: CMKD значительно упрощает анализ вызовов методов x64, использующих соглашение о вызовах x64. Это ценное дополнение к вашему арсеналу, если вам нужно отлаживать дампы оптимизированных двоичных файлов, для которых у вас нет личных символов и исходного кода.