Статьи

Получение надежных стеков вызовов потоков 64-битных процессов

Соглашение о вызовах в 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. Это ценное дополнение к вашему арсеналу, если вам нужно отлаживать дампы оптимизированных двоичных файлов, для которых у вас нет личных символов и исходного кода.