Compile, link, locate, flash, reset — what each stage actually produces, and where it lives on the chip.[1]

  1. 01 · SourceYour .c and .h files, plus the vendor’s headers and startup code.
  2. 02 · CompileOne object file per source file, with addresses still unresolved.
  3. 03 · LinkA single image, symbols resolved, laid out by the linker script.
  4. 04 · LocateSections placed at real addresses — code in flash, variables in RAM, constants where you said.
  5. 05 · ConvertA hex or binary file: the bytes, and where each byte goes.
  6. 06 · FlashThose bytes written into non-volatile memory and verified.
  7. 07 · ResetThe core released, the vector table read, and your first instruction executed.

§ 01From Text to Object Files

You write C. The chip runs binary. The distance between those two things is longer than most tutorials let on, and understanding each hop tells you why the tools fail where they do.

an oscilloscope showing a waveform in a dark lab Enlarge ⤢
Fig. 2 — The end of the journey, seen from outside the part.

The compiler is the first stage. It takes one translation unit — typically one .c file plus everything its headers pull in — and produces an object file, usually with a .o or .obj extension. An object file is not executable. It is binary machine code for your target architecture, but full of holes: every call to a function defined in another file, every reference to a global variable declared elsewhere, is left as a placeholder. The compiler has no idea where those things will live. It writes the instruction, marks the location as unresolved, and moves on. It also writes a symbol table into the object file listing what it defines and what it needs from outside.

The assembler does the same job for .s or .asm source — and most compilers pass through an assembler internally anyway. Hand-written assembly for a startup file or an ISR is just another object file by the time the next stage sees it.[2]

§ 02Linking and Locating

The linker collects every object file and every needed library module, resolves all those placeholder references, and combines the result into a single binary. A static library is simply an archive of pre-compiled object files; the linker pulls out only the modules it actually needs, which is why a large library does not automatically bloat your image.

By itself, that output is still abstract — addresses are relative, not absolute. The locator, often folded into the linker and controlled by a linker script, assigns real addresses. It reads the memory map you specify — flash starts at this address, RAM starts at that one, here is a section of tightly-coupled memory for your ISR table — and places each section accordingly. Code (.text) lands in flash. Initialised global variables have their initial values baked into flash too but are listed in a separate section (.data), along with relocation records so startup code can copy them into RAM before main() runs. Zero-initialised globals (.bss) take no flash at all; the linker just records how much RAM to clear. The linker script is the authoritative document for where everything lives, and getting it wrong produces firmware that loads without complaint then behaves bizarrely at runtime.

The output of a successful link is typically an ELF file — Executable and Linkable Format. It contains the binary, the symbol table, debug information, and the section layout. It is the master artefact of the build. From it you derive a HEX file (Intel HEX or Motorola S-record) or a raw binary (.bin), both of which strip the metadata and leave only the bytes to be written to flash.

A tool like objdump or readelf can inspect an ELF file and show you exactly which sections went where and how large each is. Running that check before you even flash the device is good practice — it confirms the map matches your chip's memory and shows you how close to the edge you are sitting.

§ 03Flashing and Reset

The programmer writes the HEX or binary into the chip's non-volatile memory — typically internal flash — over ISP, JTAG, SWD or a bootloader. Writing itself is only half the job; most programmers verify by reading back and comparing. A verify pass that fails often points to a write voltage problem, an incorrect memory-map setting in the programmer, or a timing issue with the erase cycle.

After a successful write, the chip must be released from reset cleanly. On most Arm Cortex-M devices, for example, the processor reads the initial stack pointer value from address 0x00000000 and the reset handler address from 0x00000004 — the first two words of the vector table. Get the linker script wrong and those words contain garbage; the processor jumps somewhere undefined and either hangs or faults immediately. This is one of the most common reasons a newly-flashed part refuses to start, and it looks baffling until you know to check the vector table in objdump output before you blame the hardware.

Once the reset handler runs, it is typically the startup code — often a file called startup.s or generated by your toolchain — that copies .data from flash to RAM, zeroes .bss, initialises any needed hardware clocks, and then calls main(). None of that happens by itself. It happens because the linker put the right bytes at the right addresses and the startup code knows, via symbols the linker exported, where each region begins and ends.

The whole chain — compile, assemble, link, locate, flash, reset, startup — is deterministic. Every step has a defined input and a defined output. When something goes wrong, you can step back through the chain and inspect the artefact at each stage rather than guessing. That is the mental model that makes the build system a tool rather than a mystery.

Notes

  1. The linker script is the least-read file in most projects and the one that explains the most. ↩
  2. Verify after writing. A programmer that cannot read back is a programmer you are trusting on faith. ↩