четверг, 16 апреля 2009 г.

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

Недавно застал человека за очень творческой работой, он методично унавоживал исходный код строками вида
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)


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

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

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

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

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










PseudoregisterDescription
@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 приложений" очень помогает, хотя книжка уже и старая...

Комментариев нет: