Если вы когда-нибудь дизассемблировали PE-файл или отлаживали программу под Windows, вы могли наткнуться на секцию .pdata. Она не так известна, как .text или .data, но именно в ней живёт карта того, как функции сворачивают стек при возникновении исключений. Без неё исключения в x64 Windows просто не работали бы так, как мы привыкли.
- Где находится .pdata и зачем она нужна
- Что внутри: структура записей .pdata
- UNWIND_INFO: что происходит при раскрутке стека
- Как система обрабатывает исключение с помощью .pdata
- Практический пример: как выглядит .pdata в реальном бинарнике
- Сравнение подходов: x86 SEH vs x64 .pdata
- Когда вам может понадобиться разбираться с .pdata
- Частые ошибки при работе с .pdata
- Как проверить корректность .pdata
- Что делать, если .pdata повреждена или отсутствует
- Итог
Где находится .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
Когда происходит исключение, система выполняет следующие шаги:
- Берёт текущий RIP (instruction pointer) и ищет в таблице
.pdataзапись, гдеBeginAddress <= RIP < EndAddress. - Читает
UNWIND_INFOи симулирует выполнение пролога в обратном порядке — отменяет выделение стека, восстанавливает регистры. - Если у функции есть обработчик исключений, система вызывает его и спрашивает: «Ты обработаешь это исключение?».
- Если обработчик отказывается, система переходит к предыдущему фрейму и повторяет процесс — раскрутка стека продолжается вверх.
- Если ни один обработчик не подхватил исключение, процесс завершается.
Ключевой момент: система не «знает» про стек во время выполнения. Она заново вычисляет состояние на основе таблицы. Поэтому таблица должна быть точной — малейшее расхождение приведёт к крашу.
Практический пример: как выглядит .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
Если вы подозреваете, что исключения обрабатываются неправильно, вот что можно сделать:
- Откройте исполняемый файл в WinDbg и используйте команду
!lmi— она покажет базовую информацию о модуле, включая адрес таблицы исключений. - Проверьте, что секция
.pdataприсутствует в заголовках секций:link /dump /headers <file.exe>. - Используйте
!fnent <address>для проверки конкретной функции — отладчик покажет, как он интерпретирует запись. - Запустите программу под отладчиком и вызовите исключение искусственно — посмотрите, корректно ли система раскручивает стек.
Для статической проверки можно использовать инструменты вроде 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-приложении ведут себя странно — проверьте, правильно ли сформирована эта секция.
