Соглашение о вызовах в x64 — это значительное улучшение по сравнению с ситуацией в x86. Мало кто будет спорить об этом. В конце концов, запоминание различий между __stdcall
и того __cdecl
, когда использовать каждый из них, какой API по умолчанию соответствует какому соглашению о вызовах и какие конкретные вариации __fastcall
JIT-компиляторов используют, когда предоставляется выбор — не лучшее использование времени разработчика и не лучшее с точки зрения отладки производительности.
С учетом вышесказанного соглашение о вызовах x64 часто очень затрудняет получение значений параметров из стека вызовов, если у вас нет личных символов для соответствующего кадра. В двух словах, проблема в том, что соглашение о вызовах x64 позволяет передавать много параметров в энергозависимые регистры, которые затем могут быть изменены вызываемым пользователем. Довольно часто компилятор передает параметры из энергозависимых регистров в предопределенное место в стеке, когда эти энергозависимые регистры должны использоваться для других целей. В других случаях, однако, значения параметров могут исчезнуть без трассировки и сделать восстановление стека исключительно трудным, особенно когда вы имеете дело с файлом дампа, а не с живым процессом, в котором вы можете установить точки останова и исследовать контекст в любой точке ,
Но достаточно сказать: давайте посмотрим на пример, где мы заинтересованы в получении значений параметров из стека. В этом случае у нас есть поток пользовательского интерфейса, который называется WaitForMultipleObjects
API, и нас интересуют первые два параметра, передаваемые 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. Это ценное дополнение к вашему арсеналу, если вам нужно отлаживать дампы оптимизированных двоичных файлов, для которых у вас нет личных символов и исходного кода.