Что такое структурные выравнивания в секциях EXE и зачем они нужны на практике

Что такое структурные выравнивания в секциях EXE и зачем они нужны на практике

Если ты когда-нибудь сталкивался с тем, что программа на Windows падает с ошибкой ACCESS_VIOLATION или не запускается вообще — и при этом в отладчике видишь странные смещения в памяти, где-то в секции .text или .data — скорее всего, причина в неправильном выравнивании секций. Это не про «неправильные настройки компилятора», а про фундаментальную особенность того, как Windows загружает исполняемые файлы. И если ты разрабатываешь низкоуровневый код, пишешь патчи, модифицируешь EXE или работаешь с анти-антивирусными техниками — игнорировать выравнивание нельзя. Ниже — всё, что нужно знать, чтобы не тратить дни на отладку.

Что вообще значит «выравнивание» в EXE?

EXE-файл — это не просто набор байтов. Он имеет структуру: заголовок, секции (.text, .data, .rdata и т.д.), таблицы импорта, ресурсы. Каждая секция — это блок данных, который загружается в память при запуске. Но память в Windows работает по страницам — обычно по 4096 байт (4 КБ). И вот тут возникает проблема: если секция в файле занимает, скажем, 1000 байт, а в памяти ей нужно выделить 4096 байт, как именно её расположить? И как убедиться, что при загрузке все адреса сработают корректно?

Ответ — выравнивание. Есть два ключевых параметра:

  • File Alignment — как секции упакованы в файле на диске.
  • Section Alignment — как они размещаются в памяти при загрузке.

Эти параметры задаются в заголовке PE (Portable Executable). По умолчанию в большинстве компиляторов (MSVC, GCC/MinGW) они такие:

  • File Alignment: 512 байт
  • Section Alignment: 4096 байт (страница памяти)

Зачем они разные? Потому что файл на диске не обязан быть точной копией памяти. На диске экономят место — выравнивают секции по 512 байт, чтобы файл был удобен для чтения с HDD/SSD. А в памяти — по 4096, потому что так работает виртуальная память Windows.

Почему это критично? Пример из жизни

Представь, что ты пишешь патч для старой программы. Ты добавляешь в секцию .text свой код — 300 байт. Компилятор вставляет его, и теперь размер секции стал 1024 байта. Но если ты не проверил выравнивание, и в файле секция выровнена по 512, а в памяти — по 4096, то при загрузке Windows считает: «в файле секция занимает 1024 байта, выровнена по 512 → значит, она начинается на смещении 1536 (3×512), а в памяти — на 4096».

А теперь представь, что твой код обращается к глобальной переменной, которая лежит в секции .data. Адрес этой переменной в файле — 0x1200. В памяти она должна быть сдвинута на 0x1000 (разница между выравниванием в файле и в памяти). Но если ты не пересчитал все относительные адреса (RVA — Relative Virtual Addresses), то твой код будет пытаться прочитать по адресу 0x1200 в памяти — а там ничего нет. Там, на самом деле, переменная находится по адресу 0x1200 + 0x1000 = 0x2200. И ты получаешь краш.

Это не теория. Я видел, как разработчики тратили неделю, пытаясь понять, почему их модифицированный EXE не запускается. Все смещения были правильными — до тех пор, пока не проверили FileAlignment и SectionAlignment. Оказалось, что в исходном файле FileAlignment был 2048, а не 512. Их инструменты автоматически выравнивали по 512 — и всё сломалось.

Какие значения выравнивания бывают — и когда что использовать

Не все EXE-файлы одинаковы. Вот основные сценарии и типичные значения:

Сценарий File Alignment Section Alignment Почему так
Стандартный Windows-приложение (MSVC) 512 4096 Оптимально для диска и памяти. Универсально.
Миниатюрный бинарник (например, для руткитов) 512 512 Снижает размер в памяти. Упрощает загрузку. Риск: нестабильность на некоторых системах.
Файлы, собранные с GCC/MinGW 512 4096 Совместимость с Windows API. Редко меняют.
Файлы с кастомной загрузкой (например, в память через ReadFile) 4096 4096 Упрощает загрузку: файл и память совпадают. Требует больше места на диске.
Патч для старого драйвера или 16-битного приложения 16 16 Совместимость с устаревшими загрузчиками. Редко, но встречается.

Важно: если ты не меняешь эти параметры вручную — они уже заданы компилятором. Но если ты редактируешь EXE вручную (например, через hexedit, CFF Explorer или pedump), то можешь случайно нарушить соответствие. И тогда программа начнёт вести себя странно.

Как проверить выравнивание в существующем EXE

Нет смысла гадать. Проверяй. Вот три простых способа:

  1. PE-bear (бесплатный GUI-инструмент) — открой файл, зайди в «Headers» → «Optional Header» → смотри File Alignment и Section Alignment.
  2. dumpbin (из Visual Studio):
    dumpbin /headers yourfile.exe
    Ищи строки: file alignment и section alignment.
  3. objdump (MinGW):
    objdump -x yourfile.exe | grep -E "(File Alignment|Section Alignment)"

Если ты пишешь скрипт для автоматизации — используй библиотеку pefile на Python:

import pefile

pe = pefile.PE('yourfile.exe')
print(f"File Alignment: {pe.OPTIONAL_HEADER.FileAlignment}")
print(f"Section Alignment: {pe.OPTIONAL_HEADER.SectionAlignment}")

Это не «для продвинутых» — это базовый чек-лист. Если ты работаешь с EXE, и не проверяешь эти значения — ты работаешь вслепую.

Частые ошибки — и почему они ломают всё

Вот что чаще всего идёт не так:

  • Ошибка 1: Изменяешь размер секции в файле, но не пересчитываешь RVA. Добавил 100 байт в .text — и забыл, что теперь все адреса в таблице импорта и релокациях сдвинулись. Результат: краш при запуске.
  • Ошибка 2: Выравниваешь секцию в файле по 4096, но оставляешь Section Alignment = 4096. Звучит логично, но теперь файл становится в 8 раз больше. Для небольшой программы — это 100 КБ вместо 12 КБ. Не всегда приемлемо.
  • Ошибка 3: Используешь File Alignment = 512, но в памяти секция начинается не с 4096, а с 2048 — потому что ты «вручную» сдвинул её в отладчике. Windows загружает по своим правилам — и твой «ручной» сдвиг игнорируется. Краш.
  • Ошибка 4: Ты думаешь, что если секция короткая — выравнивание не важно. Нет. Даже если секция 100 байт — если выравнивание не совпадает с ожиданиями загрузчика, Windows может не загрузить её вообще. Особенно в 64-битных системах.
  • Ошибка 5: Пытаешься «оптимизировать» EXE, выставив File Alignment = 1. Это не сработает. Windows требует, чтобы File Alignment был степенью двойки от 512 до 65536. Иначе — ошибка загрузки.

Особенно коварна ошибка №1. Ты можешь скопировать секцию, добавить код, пересчитать только часть адресов — и всё будет казаться нормальным. Пока не начнёшь вызывать функцию из DLL. Тогда — краш. Потому что адрес импорта не совпадает с тем, что ожидает загрузчик.

Как сделать правильно — пошагово

Если ты модифицируешь EXE — вот алгоритм, который не подводит:

  1. Открой исходный EXE в PE-редакторе (например, CFF Explorer).
  2. Запиши текущие значения: File Alignment и Section Alignment.
  3. Если ты добавляешь код — добавляй его в конец секции, но не выходи за пределы выравнивания. Например, если File Alignment = 512, и секция занимает 1024 байта — ты можешь добавить до 512 байт (до 1536), но не больше, иначе нужно увеличивать размер секции.
  4. Пересчитай все RVA (относительные адреса) в таблицах: импорт, экспорт, релокации, ресурсы. Используй формулу: new_RVA = old_RVA + offset.
  5. Проверь, что размер секции в файле кратен File Alignment. Если не кратен — дозаполни нулями до ближайшего кратного.
  6. Проверь, что размер секции в памяти кратен Section Alignment.
  7. Сохраняй файл. Не трогай заголовок, если не уверен — особенно SizeOfImage и SizeOfHeaders.
  8. Проверь: запусти в отладчике. Проверь, что все секции загрузились по ожидаемым адресам.

Если ты не уверен — не меняй выравнивание. Оставь как есть. Лучше увеличить размер секции на 100 байт, чем сломать загрузку.

Что выбрать в зависимости от ситуации

Вот простой сценарий выбора:

  • Ты модифицируешь стандартное Windows-приложение (например, игру или утилиту) — оставь выравнивание как есть. Меняй только содержимое. Не трогай FileAlignment и SectionAlignment.
  • Ты создаёшь миниатюрный загрузчик или payload для пентеста — выставь FileAlignment = 512, SectionAlignment = 512. Это уменьшит размер файла и упростит загрузку в память через VirtualAlloc + WriteProcessMemory.
  • Ты пишешь драйвер или работаете с ядром — используй только стандартные значения (512 / 4096). Нарушение может привести к BSOD. Там нет места для экспериментов.
  • Ты хочешь сделать EXE максимально компактным — попробуй выставить FileAlignment = 512, но не трогай SectionAlignment. Уменьшай размер секций до ближайшего кратного 512. Это даст выигрыш в 10–30% без риска.
  • Ты работаешь с устаревшими системами (Windows XP, DOS-совместимые загрузчики) — проверь, что FileAlignment ≥ 16. Некоторые старые загрузчики не понимают 512.

Если ты не знаешь, какая система будет запускать твой EXE — используй стандарт: 512 / 4096. Это работает в 99,9% случаев.

Как лучше сделать — советы от практика

  • Всегда делай бэкап исходного EXE перед редактированием. Даже если «это просто пару байт».
  • Используй инструменты, которые не ломают PE-заголовок. hexedit — опасен. Лучше CFF Explorer или PE-bear.
  • Если ты вставляешь код — не забудь про Section Characteristics. Нужно, чтобы секция была MEM_EXECUTE | MEM_READ | MEM_WRITE, если ты пишешь в неё исполняемый код.
  • Проверяй хэш файла после изменений. Если хэш изменился — это нормально. Но если программа проверяет цифровую подпись — ты её сломал. Тогда нужно либо убрать подпись, либо подписать заново.
  • Тестируй на чистой виртуальной машине. Не на своей системе. Особенно если работаешь с патчами, которые могут повредить системные файлы.

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

Итог: что делать прямо сейчас

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

  1. Открой свой EXE в PE-редакторе.
  2. Запиши значения File Alignment и Section Alignment.
  3. Если ты вносишь изменения — не меняй их. Оставь как есть.
  4. Пересчитай все RVA в таблицах. Не полагайся на автоматику — проверяй вручную хотя бы 3–5 адресов.
  5. Убедись, что размеры секций кратны соответствующим выравниваниям.
  6. Запусти в отладчике. Проверь, что все секции загрузились по ожидаемым адресам.

Если ты делаешь это впервые — потренируйся на простом EXE: возьми «Hello World» на C++, открой его, добавь один байт в .text, пересчитай RVA — и посмотри, запустится ли. Это займёт 20 минут, но сэкономит тебе 20 часов в будущем.

Выравнивание — не «нужная деталь». Это фундамент. Как и то, что вода течёт вниз. Если ты игнорируешь его — ты строишь на песке. Даже если код «работает» — он хрупкий. И в один день, на другой системе, в другой версии Windows — сломается. И ты будешь гадать, почему.

Информация в этой статье носит ознакомительный характер. Модификация исполняемых файлов может нарушать лицензионные соглашения или вызывать срабатывание антивирусных систем. Принимайте решения с учётом законодательства и сопутствующих рисков.

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