1.2.4 — от кода до работающего процесса1.2.4 — from source to a running process
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.
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 |
Разные виды «констант» физически лежат в разных секциях файла на диске — это прямой ответ на вопрос «а где это всё до запуска»: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.
Компилятор превращает каждую единицу трансляции в объектный файл — уже в 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).
Линковщик сливает одноимённые секции всех .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.
При запуске .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).
Загрузчик читает таблицу импорта (.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.
Реальная точка входа 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.
Конструкторы глобальных объектов вызываются до 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 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."
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.