Использование псевдорегистров MSVC при отладке приложений Apr 16, 2009

Недавно застал человека за очень творческой работой, он методично унавоживал исходный код строками вида

DWORD nCode = ::GetLastError();
char buf[256];
sprintf("LastError=%lu", nCode);
::OutputDebugStringA(buf);

Делалось это с целью поймать ошибку, появляющуюся, когда функция API выдает ошибку, а код ее игнорирует и продолжает дальше работать с невалидными данными.

Я тут же дал совет не портить нервы и код, а использовать псевдорегистры и условные точки останова. Выяснилось, что человек этого понятия не знает, а после разговора с другими людьми я понял, что эта техника популярностью почему-то не пользуется.

Итак, что такое псевдорегистры?

На самом деле, это не более чем удобная для программиста метафора - “они” выглядят как регистры, но на самом деле это просто средство (внутри реализуемое как подпрограммы) получить дополнительную информацию (коды ошибок, адрес TIB и т.д.) в отладчике.

Обладая тем же синтаксисом, что и обычные аппаратные регистры, они могут участвовать в любых арифметических операциях в окне Watch.

Например, если добавить @ERR,hr в окно Watch, то в любой момент можно увидеть расшифрованное значение GetLastError().

Псевдоренистры также могут участвовать в задании условия для точек останова, что серьезно облегчает отладку.

@ERR

Наиболее часто используемый при отладке псевдорегистр, содержит значение результата вызова GetLastError() в контексте активного потока.

@TIB

Кроме @ERR существует не менее важный псевдорегистр @TIB. Это адрес thread information block, который крайне удобно использовать при многопоточной отладке.

Например, если какая-то функция вызывается из разных потоков, то очень легко привязать точку останова к нужному потоку, добавив на нее условие, например @TIB==0x5ffa3000. После этого выполнение программы начнет прерываться только для заданного потока. Значение, с которым сравнивается @TIB можно посмотреть в окне Watch, введя @TIB во время первого прерывания.

Извращенные применения псевдорегистров

Так случилось, что трассироваться было некогда, а подозрение на то, что проблема получается из-за непроверенного значения GetLastError() уже было устойчивым.

В этом случае помогла достаточно простая техника (благо - ошибка могла возникать только в главном потоке приложения).

Смотрим на код GetLastError() (MSVC 2003):

_RtlGetLastWin32Error@0:
7C90FE21  mov         eax,dword ptr fs:[00000018h] 
7C90FE27  mov         eax,dword ptr [eax+34h] 
7C90FE2A  ret

В окне Watch выводим два значения - @ERR,x и (unsigned long)(@TIB+0x34),x. Убеждаемся, что они совпадают (это правильно ;-) ), далее (@TIB + 0x34) заменяем на число, которое можно получить в том же окне Watch, введя туда эту строку (мы в данный момент как раз и находимся в контексте главного потока). Добавляем новую точку останова через Ctrl-B, переключаемся в появившемся диалоге на вкладку Data и вводим условие останова - ((unsigned long)(0x7ffdf000 + 0x34)), поле Context очищаем.

Запускаем программу… и… через пять не имеющих отношения к делу ошибок…. Вуаля!

@__security_check_cookie@4:
7C8097AA  cmp         ecx,dword ptr [___security_cookie (7C8856CCh)] 
7C8097B0  jne         ___report_gsfailure (7C870E1Ch) 
7C8097B6  test        ecx,0FFFF0000h 
7C8097BC  jne         ___report_gsfailure (7C870E1Ch) 
7C8097C2  ret              
7C8097C3  mov         dword ptr [edi+34h],esi 
7C8097C6  jmp         _SetLastError@4+2Ch (7C809386h)</code></pre>

Stack:

> kernel32.dll!_SetLastError@4()  + 0x468 
  kernel32.dll!_BaseSetLastNTError@4()  + 0x17 
  kernel32.dll!_CreateFileW@28()  + 0x93a 
  kernel32.dll!_CreateFileA@28()  + 0x2b 
  DataTool_sec.exe!_sopen(const char * path=0x004250c8, int oflag=32768, int shflag=64, ...)  Line 387 + 0x20 C
  DataTool_sec.exe!_openfile(const char * filename=0x004250c8, const char * mode=0x004250ce, int shflag=64, _iobuf * str=0x00428da8)  Line 190 + 0x16 C
  DataTool_sec.exe!_fsopen(const char * file=0x004250c8, const char * mode=0x004250cc, int shflag=64)  Line 75 + 0x15 C
  DataTool_sec.exe!fopen(const char * file=0x004250c8, const char * mode=0x004250cc)  Line 116 + 0xf C</pre></code>

И ниже появилась точка сбоя.

Время на поиск ошибки - полторы минуты, этот пост я набираю существенно дольше… Впрочем, убирали ненужные контрольные печати в Araxis’e еще дольше (простой revert отбросил бы и сделанные осмысленные изменения).

Далее - просто для справки, основные псевдорегистры, доступные разработчику.

Список основных псевдорегистров

Pseudoregister Description
@ERR Last error value; the same value returned by the GetLastError() API function
@TIB Thread information block for the current thread; necessary because the debugger doesn’t handle the “FS:0” format
@CLK Undocumented clock register; usable only in the Watch window
@EAX,
@EBX,
@ECX,
@EDX,
@ESI,
@EDI,
@EIP,
@ESP,
@EBP,
@EFL
Intel CPU registers
@CS,
@DS,
@ES,
@SS,
@FS,
@GS
Intel CPU segment registers
@ST0,
@ST1,
@ST2,
@ST3,
@ST4,
@ST5,
@ST6,
@ST7
Intel CPU floating-point registers

Врочем, сознаюсь честно, регистры сопроцессора мне как-то при отладке применять еще не приходилось….

Резюме - чтение Робинсона “Отладка Windows приложений” очень помогает, хотя книжка уже и старая…