Двойной щелчок по ярлыку кажется мгновенным действием, но между ним и первым выполненным процессором байтом проходит целая цепочка операций: система находит файл, проверяет его формат, строит адресное пространство процесса, отображает секции файла в память, подключает библиотеки и только затем передаёт управление точке входа. Понимание этого механизма полезно не из академического интереса: оно объясняет, почему программы «не стартуют» с сообщением о недостающей DLL, почему один и тот же exe занимает разный объём памяти при разных запусках и почему антивирус может задержать запуск приложения.
Главный принцип, который стоит держать в голове: Windows почти никогда не копирует программу в память целиком. Она отображает файл в виртуальное адресное пространство процесса, а физическая память подгружается по мере обращения к страницам. Это ленивая модель: пока код функции не вызван, его страницы могут вообще не оказаться в оперативной памяти.
- Что происходит сразу после запуска
- Формат PE: что видит загрузчик внутри exe-файла
- Почему секции важны на практике
- Виртуальное адресное пространство: зачем нужен посредник
- Совместно используемые страницы
- Базовый адрес, релокации и ASLR
- Разрешение импортов: подключение DLL
- Статическая и динамическая загрузка
- Инициализация и точка входа
- Стек и куча: память, которую программа получает «из коробки»
- Коммит, резервирование и файл подкачки
- Что может пойти не так: типичные ошибки запуска
- Безопасность на этапе загрузки
- Быстрая загрузка: prefetcher, SuperFetch и кэширование
- Практические выводы
Что происходит сразу после запуска
Когда вы запускаете исполняемый файл, первым делом включается не сам файл, а системные компоненты. Цепочка выглядит так:
- Оболочка (проводник или командная строка) вызывает функцию создания процесса — например, CreateProcess из WinAPI.
- Ядро Windows создаёт объект процесса и объект главного потока, но пока ни одна инструкция вашей программы не выполнена.
- Менеджер памяти выделяет новому процессу собственное виртуальное адресное пространство — изолированное от других процессов.
- В это пространство отображается образ исполняемого файла (ntoskrnl и диспетчер кэша работают с файлом через механизм проецируемых секций).
- Создаётся начальный поток, который начинает выполнение с системного кода инициализации, а не с вашего 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 выполняется своя мини-загрузка:
- Загрузчик ищет библиотеку по имени: сначала среди уже загруженных модулей, затем в каталоге приложения, потом в системных каталогах Windows и по путям из переменной PATH (точный порядок поиска настраивается и зависит от версии системы и флагов вызова).
- Найденная DLL сама отображается в адресное пространство процесса — рекурсивно проходя тот же цикл: заголовки, секции, её собственные зависимости.
- Адреса импортируемых функций записываются в таблицу импорта образа, чтобы вызовы из вашего кода попадали по правильным адресам.
Отсюда практическое правило: если приложение падает с сообщением о ненайденной 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 — это построение изолированного виртуального пространства, отображение в него образа файла и его зависимостей с последующей передачей управления через цепочку инициализаторов. Физическая память расходуется экономно и лениво, а большинство проблем запуска сводится к трём причинам: файл не соответствует ожидаемому формату, не найдена зависимость или сработала защита.
Если вам нужно диагностировать конкретную проблему запуска, разумный порядок действий такой:
- Посмотрите точный текст ошибки и код в журнале событий Windows — там обычно указан сбойный модуль.
- Проверьте разрядность приложения и всех его библиотек — они должны совпадать.
- Убедитесь, что установлены актуальные распространяемые пакеты среды выполнения, требуемые приложением.
- Для глубокого анализа используйте Process Monitor: он покажет, где именно загрузчик остановился.
Понимание этого конвейера также помогает читать техническую документацию: когда в требованиях программы указаны «поддержка ASLR» или «отсутствие зависимостей от конкретного базового адреса», теперь ясно, о каком именно этапе загрузки идёт речь и почему это влияет на совместимость и безопасность.
