Что такое секция .pdata и как в ней хранится информация для обработки исключений

Если вы когда-нибудь дизассемблировали PE-файл или отлаживали программу под Windows, вы могли наткнуться на секцию .pdata. Она не так известна, как .text или .data, но именно в ней живёт карта того, как функции сворачивают стек при возникновении исключений. Без неё исключения в x64 Windows просто не работали бы так, как мы привыкли.

Где находится .pdata и зачем она нужна

Секция .pdata (Procedure Data) — это часть PE-файла под архитектурой x64. Она содержит массив записей, каждая из которых описывает одну функцию: где она начинается, где заканчивается и как обрабатывать исключения внутри неё.

В отличие от x86, где информация об обработке исключений хранилась в стеке (через цепочки FS:[0] и SEH-фреймы), на x64 архитектуре Microsoft перешла на табличную модель. Это значит, что всё описано статически в исполняемом файле, и система во время исключения просто читает таблицу, чтобы понять, что делать со стеком.

Зачем это нужно на практике:

  • Система знает, как раскрутить стек до обработчика исключения.
  • Компилятор может генерировать оптимизированный код без лишних проверок в каждой функции.
  • Отладчики и инструменты вроде WinDbg могут корректно показывать стек вызовов даже при исключениях.

Что внутри: структура записей .pdata

Каждая запись в .pdata — это структура RUNTIME_FUNCTION размером 12 байт (три 32-битных поля):

Смещение Поле Что означает
0x00 BeginAddress RVA начала функции
0x04 EndAddress RVA конца функции
0x08 UnwindInfo RVA на структуру UNWIND_INFO

Все адреса здесь — RVA (Relative Virtual Address), то есть смещение от начала образа в памяти. Чтобы получить реальный адрес, нужно прибавить базу загрузки модуля.

Система ищет нужную запись в таблице бинарным поиском по BeginAddress, когда происходит исключение. Поэтому таблица обязана быть отсортирована по возрастанию адресов — это требование к компоновщику.

UNWIND_INFO: что происходит при раскрутке стека

Поле UnwindInfo указывает на структуру, которая описывает, как функция использует стек и где лежат сохранённые регистры. Вот её ключевые поля:

  • Version (3 бита) — версия формата, сейчас обычно 1 или 2.
  • Flags (5 бит) — указывает наличие обработчика исключений или обработчика раскрутки.
  • SizeOfProlog (8 бит) — размер пролога функции в байтах.
  • CountOfCodes (8 бит) — количество UNWIND_CODE, описывающих операции пролога.
  • FrameRegister (4 бита) — регистр, используемый как указатель фрейма.
  • FrameOffset (4 бита) — смещение относительно указателя фрейма.

После этих полей идёт массив UNWIND_CODE — по одной записи на каждую операцию пролога. Каждая запись описывает, что пролог делал со стеком: сохранял регистр, выделял память, устанавливал фрейм и так далее.

Если в Flags установлен флаг UNW_FLAG_EHANDLER, после массива кодов расположен адрес обработчика исключений. Если UNW_FLAG_UHANDLER — адрес обработчика раскрутки (unwind handler).

Как система обрабатывает исключение с помощью .pdata

Когда происходит исключение, система выполняет следующие шаги:

  1. Берёт текущий RIP (instruction pointer) и ищет в таблице .pdata запись, где BeginAddress <= RIP < EndAddress.
  2. Читает UNWIND_INFO и симулирует выполнение пролога в обратном порядке — отменяет выделение стека, восстанавливает регистры.
  3. Если у функции есть обработчик исключений, система вызывает его и спрашивает: «Ты обработаешь это исключение?».
  4. Если обработчик отказывается, система переходит к предыдущему фрейму и повторяет процесс — раскрутка стека продолжается вверх.
  5. Если ни один обработчик не подхватил исключение, процесс завершается.

Ключевой момент: система не «знает» про стек во время выполнения. Она заново вычисляет состояние на основе таблицы. Поэтому таблица должна быть точной — малейшее расхождение приведёт к крашу.

Практический пример: как выглядит .pdata в реальном бинарнике

Допустим, у нас есть простая функция на C:

void example() {
    char buf[64];
    __try {
        // какие-то действия
    }
    __except(filter(GetExceptionInformation())) {
        // обработчик
    }
}

Компилятор сгенерирует пролог, который выделяет 64 байта под буфер, сохраняет нелокальные регистры и устанавливает фрейм. В .pdata появится запись, где:

  • BeginAddress — адрес начала example.
  • EndAddress — адрес конца функции.
  • UnwindInfo — указатель на структуру, описывающую пролог и содержащую ссылку на функцию filter и обработчик __except.

Если открыть такой бинарник в WinDbg и ввести команду !fnent <address>, отладчик покажет всю эту информацию в читаемом виде.

Сравнение подходов: x86 SEH vs x64 .pdata

Аспект x86 (SEH) x64 (.pdata)
Где хранится информация В стеке во время выполнения В таблице в PE-файле
Скорость обработки Быстрее при нормальном ходе Медленнее, но надёжнее
Уязвимость к повреждению стека Высокая (можно перезаписать SEH) Низкая (таблица read-only)
Сложность для компилятора Нужно генерировать код для установки фреймов Достаточно описать пролог таблично
Поддержка C++ исключений Через SEH-обёртки Нативно через таблицу

Переход на табличную модель был одним из ключевых архитектурных решений x64. Он устранил целый класс атак через перезапись SEH и сделал обработку исключений более предсказуемой.

Когда вам может понадобиться разбираться с .pdata

Вот реальные ситуации, где знание о .pdata не теоретическое:

  • Написание компилятора или ассемблера. Если вы генерируете x64-код, вы обязаны создавать корректные записи .pdata, иначе исключения не будут работать.
  • Анализ крашей в WinDbg. Когда стек повреждён и стандартный k не показывает ничего полезного, информация из .pdata — это запасной путь для восстановления цепочки вызовов.
  • Реверс-инжиниринг. Понимание структуры .pdata помогает быстро находать функции и их границы в бинарнике.
  • Работа с инструментами вроде CrashWalk или minidump. Они используют .pdata для воспроизведения стека на момент краша.

Частые ошибки при работе с .pdata

Ручное изменение пролога без обновления таблицы. Если вы патчите ассемблерный код и добавляете/убираете инструкции в прологе, но не обновляете UNWIND_INFO, исключения приведут к неопределённому поведению. Это одна из самых частых причин крашей в модифицированном ПО.

Неверный размер пролога. Поле SizeOfProlog должно точно соответствовать реальному размеру пролога. Если оно занижено, система не отменит все операции, и стек будет кривым.

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

Путаница между VA и RVA. Все адреса в .pdata — это RVA. Если вы забудете прибавить базу загрузки, вы будете смотреть не туда.

Как проверить корректность .pdata

Если вы подозреваете, что исключения обрабатываются неправильно, вот что можно сделать:

  1. Откройте исполняемый файл в WinDbg и используйте команду !lmi — она покажет базовую информацию о модуле, включая адрес таблицы исключений.
  2. Проверьте, что секция .pdata присутствует в заголовках секций: link /dump /headers <file.exe>.
  3. Используйте !fnent <address> для проверки конкретной функции — отладчик покажет, как он интерпретирует запись.
  4. Запустите программу под отладчиком и вызовите исключение искусственно — посмотрите, корректно ли система раскручивает стек.

Для статической проверки можно использовать инструменты вроде dumpbin:

dumpbin /headers /pdata your_binary.exe

Это выдаст дамп всех записей .pdata с адречами и ссылками на UNWIND_INFO.

Что делать, если .pdata повреждена или отсутствует

Если вы анализируете бинарник без отладочной информации и подозреваете проблемы с исключениями:

  • Восстановите границы функций эвристиками. Поирайте типичные прологи (mov [rsp+...], reg, sub rsp, imm) и постройте приблизительную карту.
  • Используйте стековые фреймы. Если функции используют фрейм-указатель (RBP или другой регистр), раскрутку можно восстановить по цепочке фреймов.
  • Проверьте, не обфусцирован ли файл. Некоторые протекторы намеренно портят .pdata, чтобы затруднить анализ. В этом случае стандартные инструменты не помогут.

Итог

Секция .pdata — это таблица маршрутизации для исключений в x64 Windows. Каждая запись в ней говорит системе: «Если исключение произошло в этом диапазоне адресов, вот как раскрутить стек и вот где искать обработчик».

Если вы пишете на C/C++ и используете исключения или SEH — компилятор и компоновщик сами сгенерируют корректную .pdata. Вам не нужно о ней думать. Но если вы пишете на ассемблере, разрабатываете компилятор или занимаетесь реверс-инжинирингом — понимание этой секции необходимо.

Главное, что стоит запомнить: .pdata — это не просто метаданные, а рабочий механизм, от которого зависит, корректно ли программа обработает исключение или упадёт с непонятной ошибкой. Если исключения в вашем x64-приложении ведут себя странно — проверьте, правильно ли сформирована эта секция.

Оцените статью
PEFile — Безопасность и технологии простым языком