The C Runtime (CRT)
A common misconception is that the execution of a C++ program begins at main(). In reality, main() is merely a callback function invoked by the C Runtime (CRT) after a complex Initialization sequence.
The CRT serves as the abstraction layer between the Operating System Kernel and the C++ Abstract Machine. It is responsible for setting up the stack, initializing the heap, handling signals, and, Critically for C++, orchestrating the construction of global objects.
What the CRT Provides Beyond Language Features
Section titled “What the CRT Provides Beyond Language Features”The CRT implements services that the C++ standard library relies on but that are not part of the Language itself:
- Memory allocation: The implementation of
malloc``free``operator newAndoperator delete(ultimately backed bybrk/mmapon Linux orVirtualAllocon Windows). - Thread support:
pthread_createon Linux,CreateThreadon Windows.std::threadis built on top of these. - File I/O:
fopen``fread``fwritewrap system calls (open``read``write). - Signal handling:
signal()``raise()provide POSIX signal semantics. - Exception infrastructure: Stack unwinding for C++ exceptions requires CRT support (
__cxa_begin_catch``__cxa_throwon Itanium ABI platforms). - Locale and ctype: Character classification, numeric formatting, and locale management.
- Exit and cleanup:
atexit``exit``abortAnd the termination sequence.
The Physical Entry Point
Section titled “The Physical Entry Point”When the OS Loader (e.g., ld.so on Linux or ntdll.dll on Windows) loads a binary, it jumps to The address specified in the file header (ELF or PE). This address does not point to main.
- Linux (ELF): Points to
_startProvided bycrt1.o. - Windows (PE): Points to
mainCRTStartup(orwmainCRTStartup), provided by the MSVC Runtime.
ELF Entry Point Details
Section titled “ELF Entry Point Details”The ELF header’s e_entry field specifies the virtual address of the entry point. This is Set by the linker script (ENTRY(_start)) or by the compiler driver when linking with the CRT Startup objects. You can inspect it:
readelf -h ./app | grep Entry# Entry point address: 0x401020On modern Linux with PIE (Position-Independent Executables), the entry point is a relative offset That the dynamic linker resolves at load time. The kernel sets the instruction pointer to this Address after mapping the binary’s PT_LOAD segments into memory.
The Startup Sequence
Section titled “The Startup Sequence”The full startup path on Linux (glibc) is:
- Kernel Handoff: The OS maps pages into memory and sets the Instruction Pointer (RIP) to the Entry Point (
_start). _start(incrt1.o): This is a tiny assembly stub. It popsargcfrom the stack, sets upargvAnd calls__libc_start_main.__libc_start_main(in libc.so): The main CRT initialization function. It:
- Registers the program’s
mainfunction as anatexitcallback. - Calls
__libc_csu_initWhich walks the.init_arraysection. .init_arrayprocessing: Each function pointer in.init_arrayis called. This is where C++ global constructors execute.
main()is called withargc``argvAndenvp.- Return from
main: The return value is passed toexit()Triggering the termination sequence.
// Simplified conceptual flow_start: pop %rdi // argc mov %rsp, %rsi // argv call __libc_start_main // never returns
__libc_start_main(main, argc, argv, init, fini, rtld_fini, stack_end): // ... setup ... __libc_csu_init() // runs .init_array (global constructors) result = main(argc, argv, envp) exit(result)Detailed Stack Layout at _start
Section titled “Detailed Stack Layout at _start”When the kernel transfers control to _startThe stack contains the program’s execution Parameters, laid out by the kernel in a specific format defined by the System V ABI:
High Address+------------------+| argc | (%rsp)+------------------+| argv[0] | 8(%rsp)| argv[1] | 16(%rsp)| ... || NULL |+------------------+| envp[0] || envp[1] || ... || NULL |+------------------+| auxv entries | (ELF auxiliary vector)| ... |+------------------+| padding || strings... || ... |+------------------+Low AddressThe _start stub reads argc from the top of the stack, computes argv as RSP + 8And envp As RSP + 8 + (argc + 1) * 8. This layout is guaranteed by the System V AMD64 ABI and is Platform-specific (Windows uses a different layout passed via the MSVC CRT).
C++ Static Initialization
Section titled “C++ Static Initialization”The most architecturally significant phase of startup is C++ Initialization. This allows code to Run before main.
The .init_array Section
Section titled “The .init_array Section”The compiler generates a list of function pointers for every global or static object that requires a Constructor. These pointers are stored in specific binary sections.
Sections:
.init_array: An array of function pointers executed by the CRT startup routine (__libc_csu_init)..fini_array: An array of function pointers executed at termination.
Inspection:
readelf -x .init_array ./appSections:
.CRT$XCU: The section used by the MSVC CRT to discover C++ initializers.
Inspection:
dumpbin /SECTION:.CRT$XCU /RAWDATA app.exeInitialization Order and the Static Init Fiasco
Section titled “Initialization Order and the Static Init Fiasco”The C++ Standard [N4950 S6.6.3.2] guarantees that global objects within a single Translation Unit Are initialized in the order of definition. However, the order of initialization across different Translation Units is unspecified.
Scenario:
FileA.cpp: Definesint x = 42;FileB.cpp: Definesint y = x + 1;
If the linker arranges FileB to initialize before FileA``y will be initialized to garbage (or Zero) + 1, not 43.
Proof: Static Initialization Order Across TUs Is Unspecified
Section titled “Proof: Static Initialization Order Across TUs Is Unspecified”Per [N4950 S6.6.3.2 p2]: “Dynamic initialization of a non-local variable with static storage Duration is either ordered or unordered.” For variables in different translation units, the standard Classifies initialization as unordered unless the variable has a constant initializer or is an Inline variable.
Formally, let be defined in and be defined in Where ‘s initializer Depends on . The standard does not require ‘s initialization to complete before ‘s Initialization begins. The implementation is free to order them in any way, and this ordering may Change between compiler versions, link orders, or optimization levels.
This is not merely an academic concern. In practice, the initialization order depends on the order In which object files appear in the linker’s input, which is determined by the build system. A Seemingly unrelated change to CMakeLists.txt (adding a new source file) can silently reorder the Linker input and cause a previously correct program to crash during startup.
Architectural Mitigation:
- Constinit (C++20): Use
constinitvariables which are guaranteed to be initialized at compile-time (placed in.data), avoiding runtime execution code entirely. - Construct On First Use: Wrap static globals in a function.
// Thread-safe in C++11+ (Magic Statics) [N4950 S6.8]int& get_global() { static int x = 42; // Initialized only when get_global() is first called return x;}The “magic statics” guarantee [N4950 S6.8 p8] ensures that the initialization of function-local Statics is thread-safe and happens exactly once, on first call. This defers the initialization to a Point where all dependencies are guaranteed to be available.
How the CRT Finds Constructors
Section titled “How the CRT Finds Constructors”The compiler generates a special initialization function for each TU that has global constructors. For GCC/Clang, this function is named _GLOBAL__sub_I_<filename> and is placed in the .init_array Section via a linker attribute:
// What the compiler emits (conceptually)__attribute__((constructor))static void _GLOBAL__sub_I_file_cpp() { // Call constructors for all global objects in this TU global_obj.~T(); // actually constructor call}The linker collects all _GLOBAL__sub_I_* functions from all object files and places their Addresses in the .init_array section. The CRT iterates this array during startup.
atexit and Destructors
Section titled “atexit and Destructors”The CRT uses atexit to manage cleanup. When a C++ program returns from main()The following Sequence runs:
atexithandlers: Functions registered viastd::atexit()are called in reverse registration order.- Static destructors: Destructors for global/static C++ objects are called in reverse order of construction.
- Stream flushing:
std::coutandprintfbuffers are flushed to file descriptors. - OS Exit: The CRT invokes the
exit_groupsyscall (Linux) orExitProcessAPI (Windows).
The CRT internally registers static destructors as atexit callbacks during startup. This means the Destructor order interleaves with explicitly registered atexit handlers based on registration Time.
#include <cstdlib>
struct Logger { ~Logger() { /* flush logs */ }};
Logger global_logger; // Destructor registered as atexit callback during startup
void on_exit() { // Also registered as atexit callback}
int main() { std::atexit(on_exit); return 0; // atexit callbacks run in reverse order: // 1. on_exit // 2. ~Logger (global_logger destructor)}CRT on Different Platforms
Section titled “CRT on Different Platforms”Glibc is the most common CRT on Linux. Key characteristics:
- Dynamic linking by default (libc.so.6).
_startincrt1.o``__libc_start_mainin libc.so.- Thread-local storage via
tls_setupinld.so. - Robust
dlopen/dlsymfor dynamic loading.
# Inspect the CRT objects linked into your binaryldd ./appreadelf -d ./app | grep NEEDEDMusl is a lightweight, BSD-licensed CRT. Key differences from glibc:
- Smaller binary footprint (ideal for containers and embedded).
- Simpler startup sequence.
- No
libpthreadseparate library (pthreads are built in). - Different
__libc_start_mainimplementation.
# Alpine Linux uses musl by defaultdocker run --rm alpine sh -c "ldd /bin/ls"MSVC provides two CRT variants with significantly different behavior:
- Dynamic (
/MD): Links againstucrtbase.dll(Universal C Runtime) andvcruntime140.dll. - Static (
/MT): Embeds the CRT into the executable.
The MSVC startup calls mainCRTStartupWhich initializes the heap, runs .CRT$XCU initializers, And calls main.
dumpbin /DEPENDENTS app.exeFreestanding vs. Hosted Environments
Section titled “Freestanding vs. Hosted Environments”The C++ standard defines two execution environments [N4950 S6.9.1]:
Hosted Environment
Section titled “Hosted Environment”The full C++ language is available. The CRT provides mainStartup/termination, dynamic memory, Exceptions, and the entire standard library. This is the default for all desktop, server, and mobile Platforms.
Freestanding Environment
Section titled “Freestanding Environment”Only a minimal subset of the language is available:
- No
main()required (a custom entry point may be used). - No dynamic memory allocation (no
new``delete``malloc). - No exceptions (no
try/catchNothrow). - No RTTI (no
dynamic_castNotypeid). - Only these standard library headers are required:
<cstddef>``<cfloat><climits>``<cstdalign>``<cstdarg>``<cstdbool>``<cstdlib>(onlyabort``atexit``at_quick_exit``exit``quick_exit``_Exit),<cstdint><cstdio>``<cstring>``<ctime>``<type_traits>``<limits><new>(placement new only),<initializer_list>``<ciso646>.
Per [N4950 S6.9.1 p4], in a freestanding environment, the startup and termination semantics are Implementation-defined. There is no guarantee that .init_array is processed or that atexit Functions are called.
The -ffreestanding Flag
Section titled “The -ffreestanding Flag”GCC and Clang support -ffreestanding to compile for a freestanding environment:
# Compile for freestanding (OS kernel, bootloader)clang++ -ffreestanding -nostdlib -c kernel.cpp -o kernel.oFlags commonly used together with -ffreestanding:
-nostdlib: Do not link the standard library or startup files.-nostdinc: Do not search standard include paths.-nostdinc++: Do not search C++ standard include paths.-fno-exceptions: Disable exception support.-fno-rtti: Disable RTTI.
Writing a Custom Entry Point
Section titled “Writing a Custom Entry Point”In a freestanding environment, you provide your own entry point:
// kernel_entry.cpp (freestanding)extern "C" void _start() { // Custom initialization // No CRT, no global constructors, no atexit
// Must call exit syscall manually (or loop forever for a kernel) while (true) { __asm__ volatile("hlt"); }}
// Link without CRT// clang++ -ffreestanding -nostdlib -fuse-ld=lld -T linker.ld kernel_entry.cpp -o kernel.elfStack Initialization in Freestanding Environments
Section titled “Stack Initialization in Freestanding Environments”In a hosted environment, the kernel sets up the stack before jumping to _start. In a freestanding Environment (e.g., a bare-metal bootloader), the stack must be configured manually, in the Assembly entry point or via a linker script:
SECTIONS{ . = 0x80000; .text : { *(.text) } .rodata : { *(.rodata) } .data : { *(.data) } .bss : { __bss_start = .; *(.bss) __bss_end = .; } . = ALIGN(16); . = . + 0x4000; /* 16 KB stack */ __stack_top = .;}// Assembly stub sets RSP to __stack_top before calling _startextern "C" void _start();
__asm__( ".global _entry\n" "_entry:\n" " ldr sp, =__stack_top\n" " bl _start\n");Program Termination
Section titled “Program Termination”Returning from main() is functionally equivalent to calling std::exit(). The process does not End immediately; the CRT must unwind the environment.
The Termination Sequence
Section titled “The Termination Sequence”- Return from
main: The return value is passed to the CRT. atexitHandlers: Functions registered viastd::atexitare called in reverse order of registration.- Static Destructors: Destructors for global/static C++ objects are called (reverse order of construction).
- Stream Flushing:
std::cout/printfbuffers are flushed to file descriptors. - OS Exit: The CRT invokes the
exitsyscall (Linux) orExitProcessAPI (Windows), returning control to the kernel.
:::caution std::terminate vs std::exit If an exception escapes mainOr an unjoinable std::thread is destroyed, the CRT calls std::terminate. This calls std::abortWhich kills the Process without running static destructors or file buffer flushing. This often results in Truncated logs or corrupted data files. :::
std::exit vs std::quick_exit vs std::_Exit
Section titled “std::exit vs std::quick_exit vs std::_Exit”Per [N4950 S18.5], the C++ standard provides three termination functions with distinct semantics:
| Function | atexit handlers | at_quick_exit handlers | Static destructors | Stream flush |
|---|---|---|---|---|
std::exit | Yes (reverse) | No | Yes (reverse) | Yes |
std::quick_exit | No | Yes (reverse) | No | No |
std::_Exit | No | No | No | No |
std::abort | No | No | No | No |
std::quick_exit was introduced in C++11 for scenarios where fast termination is needed (e.g., Process restart in a supervised environment) and cleanup is handled externally. It is the Recommended alternative to abort when you need to skip destructors intentionally.
CRT Linkage Modes (Windows Specific)
Section titled “CRT Linkage Modes (Windows Specific)”On Linux, the CRT is almost always linked dynamically (glibc). On Windows, MSVC offers a choice That profoundly affects architecture.
1. Dynamic Linking (/MD``/MDd)
Section titled “1. Dynamic Linking (/MD``/MDd)”- Mechanism: The executable relies on
VCRUNTIME140.DLLandUCRTBASE.DLLpresent on the system. - Pros: Smaller binary; OS patches to the CRT apply automatically; Memory ownership (Heap) is shared across DLL boundaries.
- Cons: “DLL Hell” (missing redistributables).
2. Static Linking (/MT``/MTd)
Section titled “2. Static Linking (/MT``/MTd)”- Mechanism: The CRT code is copied directly into the
.exe. - Pros: Standalone executable (no dependencies).
- Cons: Bloated binary; Heap Isolation.
The Heap Isolation Trap
Section titled “The Heap Isolation Trap”If App.exe is linked with /MT and Lib.dll is linked with /MTThey essentially have separate Heaps.
- Allocating memory in
Lib.dlland freeing it inApp.execauses a Heap Corruption crash. - Best Practice: Always use
/MD(Dynamic) for non-trivial systems involving multiple DLLs.
Architectural Verification
Section titled “Architectural Verification”To understand the startup cost of your application, you can profile the time spent before main.
Linux (LD_DEBUG)
Section titled “Linux (LD_DEBUG)”The dynamic linker can report relocation processing time.
LD_DEBUG=statistics ./appAnalyzing Initializers
Section titled “Analyzing Initializers”If startup is slow, you may have excessive global constructors. Use nm to count them.
# Look for internal initialization functions generated by GCC/Clangnm -C ./app | grep _GLOBAL__sub_IEach entry represents a function that runs before main. Minimizing these is a key optimization for CLI tools and short-lived microservices.
Common Pitfalls
Section titled “Common Pitfalls”- Static init fiasco: Global objects in different TUs have undefined initialization order. Use “construct on first use” (magic statics) or
constinit. std::terminateskips destructors: An uncaught exception or destroyed joinable thread callsstd::abortBypassing cleanup. Always catch exceptions inmainor usestd::set_terminate.- Heap isolation on Windows with
/MT: Memory allocated in one DLL must be freed in the same DLL. Use/MDor provide deallocation functions in the DLL. - Excessive global constructors in CLI tools: Each global constructor adds startup latency. Profile with
LD_DEBUG=statisticsand minimize. - Freestanding without
-nostdlib: Using-ffreestandingalone still links the CRT. Use-nostdliband provide your own_startfor true freestanding. - Using
std::quick_exitwithout registering handlers: Unlikestd::exit``quick_exitdoes not run static destructors or flush streams. If you use it, register any necessary cleanup withstd::at_quick_exit. - Assuming the stack is initialized in freestanding environments: The kernel initializes the stack in hosted environments, but in bare-metal contexts, you must set
SPmanually before calling any C++ code.
Thread-Local Storage Initialization
Section titled “Thread-Local Storage Initialization”TLS variables have their own initialization lifecycle that interacts with the CRT startup sequence. The two categories have fundamentally different performance characteristics:
- Static TLS: Variables declared
thread_localwith constant initializers (zero-initialization or constant-expression initialization) are placed in the.tbssor.tdataELF sections. The dynamic linker (ld.so) allocates and initializes these when a new thread is created via the TLS block template. Access cost is a single segment register load (%fs:offseton x86-64) — effectively free after thread creation. - Dynamic TLS: Variables with non-constant initializers (including function-local
thread_localand types with non-trivial constructors) require a guard variable and an initialization function. On first access, the CRT checks the guard atomically, calls the initializer if needed, and registers a destructor via__cxa_thread_atexit. First access has significant overhead compared to static TLS.
#include <iostream>#include <string>
thread_local int tls_zero = 0;
thread_local std::string tls_dynamic = "hello";
void thread_entry() { std::cout << tls_dynamic << "\n";}On glibc, __cxa_thread_atexit registers per-thread destructors that run when the thread exits, Analogous to how atexit works for the main thread. If the main thread accesses dynamic TLS, the Destructors run during the normal termination sequence. On Windows, the mechanism is DllMain with DLL_THREAD_DETACH.
:::caution Dynamic TLS has a significant first-access penalty (guard variable check, potential Initialization, destructor registration). On hot paths, prefer static TLS (constant initialization) Or pass data explicitly via function parameters. :::
DSO Constructor and Destructor Ordering
Section titled “DSO Constructor and Destructor Ordering”When a program links against shared libraries (.so on Linux, .dll on Windows), each DSO has its Own .init_array and .fini_array. The dynamic linker coordinates initialization across all DSOs:
- The dynamic linker loads all DSOs in breadth-first dependency order.
- Constructors run in reverse dependency order: leaf DSOs initialize first, then their dependencies.
- Destructors run in dependency order: dependencies destroyed first, then leaf DSOs.
LD_DEBUG=init ./app# init: init=0x7f... libbar.so# init: init=0x7f... libfoo.so# init: init=0x7f... processA common pitfall is using a global object from one DSO during the construction of a global object in Another DSO — if the DSO ordering is wrong, the dependency may not yet be constructed. The “construct on first use” pattern (magic statics) mitigates this by deferring initialization to first Access rather than load time.
:::caution LD_PRELOAD interposes symbols but does not change .init_array ordering. A preloaded Library’s constructors still run in dependency order relative to other DSOs. If the preloaded Library depends on symbols from the main executable, those symbols may not yet be initialized. :::
The main Function Signature and Return Value
Section titled “The main Function Signature and Return Value”The C++ standard specifies two valid signatures for main [N4950 S6.6.1]:
int main() { }int main(int argc, char* argv[]) { }The return type of main must be int. If main terminates without a return statement, the CRT Implicitly returns 0 [N4950 S6.6.1 p5]. The return value is passed to std::exit()Which Translates it to the process exit status:
0indicates success.- Non-zero values are implementation-defined but conventionally indicate failure.
- Only the low 8 bits of the exit status are visible to the parent process (via
waitpidon Linux orGetExitCodeProcesson Windows). Values through are representable.
Per [N4950 S6.6.1 p3], the argv[0] element points to the name used to invoke the program (or an Empty string if the name is not available). The argv array is terminated by a null pointer, and argc equals the number of elements in argv excluding the null terminator.
Heap and Stack Initialization
Section titled “Heap and Stack Initialization”Stack Initialization
Section titled “Stack Initialization”In a hosted environment, the kernel allocates the main thread’s stack as part of process creation. The stack size is configurable and platform-dependent:
# Linux: view and set stack size (default is typically 8 MB)ulimit -sulimit -s 65536 # Set to 64 MBThe stack grows downward on x86/x86_64 and upward on ARM. The stack pointer is initialized by the Kernel to the top of the allocated stack region before jumping to _start. The CRT does not Explicitly zero the stack; stack memory contains whatever data was left by the kernel’s page Allocation mechanism ( zeroed pages from the page cache, but this is not guaranteed).
Heap Initialization
Section titled “Heap Initialization”The CRT initializes the heap allocator before any user code runs. The heap implementation varies by CRT:
| CRT | Heap Implementation | Backing System Calls |
|---|---|---|
| glibc | ptmalloc2 (malloc/free) | brk (small), mmap (large, > 128 KB) |
| musl | oom-safe malloc | mmap with MAP_ANONYMOUS |
| MSVC | Low-fragmentation heap | VirtualAlloc / HeapAlloc |
| jemalloc | Arena-based allocator | mmap / sbrk |
The heap is initialized during __libc_start_main (glibc) or mainCRTStartup (MSVC) before the .init_array processing. This ensures that global constructors can safely use new/malloc.
:::caution The heap is not thread-safe at initialization time. If a global constructor spawns a Thread that allocates memory, the thread may encounter a partially-initialized heap. In practice, This is safe on glibc and MSVC because the heap is fully initialized before .init_array Processing, but it is a theoretical concern on custom CRTs. :::
operator new and the CRT
Section titled “operator new and the CRT”C++ operator new ultimately calls malloc (or a CRT-specific allocator). The CRT provides the Default implementation, but it can be replaced by defining a custom operator new:
#include <cstdlib>#include <new>
void* operator new(std::size_t size) { void* ptr = std::malloc(size); if (!ptr) throw std::bad_alloc(); return ptr;}
void operator delete(void* ptr) noexcept { std::free(ptr);}Per [N4950 S17.7.3], replacing the global operator new is allowed but must be done consistently Across all TUs in the program. If one TU replaces operator new and another does not, the ODR is Violated and behavior is undefined. This is because operator new has external linkage and the Linker resolves it to a single definition.
Signal Handling and the CRT
Section titled “Signal Handling and the CRT”The CRT provides the interface between POSIX/OS signals and C++ exception handling. When a signal is Delivered (e.g., SIGSEGV``SIGFPE), the CRT’s signal handler may:
- Translate to C++ exception: Some CRTs translate signals like
SIGFPE(divide by zero) into C++ exceptions, allowingtry/catchto handle hardware faults. - Call the registered handler: If the program registered a handler via
std::signal()The CRT invokes it. - Default action: If no handler is registered, the default action ( termination) is taken.
On Itanium ABI platforms (Linux, macOS), the CRT installs a signal handler during startup that uses The .eh_frame section to unwind the stack when an exception is thrown. The unwinder (_Unwind_* Functions) is part of the CRT, not the compiler.
Environment Variables and the CRT
Section titled “Environment Variables and the CRT”The CRT makes the process environment available to main via the third parameter (on some Platforms) or via std::getenv():
#include <cstdlib>
int main(int argc, char* argv[]) { const char* path = std::getenv("PATH"); if (path) { // PATH is available }}On Linux, the environment variables are located on the stack above argv (see the stack layout Diagram in the startup sequence section). The CRT constructs envp from this data and passes it to main on platforms that support it.
:::note Per [N4950 S6.6.1], the main function signature with char* envp[] as a third parameter Is a common extension but not standard C++. Portable code should use std::getenv() instead.
See Also
Section titled “See Also”Summary
Section titled “Summary”This topic covers the essential concepts and techniques related to the c runtime (crt), including key principles and practical applications.
Key concepts include:
- core concepts and definitions
- key principles and frameworks
- practical applications
- common techniques and methods
- evaluation and critical analysis
A thorough understanding of these concepts, combined with regular practice and review, is essential for mastery of this topic.
Worked Examples
Section titled “Worked Examples”Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.
:::