Изучаем отладчик, часть вторая / Хабр

Exception_debug_event:

Все восемь вышеперечисленных события в принципе второстепенны. Основная работа начинается только после прихода события EXCEPTION_DEBUG_EVENT.

Его параметры идут в структуре

Генерация данного события означает что в отлаживаем приложении произошло некое исключение, тип которого можно узнать из параметра DebugEvent.Exception.ExceptionRecord.ExceptionCode. Помните, в первой части статьи я упоминал что отладка производится через механизм структурной обработки ошибок (SEH)? Вот сейчас мы это и рассмотрим более подробно.

Большинство исключений в процессе отладки являются наведенными. То есть получение исключения не означает что в самой программе произошла ошибка. Скорее всего исключение произошло из-за вмешательства отладчика в работу приложения, например посредством установки ВР.

Нюанс: Если в самом приложении происходит ошибка, мы ее так-же получим в виде отладочного исключения и нам нужно будет реализовать код отладчика таким образом, чтобы мы могли отличать наши наведенные ошибки, от пользовательских.

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

  1. Стандартный ВР на строчку кода.
  2. ВР на адрес памяти (Memory Breakpoint или урезанный Data Preakpoint в Delphi).
  3. Hardware BP (отсутствует в Delphi).

Для работы с ними достаточно обработки трех типов исключений:

EXCEPTION_DEBUG_EVENT:
begin
  ThreadIndex := GetThreadIndex(DebugEvent.dwThreadId);
  case DebugEvent.Exception.ExceptionRecord.ExceptionCode of
 
    EXCEPTION_BREAKPOINT:
      ProcessExceptionBreakPoint(ThreadIndex, DebugEvent);
 
    EXCEPTION_SINGLE_STEP:
      ProcessExceptionSingleStep(ThreadIndex, DebugEvent);
 
    EXCEPTION_GUARD_PAGE:
      ProcessExceptionGuardPage(ThreadIndex, DebugEvent);
 
  else
    CallUnhandledExceptionEvents(ThreadIndex, CodeDataToExceptionCode(
      DebugEvent.Exception.ExceptionRecord.ExceptionCode), DebugEvent);
  end;
end;

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

Реализация точки остановки на строчку кода:

Установка ВР на строку кода производится при помощи модификации кода отлаживаемого приложения. Классически это делается записью опкода 0xCC по адресу устанавливаемой ВР, означающего инструкцию «INT3».

Встречаются и другие варианты, например опкод 0xCD03, так же представляющий из себя инструкцию «INT3». Второй вариант больше применяется при антиотладке и устанавливается в большинстве случаев самим приложением, пытаясь поймать наличие отладчика на том, что ядерный _KiTrap03() умеет работать только с однобайтовым опкодом и немного неверно обрабатывает двухбайтовый.

Но, все это лирика, нас интересует именно первый опкод.

Для хранения списка установленных ВР класс TFWDebugerCore использует следующие структуры:

// Список поддерживаемых типов точек остановки (далее ВР)
 
TBreakpointType = (
  btBreakpoint,           // WriteProcessMemoryEx   0xCC
  btMemoryBreakpoint      // VirtualProtectEx   PAGE_GUARD
);
 
// структуры для хранения данных об известных отладчику ВР
 
TInt3Breakpoint = record
  Address: Pointer;
  ByteCode: Byte;
end;
 
TMemotyBreakPoint = record
  Address: Pointer;
  Size: DWORD;
  BreakOnWrite: Boolean;
  RegionStart: Pointer;
  RegionSize: DWORD;
  PreviosRegionProtect: DWORD;
end;
 
TBreakpoint = packed record
  bpType: TBreakpointType;
  Description: ShortString;
  Active: Boolean;
  case Integer of
    0: (Int3: TInt3Breakpoint;);
    1: (Memory: TMemotyBreakPoint);
end;
 
TBreakpointList = array of TBreakpoint;

Перед тем как добавить новый ВР он инициализирует запись TBreakpoint, заполняя ее необходимыми параметрами, после чего добавляет ее в общий список точек остановки.

Для ВР на строку кода нам необходимо хранить только два значения, адрес ВР и значение байта, хранящегося по данному адресу перед тем как мы затрем его опкодом 0xCC.

Установка ВР в отлаживаемом приложении выглядит примерно таким образом:

function TFWDebugerCore.SetBreakpoint(Address: DWORD;
  const Description: string): Boolean;
var
  Breakpoint: TBreakpoint;
  OldProtect: DWORD;
  Dummy: DWORD;
begin
  ZeroMemory(@Breakpoint, SizeOf(TBreakpoint));
  Breakpoint.bpType := btBreakpoint;
  Breakpoint.Int3.Address := Pointer(Address);
  Breakpoint.Description := Description;
  Check(VirtualProtectEx(FProcessInfo.AttachedProcessHandle,
    Pointer(Address), 1, PAGE_READWRITE, OldProtect));
  try
    Check(ReadProcessMemory(FProcessInfo.AttachedProcessHandle,
      Pointer(Address), @Breakpoint.Int3.ByteCode, 1, Dummy));
    Check(WriteProcessMemory(FProcessInfo.AttachedProcessHandle,
      Pointer(Address), @BPOpcode, 1, Dummy));
  finally
    Check(VirtualProtectEx(FProcessInfo.AttachedProcessHandle,
      Pointer(Address), 1, OldProtect, OldProtect));
  end;
  Result := AddNewBreakPoint(Breakpoint);
end;

Первоначально производится инициализация структуры, устанавливается тип ВР, его описание и параметры. Так как производится запись в область кода, которая обычно не имеет прав на запись, выставляются соответствующие права, читается оригинальное значение, расположенное по адресу ВР, пишется инструкция 0xCC представленная константой BPOpcode и возвращаются изначальные атрибуты страницы повторным вызовом VirtualProtectEx(). В завершении всего, если не произошло ошибок, запись об установленном ВР помещается в общий список класса.

Ну а теперь начинается самое интересное:

После установки ВР отлаживаемое приложение продолжит свое нормальное функционирование, пока не произойдет переход на записанную нами инструкцию «INT3». В этот момент будет сгенерировано событие EXCEPTION_DEBUG_EVENT с кодом исключения EXCEPTION_BREAKPOINT.

Параметры исключения будут переданы нам в виде структуры DebugEvent.Exception.ExceptionRecord (EXCEPTION_DEBUG_INFO structure).

Как я и описывал ранее, ВР может быть установлено и самим отлаживаемым приложением, поэтому ориентируясь на данные параметры необходимо разобраться, что за ВР сработал?

Для этого нам пригодится список ранее сохраненных точек остановки. Пробежавшись в цикле по нему и сравнив адрес хранящийся в параметре DebugEvent.Exception.ExceptionRecord.ExceptionAddress с полем Address каждой записи с типом btBreakpoint, мы можем определить, устанавливали ли мы по данному адресу ВР или это что-то не наше.

Если мы определили что ВР действительно наш, то поднимаем внешнее событие (дабы показать пользователю что мы не просто так тут, а даже работаем) и после его обработки приступаем к восстановлению отлаживаемого приложения.

Последствия установки ВР:

Устанавливая ВР мы затерли часть исполняемого кода.

Для примера изначально был такой код:

После проведенных нами манипуляций он превратился в следующее:

Первым шагом мы должны восстановить значение изначальной инструкции.

Чтобы это было сделать более удобно, у структуры TBreakpoint есть параметр Active, указывающий на состояние точки остановки. Ориентируясь на данный параметр класс TFWDebugerCore знает о их активности, а для переключения состояния он реализует метод ToggleInt3Breakpoint, в котором в зависимости от флага включает и отключает ВР возвращая на место затертый байт.

procedure TFWDebugerCore.ToggleInt3Breakpoint(Index: Integer;
  Active: Boolean);
var
  OldProtect: DWORD;
  Dummy: DWORD;
begin
  CheckBreakpointIndex(Index);
  if FBreakpointList[Index].bpType <> btBreakpoint then Exit;
  if FBreakpointList[Index].Active = Active then Exit;
  Check(VirtualProtectEx(FProcessInfo.AttachedProcessHandle,
    FBreakpointList[Index].Int3.Address, 1, PAGE_READWRITE, OldProtect));
  try
    if Active then
      Check(WriteProcessMemory(FProcessInfo.AttachedProcessHandle,
        FBreakpointList[Index].Int3.Address, @BPOpcode, 1, Dummy))
    else
      Check(WriteProcessMemory(FProcessInfo.AttachedProcessHandle,
        FBreakpointList[Index].Int3.Address,
        @FBreakpointList[Index].Int3.ByteCode, 1, Dummy));
  finally
    Check(VirtualProtectEx(FProcessInfo.AttachedProcessHandle,
      FBreakpointList[Index].Int3.Address, 1, OldProtect, OldProtect));
  end;
  FBreakpointList[Index].Active := Active;
end;

Реализация практически похожа на код устанавливающий ВР за исключением того момента, что при повторной установке ВР не требуется заново считывать значение байта (т.к. он нам уже известен).

Теперь нюанс: если мы прямо сейчас запустим отлаживаемое приложение на выполнение, то получим ошибку. А все потому что инструкция «INT3» уже выполнилась, и даже если мы вернули затертый нами байт на место по адресу 0x452220, отлаживаемая программа продолжит выполнение с адреса 0x452221, где расположена инструкция «mov ebp, esp», а не с того места где было сгенерировано отладочное исключение.

Работа с командной строкой windows, программа debug и её использование

Запуск Debug.exe, программы для проверки и отладки исполнительных
файлов MS-DOS. Выполненная без параметров команда debug запускает
программу Debug.exe и выводит приглашение команды debug, представленное
дефисом (-).

Пуск -> Выполнить -> Вводим cmd и нажимаем Enter.

В консоли: ввести  debug, затем (?).

A (assemble) Транслирование команд ассемблера в машинный код. Адрес по умолчаниюCS:0100h.

С (compare) Сравнение содержимого двух областей памяти. По умолчанию используется DS . В команде указывается либо длина участков, либо диапазон адресов.

D (dump) Вывод содержимого области памяти в шестнадцатеричном и ASCII-форматах. По умолчанию используется DS . Можно указывать длину или диапазон.

E (enter) Ввод в память данные или инструкции машинного кода. По умолчанию используется DS .

F (fill) Заполнение области памяти данными из списка. По умолчанию используется DS . Использовать можно как длину, так и диапазон.

G (go) Выполнение отлаженной программы на машинном языке до указанной точки останова. По умолчанию используется DS . При этом следует убедиться, что IP содержит корректный адрес.

H (hexadecimal) Вычисление суммы и разности двух шестнадцатеричных величин.

I (input) Считывание и вывод одного байта из порта.

L (load) Загрузка файла или данных из секторов диска в память. . По умолчанию CS:100h. Файл можно указать с помощью команды N или аргумента при запуске debug.exe.

M (move) Копирование содержимого ячеек памяти; по умолчанию используется DS . Можно указывать как длину, так и диапазон.

N (name) Указание имени файла для команд L (LOAD) и W (WRITE) .

O (output)  Отсылка байта в порт вывода.

P (proceed)  Выполнение инструкций CALL, LOOP, INT (цикла, прерывания, процедур) или повторяемой строковой инструкции с префиксами REPnn , переходя к следующей инструкции.

Q (quit)  Завершение работы debug.exe . Без сохранения тестируемого файла.

R (register)  Вывод содержимого регистров и следующей инструкции.

S (search)  Поиск в памяти символов из списка. По умолчанию используется DS . Можно указывать как длину, так и диапазон.

T (trace)  Пошаговое выполнение программы. Как и в команде P , по умолчанию используется пара CS:IP . Но для выполнения прерываний лучше пользоваться командой P .

:/>  Режим разработчика Андроид: как включить или отключить

U (unassemble)  Дизассемблирование машинного кода. По умолчанию используется пара CS:IP .

W (write)  Запись файла из debug.exe на диск. Необходимо обязательно задать имя файла командой N , если он не был загружен.

Наберем команду R.

Регистры CS, DS,
ES, SS в этот момент инициализированы адресом 256-байтного
префикса сегмента програмы
, а рабочая области в памяти будет начинаться с адреса
этого префикса 100h
.

Первые 16 байт области данных BIOS содержат адреса параллельных и
последовательных портов. Первые выведенные восемь байтов указывают на адреса
последовательных портов COM1-COM4. Следующие 8 байтов указывают на адреса
параллельных портов LPT1-LPT4. Адрес порта 78 03 – записывается в обращенной
форме.

В области данных BIOS по адресу 417h находится первый байт, который хранит состояние регистра клавиатуры..

Сведения об авторских правах на BIOS встроены в ROM BIOS по адресу FE00:0 . Строку с копирайтом можно легко найти в ASCII -последовательности, а серийный номер – в виде шестнадцатеричного числа. На экране видим семизначный номер компьютера и дата копирайт.
Хотя, строка с указанием авторских прав может быть длинной и не умещаться в выведенную область памяти. В таком случае следует просто ввести еще раз D.

Дата также записана в ROM BIOS начиная с адреса FFFF:5 . После выполнения соответствующей команды в ASCII -последовательности будет находиться эта дата, записанная в формате мм/дд/гг .

Рассмотрим создание программы на машинном языке, ее представление в памяти и результаты выполнения.
Команда отладчика A (Assemble) переводит DEBUG в режим приема команд ассемблера и перевода их в машинные коды.

Теперь, когда программа введена в память, попробуем управлять ее выполнением. Для начала проверим текущее состояние регистров и флагов, для этого вводим команду R.

Выполним её. Необходимо сообщить отладчику адреса первой и последней команды, которые необходимо просмотреть (у Нас 100 и 107). Появятся инструкции, находящиеся в указанном диапазоне, на ассемблере, в машинных кодах, а также адрес каждой инструкции. Выполним программу пошагово, используя команду T.

Использовав во второй раз команду T, мы выполнили инструкцию MOV. Машинный код операнда инструкции – 00D8. Операция прибавляет AL к BL.
Для выхода введем Q. И вновь дизассемблируем созданный testpi.com.

Изменение или копирование кода BIOS может нарушить авторское право производителя BIOS. BIOS можно копировать или модифицировать только для целей индивидуального пользования, но не для распространения. Типичная процедура копирования BIOS с помощью программы DEBUG приведем ниже.
Эта процедура сохранит в файле весь сегмент в 64 Кбайт с адреса F000:0000h по F0000:FFFFh.

Реализация аппаратных точек остановки:

Так называемые Hardware BreakPoint (далее HBP). Достаточно мощный инструмент отладки. В отличие от ВР и МВР данные точки остановки не производят модификаций в памяти отлаживаемого приложения. Плохо только то, что их очень мало, всего лишь четыре штуки на каждую нить.

Но в отличии от других НВР предоставляет достаточно гибкие условия контроля отлаживаемого приложение.

Сравним:ВР — отслеживает только обращения к исполняемому коду (скажем режим Исполнения)МВР — позволяет отслеживать доступ в режиме Чтения или Чтения/Записи.НВР — позволяет выставить условие более точно, он различает режимы Записи, Чтения/Записи, IO режим (доступ к порту ввода/вывода) и режим Исполнения.

Т.е. НВР может проэмулировать работу как ВР (в режиме Исполнение) так и МВР (в режимах Запись — Чтение/Запись). Правда в отличие от МВР он не может контролировать большой диапазон области памяти, т.к. умеет работать только с блоками фиксированного размера 1, 2 или 4 байта.

Настройки НВР хранятся в контексте каждой нити, для этого используются DR регистры, доступ к которым осуществляется при указании флага CONTEXT_DEBUG_REGISTERS.Всего их шесть. Dr0..Dr3, Dr6, Dr7. (Dr4 и Dr5 зарезервированы).Первые 4 регистра хранят в себе адрес каждого из НВР.

Класс TFWDebugerCore хранит информацию о НВР в виде следующей структуры:

THWBPIndex = 0..3;
THWBPSize = (hsByte, hdWord, hsDWord);
THWBPMode = (hmExecute, hmWrite, hmIO, hmReadWrite);
 
THardwareBreakpoint = packed record
  Address: array [THWBPIndex] of Pointer;
  Size: array [THWBPIndex] of THWBPSize;
  Mode: array [THWBPIndex] of THWBPMode;
  Description: array [THWBPIndex] of ShortString;
  Active: array [THWBPIndex] of Boolean;
end;

Так как все 4 НВР у каждой конкретной нити свои, они не хранятся в общем списке ВР класса.Помните еще в начале я говорил что мы будем хранить данные о нитях в отдельном списке в виде пары ID = hThreadHandle. На самом деле этот список выглядит следующим образом:

TThreadData = record
  ThreadID: DWORD;
  ThreadHandle: THandle;
  Breakpoint: THardwareBreakpoint;
end;
TThreadList = array of TThreadData;

Т.е. помимо этих двух параметров у каждой нити присутствует своя структура, описывающая настройки принадлежащих ей НВР.

Работа с установкой, изменением состояния и удалением НВР реализована крайне просто.

Установка выглядит так:

procedure TFWDebugerCore.SetHardwareBreakpoint(ThreadIndex: Integer;
  Address: Pointer; Size: THWBPSize; Mode: THWBPMode;
  HWIndex: THWBPIndex; const Description: string);
begin
  if ThreadIndex < 0 then Exit;
  FThreadList[ThreadIndex].Breakpoint.Address[HWIndex] := Address;
  FThreadList[ThreadIndex].Breakpoint.Size[HWIndex] := Size;
  FThreadList[ThreadIndex].Breakpoint.Mode[HWIndex] := Mode;
  FThreadList[ThreadIndex].Breakpoint.Description[HWIndex] :=
    ShortString(Description);
  FThreadList[ThreadIndex].Breakpoint.Active[HWIndex] := True;
  UpdateHardwareBreakpoints(ThreadIndex);
end;

Просто инициализируем структуру и вызываем метод UpdateHardwareBreakpoints.

Модификация состояния реализована следующим кодом:

procedure TFWDebugerCore.ToggleHardwareBreakpoint(ThreadIndex: Integer;
  Index: THWBPIndex; Active: Boolean);
begin
  if ThreadIndex < 0 then Exit;
  if FThreadList[ThreadIndex].Breakpoint.Active[Index] = Active then Exit;
  FThreadList[ThreadIndex].Breakpoint.Active[Index] := Active;
  UpdateHardwareBreakpoints(ThreadIndex);
end;

Просто меняем флаг Active и опять же вызываем UpdateHardwareBreakpoints.

Ну и удаление:

procedure TFWDebugerCore.DropHardwareBreakpoint(ThreadIndex: Integer;
  Index: THWBPIndex);
begin
  if ThreadIndex < 0 then Exit;
  if FThreadList[ThreadIndex].Breakpoint.Address[Index] = nil then Exit;
  FThreadList[ThreadIndex].Breakpoint.Address[Index] := nil;
  UpdateHardwareBreakpoints(ThreadIndex);
end;

Обнуляем адрес НВР и опять вызываем UpdateHardwareBreakpoints.

Весь нюанс кроется именно в методе UpdateHardwareBreakpoints.Основная его задача заполнить регистры Dr0-Dr3 адресами активных НВР и провести правильную инициализацию регистра Dr7.

Вот с ним-то придется повозится.

Данный регистр представляет из себя набор битовых флагов, определяющих настройку каждой из НВР и формально все выглядит следующим образом:

Самые старшие 4 бита (31-28) хранят настройки регистра Dr3.Выглядит это следующим образом:

Старшие 2 бита (LENi) из четырех отвечают за размер контролируемой НВР памяти.00 — 1 байт01 — 2 байта10 — такая комбинация битов не используется.11 — 4 байта

Младшие 2 бита (RWi) из 4 отвечают за настройку режима работы НВР00 — Execute01 — Write10 — IO Read/Write11 — Read/Write

Таким образом, если мы хотим чтобы НВР из регистра Dr3 реагировала на запись в любые 4 байта начиная со значения, указанного в Dr3, старшие 4 бита регистра Dr7 должны выглядеть как 1101.

Следующие 4 бита (27-24) используются для настройки НВР регистра Dr2Биты 23-20 относятся к Dr1 и, в завершение, биты 19-16 к регистру Dr0.

Бит 13 регистра Dr7 (GD — Global Debug Register Access Detect) — отвечает за целостность данных в отладочных регистрах. Например если отлаживаемой программе вдруг вздумалось хранить в этих регистрах свои значения, отладчик будет уведомлен об этом.

Бит 9 регистра Dr7 (GE — Global Exact data breakpoint match) — включает работу с глобальными НВР.Бит 8 регистра Dr7 (LE — Local Exact data breakpoint match) — включает работу с локальными НВР.

LE бит сбрасывается при переключении задач, более подробнее можно узнать в интеловских мануалах.

Остались 8 бит (7-0) представленные в виде пары флагов Gi и Li для каждого из регистров включающих HBP в глобальном или локальном режиме.

Бит 7 (Gi — Global breakpoint enable) — включает глобальный режим регистру Dr3Бит 6 (Li — Local breakpoint enable) — включает локальный режим регистру Dr35- 4 то же для Dr23- 2 для Dr1 и 1-0 для Dr0

Запутались?

Ну тогда вот картинка:

В виде исходного кода все выглядит достаточно просто.

procedure TFWDebugerCore.UpdateHardwareBreakpoints(ThreadIndex: Integer);
const
  DR7_SET_LOC_DR0 = $01;
  DR7_SET_GLB_DR0 = $02;
  DR7_SET_LOC_DR1 = $04;
  DR7_SET_GLB_DR1 = $08;
  DR7_SET_LOC_DR2 = $10;
  DR7_SET_GLB_DR2 = $20;
  DR7_SET_LOC_DR3 = $40;
  DR7_SET_GLB_DR3 = $80;
 
  DR7_SET_LOC_ON  = $100;
  DR7_SET_GLB_ON  = $200;
 
  DR7_PROTECT     = $2000;
 
  DR_SIZE_BYTE    = 0;
  DR_SIZE_WORD    = 1;
  DR_SIZE_DWORD   = 3;
 
  DR_MODE_E       = 0;
  DR_MODE_W       = 1;
  DR_MODE_I       = 2;
  DR_MODE_R       = 3;
 
  DR7_MODE_DR0_E  = DR_MODE_E shl 16;
  DR7_MODE_DR0_W  = DR_MODE_W shl 16;
  DR7_MODE_DR0_I  = DR_MODE_I shl 16;
  DR7_MODE_DR0_R  = DR_MODE_R shl 16;
 
  DR7_SIZE_DR0_B  = DR_SIZE_BYTE shl 18;
  DR7_SIZE_DR0_W  = DR_SIZE_WORD shl 18;
  DR7_SIZE_DR0_D  = DR_SIZE_DWORD shl 18;
 
  DR7_MODE_DR1_E  = DR_MODE_E shl 20;
  DR7_MODE_DR1_W  = DR_MODE_W shl 20;
  DR7_MODE_DR1_I  = DR_MODE_I shl 20;
  DR7_MODE_DR1_R  = DR_MODE_R shl 20;
 
  DR7_SIZE_DR1_B  = DR_SIZE_BYTE shl 22;
  DR7_SIZE_DR1_W  = DR_SIZE_WORD shl 22;
  DR7_SIZE_DR1_D  = DR_SIZE_DWORD shl 22;
 
  DR7_MODE_DR2_E  = DR_MODE_E shl 24;
  DR7_MODE_DR2_W  = DR_MODE_W shl 24;
  DR7_MODE_DR2_I  = DR_MODE_I shl 24;
  DR7_MODE_DR2_R  = DR_MODE_R shl 24;
 
  DR7_SIZE_DR2_B  = DR_SIZE_BYTE shl 26;
  DR7_SIZE_DR2_W  = DR_SIZE_WORD shl 26;
  DR7_SIZE_DR2_D  = DR_SIZE_DWORD shl 26;
 
  DR7_MODE_DR3_E  = DR_MODE_E shl 28;
  DR7_MODE_DR3_W  = DR_MODE_W shl 28;
  DR7_MODE_DR3_I  = DR_MODE_I shl 28;
  DR7_MODE_DR3_R  = DR_MODE_R shl 28;
 
  DR7_SIZE_DR3_B  = DR_SIZE_BYTE shl 30;
  DR7_SIZE_DR3_W  = DR_SIZE_WORD shl 30;
  DR7_SIZE_DR3_D  = $C0000000; //DR_SIZE_DWORD shl 30;
 
  DR_On: array [THWBPIndex] of DWORD = (
    DR7_SET_LOC_DR0,
    DR7_SET_LOC_DR1,
    DR7_SET_LOC_DR2,
    DR7_SET_LOC_DR3
  );
 
  DR_Mode: array [THWBPIndex] of array [THWBPMode] of DWORD = (
    (DR7_MODE_DR0_E, DR7_MODE_DR0_W, DR7_MODE_DR0_I, DR7_MODE_DR0_R),
    (DR7_MODE_DR1_E, DR7_MODE_DR1_W, DR7_MODE_DR1_I, DR7_MODE_DR1_R),
    (DR7_MODE_DR2_E, DR7_MODE_DR2_W, DR7_MODE_DR2_I, DR7_MODE_DR2_R),
    (DR7_MODE_DR3_E, DR7_MODE_DR3_W, DR7_MODE_DR3_I, DR7_MODE_DR3_R)
  );
 
  DR_Size: array [THWBPIndex] of array [THWBPSize] of DWORD = (
    (DR7_SIZE_DR0_B, DR7_SIZE_DR0_W, DR7_SIZE_DR0_D),
    (DR7_SIZE_DR1_B, DR7_SIZE_DR1_W, DR7_SIZE_DR1_D),
    (DR7_SIZE_DR2_B, DR7_SIZE_DR2_W, DR7_SIZE_DR2_D),
    (DR7_SIZE_DR3_B, DR7_SIZE_DR3_W, DR7_SIZE_DR3_D)
  );
 
var
  Context: TContext;
  I: THWBPIndex;
begin
  if ThreadIndex < 0 then Exit;
 
  ZeroMemory(@Context, SizeOf(TContext));
  Context.ContextFlags := CONTEXT_DEBUG_REGISTERS;
 
  for I := 0 to 3 do
  begin
    if not FThreadList[ThreadIndex].Breakpoint.Active[I] then Continue;
    if FThreadList[ThreadIndex].Breakpoint.Address[I] <> nil then
    begin
      Context.Dr7 := Context.Dr7 or DR7_SET_LOC_ON;
      case I of
        0: Context.Dr0 := DWORD(FThreadList[ThreadIndex].Breakpoint.Address[I]);
        1: Context.Dr1 := DWORD(FThreadList[ThreadIndex].Breakpoint.Address[I]);
        2: Context.Dr2 := DWORD(FThreadList[ThreadIndex].Breakpoint.Address[I]);
        3: Context.Dr3 := DWORD(FThreadList[ThreadIndex].Breakpoint.Address[I]);
      end;
      Context.Dr7 := Context.Dr7 or DR_On[I];
      Context.Dr7 := Context.Dr7 or DR_Mode[I,
        FThreadList[ThreadIndex].Breakpoint.Mode[I]];
      Context.Dr7 := Context.Dr7 or DR_Size[I,
        FThreadList[ThreadIndex].Breakpoint.Size[I]];
    end;
  end;
 
  Check(SetThreadContext(FThreadList[ThreadIndex].ThreadHandle, Context));
end;

Если не обращать внимание на блок констант, предшествующий коду, то инициализация регистра Dr7 реализована всего лишь тремя строчками.

Context.Dr7 := Context.Dr7 or DR_On[I];
Context.Dr7 := Context.Dr7 or DR_Mode[I,
  FThreadList[ThreadIndex].Breakpoint.Mode[I]];
Context.Dr7 := Context.Dr7 or DR_Size[I,
  FThreadList[ThreadIndex].Breakpoint.Size[I]];

Ну не считая включения бита LE представленного константой DR7_SET_LOC_ON.

:/>  Windows Command Prompt Alternatives - Page 2 | AlternativeTo

Теперь перейдем к обработке НВР.

При срабатывании ВР мы получали код EXCEPTION_BREAKPOINT.При срабатывании МВР код был EXCEPTION_GUARD_PAGE.А при прерывании на НВР нам будет сгенерировано событие EXCEPTION_DEBUG_EVENT с кодом EXCEPTION_SINGLE_STEP, которое помимо прочего используется для восстановления состояния ВР и МВР (поэтому я и не стал приводить его реализацию в начале статьи).

При получении EXCEPTION_SINGLE_STEP самым первым вызывается обработчик НВР реализованный следующим образом:

function TFWDebugerCore.ProcessHardwareBreakpoint(ThreadIndex: Integer;
  DebugEvent: TDebugEvent): Boolean;
var
  Index: Integer;
  Context: TContext;
  ReleaseBP: Boolean;
begin
  ZeroMemory(@Context, SizeOf(TContext));
  Context.ContextFlags := CONTEXT_DEBUG_REGISTERS;
  Check(GetThreadContext(FThreadList[ThreadIndex].ThreadHandle, Context));
  Result := Context.Dr6 and $F <> 0;
  if not Result then Exit;
 
  Index := -1;
  if Context.Dr6 and 1 <> 0 then
    Index := 0;
  if Context.Dr6 and 2 <> 0 then
    Index := 1;
  if Context.Dr6 and 4 <> 0 then
    Index := 2;
  if Context.Dr6 and 8 <> 0 then
    Index := 3;
  if Index < 0 then
  begin
    Result := False;
    Exit;
  end;
 
  ReleaseBP := False;
 
  if Assigned(FHardwareBreakpoint) then
    FHardwareBreakpoint(Self, ThreadIndex, DebugEvent.Exception.ExceptionRecord,
      Index, ReleaseBP);
 
  ToggleHardwareBreakpoint(ThreadIndex, Index, False);
  SetSingleStepMode(ThreadIndex, False);
 
  if ReleaseBP then
    DropHardwareBreakpoint(ThreadIndex, Index)
  else
  begin
 
    // если два HWBP идут друг за другом,
    // то т.к. восстановление происходит через индексы
    // в ProcessExceptionSingleStep, индекс предыдущего HWBP будет претерт
    // поэтому перед перетиранием индексов нужно восстановить предыдущий HWBP
    if (FRestoredThread >= 0) and (FRestoredHWBPIndex >= 0) then
      ToggleHardwareBreakpoint(FRestoredThread, FRestoredHWBPIndex, True);
 
    FRestoredHWBPIndex := Index;
    FRestoredThread := ThreadIndex;
  end;
end;

Его задача определить какой именно НВР вызвал прерывание, вызвать внешнее событие и выполнить алгоритм финализации примерно похожий на уже показанные в обработчиках ВР и МВР.

Для определения номера НВР необходимо считать значение регистра Dr6 из контекста нити.Младшие 4 бита данного регистра представляют из себя флаги принимающие значение 1 в том случае, если сработал соответствующий им DrX регистр.

Все достаточно просто, после определения необходимого НВР вызываем внешнее событие, отключаем НВР, переводим процессор в режим трассировки (без правки EIP) после чего либо удаляем НВР, либо запоминаем его индекс в двух переменных, ориентируясь на которые обработчик EXCEPTION_SINGLE_STEP восстановит состояние НВР.

Ну что ж, вот кажется мы и подошли к логическому завершению.Осталось только показать реализацию самого обработчика EXCEPTION_SINGLE_STEP.

Выглядит он следующим образом:

procedure TFWDebugerCore.ProcessExceptionSingleStep(ThreadIndex: Integer;
  DebugEvent: TDebugEvent);
var
  Handled: Boolean;
begin
  // Обрабатываем HWBP
  Handled := ProcessHardwareBreakpoint(ThreadIndex, DebugEvent);
 
  // Если событие поднято из-за HWPB восстанавливаем предыдущий HWBP
  if not Handled and (FRestoredThread >= 0) and
    (FRestoredHWBPIndex >= 0) then
  begin
    ToggleHardwareBreakpoint(FRestoredThread, FRestoredHWBPIndex, True);
    FRestoredThread := -1;
    FRestoredHWBPIndex := -1;
  end;
 
  // Восстанавливаем ВР
  if FRestoreBPIndex >= 0 then
  begin
    CheckBreakpointIndex(FRestoreBPIndex);
    if FBreakpointList[FRestoreBPIndex].bpType = btBreakpoint then
      ToggleInt3Breakpoint(FRestoreBPIndex, True);
    FRestoreBPIndex := -1;
  end;
 
  // Восстанавливаем MВР
  if FRestoreMBPIndex >= 0 then
  begin
    CheckBreakpointIndex(FRestoreMBPIndex);
    if FBreakpointList[FRestoreMBPIndex].bpType = btMemoryBreakpoint then
      ToggleMemoryBreakpoint(FRestoreMBPIndex, True);
    FRestoreMBPIndex := -1;
  end;
 
  // если на предыдущий итерации был выставлен режим трассировки
  // уведомляем о нем пользователя
  if ResumeAction <> raRun then
  begin
    CallUnhandledExceptionEvents(ThreadIndex, ecSingleStep, DebugEvent);
 
    // после чего настраиваем отладчик в зависимости от команды пользователя
    DoResumeAction(ThreadIndex);
  end;
end;

В его задачу входит первоначально определить, произошла ли генерация исключения по причине остановки на НВР. Если это действительно так, то вызовом ToggleHardwareBreakpoint НВР возвращается на место.Если же исключение было поднято по причине включенного флага трассировки после обработки ВР или МВР, переменные FRestoreBPIndex и FRestoreMBPIndex будут указывать на индекс точки остановки, которую требуется вернуть на место.В зависимости от ее типа производятся вызовы методов ToggleInt3Breakpoint или ToggleMemoryBreakpoint.

Реализация вр на адрес памяти:


Следующий тип ВР применяется для контроля изменений данных в памяти отлаживаемого приложения. Более известен как Memory Breakpoint (далее MBP).

Реализуется он следующим образом: вся память приложения представлена в виде набора страниц, которые можно перечислить и получить их атрибуты. (см. демо-приложение: Карта памяти процесса). Когда мы хотим поставить MBP на какой либо адрес нам нужно вычислить границы страницы, к которой этот адрес принадлежит и выставить ей флаг PAGE_GUARD вызовом функции VirtualProtectEx.

Нюанс: можно конечно и не вычислять адрес страницы, а просто вызвать VirtualProtectEx по требуемому адресу, но особенность страничной адресации в том, что нельзя изменить защиту небольшого участка страницы оставив все остальные адреса неизменными. Атрибуты защиты всегда назначается целиком на страницу.

Второй нюанс: большинство отладчиков не дают ставить одновременно два и более MBP расположенных в пределах одной станицы. Обусловлено это поведение скорее всего следующим моментом: при установке MBP необходимо запомнить текущее состояние поля Protect страницы, для того чтобы вернуть его обратно, когда придет время снимать MBP.

В том случае если на странице уже установлен MBP атрибуты ее защиты изменены. Дабы обойти этот момент в классе TFWDebugerCore применяется следующий подход. При установке нового MBP сначала проверяется, есть ли еще один MBP контролирующий данную страницу.

Код установки МВР выглядит так:

function TFWDebugerCore.SetMemoryBreakpoint(Address: Pointer; Size: DWORD;
  BreakOnWrite: Boolean; const Description: string): Boolean;
var
  Breakpoint: TBreakpoint;
  MBI: TMemoryBasicInformation;
  Index: Integer;
begin
  Index := GetMBPIndex(DWORD(Address));
  if (Index >= 0) and (FBreakpointList[Index].bpType = btMemoryBreakpoint) then
  begin
    MBI.BaseAddress := FBreakpointList[Index].Memory.RegionStart;
    MBI.RegionSize := FBreakpointList[Index].Memory.RegionSize;
    MBI.Protect := FBreakpointList[Index].Memory.PreviosRegionProtect;
  end
  else
    Check(VirtualQueryEx(DebugProcessData.AttachedProcessHandle,
      Address, MBI, SizeOf(TMemoryBasicInformation)) > 0);
  ZeroMemory(@Breakpoint, SizeOf(TBreakpoint));
  Breakpoint.bpType := btMemoryBreakpoint;
  Breakpoint.Description := ShortString(Description);
  Breakpoint.Memory.Address := Address;
  Breakpoint.Memory.Size := Size;
  Breakpoint.Memory.BreakOnWrite := BreakOnWrite;
  Breakpoint.Memory.RegionStart := MBI.BaseAddress;
  Breakpoint.Memory.RegionSize := MBI.RegionSize;
  Check(VirtualProtectEx(FProcessInfo.AttachedProcessHandle,
    Address, Size, MBI.Protect or PAGE_GUARD,
    Breakpoint.Memory.PreviosRegionProtect));
  if Index >= 0 then
    Breakpoint.Memory.PreviosRegionProtect := MBI.Protect;
  Result := AddNewBreakPoint(Breakpoint);
end;

Для MBP требуется хранить гораздо больше параметров, в отличие от ВР. Помимо адреса контролируемой области и ее размера, хранится параметр конфигурирующий сам МВР — BreakOnWrite, отвечающий за условия вызова внешнего события (при чтении данных с контролируемой области или при записи в нее). Так же хранится адрес начала страницы и ее размер.

После установки МВР можно запускать программу на выполнение. Как только произойдет доступ к контролируемой странице отладчику будет сгенерированно событие EXCEPTION_DEBUG_EVENT с кодом исключения EXCEPTION_GUARD_PAGE.

Здесь так-же есть нюанс. Когда мы выставляем флаг PAGE_GUARD странице, при первом к ней обращении поднимается исключение и этот флаг снимается. То есть в отличие от ВР самостоятельно заниматься отключением МВР нам не потребуется. Но есть небольшая проблема.

Как я и говорил ранее, исключение EXCEPTION_GUARD_PAGE произойдет при любом обращении к странице, не только к адресу, по которому установлен МВР, поэтому отладчик должен уметь восстанавливать флаг PAGE_GUARD чтобы установленные МВР продолжали корректно работать.

Код обработчика выглядит так:

procedure TFWDebugerCore.ProcessExceptionGuardPage(ThreadIndex: Integer;
  DebugEvent: TDebugEvent);
var
  CurrentMBPIndex: Integer;
 
  function CheckWriteMode: Boolean;
  begin
    Result := not FBreakpointList[CurrentMBPIndex].Memory.BreakOnWrite;
    if not Result then
      Result := DebugEvent.Exception.ExceptionRecord.ExceptionInformation[0] = 1;
  end;
 
var
  MBPIndex: Integer;
  ReleaseMBP: Boolean;
  dwGuardedAddr: DWORD;
begin
  ReleaseMBP := False;
  dwGuardedAddr :=
    DebugEvent.Exception.ExceptionRecord.ExceptionInformation[1];
  MBPIndex := GetMBPIndex(dwGuardedAddr);
  if MBPIndex >= 0 then
  begin
    CurrentMBPIndex := MBPIndex;
    while not CheckIsAddrInRealMemoryBPRegion(CurrentMBPIndex, dwGuardedAddr) do
    begin
      CurrentMBPIndex := GetMBPIndex(dwGuardedAddr, CurrentMBPIndex   1);
      if CurrentMBPIndex < 0 then Break;
    end;
 
    if CurrentMBPIndex >= 0 then
    begin
      MBPIndex := CurrentMBPIndex;
      if Assigned(FBreakPoint) and CheckWriteMode then
        FBreakPoint(Self, ThreadIndex,
          DebugEvent.Exception.ExceptionRecord, MBPIndex, ReleaseMBP)
      else
        CallUnhandledExceptionEvents(ThreadIndex, ecGuard, DebugEvent);
    end
    else
      CallUnhandledExceptionEvents(ThreadIndex, ecGuard, DebugEvent);
 
    FBreakpointList[MBPIndex].Active := False;
    SetSingleStepMode(ThreadIndex, False);
    if ReleaseMBP then
      RemoveBreakpoint(MBPIndex)
    else
      FRestoreMBPIndex := MBPIndex;
  end
  else
    CallUnhandledExceptionEvents(ThreadIndex, ecGuard, DebugEvent);
end;

Первоначально отладчик получает адрес к которому произошло обращение из-за которого произошло исключение. Этот адрес хранится в массиве ExceptionRecord.ExceptionInformation вторым параметром, первым параметром в данном массиве идет флаг операции. Ноль означает попытку чтения по адресу, единица — попытку записи по адресу.

Если подходящий нашелся, проверяется параметр BreakOnWrite.Значение данного параметра сравнивается со значением первого параметра ExceptionInformation. Если BreakOnWrite включен, то вызов внешнего события происходит только в случае если в ExceptionInformation содержится единица, в противном случае, если BreakOnWrite отключен, вызов события происходит всегда.

:/>  Как включить расширение файлов в Windows 7

После всех проверок код реализован по аналогии с обработкой ВР, единственное отличие от обработки ВР в том, что в данном случае нам не нужно править значение регистра EIP. Для этого в метод SetSingleStepMode вторым параметром передается False.

Восстановление снятого МВР по аналогии происходит в обработчике EXCEPTION_SINGLE_STEP на основе индекса FRestoreMBPIndex.

За переключение активности МВР отвечает следующий код:

procedure TFWDebugerCore.ToggleMemoryBreakpoint(Index: Integer;
  Active: Boolean);
var
  Dummy: DWORD;
begin
  CheckBreakpointIndex(Index);
  if FBreakpointList[Index].bpType <> btMemoryBreakpoint then Exit;
  if FBreakpointList[Index].Active = Active then Exit;
  if Active then
    Check(VirtualProtectEx(FProcessInfo.AttachedProcessHandle,
      FBreakpointList[Index].Memory.Address,
      FBreakpointList[Index].Memory.Size,
      FBreakpointList[Index].Memory.PreviosRegionProtect or PAGE_GUARD, Dummy))
  else
    Check(VirtualProtectEx(FProcessInfo.AttachedProcessHandle,
      FBreakpointList[Index].Memory.Address,
      FBreakpointList[Index].Memory.Size,
      FBreakpointList[Index].Memory.PreviosRegionProtect, Dummy));
  FBreakpointList[Index].Active := Active;
end;

Удаление МВР производится тем-же методом что и ВР.

В принципе каких-то сильных кардинальных различий, за исключением пары нюансов, у ВР и МВР нет. Они просто применяются каждый для своих задач.

Например, иногда МВР применяется в качестве трассировщика. Для осуществления данной возможности просто выставляется МБР на область кода и после запуска отладчика нам начинают приходить уведомлении о изменении текущего EIP в удаленном приложении. Достаточно удобная вещь, жаль не осуществимая в Delphi отладчике.

Пример такого использования я покажу в конце статьи. А сейчас перейдем к третьему и последнему типу точек остановки.

Реализация трассировки:

Теперь попробуем поработать с отладчиком. Для начала рассмотрим два варианта трассировки, первый через TF флаг, второй через MBP. Трассировать будем первые 40 байт с точки входа программы. Давайте сразу посмотрим как они выглядят:

Для того чтобы начать трассировку по хорошему нужно дождаться инициализации процесса, после чего можно смело ставить ВР/МВР и прочее. Для этого необходимо указать отладчику установить ВР на точку входа приложения. Отвечает за это второй параметр функции DebugNewProcess.

procedure TdlgDebuger.OnBreakPoint(Sender: TObject; ThreadIndex: Integer;
  ExceptionRecord: Windows.TExceptionRecord; BreakPointIndex: Integer;
  var ReleaseBreakpoint: Boolean);
begin
  // Выводим пойманный ВР в лог
  Writeln(Format('!!! --> Breakpoint "%s"',
    [FCore.BreakpointItem(BreakPointIndex).Description]));
  // Снимаем его (больше он нам не потребуется)
  ReleaseBreakpoint := True;
  // Включаем режим трассировки
  FCore.ResumeAction := raTraceInto;
  // Инициализируем количество трассировочных прерываний
  FStepCount := 0;
end;

Так как трассировка происходит через генерацию события OnSingleStep, реализуем и его:

procedure TdlgDebuger.OnSingleStep(Sender: TObject; ThreadIndex: Integer;
  ExceptionRecord: Windows.TExceptionRecord);
begin
  // Выводим пойманный ВР в лог
  Inc(FStepCount);
  Writeln(Format('!!! --> trace step №%d at addr 0x%p',
    [FStepCount, ExceptionRecord.ExceptionAddress]));
  // В зависимости от количества срабатываний отключаем трассировку
  if FStepCount > 10 then
    FCore.ResumeAction := raRun
  else
    FCore.ResumeAction := raTraceInto;
end;

Результатом получится следующий вывод:

Мы сделали трассировку StepIn, об этом нам явно показывает пятый шаг трассировки, который произошел по адресу 0x00409FF4, это начало функции _InitExe(), вызов которой происходит по адресу 0x00409C53. Трассировка достаточно медленный процесс и ждать, когда управление вернется из функции _InitExe(), я не стал, для демонстрации ограничившись десятком шагов.

Второй режим трассировки — установка МВР.Для того чтобы ее продемонстрировать, необходимо перекрыть событие OnPageGuard и при достижении точки входа, вызвать метод SetMemoryBreakpoint с диапазоном контролируемой памяти равным нулю. В этом случае отладчик будет знать о контролируемой МВР странице, но обработчик OnBreakPoint для данной МВР вызываться не будет.

Реализацию данной варианта трассировки оставляю на ваше усмотрение, единственно дам подсказку, из обработчиков отладочных событий крайне не рекомендуется вызывать метод RemoveBreakpoint (уплывут индексы), для удаления ВР/МВР/НВР в данной реализации отладчика предусмотрено два штатных метода, параметр ReleaseBreakpoint при вызове обработчика ВР, или процедура RemoveCurrentBreakpoint доступная в любом из обработчиков.

Трассировка, это конечно хорошо, но не о ней я хотел рассказать в практической части статьи поэтому в демопримере, идущем в составе статьи примеры трассировки отсутствуют.

Получение отладочной строки:

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

//
//  Вывод отладочной строки
// =============================================================================
procedure TForm1.btnDebugStringClick(Sender: TObject);
begin
  OutputDebugString('Test debug string');
end;

После чего в отладчике перекройте событие OnDebugString, реализовав в нем следующий код:

procedure TdlgDebuger.OnDebugString(Sender: TObject; ThreadIndex: Integer;
  Data: TOutputDebugStringInfo);
begin
  if Data.fUnicode = 1 then
    Writeln('DebugString: '   
      PWideChar(FCore.ReadStringW(Data.lpDebugStringData, Data.nDebugStringLength)))
  else
    Writeln('DebugString: '   
      PAnsiChar(FCore.ReadStringA(Data.lpDebugStringData, Data.nDebugStringLength)));
end;

Запускайте отладчик, в нем отлаживаемое приложение и пощелкайте на кнопке. Сообщение «Test debug string» выводится в лог? Если да, то значит все сделали правильно 🙂

Обработка исключений:

Помните я говорил о использовании ВР в качестве антиотладочного в приложении? Сейчас попробуем рассмотреть примерно такой-же вариант. В тестовом приложении добавьте еще одну кнопку и в ней пропишите следующий код:

//
//  Детектируем отладчик через поднятие отладочного прерывания
// =============================================================================
procedure TForm1.btnExceptClick(Sender: TObject);
begin
  try
    asm
      int 3
    end;
    ShowMessage('Debugger detected.');
  except
    ShowMessage('Debugger not found.');
  end;
end;

Это в принципе даже не антиотладка, но как ни странно иногда некоторые реверсеры палятся даже на такой примитивной схеме.

Суть данного метода в следующем: в начале статьи я приводил пример отладочного цикла, в нем на каждой итерации параметр ContinueStatus, с которым вызывается функция ContinueDebugEvent, инициализировался константой DBG_CONTINUE. Что это означает? Это сигнал что наш отладчик успешно обработал возникшее исключение и дальше с ним возится не стоит.

Ну а теперь что это означает на примере приведенного кода: вызовом инструкции «INT3» мы поднимаем исключение. При нормальной работе приложения данное исключение обрабатывать некому, поэтому при его возникновении происходит переход на обработчик exception..end, в котором мы говорим что все нормально. Если же мы под отладчиком, то это исключение поймает он и вызова обработчика приложения не произойдет.

Можете проверить, запустить приложение и нажмите кнопку с этим кодом, оно честно скажет — мы под отладчиком.

Побороть данный код так же достаточно просто, достаточно перекрыть событие OnUnknownBreakPoint (int3 — это точка остановки, причем поставленная не нами, поэтому и ловить ее будет в данном событии). В обработчике события напишите следующий код:

procedure TdlgDebuger.OnUnknownBreakPoint(Sender: TObject;
  ThreadIndex: Integer; ExceptionRecord: Windows.TExceptionRecord);
var
  ApplicationBP: Boolean;
begin
  ApplicationBP :=
    (DWORD(ExceptionRecord.ExceptionAddress) > FCore.DebugProcessData.EntryPoint) and
    (DWORD(ExceptionRecord.ExceptionAddress) < $500000);
 
  Writeln;
  if ApplicationBP then
  begin
    Writeln(Format('!!! --> Unknown application breakpoint at addr 0X%p',
      [ExceptionRecord.ExceptionAddress]));
    Writeln('!!! --> Exception not handled.');
    FCore.ContinueStatus := DBG_EXCEPTION_NOT_HANDLED;
  end
  else
  begin
    Writeln(Format('!!! --> Unknown breakpoint at addr 0X%p',
      [ExceptionRecord.ExceptionAddress]));
    Writeln('!!! --> Exception handled.');
    FCore.ContinueStatus := DBG_CONTINUE;
  end;
  Writeln;
end;

В нем все просто, на основании адреса по которому установлен ВР определяем его расположение, в теле приложения или нет (грубо взяв диапазон от адреса загрузки приложения до $500000). Если ВР установлено в теле приложения — значит это какая-то антиотладка.

В результате таких телодвижений исключение, искусственно поднятое приложением, обработано не будет и оно радостно сообщит нам, что отладчика не обнаружило 🙂

Что происходит при переполнении стека:

Ну и последнее что я хотел показать вам, это то, как выглядит переполнение стека со стороны отладчика. Пример переполнения возьму из одной из предыдущих статей, ну например вот такой:

//
//  Разрушаем стек приложения через переполнение
// =============================================================================
procedure TForm1.btnKillStackClick(Sender: TObject);
  
  procedure T;
  var
    HugeBuff: array [0..10000] of DWORD;
  begin
    if HugeBuff[0] <> HugeBuff[10000] then
      Inc(HugeBuff[0]);
    T;
  end;
  
begin
  try
    T;
  except
    T;
  end;
end;

Добавьте этот код в тестовое приложения и нажмите кнопку. Реакция отладчика может быть разная, но результат всегда будет один — отладчику станет очень плохо. Что происходит в данном случае? Механизм детектирования переполнения стека достаточно прост, граница, за которую нельзя выходить представлена отдельной страницей, помеченой флагом PAGE_GUARD.

Ну да, это тот-же механизм, при помощи которого мы расставляли наши МВР, но в данном случае он используется для других целей. При переполнении первоначально отладчику придет уведомление EXCEPTION_STACK_OVERFLOW. В принципе уже прямо здесь можно «сушить весла» и завершать работу отладчика, но мы же настырные и запустим работу тестового приложения дальше.

Если помните, нюанс работы флага PAGE_GUARD в том, что после первого обращения он снимается, вот здесь такой-же случай. При повторном обращении к данной странице мы поймаем не что иное, как EXCEPTION_ACCESS_VIOLATION и вот тут-то уже действительно «все», дальше барахтаться уже смысла не имеет, остается только выставить DBG_CONTROL_C и прекратить отладку (ну если вы конечно не хотите понаблюдать на вечный цикл с выдачей AV).

Обработчик переполнения реализуем в событии OnUnknownException, т.к. TFWDebugerCore не выносит эти два исключения в виде отдельных событий. В нем напишем следующее:

Оставьте комментарий

Adblock
detector