The first instruction: where does a chip even begin?
From guide 1 you already accept the strange premise of bare-metal programming: there is no operating system underneath you, no loader that reads an ELF file and arranges things, no main() that some runtime politely calls. The microcontroller just powers on and starts executing instructions. So the very first question is brutally concrete: when voltage stabilizes, which byte in memory does the CPU fetch first? The chip cannot guess. The answer is fixed in silicon: the core reads a known, hardwired address and treats whatever it finds there as the starting point.
On a Cortex-M chip — the family this rung uses as its running example — that hardwired spot is the very bottom of the address map. At power-on the core reads two 32-bit words from the start of flash: the word at address 0x00000000 is the initial stack pointer, and the word at 0x00000004 is the reset vector — the address of the first instruction to run. So before a single line of your code executes, the hardware has already loaded the stack pointer and jumped to whatever address you stored in that second slot. That target is, by convention, a routine usually called Reset_Handler.
crt0: the tiny program that builds the C world
Here is the part that surprises people coming from hosted programming. When you write a normal program on Linux, you take for granted that by the time main() runs, your global variables already hold their initial values, your zero-initialized globals are actually zero, and the stack is ready. On a desktop, the C runtime startup code — historically the object file called crt0 — does that work, invisibly, before main(). On bare metal there is nobody to do it for you. So you (or your toolchain's tiny startup code) must hand-build the entire C runtime environment, and the reset handler is exactly where that happens.
Why is there even work to do? Because of where things live. Your program sits in flash, which is non-volatile and read-only at run time; your variables live in RAM, which is fast, writable, and completely uninitialized at power-on — it holds garbage. A global like `int counter = 7;` has a problem: the value 7 has to come from somewhere persistent (flash), but counter itself must end up in writable RAM so the program can change it. The compiler can store the initial image of all such variables in flash, but it cannot place them in RAM, because flash is the only thing that survives a power cut. Bridging that gap is the startup code's first real job.
So the reset handler does three concrete chores before it dares call main(). First it copies the initial values of all initialized globals — the .data region — from their flash image into their final home in RAM, word by word. Second it zeroes the .bss region, which holds every global that the C standard says starts at zero (like `int total;` with no initializer); the standard promises these are zero, and on bare metal that promise is true only because the startup code makes it true by writing zeros. Third it may set up the stack and any library state. Only then does it call main(). Skip the .data copy and your counter reads garbage; skip the .bss zeroing and a variable you assumed was 0 is whatever junk RAM held at power-on — a bug that hides until that one uninitialized read bites you.
The memory map: who decides where flash and RAM are?
The startup code keeps saying "copy from flash to RAM," which raises an obvious question: how does it know the addresses of flash and RAM, of the .data image, of the .bss region? On a hosted system those addresses are chosen by the OS at load time. On bare metal they are not negotiable — they are physical facts about your specific chip, written in its datasheet. This is the MCU memory map: a single flat 32-bit address space carved into fixed ranges, where flash might occupy 0x08000000 onward, RAM might start at 0x20000000, and the peripheral registers live at their own ranges higher up. Different ranges of the same address space reach different physical hardware.
Picture it as a tall ladder of addresses. Down at the bottom sits the vector table; flash begins around 0x08000000 and holds, in order, the vector table (.isr_vector), your code (.text), your read-only const data (.rodata), and the flash image of .data; RAM begins around 0x20000000 and holds the live .data, then .bss, then the heap growing upward while the stack grows downward from the top of RAM; and high up around 0x40000000 sit the peripheral registers you will read and write in guide 4. The crucial subtlety is that .data appears twice: a permanent image in flash and a live copy in RAM. The startup copy you just met is precisely the bridge between those two homes.
Sketching this map also exposes a constraint you cannot wish away: flash and RAM are finite and small. A modest chip might have 256 KiB of flash and 64 KiB of RAM — not megabytes, not gigabytes. Your code plus all your const data plus the .data image must fit in flash; your stack plus heap plus all live variables must fit in RAM, at the same time, with no virtual memory to paper over a shortfall and no OS to kill you politely if you overflow. Run off the end of RAM and you do not get a segfault — there is nothing to catch it — you silently corrupt whatever sat next door. So something must lay each piece into its exact range and verify it fits. That something is the linker script.
The linker script: a floor plan for memory
You met the linker in the linkers-and-loaders rung as the tool that resolves symbols and patches addresses. On a hosted system it used a built-in default layout you never had to think about. On bare metal you must hand it an explicit floor plan, and that plan is the linker script (typically a .ld file). It does two things. First, MEMORY: it declares the actual regions of your chip — names, start addresses, and sizes — straight from the datasheet, e.g. a FLASH region at 0x08000000 of length 256K and a RAM region at 0x20000000 of length 64K. Second, SECTIONS: it says which output sections go into which region, and in what order.
The linker script is also where the startup code and the layout shake hands, and this is the satisfying part. Remember the reset handler needs to know exactly where the .data image starts in flash, where .data must land in RAM, and where .bss begins and ends — but those addresses are outputs of the layout, not things the C programmer can know in advance. The linker script solves this by defining symbols at the relevant boundaries: it places a symbol like _sdata at the start of RAM .data, _edata at its end, _sidata at the .data image in flash, and _sbss / _ebss around the .bss region. The startup code then refers to those symbols by name, and the linker fills in their real addresses. The script draws the floor plan; the startup code reads the dimensions off it.
/* sketch of an embedded linker script (.ld) */
MEMORY {
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 256K
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
}
SECTIONS {
.isr_vector : { *(.isr_vector) } > FLASH /* vector table first */
.text : { *(.text*) *(.rodata*) } > FLASH
_sidata = LOADADDR(.data); /* .data image, in FLASH */
.data : {
_sdata = .; /* start of .data in RAM */
*(.data*)
_edata = .; /* end of .data in RAM */
} > RAM AT> FLASH /* lives in RAM, stored in FLASH */
.bss : {
_sbss = .;
*(.bss*) *(COMMON)
_ebss = .;
} > RAM
}Putting it together: power-on to main(), step by step
Now the whole chain is in view, and it is worth walking once end to end, because the pieces only make sense as a relay. The linker script decided the addresses; the reset vector points at the startup code; the startup code uses the linker-defined symbols to build the C world; and only then does your main() run in an environment that finally matches the assumptions C makes everywhere else. Here is the relay, baton by baton.
- Power on. The core reads two words from the bottom of the map: the initial stack pointer (0x00000000) and the reset vector (0x00000004). It loads the stack pointer and jumps to the reset vector's target — Reset_Handler. No code of yours has run yet.
- Copy .data. The handler reads _sidata, _sdata, _edata (all addresses the linker script defined) and copies the initialized-globals image from flash into RAM, word by word, so a global like counter = 7 actually reads 7.
- Zero .bss. Using _sbss and _ebss, the handler writes zeros across the whole .bss region, making the C standard's "uninitialized statics start at zero" promise actually true on this hardware.
- Optional setup. Initialize any C library state, set the clock, and (on some toolchains) run the constructors for C++ global objects. The environment is now fully built.
- Call main(). Finally the handler calls your main(). On bare metal main() must never return — there is no shell to return to — so its last act is typically an infinite loop, and if it ever does fall through, the startup code traps in a loop too.