← Back to Writings

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

LinuxOS InternalsC/C++
8 min read11 Mar '26
cover
cover

Take a situation where you run a program from your command line like this

./program arg1 arg2

we'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

plaintext
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

plaintext
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,

plaintext
./program karthik anush aadhavan

The shell runs,

plaintext
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

Initial process stack layout
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

x86asm
_start:    xor rbp, rbp    mov rdi, rsp        ; save stack pointer    andq $~15, rsp      ; align stack    call runtime·rt0_go ; jump into Go's heavy bootstrap

then 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

Go assembly entry point `_rt0_amd64_linux`
Go assembly entry point `_rt0_amd64_linux`

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