What the Linux process stack actually looks like the moment your program starts.

Take a situation where you run a program from your command line like this
./program arg1 arg2we've learnt that main() function is the starting point to a program but that's not actually true.
in a programmer's level main() is the starting point but to the OS / machine level it's a symbol named _start
This is how the universal startup chain looks like, (every linux program roughly follows this pattern)
kernel ↓ELF entry point (_start) ↓language/runtime initialization ↓main()To understand this more we gotta understand what this ELF and ELF entrypoint is,
ELF = Executable and Linkable Format
it is the file format Linux uses for binaries. a compiled binary like a.out is not just a raw machine code it's a structured binary file following the ELF format.
ELF is a container that holds everything needed to run the program it contains things like machine code, program entry point, library dependencies, and many more...
One of the most important fields inside ELF header is Entry point address it tells the kernel to start the execution here.
Let's take an example of a go program to understand this better
When you compile a go program first it gives out a .o file for each package with complete machine instructions, relocations marking addresses that need fixing.
Then go linker to take that object files and links it with go runtime, resolves function references, creates the ELF file and very importantly Sets the Entry point.
Complete flow of a go program
kernel ↓ELF entry point (_start) ↓runtime.rt0_go ↓Go runtime initialization ↓create first goroutine ↓call main.main()notice that main.main() that is the reason we're required to start with main() function
runtime.rt0_go is the go runtime startup function. it's written in assembly
So now you understand what a ELF entrypoint is and how a program initially starts(roughly)
but there's more
The process stack
when linux starts a program it builds a stack for that process and places the arguments there.
program simply reads what the OS already placed in memory
Top of stack ↓[ new data ][ new data ][ new data ]The stack grows downward in most architectures (downward means the stack pointer goes to a smaller address, why? let's not go there as of now)
this stack is used to store
- function calls
- local variables
- return addresses
- arguments
assume i run a program like this,
./program karthik anush aadhavanThe shell runs,
execve("./program", ["./program","karthik","anush","aadhavan"], envp)When execve() runs, the kernel does these things:
- Loads the ELF binary
- Maps program segments (code, data, etc.)
- Allocates a new stack
- Pre-populates the stack with startup data
then the kernel sets the stack pointer to the top of that prepared stack and then the CPU jumps the the ELF entry point (_start)
Initial process stack layout

the runtime reads entries until it reaches AT_NULL
argv is a pointer to the first pointer of the array which stores the arguements
envp stores the env vars
auxv is a small table contains system information like page size, cpu capabilities, location of program headers, user ID's for example (AT_PAGESZ, AT_UID, AT_RANDOM, AT_EXECFN, AT_PHDR, AT_PLATFORM) these are just key-value pairs giving runtime information
As i said earlier kernel has already created the entire process stack before your program even starts.
next _start begins execution with the stack already containing argc, argv, envp, auxv all placed by the kernel
_start is a assembly code which looks like this
_start: xor rbp, rbp mov rdi, rsp ; save stack pointer andq $~15, rsp ; align stack call runtime·rt0_go ; jump into Go's heavy bootstrapthen the runtime.rt0_go does the bootstrapping like
- goroutine scheduler
- garbage collector
- Go memory allocator
- thread management
- signal handlers
- runtime structures
for other languages _start calls their respective own assembly functions which are provided by the language's runtime and placed by the linker

look at the top of the image: _rt0_amd64_linux is the GO's assembly entry point. it contains the _start function for go programs
it's first job is to pop argc off the stack,
calculate where argv starts, calculate where envp starts
and store these in global variables so that when you call os.Args in your Go code, the runtime knows exactly which memory address to read.
why the stack is designed this way?
The kernel must start any program, regardless of the language
it doesn't know a program is c++, go , rust etc..
so Linux follows this universal ABI contract which says At program start, the stack will contain argc, argv, envp and auxillary vectors
every runtime interprets it.
You can technically write a program without main() function by writing a custom _start but you won't get the bootstrapping benefits the runtime does for you.
Related
Continue reading
All writings