The C Runtime?

I'm a dedicated software engineer with a passion for bringing hardware and software together to create robust and efficient solutions. I thrive on optimizing performance, managing power consumption, and ensuring the reliability of devices from concept to deployment.
🦊
I always thought that when we compile a C code, it ‘just works’. No other fancy code runs to make our code function, like in the case for Java and Python. But turns out, it’s not exactly true. There are some work that happens behind the scenes when we run a binary written in C.
This code is called the C Runtime (CRT), but don’t confuse this with the Java Runtime Environment (JRE) or the Python Runtime Environment (PRE). Unlike Java and Python code, C code directly runs on the bare-metal, but there does exist some work that needs to be done before, while and after our code runs. This is handled by the C Runtime. Let’s dive a bit deeper…
The CRT consists of four main components:
Startup Sequence: Before our
main()function even runs, the CRT performs initialization. It sets up the stack, initializes global variables, and gathers command-line arguments.Memory Management: It provides the logic for functions like
malloc()andfree(), managing the "Heap" memory, so our program can request space dynamically.Standard Library (libc): This is the bulk of the CRT. It includes the math functions, string manipulation tools, and input/output handlers (like
printf()andscanf()).The Stack and Heap: The CRT manages the memory layout. The Stack handles local variables and function calls, while the Heap handles dynamic data.
Let’s dive a little bit deeper into these four tasks of the CRT.
The Startup Sequence (The "C Bootstrap")
When we click "Run," the Operating System doesn't jump straight to our int main(). It jumps to an entry point provided by the CRT (often called _start or mainCRTStartup).
Setting the Environment: The CRT retrieves the Environment Variables (like PATH) and the Command Line Arguments from the OS. It parses them into the
argcandargvparameters we see inmain().Initialization of Statics: It clears the BSS segment (setting un-initialized global variables to zero) and copies initial values into the Data segment for variables like
int x = 10;.Constructors: In C++ (or C with specific compiler extensions), the CRT calls "global constructors" before main starts.
The Handshake: Finally, it calls
main(). Whenmain()finishes, the CRT takes the return value (e.g., 0) and passes it back to the OS via anexit()system call.
Memory Management (malloc and free)
The Operating System is strict; it only gives memory to programs in large "pages" (usually 4KB). If we only need 20 bytes, asking the OS directly is incredibly slow and wasteful.
The Middleman: The CRT maintains a "pool" of memory. When we call
malloc(20), the CRT looks at its pool, carves out 20 bytes, marks them as "in use," and gives you the pointer.Bookkeeping: The CRT keeps a hidden header just before our pointer that stores the size of the block. This is how
free()knows exactly how much memory to release without us telling it the size.Heap Expansion: If the CRT runs out of memory in its pool, it issues a system call (like
sbrkon Linux orVirtualAllocon Windows) to ask the OS for another big chunk of RAM.
The Stack vs. The Heap
These are the two primary ways a program uses RAM, and the CRT (along with the CPU) manages the boundary between them.
The Stack: Every time we call a function, the CRT pushes a "Frame" onto the stack. This frame holds our local variables and the "return address" (where to go when the function ends).
The Heap: The "pile" of memory used for data that needs to live a long time or is too big for the stack (like a high-resolution image).
The Standard Library (libc)
This is the "toolbox" of C. It is a collection of pre-compiled code that performs common tasks that would be hard to write from scratch.
System Call Wrappers: Functions like
printf()orfopen()are wrappers. They take our high-level request, format it, and then perform a System Call to ask the OS kernel to actually write pixels to a screen or bits to a disk.Portability Layer:
libcis the reason our code can run on both Windows and Linux. On Windows,printf()might callWriteFile, while on Linux, it callswrite. We don't have to care; the CRT handles the translation.Utility Functions: These are pure logic. Things like
sqrt(),strlen(), orsort(). They don't talk to the OS; they just provide optimized algorithms so we don't have to reinvent the wheel.
The Code
Curious as to how these are managed? Checkout the glibc repository to see these components in action. The _start code is under sysdeps/x86_64/start.S. This is in Assembly to facilitate the required low-level work. The memory allocation logic is under malloc/malloc.c. This is quite a complicated bit of code.
Conclusion
So what did we learn? Even C is not about compile and run. There’s a lot more happening under the hood than what we’re doing ourselves. It’s quite interesting how far these tools have come. So next time you write a new main(), think of all the work happening under the hood that keeps your code running correctly.





