1.2.4 — от кода до работающего процесса1.2.4 — from source to a running process

PE-файл и жизненный цикл процессаThe PE file & process lifecycle

Executable — это не «код», а размеченная на секции статическая структура на диске: инструкции, готовые данные и рецепт их размещения в памяти. Загрузчик ОС читает эту разметку и превращает файл в живой процесс с адресным пространством ещё до того, как выполнилась хоть одна строчка main.An executable isn't "code" — it's a static structure on disk laid out into sections: instructions, ready-made data, and a recipe for placing them in memory. The OS loader reads that layout and turns the file into a live process with an address space before a single line of main has run.

частый вопрос на собеседованииcommon interview question Windows PE (с параллелями на Linux ELF)(with Linux ELF parallels)

Что такое PE-файлWhat a PE file is

PE (Portable Executable) — формат исполняемых файлов Windows (.exe, .dll). Это не «сплошной блоб машинного кода», а структура из заголовков и именованных секций, каждая из которых имеет собственные атрибуты доступа (читать/писать/исполнять) и собственное содержимое. Linux/macOS используют структурно похожие, но other форматы (ELF, Mach-O) — идея секций, заголовков и таблиц импорта у них та же, различаются детали и названия.PE (Portable Executable) is Windows's executable file format (.exe, .dll). It isn't "one solid blob of machine code" — it's a structure of headers and named sections, each with its own access attributes (read/write/execute) and its own content. Linux/macOS use structurally similar but different formats (ELF, Mach-O) — the idea of sections, headers, and import tables is the same; the details and names differ.

секция PEPE sectionаналог в ELFELF equivalentсодержимоеcontentправа доступаaccess rights
.text.textмашинные инструкции — скомпилированный код функцийmachine instructions — compiled function bodiesчтение + выполнениеread + execute
.rdata.rodataконстанты только для чтения: строковые литералы, const-глобали, vtablesread-only constants: string literals, const globals, vtablesтолько чтениеread-only
.data.dataинициализированные ненулевым значением глобальные/статические переменныеinitialized (non-zero) global/static variablesчтение + записьread + write
.bss.bssнеинициализированные/нулевые глобальные переменные — не хранится на диске, только размерuninitialized/zero globals — not stored on disk, just a sizeчтение + записьread + write
.idata.dynamic/.gotтаблица импорта — какие функции берутся из каких DLL/.sothe import table — which functions come from which DLLs/.so filesчтениеread

Где хранятся константы в незапущенном файлеWhere constants live in a file that isn't running

Разные виды «констант» физически лежат в разных секциях файла на диске — это прямой ответ на вопрос «а где это всё до запуска»:Different kinds of "constants" physically live in different sections of the file on disk — this directly answers "where is all this before the program even runs":

const char* greeting = "hello";   // "hello" → .rdata, указатель greeting → .data// "hello" → .rdata, the pointer greeting itself → .data
const int MAX_SIZE = 100;      // почти всегда инлайнится компилятором в .text напрямую как immediate// almost always inlined by the compiler straight into .text as an immediate
int global_counter = 0;       // в .bss — 0 не хранится, только "выдели 4 байта и обнули"// in .bss — the 0 isn't stored, just "allocate 4 bytes and zero them"
int initial_value = 42;      // в .data — 42 реально записано в файл на диске// in .data — the 42 is actually written into the file on disk
static int local_static = 7;  // тоже .data — область видимости не влияет на секцию// also .data — scope doesn't affect which section it lands in

void f() {
    int local = 5;             // нигде в файле — существует только на стеке в рантайме// nowhere in the file — exists only on the stack at runtime
}

.bss — самая контринтуитивная деталь: раздел называется Block Started by Symbol, и в файле на диске для него хранится только число — «здесь нужно N байт, заполненных нулями». Если бы 10 МБ глобального массива нулей реально записывались в файл побайтово, каждый такой файл был бы аномально раздут. Загрузчик сам выделяет и обнуляет эту память при старте процесса — на диске нулей физически нет.The .bss section is the most counterintuitive detail: the name stands for Block Started by Symbol, and on disk it stores only a number — "N bytes are needed here, filled with zeros." If a 10 MB global array of zeros were actually written byte-by-byte into the file, every such binary would be absurdly bloated. The loader allocates and zeroes that memory itself when the process starts — no zeros physically exist on disk.

Полный путь: от .cpp до работающего процессаThe full path: from .cpp to a running process

01

Компиляция: .cpp → object file (.obj/.o)Compilation: .cpp → object file (.obj/.o)

Компилятор превращает каждую единицу трансляции в объектный файл — уже в PE/COFF- или ELF-подобном формате, с теми же секциями (.text/.data/.bss), но с «дырками» вместо адресов внешних функций/переменных (они разрешатся линковщиком).The compiler turns each translation unit into an object file — already in a PE/COFF- or ELF-like format, with the same sections (.text/.data/.bss), but with "holes" instead of addresses for external functions/variables (those get resolved by the linker).

02

Линковка: object files → единый исполняемый файлLinking: object files → a single executable

Линковщик сливает одноимённые секции всех .obj-файлов в одну (все .text → общий .text итогового файла), разрешает «дырки»-символы, вычисляет финальные относительные адреса и записывает готовые заголовки PE, включая таблицу импорта нужных DLL.The linker merges same-named sections from every .obj file into one (all the .text sections become the final file's single .text), resolves the symbol "holes," computes final relative addresses, and writes the finished PE headers, including the import table for the required DLLs.

03

Загрузка: ОС читает заголовки и строит адресное пространствоLoading: the OS reads the headers and builds the address space

При запуске .exe загрузчик ОС читает PE-заголовок, резервирует виртуальное адресное пространство под процесс, отображает (memory-maps) секции файла в память с указанными в заголовке правами доступа — .text становится read+execute страницами, .data read+write, а под .bss просто выделяется и обнуляется столько памяти, сколько указано в заголовке (без чтения с диска).When the .exe launches, the OS loader reads the PE header, reserves a virtual address space for the process, memory-maps the file's sections with the access rights specified in the header — .text becomes read+execute pages, .data becomes read+write, and for .bss it simply allocates and zeroes as much memory as the header specifies (with nothing read from disk).

04

Динамическая линковка: загрузка DLL/.so и разрешение импортовDynamic linking: loading DLLs/.so files and resolving imports

Загрузчик читает таблицу импорта (.idata), находит и мапит в память каждую требуемую DLL (или переиспользует уже загруженную другим процессом копию), а затем патчит адреса вызовов импортированных функций в памяти процесса — этот этап происходит целиком до main.The loader reads the import table (.idata), locates and maps each required DLL into memory (or reuses a copy already loaded by another process), then patches the call addresses for imported functions in the process's memory — this entire step happens before main.

05

Точка входа рантайма и запуск mainThe runtime entry point and launching main

Реальная точка входа PE-файла — не main, а функция из рантайм-библиотеки (mainCRTStartup в MSVC, _start/__libc_start_main в связке glibc+ld.so). Она настраивает стек, инициализирует глобальные объекты с нетривиальными конструкторами (C++ static/global objects), а уже потом вызывает main с переданными аргументами.The real entry point of a PE file isn't main — it's a function from the runtime library (mainCRTStartup in MSVC, _start/__libc_start_main in the glibc+ld.so combo). It sets up the stack, initializes global objects with non-trivial constructors (C++ static/global objects), and only then calls main with the passed-in arguments.

ГраблиGotchas

Порядок инициализации глобальных объектов между единицами трансляции не определён (Static Initialization Order Fiasco)Initialization order across translation units is unspecified (the Static Initialization Order Fiasco)

Конструкторы глобальных объектов вызываются до main, но стандарт C++ не гарантирует порядок между разными .cpp-файлами — глобальный объект в одном файле может обратиться к ещё не сконструированному глобальному объекту из другого файла, получив мусор или падение. Классическое решение — «Construct on First Use» через функцию-обёртку с локальной static-переменной.Global object constructors run before main, but the C++ standard makes no guarantee about ordering across different .cpp files — a global object in one file can reference a not-yet-constructed global object from another file, reading garbage or crashing. The classic fix is "Construct on First Use" via a wrapper function with a local static variable.

const не гарантирует размещение в .rdataconst doesn't guarantee placement in .rdata

Компилятор чаще всего инлайнит const int прямо в код как immediate-значение, вообще не выделяя под неё память ни в какой секции — а вот const объект нетривиального типа (со своим конструктором) физически не может быть чисто «данными» до запуска и требует рантайм-инициализации, попадая фактически в категорию «глобальный объект», а не «константа времени компиляции».The compiler most often inlines a const int straight into the code as an immediate value, allocating it no storage in any section at all — whereas a const object of a non-trivial type (with its own constructor) physically cannot be pure "data" before the program runs and requires runtime initialization, effectively landing in the "global object" category rather than "compile-time constant."

ASLR рандомизирует адреса секций при каждом запускеASLR randomizes section addresses on every launch

Address Space Layout Randomization смещает базовый адрес загрузки образа (и стека, и кучи) при каждом запуске процесса как защита от эксплойтов — абсолютные адреса функций и глобальных переменных различаются между запусками одного и того же неизменного файла, поэтому PE использует relocation-таблицу для патчинга адресов на месте.Address Space Layout Randomization shifts the image's (and the stack's and heap's) load base address on every process launch, as a defense against exploits — the absolute addresses of functions and global variables differ between runs of the exact same, unmodified file, which is why PE carries a relocation table to patch addresses in place.