Найти в Дзене
Под капотом ПО

Как Windows оживляет программы? Разбираем загрузчик PE-файлов

Привет! 🌟 Сегодня мы заглянем в "кухню" операционной системы Windows и разберем, как работает загрузчик исполняемых файлов. Это как рецепт волшебного зелья: смешиваем технические детали, код на C++ и понятные аналогии. Поехали! PE (Portable Executable) - это формат исполняемых файлов в Windows (.exe, .dll). Представьте его как "чемодан" для переезда: 🔑 Ключевой термин: RVA (Relative Virtual Address) смещение от базового адреса загрузки. Шаг 1️⃣: Проверка файла Загрузчик ищет сигнатуру "PE\0\0" (как проверка паспорта): // Читам заголовки IMAGE_DOS_HEADER* dosHeader = (IMAGE_DOS_HEADER*)fileData; IMAGE_NT_HEADERS* ntHeaders = (IMAGE_NT_HEADERS*)(fileData + dosHeader->e_lfanew); // Магическая проверка if (ntHeaders->Signature != IMAGE_NT_SIGNATURE) { throw "Это не PE-файл!"; } Шаг 2️⃣: Выделение "жилплощади" в памяти Пытаемся загрузить по адресу ImageBase: void* imageBase = VirtualAlloc( (LPVOID)ntHeaders->OptionalHeader.ImageBase, ntHeaders->OptionalHeader.SizeOfImage,
Оглавление

Привет! 🌟 Сегодня мы заглянем в "кухню" операционной системы Windows и разберем, как работает загрузчик исполняемых файлов. Это как рецепт волшебного зелья: смешиваем технические детали, код на C++ и понятные аналогии. Поехали!

🧩 Что такое PE-файл?

PE (Portable Executable) - это формат исполняемых файлов в Windows (.exe, .dll). Представьте его как "чемодан" для переезда:

  • Код программы - ваши ценные вещи
  • Данные - инструкции по распаковке
  • Системные заголовки - наклейки "Осторожно, стекло!"

🔍 Структура PE-файла (Разобрали в пред. статье)

🔑 Ключевой термин: RVA (Relative Virtual Address) смещение от базового адреса загрузки.

🚀 Алгоритм работы загрузчика: 6 шагов к запуску

Шаг 1️⃣: Проверка файла

Загрузчик ищет сигнатуру "PE\0\0" (как проверка паспорта):

// Читам заголовки
IMAGE_DOS_HEADER* dosHeader = (IMAGE_DOS_HEADER*)fileData;
IMAGE_NT_HEADERS* ntHeaders = (IMAGE_NT_HEADERS*)(fileData + dosHeader->e_lfanew);
// Магическая проверка
if (ntHeaders->Signature != IMAGE_NT_SIGNATURE) {
throw "Это не PE-файл!";
}

Шаг 2️⃣: Выделение "жилплощади" в памяти

Пытаемся загрузить по адресу ImageBase:

void* imageBase = VirtualAlloc(
(LPVOID)ntHeaders->OptionalHeader.ImageBase,
ntHeaders->OptionalHeader.SizeOfImage,
MEM_RESERVE | MEM_COMMIT,
PAGE_READWRITE
);
// Если не получилось - ищем свободный адрес
if (!imageBase) {
imageBase = VirtualAlloc(NULL, ...); // Автораспределение ОС
}

Шаг 3️⃣: "Распаковка чемодана" - загрузка секций

Копируем код и данные в память:

// Для каждой секции в таблице:
for (int i = 0; i < ntHeaders->FileHeader.NumberOfSections; i++) {
IMAGE_SECTION_HEADER* section = &sectionHeaders[i];
// Копируем из файла в память
memcpy(
(BYTE*)imageBase + section->VirtualAddress, // Адрес в памяти
fileData + section->PointerToRawData, // Данные в файле
section->SizeOfRawData
);
}

Шаг 4️⃣: Игра в "пазл" - разрешение импорта

Загрузчик ищет нужные DLL (например, kernel32.dll) и подключает функции:

// Перебираем записи в таблице импорта
IMAGE_IMPORT_DESCRIPTOR* importDesc = ...;
while (importDesc->Name) {
const char* dllName = (char*)(imageBase + importDesc->Name);
HMODULE dllHandle = LoadLibraryA(dllName);
// Решаем адреса функций
IMAGE_THUNK_DATA* thunk = ...;
while (thunk->u1.AddressOfData) {
FARPROC funcAddr = GetProcAddress(dllHandle, ...);
thunk->u1.Function = (DWORD_PTR)funcAddr; // Подставляем адрес!
thunk++;
}
importDesc++;
}

Шаг 5️⃣: "Переезд" - обработка релокаций

Если загрузка прошла не по ImageBase, нужно поправить адреса:

// Получаем данные релокаций
IMAGE_BASE_RELOCATION* reloc = ...;
while (reloc->SizeOfBlock) {
DWORD* entries = (DWORD*)(reloc + 1);
int numEntries = (reloc->SizeOfBlock - sizeof(*reloc)) / sizeof(WORD);
for (int i = 0; i < numEntries; i++) {
if (entries[i] >> 12 == IMAGE_REL_BASED_HIGHLOW) {
DWORD* patchAddr = (DWORD*)(imageBase + reloc->VirtualAddress + (entries[i] & 0xFFF));
*patchAddr += (DWORD)imageBase - ntHeaders->OptionalHeader.ImageBase; // Корректируем!
}
}
reloc = (IMAGE_BASE_RELOCATION*)((BYTE*)reloc + reloc->SizeOfBlock);
}

Шаг 6️⃣: Старт программы!

Передаём управление на точку входа:

// Тип точки входа: int __stdcall EntryPoint(void)
typedef int (__stdcall *EntryPointFunc)();
EntryPointFunc entry = (EntryPointFunc)(
(BYTE*)imageBase + ntHeaders->OptionalHeader.AddressOfEntryPoint
);
entry(); // Запуск!

💡 Почему это важно?

  • Безопасность: Понимание загрузки помогает бороться с вирусами
  • Оптимизация: Знание структуры PE ускоряет отладку
  • Магия: Вы больше не верите, что "программы просто запускаются"
💡 Совет начинающим: Поиграйтесь с утилитой PEView - она покажет "внутренности" любого PE-файла как на ладони!

🎯 Заключение

Теперь вы знаете, что загрузчик - это не просто "черный ящик", а сложный механизм, который:

  1. Проверяет файл
  2. Выделяет память
  3. Загружает секции
  4. Разрешает зависимости
  5. Исправляет адреса
  6. Запускает код

Главный секрет: Загрузчик делает программу "домашней" в памяти Windows, прежде чем та начнет работать. Как переезд в новую квартиру: пока не расставишь мебель - жить нельзя! 🏠

Удачи в исследованиях! Если что-то непонятно - пишите в комментариях, разберем вместе 😊