Как Windows загружает программу в память: путь от двойного щелчка до первого выполненного процессором байта

Двойной щелчок по ярлыку кажется мгновенным действием, но между ним и первым выполненным процессором байтом проходит целая цепочка операций: система находит файл, проверяет его формат, строит адресное пространство процесса, отображает секции файла в память, подключает библиотеки и только затем передаёт управление точке входа. Понимание этого механизма полезно не из академического интереса: оно объясняет, почему программы «не стартуют» с сообщением о недостающей DLL, почему один и тот же exe занимает разный объём памяти при разных запусках и почему антивирус может задержать запуск приложения.

Главный принцип, который стоит держать в голове: Windows почти никогда не копирует программу в память целиком. Она отображает файл в виртуальное адресное пространство процесса, а физическая память подгружается по мере обращения к страницам. Это ленивая модель: пока код функции не вызван, его страницы могут вообще не оказаться в оперативной памяти.

Что происходит сразу после запуска

Когда вы запускаете исполняемый файл, первым делом включается не сам файл, а системные компоненты. Цепочка выглядит так:

  1. Оболочка (проводник или командная строка) вызывает функцию создания процесса — например, CreateProcess из WinAPI.
  2. Ядро Windows создаёт объект процесса и объект главного потока, но пока ни одна инструкция вашей программы не выполнена.
  3. Менеджер памяти выделяет новому процессу собственное виртуальное адресное пространство — изолированное от других процессов.
  4. В это пространство отображается образ исполняемого файла (ntoskrnl и диспетчер кэша работают с файлом через механизм проецируемых секций).
  5. Создаётся начальный поток, который начинает выполнение с системного кода инициализации, а не с вашего main().

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

Формат PE: что видит загрузчик внутри exe-файла

Исполняемые файлы Windows имеют формат PE (Portable Executable). Это структурированный контейнер, в котором описано всё, что нужно для запуска:

  • DOS-заголовок — историческое наследие; сегодня важен лишь указатель на следующий блок.
  • PE-заголовок — сигнатура формата, тип машины (x86, x64, ARM64), число секций, метка времени сборки.
  • Опциональный заголовок — несмотря на название, он обязателен: здесь указаны точка входа, предпочтительный базовый адрес загрузки, размер стека и кучи, версия подсистемы (консольная или графическая).
  • Таблица секций — описание участков файла: .text (код), .data (инициализированные данные), .rdata (константы и таблицы импорта), .rsrc (ресурсы), .reloc (таблица перемещений).
  • Каталог данных — указатели на таблицу импорта, таблицу экспорта, отладочную информацию, сертификат подписи.

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

Почему секции важны на практике

Каждая секция получает свои атрибуты защиты памяти. Кодовая секция .text отображается как исполнимая, но доступная только для чтения — попытка записи в неё вызывает исключение. Секции данных доступны для записи, но обычно не исполнимы. Именно поэтому классические эксплойты, записывающие код в буфер данных и пытающиеся его выполнить, на современных версиях Windows блокируются аппаратно: процессор отказывается исполнять страницу без флага исполнения (механизм DEP).

Виртуальное адресное пространство: зачем нужен посредник

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

Эта схема даёт несколько практических следствий:

  • Процессы изолированы: программа не может прочитать память другой программы напрямую — обращение к чужому адресу приведёт к нарушению доступа.
  • Один и тот же виртуальный адрес в разных процессах указывает на разные физические страницы.
  • Физическую память можно выделять лениво: страница резервируется, но реальный кадр ОЗУ появляется только при первом обращении.
  • Неиспользуемые страницы можно выгрузить в файл подкачки и вернуть обратно незаметно для приложения.

Когда загрузчик отображает exe-файл, он не читает его содержимое в ОЗУ. Он создаёт проекцию: раздел файла связывается с диапазоном виртуальных адресов. При первом обращении процессора к странице возникает страничное прерывание, ядро подгружает нужный участок файла с диска в физическую страницу и обновляет таблицу страниц. Для программиста всё это прозрачно.

Совместно используемые страницы

Если десять процессов используют одну и ту же системную библиотеку, физические страницы её кода существуют в единственном экземпляре и отображаются в адресные пространства всех десяти. Экономия существенная: именно поэтому десятки приложений могут работать одновременно, не дублируя код ntdll или kernel32 в памяти. Страницы данных у каждого процесса свои, поскольку каждая программа пишет в них независимо.

Базовый адрес, релокации и ASLR

Компилятор генерирует код в предположении, что модуль будет загружен по определённому адресу — предпочтительному базовому адресу из заголовка. Но этот адрес может быть занят: например, две разные DLL собраны с одинаковым предпочтительным адресом. Тогда загрузчику приходится применять релокации — корректировать абсолютные адреса в коде и данных по таблице .reloc.

Релокации стоят времени: каждую запись нужно исправить. Поэтому разработчики стараются назначать библиотекам непересекающиеся базовые адреса. Однако с появлением ASLR (Address Space Layout Randomization) ситуация изменилась принципиально.

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

Разрешение импортов: подключение DLL

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

  1. Загрузчик ищет библиотеку по имени: сначала среди уже загруженных модулей, затем в каталоге приложения, потом в системных каталогах Windows и по путям из переменной PATH (точный порядок поиска настраивается и зависит от версии системы и флагов вызова).
  2. Найденная DLL сама отображается в адресное пространство процесса — рекурсивно проходя тот же цикл: заголовки, секции, её собственные зависимости.
  3. Адреса импортируемых функций записываются в таблицу импорта образа, чтобы вызовы из вашего кода попадали по правильным адресам.

Отсюда практическое правило: если приложение падает с сообщением о ненайденной DLL, проблема в порядке поиска или в отсутствии файла, а не в «повреждении памяти». Типичные причины — библиотека лежит не там, где ищет загрузчик, установлена не та разрядность (32-битный процесс не загрузит 64-битную DLL) или отсутствует распространяемый пакет среды выполнения, например Visual C++ Redistributable.

Статическая и динамическая загрузка

Импорт из таблицы — это раннее связывание: все библиотеки подгружаются до выполнения основной программы, и отсутствие любой из них сорвёт запуск целиком. Альтернатива — поздняя загрузка через LoadLibrary и GetProcAddress: программа сама решает, когда подтянуть модуль. Так делают плагинные системы, драйверы устройств и приложения, поддерживающие необязательные компоненты. Компромисс очевиден: гибкость против более сложного кода обработки ошибок.

Инициализация и точка входа

Когда все модули загружены и импорты разрешены, начинается цепочка инициализации. Загрузчик вызывает функцию DllMain каждой подключённой библиотеки с причиной DLL_PROCESS_ATTACH — библиотеки получают шанс подготовить внутренние структуры. Затем стартует начальный поток процесса.

Начинает поток не ваш код, а стандартная библиотека среды выполнения (CRT). Её задачи: инициализировать глобальные переменные, настроить кучу, обработать аргументы командной строки и окружение. И только после этого вызывается ваша точка входа — main, wmain, WinMain или их Unicode-варианты. В обратном порядке всё демонтируется при завершении: CRT очищает ресурсы, библиотеки получают DLL_PROCESS_DETACH, процесс уничтожается ядром.

Это объясняет частый вопрос: почему «пустая» программа потребляет несколько мегабайт памяти? Потому что вместе с ней загружены системные библиотеки, структуры ядра, куча и стек потока — минимальная инфраструктура любого процесса.

Стек и куча: память, которую программа получает «из коробки»

Помимо образа файла, процессу нужны области для работы:

  • Стек потока — хранение локальных переменных, параметров и адресов возврата. Размер задаётся в PE-заголовке (обычно по умолчанию около одного мегабайта, но значение зависит от компилятора и настроек проекта); фактически коммитится по мере роста.
  • Куча — область для динамического выделения памяти через malloc, new и HeapAlloc. Менеджер кучи запрашивает у ядра страницы крупными блоками и раздаёт их мелкими порциями.
  • Сегмент TLS — память, уникальная для каждого потока.

Переполнение стека (например, бесконечной рекурсией) приводит к исключению stack overflow — стек просто упирается в защитную страницу. Утечки памяти проявляются иначе: куча растёт, ядро вынуждено вытеснять страницы в файл подкачки, и система замедляется задолго до исчерпания физической ОЗУ.

Коммит, резервирование и файл подкачки

Windows различает три состояния памяти, и путаница между ними — источник многих мифов о «съеденной» оперативке:

Состояние Что означает Гарантия системы
Резервированная (reserved) Диапазон адресов закреплён за процессом, но физической памяти нет Адреса не отдадут другим процессам
Переданная (committed) Резерв подтверждён, страница может получить физический кадр при обращении Поддерживается ОЗУ или файлом подкачки
Отображённая на файл (mapped) Страницы backed файлом (exe, DLL, memory-mapped file) Содержимое можно сбросить и перечитать с диска

Числа в диспетчере задач смешивают эти категории, поэтому «рабочий набор» процесса (реально находящиеся в ОЗУ страницы) почти всегда меньше значения «выделено». Кодовые страницы exe и DLL вообще не занимают место в подкачке: их всегда можно перечитать из исходного файла.

Что может пойти не так: типичные ошибки запуска

Знание механизма загрузки позволяет диагностировать проблемы запуска по симптомам:

  • «Приложение не является приложением Win32» — файл повреждён, скачан не полностью либо это исполняемый файл для другой архитектуры или платформы.
  • Ошибка о ненайденной DLL — нарушение порядка поиска библиотек, неверная разрядность или отсутствие пакета среды выполнения.
  • Запуск «висит» на антивирусе — сканирование образа при создании секции; SmartScreen и Defender проверяют неподписанные файлы из интернета.
  • 0xc000007b — классический признак смешения 32-битных и 64-битных компонентов в одном процессе.
  • Access violation сразу при старте — часто повреждение зависимой библиотеки или конфликт версий одной и той же DLL, загруженной из разных мест.

Полезный инструмент диагностики — встроенные средства вроде Event Viewer (журнал приложений фиксирует сбои загрузки модулей) и утилита Process Monitor, которая показывает каждый шаг поиска DLL и каждое обращение к реестру при запуске. По логам видно, какой именно файл загрузчик не смог найти или открыть.

Безопасность на этапе загрузки

Момент загрузки — критическая точка с точки зрения безопасности, и Windows встраивает сюда несколько уровней защиты:

  • Проверка цифровой подписи — для системных компонентов и драйверов подпись обязательна; пользовательские программы могут быть неподписанными, но тогда теряется подтверждение подлинности издателя.
  • DEP/NX — запрет исполнения страниц данных.
  • ASLR — рандомизация базовых адресов модулей, стека и кучи.
  • CFG (Control Flow Guard) — проверка допустимости косвенных переходов, затрудняет эксплуатацию переполнений.
  • Целостность кода и подпись образов — ядро может отказаться загружать неподписанные или изменённые бинарники, если политика включена.

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

Быстрая загрузка: prefetcher, SuperFetch и кэширование

Повторный запуск программы происходит заметно быстрее первого, и причина снова в механике отображения файлов. После первого запуска образ exe и его DLL остаются в системном кэше файловой системы в оперативной памяти — повторное чтение с диска не требуется. Дополнительно Windows ведёт статистику запусков и заранее подгружает часто используемые файлы (исторически эта функция называлась Prefetcher, позднее её развитие вошло в SysMain/SuperFetch). Поэтому «холодный» запуск после перезагрузки всегда медленнее «тёплого», даже на SSD.

Практические выводы

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

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

  1. Посмотрите точный текст ошибки и код в журнале событий Windows — там обычно указан сбойный модуль.
  2. Проверьте разрядность приложения и всех его библиотек — они должны совпадать.
  3. Убедитесь, что установлены актуальные распространяемые пакеты среды выполнения, требуемые приложением.
  4. Для глубокого анализа используйте Process Monitor: он покажет, где именно загрузчик остановился.

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

PEFile.ru