Your Program Does Not Start at main
por Frank de Alcantara em 10/07/2026
Este artigo também está disponível em português.
There is a small pedagogical lie that we teach, repeat, put on slides, and then pretend it was not us: “the program starts at the main() function”. It works well in the first class. It avoids trauma. It gives the student a place to put her first printf, or her first std::cout, and get on with life.
This article was translated into English using the Claude AI assistant, running the Fable 5 model, on July 10, 2026.
Ah! How good the simple life is!
The problem is that the sentence is false. Useful, but false. Like almost every didactic simplification that survives too long, it goes from ladder to crutch. At some point, we need to get it out of the way.
I decided to write about this now out of guilt. I just saw a post, by a former student, repeating it as if it were one of the commandments handed down by God, or an absolute truth of the universe. My conscience ached. He never took imperative programming with me, nor operating systems. Maybe the fault is not even mine. But I felt a disturbance in the force.
In C and C++, main() is the entry point of your application code in a hosted environment. But it is not, in general, the first address executed by the process. Before main() exists as a concrete experience, the operating system, the program loader, the dynamic linker, and the runtime library have already staged a small technical conspiracy behind the scenes.
When you type:
int main() {
return 0;
}
the compiler does not look at that and say: “perfect, let us start exactly here, at the first byte of this function”. That would be beautiful. It would also be naive. The real world rarely misses an opportunity to place one more layer between you and the truth. The hard truth.
The Convenient Lie
In an introductory class, saying that “the program starts at main()” is acceptable because, from the beginner programmer’s point of view, that is what matters. The first code they write is usually there. The logical flow they control starts there. The debugger usually stops there. The exercises start there. It has every appearance of a starting point.
But that sentence hides an important distinction:
main()is the conventional beginning of the application’s logic, not the absolute beginning of the process’s execution.
The process was born earlier. The operating system has already created a virtual address space. The executable has already been mapped into memory. The initial stack has already been prepared. Shared libraries may have been loaded. Addresses may have been resolved. Global variables may have been initialized. In C++, constructors of global objects may have run.
In other words: by the time main() starts, plenty of people have already moved the furniture and the house is already tidy.
What the Compiler Actually Delivers
When we compile a C or C++ program, we are not generating only the instructions corresponding to the functions we wrote. The final result goes through a process involving compiler, assembler, linker, libraries, and auxiliary startup files.
On Unix-like systems, names like crt1.o, crti.o, crtn.o and close relatives show up. The crt prefix comes from C Runtime. On Windows, we find names like mainCRTStartup, WinMainCRTStartup and other variations, depending on the subsystem, the compiler, and the configuration.
These components do the unglamorous work that must happen before your function gets called. And, like all unglamorous work, it only receives attention when something breaks.
The runtime library prepares the environment the language expects. It organizes command-line arguments, initializes parts of the standard library, registers finalization functions, prepares exception support, initializes thread-related structures, runs constructors of global objects in C++ and, only then, calls main().
This explains why a minimal C or C++ program is not so minimal after all. You wrote three lines, but you are hanging from a small infrastructure. The compiler merely had the kindness of not throwing all of it in your face at the very first “Hello, world”. Small mercies still exist. They are few, well disguised, but they exist.
The Sequence on a Typical System
Let us consider an ordinary program, compiled for Linux or Windows, using standard libraries and the compiler’s normal runtime.
1. The Operating System Loads the Executable
On Linux, the executable is usually in the ELF format (Executable and Linkable Format). On Windows, in the PE format (Portable Executable). The kernel reads the file’s metadata, creates the process, sets up the virtual address space, and maps the necessary regions into memory.
These regions include, for example:
- the code segment, normally read-only and executable;
- initialized data;
- zeroed data, such as uninitialized global variables;
- the initial stack;
- auxiliary information used by the runtime and by the loader.
It is common to say that the operating system “creates the stack and the heap”. The initial stack, yes, comes in as an essential part of preparing the process. The heap, however, deserves care: on many modern systems it does not appear as one big drawer, ready and full. What exists is an address space and mechanisms to request memory as execution advances, using calls such as brk, mmap, VirtualAlloc, or equivalent layers hidden behind malloc, new, and company.
Think of a complicated thing. This heap business.
The correct sentence is less friendly, but more truthful: the operating system prepares the process’s memory environment and the runtime takes over managing dynamic allocations on top of the mechanisms the system provides.
Imagine hearing that in the first class of imperative programming.
2. The Loader and the Dynamic Linker Enter the Scene
If the program uses shared libraries, such as .so on Linux or .dll on Windows, someone needs to load them, map their pages into memory, and resolve the necessary symbols.
On Linux, when the executable is dynamically linked, the kernel may initially transfer control to the interpreter indicated in the ELF itself, normally something like ld-linux. This dynamic linker loads dependencies, resolves relocations, and paves the way for the executable’s code.
On Windows, the system loader maps the PE file, loads the required DLLs, applies relocations when necessary, and calls initialization routines associated with library loading.
This stage is one of those parts of computing that seem invisible until you get a library version wrong, mix incompatible ABIs, or discover that “it works on my machine” is not a software distribution architecture.
3. The Real Entry Point Is Executed
The executable has an entry address recorded in its metadata. This address normally does not point to main(). It points to an initialization routine provided by the runtime.
On Linux with glibc, we commonly see a function called _start. On Windows, names like mainCRTStartup or WinMainCRTStartup appear frequently. The exact name depends on the compiler, the C library, the link-editing setup (that one came straight from the voices in my head), and the type of application.
This real entry function is not decoration. It is the bridge between the raw world of the newly created process and the comfortable world in which main(int argc, char** argv) seems to simply exist.
A simplified view would be:
Operating system
-> loader / dynamic linker
-> real entry point (_start, mainCRTStartup, ...)
-> runtime initialization
-> main(argc, argv)
-> runtime finalization
-> process termination
No, I am not joking, this is a simplified version. It is not, however, a universal, byte-by-byte sequence for all systems, compilers, and formats. It is a good mental model for ordinary C/C++ programs in modern environments.
What Runs Before main()
Before main() is called, the runtime needs to build the illusion that the program starts in a civilized environment.
Among the common tasks are:
- preparing
argc,argvand, when available, environment variables; - initializing internal structures of the C library;
- setting up support for
stdin,stdout, andstderr; - preparing the mechanisms used by
malloc,free,new, anddelete; - initializing exception support and stack unwinding information;
- setting up thread-local data, when applicable;
- initializing parts of the C++ standard library;
- running constructors of global and static objects with dynamic initialization;
- registering functions that must run at the end, such as those passed to
atexit().
Here lives an important gotcha: in C++, there is programmer code that can run before main(). Constructors of global objects are the classic example. The sentence “your code starts at main()” becomes, therefore, technically suspect. Part of your code may have been executed before, perhaps with side effects, perhaps opening files, perhaps writing logs, perhaps creating that bug that only shows up on the customer’s machine at 5:58 pm on a Friday. The universe has a sense of timing and no mercy.
Repeat after me three times: never deploy on a Friday.
An Example that Gives Away the Trick
The example below is simple and sufficient to dismantle the innocent version of the story.
#include <iostream>
class MyClass {
public:
MyClass() {
std::cout << "Global object constructor ran before main()\n";
}
~MyClass() {
std::cout << "Global object destructor ran after main()\n";
}
};
MyClass global_object;
int main() {
std::cout << "Now we are inside main()\n";
return 0;
}
I tested it only on Visual Studio Community Edition; the code runs and the expected output is:
Global object constructor ran before main()
Now we are inside main()
Global object destructor ran after main()
The constructor ran before main(). The destructor ran after. The checkout opened, closed, and there were people working before and after it.
Whoever thinks the supermarket begins when the cashier smiles at the banana is confusing shopping experience with supply chain.
This analogy is childish, but it works. main() is the checkout. The complete program involves farm, truck, warehouse, invoice, shelf, scale, manager, and a surprising amount of operational bureaucracy. The banana was not born on the shelf.
What Happens After main()
Another frequently forgotten part is the end of the program. When main() returns, the process does not always evaporate into thin air. There is an organized finalization process.
In a typical C++ program, after main() returns:
- the return value of
main()is treated as the exit code; - functions registered with
atexit()are called; - destructors of static and global objects are executed;
- output buffers may be flushed;
- internal runtime resources are finalized;
- the operating system receives the request to terminate the process.
In C++, this is particularly relevant because destructors have semantics. If a global object opened a file, kept a log, held an external resource, or represented important state, its finalization happens outside of main().
Naturally, there are ways to escape this path. Calls like _Exit, std::quick_exit, abort, or abrupt terminations can skip parts of the normal cleanup. The C/C++ world offers several ways to leave through the window. Some are useful. Others are just invitations to self-inflicted debugging.
How to See the Entry Point
If you want to verify that main() is not the real entry point, you can inspect the executable.
On Linux, after compiling:
g++ example.cpp -o example
readelf -h ./example
You will see a field that looks like:
Entry point address: 0x...
That address is the initial point recorded in the ELF. To investigate the corresponding symbol, tools like objdump, nm, and readelf -s help:
objdump -d ./example | less
nm -n ./example | head
In many cases, you will find _start before reaching main.
On Windows, tools like dumpbin or equivalent utilities allow you to examine the PE header:
dumpbin /headers example.exe
The executable will have an entry address, and that address will normally lead to the CRT’s startup routine, not directly to main.
Linux, Windows, and the Difference Between Idea and Implementation
It is tempting to memorize a rigid sequence:
- kernel loads;
- dynamic linker resolves;
_startruns;- runtime calls
main; - the end.
As a mental model, great. As a universal description, dangerous.
On Linux, the story depends on whether the program is static or dynamic, on the libc used, on the linking options, and on the architecture. glibc, musl, and other libraries do not need to organize everything exactly the same way, although the general idea is similar.
On Windows, the entry also varies with the type of application. Console programs usually use main or wmain; classic graphical applications may use WinMain or wWinMain; underneath all that, the CRT still needs to bridge the executable’s entry point and the function the programmer expects.
In both worlds, the central message remains:
The function you write is generally not the first function executed.
main() is a convention of the language and the runtime in a hosted environment. The process, however, starts at an entry address defined by the executable format and by the system.
When This Does Not Apply
There are environments where the story changes considerably.
In embedded systems, firmware, bootloaders, and kernels, we may be in a freestanding environment. In that case, the language does not promise the same infrastructure as a complete operating system. There may be no argc, argv, stdin, conventional heap, files, processes, or ready-made runtime.
On a microcontroller, the real entry point may live in a vector table. The processor resets, loads the initial stack pointer from a fixed address, jumps to a Reset_Handler, copies initialized data from flash to RAM, zeroes the .bss section, configures the clock, perhaps initializes minimal parts of the runtime, and only then calls main(). Or does not call it at all. Depends on the project.
In pure Assembly, you can define the entry symbol directly. On Linux, for example, a minimal program can declare _start and make a system call to exit, without going through libc:
global _start
section .text
_start:
mov rax, 60 ; syscall: exit
xor rdi, rdi ; status 0
syscall
There is no main() here because nobody asked for one. The operating system enters at the address defined by the executable, and from there on the responsibility is yours. Total freedom, including the freedom to fail without a safety net. A classic.
It is also possible, in C/C++, to use options like -nostartfiles or custom linker scripts to replace the default startup routine. This shows up in kernels, custom runtimes, embedded systems, and educational experiments. It is excellent for learning. It is also excellent for discovering how many things the runtime was doing for you while you complained about it.
Why This Matters
At first glance, this looks like trivia for people who have enjoyed taking clocks apart since childhood. But understanding what happens before and after main() has practical consequences.
It helps you understand static initialization errors in C++. It helps you debug problems with shared libraries. It helps you understand why a program fails before reaching the first breakpoint inside main(). It helps you write firmware. It helps you understand ABI, linking, loaders, symbols, relocations, and system calls.
It helps, above all, to realize that “compiling” is not magically turning text into a program, but fitting several pieces into an executable contract.
It also helps you maintain a healthy humility. Not too much, so it does not look like provocation, and not too little, so it does not look like pretense. Compilers can smell your fear.
The main() function looks sovereign because it appears in the book, in the course, and in the exercise. But it is more like a shift manager: important, visible, necessary, yet surrounded by people who arrived earlier, prepared the environment, and will stay after to turn off the lights.
So, yes: to teach programming, we can say the program starts at main(). It is an acceptable approximation.
Technically, a typical C/C++ program starts earlier. The operating system creates the process, the loader prepares the executable, the runtime initializes the environment, global constructors may run, and only then is your main() called. When it finishes, there is still finalization, destructors, atexit(), and cleanup.
main() is the beginning of the narrative the beginner programmer can see. It is not the beginning of the process. This distinction separates those who merely write code from those who begin to understand what the computer is doing while it pretends to obey.
(Updated: )