RayBench EmbeddedInteractive engineering labs
FOUNDATIONS

From Power-On to main()

What runs before main(): the Cortex-M reset sequence that gives C the machine it assumes, the linker script that decides where every section lives and why .data has two addresses, the four build stages and which one your error came from, and the order to work through a debug probe that will not connect.

Reviewed 2026-08-225,418 words

The mental model

"Getting connected" is not one link, it's a chain: firmware -> UART peripheral -> physical TX/RX pins -> USB-to-UART bridge chip -> USB cable -> PC operating system driver -> virtual COM port -> terminal application. Each link can fail independently, and knowing the chain turns "it doesn't work" into "which link is broken."

Almost every newcomer's "my board isn't showing up" problem traces to exactly one link in this chain, and the fix is different for each one - a bad cable needs a new cable, a missing driver needs a driver, a busy port needs a different port selection. Skipping this mental model wastes hours re-flashing perfectly good firmware.

Core rules

The MCU never speaks USB directly

On cheap dev boards, a separate bridge chip (CH340/FTDI/CP2102) does the USB framing; the MCU only ever drives a simple two-wire UART.

A USB cable needs its data pins wired, not just power

Many cheap cables only connect VBUS/GND. If a board has literally never once enumerated, suspect the cable before the firmware.

Every bridge chip needs a matching OS driver

An unrecognized or generic-looking device in Device Manager almost always means the wrong driver for that specific chip, not a hardware fault.

A COM port is a software assignment, not a fact about the board

The same board can appear as a different COM port number on a different USB port or after replugging - always re-check which port is actually assigned before debugging further.

Baud rate must match on both ends

The terminal's baud setting and the firmware's UART init value are two independent numbers that only work together if they're equal.

Workflow

  1. Confirm the USB cable actually carries data, not just charge.
  2. Check whether the board shows up in the OS device list at all (Device Manager / lsusb / System Report).
  3. If unrecognized, identify the bridge chip and install its matching driver.
  4. Note which COM port the board is actually assigned.
  5. Match the terminal's baud rate to the firmware's UART configuration.
  6. Only then treat continued silence as a firmware or wiring problem.

Worked example

A UART peripheral's baud config has to match the terminal
// Firmware side
UART_Init(&huart1, 115200, UART_8N1);

// If a serial terminal is opened at 9600 baud instead of 115200,
// every byte decodes as garbage - even though transmission itself
// is working perfectly fine on the wire.

Baud rate is not negotiated automatically over UART the way it can be over some other buses - both sides must already agree on it before any byte makes sense.

Vocabulary

UART
Universal Asynchronous Receiver/Transmitter - the simple two-wire (TX/RX) serial peripheral almost every MCU has.
Bridge chip
A separate chip (CH340, FTDI FT232, CP2102) that translates UART signaling into USB packets and back.
VCP
Virtual COM Port - the OS abstraction that makes a USB-serial device look like an old-style serial port to software.
Enumerate
The process by which a USB device identifies itself to the host OS when first plugged in.
Baud rate
The number of bits per second on a UART line - must be configured identically on both ends.

The mental model

The compiler creates pieces; the linker builds the final memory image. The linker script is the contract between the binary and the MCU memory map, while the build system describes how source changes produce that binary.

Startup, vector placement, initialized data, zeroed data, bootloaders, RAM functions, DMA regions, and image size all depend on link-time decisions. A project can compile perfectly and still be impossible for the target to boot.

Core rules

VMA and LMA can differ

Initialized data is stored in flash at its load address and copied to its run address in RAM during startup.

Linker symbols are addresses

Symbols such as _sdata and _ebss normally mark boundaries. C startup code uses their addresses rather than reading objects stored there.

The vector table must be retained

Hardware references are invisible to section garbage collection. KEEP the vector input section and place it at the required address.

The map file is evidence

Use it to answer which object owns bytes, why a symbol was selected, where a section landed, and how much padding was inserted.

Dependencies must be generated

A correct Make build tracks included headers, not only .c timestamps. Compiler-generated .d files prevent stale object files.

Workflow

  1. Verify MEMORY origins and lengths against the exact part number.
  2. Trace each output section to its region and input patterns.
  3. Check startup copy/zero loops against linker boundaries.
  4. Generate and inspect the map plus size report.
  5. Test clean, incremental, parallel, and header-change builds.

Worked example

The essential placement pattern
MEMORY {
  FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 512K
  RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 128K
}

SECTIONS {
  .isr_vector : { KEEP(*(.isr_vector)) } > FLASH
  .text : { *(.text*) *(.rodata*) } > FLASH
  .data : { _sdata = .; *(.data*) _edata = .; } > RAM AT> FLASH
  _sidata = LOADADDR(.data);
  .bss : { _sbss = .; *(.bss*) *(COMMON) _ebss = .; } > RAM
}

Code and constants execute from flash. .data runs from RAM but has initial bytes stored after the flash image, and .bss reserves RAM that startup clears to zero.

Vocabulary

object file
Compiled code and data plus symbols and relocation records, not yet placed at final addresses.
section
A named group of code or data with similar placement and access needs.
VMA
Virtual memory address: where a section runs or is accessed.
LMA
Load memory address: where a section's initial bytes are stored in the image.
relocation
A link-time request to fill an address-dependent value after placement.

What runs before main()

main() is not the entry point. Between the processor leaving reset and your first line of C, a startup sequence has to put the machine into the state the C language assumes: a valid stack pointer, initialised global variables, zeroed statics, and a vector table the hardware can find. On a desktop the C library does this invisibly; on a microcontroller it is code in your own project that you can read, and understanding it is the difference between debugging a startup failure and guessing at one.

How it is built

  • On Cortex-M the processor loads the initial stack pointer from address 0 and the reset vector from address 4, then branches there - so the vector table's first two words are not code.
  • The reset handler copies .data from its load address in flash to its run address in RAM, because initialised globals must exist in both places.
  • It then zeroes .bss, which is why an uninitialised static is guaranteed to be zero and an uninitialised automatic is not.
  • It configures the clock tree before anything time-dependent, since the part boots on a slow internal oscillator.
  • It calls any C library initialisation and static constructors, and only then branches to main().

Design procedure

  1. Read your project's startup file once, in full; it is usually under two hundred lines and it explains most startup mysteries.
  2. When a global has the wrong value at the first line of main, check whether the .data copy loop ran and whether the linker script's load and run addresses agree.
  3. When the part behaves as though it is running slowly, check clock configuration before suspecting your code.
  4. Put a breakpoint on the reset handler rather than on main when a fault happens before your code, since main is already too late.
  5. Do not assume main() is reached at all: a fault in the copy loop or an unaligned stack pointer traps before it.

Key terms

Reset vector
The address the processor branches to out of reset, at offset 4 in the Cortex-M vector table.
.data copy
The startup loop moving initialised globals from flash to RAM.
.bss zeroing
The loop clearing zero-initialised statics. Why they are guaranteed zero and locals are not.
Vector table
The array of handler addresses at the start of flash, beginning with the initial stack pointer.
Startup file
The assembly or C file implementing the reset handler. Part of your project, not the toolchain.

Worked example

The Cortex-M reset sequence, in order:

  address 0x00000000   initial stack pointer   <- loaded into SP
  address 0x00000004   reset handler address   <- branched to

  Reset_Handler:
      copy .data from &_sidata (flash) to &_sdata..&_edata (RAM)
      zero .bss from &_sbss to &_ebss
      SystemInit()          clock tree, flash wait states, FPU
      __libc_init_array()   static constructors, if any
      main()

Which explains two things people rediscover the hard way:

  static int a;          guaranteed 0 - the .bss loop wrote it
  void f(void) {
      int b;             NOT guaranteed 0 - nothing ran for it
  }

And why the very first word matters:

  If the initial stack pointer word is wrong - a linker script that
  places RAM incorrectly - the processor faults on the first push,
  before the first instruction of the reset handler completes. The
  debugger shows a HardFault with no useful call stack, because the
  stack it would walk is the one that is wrong.

Common pitfalls

The linker script: where everything is placed

The linker script is the file that decides where each section of your program lives in the address space. On a desktop this is supplied by the toolchain and never seen; on a microcontroller it is part of the project, and every question of the form 'why is my variable at that address' or 'why does the image not fit' is answered in it. It has three jobs: describe the memory regions, assign sections to them, and export the symbols the startup code needs.

How it is built

  • MEMORY declares each physical region with an origin, a length, and attributes saying whether it is readable, writable or executable.
  • SECTIONS assigns each input section to an output region: .text and .rodata to flash, .data and .bss to RAM.
  • .data has two addresses - a load address in flash where its initialiser lives, and a run address in RAM where it is used - which is what the startup copy loop reconciles.
  • The script exports symbols such as _sdata, _edata and _sbss that the startup code uses as loop bounds; they are addresses, not variables.
  • The stack is usually placed at the top of RAM growing down, and the heap after .bss growing up, with nothing between them but hope unless you add a guard.

Design procedure

  1. Read the map file when something is not where you expect; it lists every symbol and its final address.
  2. Check the load and run addresses of .data when initialised globals come up wrong.
  3. Place a section explicitly with a section attribute when it must be at a known address, such as a bootloader header or a DMA buffer in a particular RAM bank.
  4. Add a stack guard region or fill the stack with a pattern at startup, since collision with .bss is otherwise silent.
  5. Compare the region sizes in the script against the part's datasheet after any part change; a script copied from a larger variant overflows without warning until the image grows.

Key terms

MEMORY block
The declaration of physical regions with origin, length and attributes.
Load address (LMA)
Where a section's contents sit in the image. For .data, in flash.
Run address (VMA)
Where the section is used from. For .data, in RAM.
Map file
The linker's report of what went where. The authoritative answer to any placement question.
Linker symbol
A symbol whose ADDRESS is the value of interest, so it is used as &_sdata rather than _sdata.

Worked example

The structure, and the two addresses that confuse everyone:

  MEMORY {
      FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 256K
      RAM   (rwx) : ORIGIN = 0x20000000, LENGTH = 64K
  }

  SECTIONS {
      .text   : { *(.text*) *(.rodata*) } > FLASH

      .data   : {
          _sdata = .;
          *(.data*)
          _edata = .;
      } > RAM AT> FLASH          <- run in RAM, LOAD from flash
      _sidata = LOADADDR(.data);

      .bss    : { _sbss = .; *(.bss*) _ebss = .; } > RAM
  }

  "> RAM AT> FLASH" is the whole trick: the section's addresses are
  in RAM, and its contents are stored in flash. The startup loop
  copies from _sidata to _sdata..._edata, and that is why an
  initialised global costs both flash and RAM while a zeroed one
  costs only RAM.

And why linker symbols are used by address:

  extern uint32_t _sdata;      // declares a variable...
  uint32_t *p = &_sdata;       // ...whose ADDRESS is the value
                               // reading _sdata itself reads
                               // whatever bytes happen to be there

Common pitfalls

Build, link, and what the toolchain actually produces

Turning source into a running image is four distinct steps, and knowing which one failed is most of debugging a build. The preprocessor produces expanded text, the compiler produces assembly, the assembler produces relocatable object code, and the linker resolves symbols between objects and places everything at final addresses. Each step has its own error vocabulary, and an error's stage tells you where to look.

How it is built

  • The preprocessor handles includes, macros and conditionals, producing a single translation unit of pure C. Errors here are about missing headers and macro expansion.
  • The compiler translates one translation unit to assembly, and its errors are about the language: types, syntax, undeclared identifiers.
  • The assembler produces an object file with unresolved references and relocation entries listing what the linker must fix.
  • The linker combines objects and libraries, resolves every reference, applies the linker script's placement, and emits the ELF.
  • A final step converts the ELF to a plain binary or hex image for the programmer, discarding the symbol and debug information the ELF carries.

Design procedure

  1. Read the error's stage first: undefined reference is the linker, undeclared identifier is the compiler, no such file is the preprocessor.
  2. Use -E to inspect preprocessed output when a macro is not expanding as expected; the answer is always visible there.
  3. Use -S to see the generated assembly when the behaviour disagrees with the source, which is the ground truth for optimisation questions.
  4. Use nm and objdump on the object file to see what a translation unit defines and requires.
  5. Keep the ELF, not just the binary; the addresses in a fault report mean nothing without it.

Key terms

Translation unit
One source file after preprocessing, including everything it included. The compiler's unit of work.
Relocation
A note in an object file saying an address must be filled in once placement is known.
Undefined reference
A linker error: something is called that no object defines.
ELF
The object and executable format, carrying sections, symbols and debug information.
Symbol table
What a file defines and what it needs, readable with nm.

Worked example

The four stages, and the error each produces:

  main.c
    |  preprocessor   cpp
    v                 error: no such file or directory
  main.i  (expanded C)
    |  compiler       cc1
    v                 error: 'x' undeclared
  main.s  (assembly)
    |  assembler      as
    v                 error: invalid instruction
  main.o  (relocatable)
    |  linker         ld + linker script
    v                 error: undefined reference to 'foo'
  firmware.elf
    |  objcopy
    v
  firmware.bin

Which makes the two commonest errors immediately locatable:

  undefined reference to `uart_init'
     the LINKER. Declared somewhere, defined nowhere. The .c file
     is missing from the build, or the name is misspelled, or a C
     symbol is being mangled by a C++ compiler.

  multiple definition of `buffer'
     also the linker. Defined in two translation units - usually a
     variable defined in a header rather than declared extern.

Common pitfalls

Getting connected: the debug probe and the first bring-up

Before any of the above can be observed, the debug probe has to be able to reach the part. Bring-up failures cluster tightly - power, the debug pins, a clock the part needs, and the reset line - and they present almost identically, as a probe that cannot connect. Working through them in order is faster than any amount of software investigation, because none of the software has run.

How it is built

  • SWD needs two pins plus ground: SWDIO and SWCLK. JTAG needs four plus ground, and most Cortex-M parts support both on shared pins.
  • The probe must share a ground reference with the target and must know the target's I/O voltage, which is what the reference pin is for.
  • Reset is often optional to connect and essential to recover a part whose firmware disables the debug pins early.
  • A part can be locked out by its own firmware: code that reconfigures SWD pins as GPIO within the first milliseconds leaves no window to attach.
  • Connect-under-reset holds the part in reset while attaching, which is the standard recovery for exactly that case.

Design procedure

  1. Check power at the part's own pins, not at the regulator, before anything else.
  2. Verify SWDIO, SWCLK and ground are connected and that the reference voltage pin sees the target's supply.
  3. If the probe sees no device, try connect-under-reset before assuming the part is damaged.
  4. If that fails, check whether the firmware disables the debug pins, and use the part's full-chip erase to recover it.
  5. Confirm the part number and core in the probe's settings; a mismatch produces a connection that fails in confusing ways rather than cleanly.

Key terms

SWD
Serial Wire Debug: two pins, the usual Arm debug interface.
Connect under reset
Attaching while holding the part in reset, so firmware has not run yet.
Reference voltage
The probe pin sensing the target's I/O voltage so it drives the right levels.
Full chip erase
Erasing all flash, including the firmware that locked the debug pins. The recovery of last resort.
Read-out protection
A part setting that disables debug access entirely; usually reversible only by mass erase.

Worked example

The bring-up order, which is the fastest path through it:

  1  power at the PART's pins        3.3 V present?
  2  ground shared with the probe    one wire, frequently forgotten
  3  SWDIO and SWCLK continuity      probe to pin, not to header
  4  reference voltage pin           probe knows the I/O level
  5  probe target settings           right part, right core
  6  connect under reset             if the probe sees nothing
  7  full chip erase                 if firmware locked the pins

The self-inflicted lockout, which is worth recognising early:

  int main(void) {
      GPIOA->MODER = 0x00000000;   // resets PA13/PA14 to inputs
      ...                          // which ARE SWDIO and SWCLK
  }

  The part boots, runs this within microseconds, and the debug
  interface stops responding. The board looks dead and is not.
  Connect-under-reset attaches before main() runs; a full erase
  removes the offending code.

Common pitfalls

Start Here: What a Computer Actually Is

Underneath every abstraction a computer is a very small idea repeated at enormous speed: a store of numbers, a unit that transforms them, and a counter saying which instruction to fetch next. Everything else - operating systems, languages, networks - is built on that loop. Starting there rather than at a language matters because every performance surprise and most bugs in embedded work are explained by the machine underneath, not by the code on the page.

How it is built

  • Memory is an addressable array of bytes. An address is just an index, a pointer is a variable holding one, and the fact that code and data live in the same array is what makes a program able to load another program.
  • The processor repeats fetch, decode, execute. A program counter holds the address of the next instruction; executing one usually increments it, and a branch writes a different value into it. That is the whole of control flow.
  • Registers are a handful of storage locations inside the processor, orders of magnitude faster than memory. Almost all optimisation is about keeping working values in them.
  • The gap between register speed and memory speed is the central fact of modern performance. Caches exist to hide it, and cache behaviour explains more real-world speed differences than instruction count does.
  • Interrupts break the fetch-decode-execute loop: hardware forces the program counter elsewhere, the handler runs, and the interrupted state is restored. Without them a processor could only poll.
  • Peripherals appear as memory addresses. Writing to a particular address sets a pin or starts a transfer, which is why embedded code manipulates hardware by dereferencing pointers.

Design procedure

  1. Learn to read a memory map before writing code for a new part - what lives at which addresses is the shape of the machine you are programming.
  2. Follow one instruction through fetch, decode and execute by hand, including how the program counter changes. It makes branches, calls and returns concrete rather than magical.
  3. Write a value to a peripheral register directly with a volatile pointer, before using any library. Seeing a pin move from a single store is what connects code to hardware.
  4. Look at the disassembly of something you wrote. The gap between what you expected and what the compiler produced is where most of the useful learning is.
  5. Measure something. A loop that takes ten times longer than the instruction count suggests is a cache effect, and noticing that early builds the right instincts.
  6. Keep the loop in mind when debugging: if the program counter is somewhere unexpected, ask what wrote it - a branch, a call, an interrupt, or a corrupted return address.

Key terms

Address
An index into the byte array that is memory.
Program counter
Holds the address of the next instruction. Control flow is writing to it.
Register
Storage inside the processor. Far faster than memory, and there are few of them.
Fetch-decode-execute
The loop every processor repeats.
Memory-mapped I/O
Peripherals appearing as addresses, so hardware is controlled by stores.
Interrupt
Hardware forcing the program counter elsewhere, then restoring it.
Volatile
Tells the compiler a location can change outside the program's control.

Worked example

Turning on an LED is one store. The port's output register lives at a fixed address; writing a bit there drives a pin high. In C that is a dereference of a volatile pointer to that address, and it compiles to a single store instruction. Every library, framework and abstraction above it eventually does exactly this - and knowing that means a pin that will not move can be debugged by checking the address, the bit and whether the clock to that peripheral is enabled, rather than by reading library documentation.

Common pitfalls

MCU Startup: What Happens Between Reset and main()

A great deal happens before the first line of application code runs, and almost none of it is written by the application author. The processor fetches an initial stack pointer and a reset vector, jumps to startup code that copies initialised data into RAM and zeroes the rest, configures the clock tree, and only then calls main. When a board comes up dead, the fault is usually somewhere in that sequence rather than in the program - and the sequence is short enough to check step by step.

How it is built

  • On a Cortex-M the vector table sits at the start of flash. Its first word is the initial stack pointer and its second is the reset handler's address - both loaded by hardware before any instruction executes.
  • The reset handler copies .data from its flash image into RAM, zeroes .bss, and optionally runs C++ constructors. Skipping either step leaves initialised variables holding whatever was in RAM at power-up.
  • The clock tree starts on an internal oscillator, typically slow and imprecise. Switching to an external crystal or a PLL is application code, and until it runs the part is executing at a fraction of its rated speed.
  • PLL configuration has an order that must be respected: enable the source, wait for it to stabilise, configure the dividers, switch the system clock, and only then increase the flash wait states if the new frequency requires them.
  • Flash wait states are a common trap in the other direction: raising the clock before increasing wait states means instructions are fetched before flash can supply them, and the part faults or behaves erratically.
  • Peripheral clocks are usually gated off at reset to save power. A peripheral whose clock has not been enabled reads back zeros and ignores writes, which looks exactly like a dead peripheral.

Design procedure

  1. Confirm power and reset first with a meter, then confirm the part is running at all by toggling a pin as the very first instruction of the reset handler.
  2. Check the vector table's location and contents. A wrong initial stack pointer faults on the first push, before any code the author wrote.
  3. Verify the .data copy and .bss zero actually run. A global initialised to a non-zero value reading as garbage means the copy loop did not execute.
  4. Bring the clock up in the documented order, and check the achieved frequency by outputting it on a clock pin rather than assuming the configuration took.
  5. Set flash wait states before raising the frequency, and reduce them only after lowering it. The order is opposite in the two directions and getting it wrong is intermittent.
  6. Enable a peripheral's clock before touching any of its registers, and treat a peripheral reading all zeros as a clock-gating problem until proven otherwise.

Key terms

Vector table
Initial stack pointer and handler addresses. The first two words are used by hardware.
Reset handler
Startup code: copies .data, zeroes .bss, then calls main.
PLL
Multiplies a reference to the system clock. Must be locked before switching to it.
Flash wait states
Cycles the core waits for flash. Must rise before the clock does.
Peripheral clock gating
Clocks off at reset. A gated peripheral reads zeros and ignores writes.
Watchdog
Sometimes enabled at reset. Resets the part mid-startup if not serviced or disabled.
Boot pins
Strapping that selects where the part fetches from. Sampled at reset.

Worked example

A board runs but every peripheral reads zero. Power is fine, the clock is fine, and the code looks correct. The cause is that the peripheral clock enable register was written before the bus clock feeding that register was itself enabled, so the write went nowhere. The fix is one line moved earlier. This failure is indistinguishable from a dead part until the enable register is read back and found to be zero, which is why reading back a control register after writing it is worth the two instructions during bring-up.

Common pitfalls

Connecting a Board: Power, Ground, Clock, Reset and Debug

Before any code matters, five things must be right, and they are right or wrong in a fixed order: power, ground, clock, reset, then debug access. Almost every dead board is one of those five, and checking them in order is faster than any amount of reading the firmware - because every step above a broken one produces symptoms that point somewhere else entirely.

How it is built

  • Power means every rail at its correct voltage under load, not just the main supply. A part with separate analog and I/O rails needs all of them, and a rail sagging under load looks like an intermittent fault.
  • Ground must be common and low-impedance. Two boards communicating without a shared ground can show a signal on a scope and never decode, because the receiver's threshold is referenced to a different zero.
  • Decoupling capacitors supply the transient current a switching gate needs. Without them close to the pins, the local supply dips on every edge and the part behaves erratically at speed while working slowly.
  • The clock source must actually oscillate. A crystal with wrong load capacitors either does not start or runs at the wrong frequency, and both look like a software fault.
  • Reset must be released cleanly and stay released. A reset line held by a supervisor waiting for a rail, or bouncing, produces a part that appears to run and restart.
  • Debug access - SWD or JTAG - needs its pins not to have been reconfigured as something else. Firmware that repurposes them locks the part out, which is recoverable and alarming.

Design procedure

  1. Measure every supply rail at the part's pins under load, not at the regulator. A drop across a trace is invisible at the source.
  2. Confirm ground continuity between boards with a meter before assuming any signal problem, especially where two supplies are involved.
  3. Scope the clock pin. A crystal that is not oscillating is the clearest possible answer, and it is one measurement.
  4. Check the reset pin's level and its release timing. Hold-and-release problems appear as a part that starts and immediately restarts.
  5. Connect the debugger before flashing anything, and confirm the part is identified. An unidentified part is a hardware problem, not a software one.
  6. Only when all five pass should firmware be suspected. Anything above a broken layer produces misleading symptoms.

Key terms

Rail
A supply voltage. Parts often need several, and all must be present.
Common ground
A shared zero reference. Without it, signals are meaningless between boards.
Decoupling
Local capacitance supplying switching transients. Must be close to the pins.
Crystal load capacitors
Set the oscillator's frequency and whether it starts at all.
Reset supervisor
Holds reset until the supply is valid. Can hold it indefinitely if a rail never arrives.
SWD / JTAG
Debug access. Lost if firmware repurposes those pins.
Brown-out detector
Resets the part when the supply dips. Produces a boot loop on a marginal supply.

Worked example

A board runs correctly from a bench supply and resets randomly on battery. Every rail measures correct with a meter, which averages. A scope on the supply during a radio transmission shows a 200 mV dip lasting microseconds - below the brown-out threshold, so the part resets. The meter could never have shown it. The fix is bulk capacitance near the radio, and the diagnosis took one measurement with the right instrument after several hours with the wrong one.

Common pitfalls

Linkers and Build Systems: How Source Becomes an Image

Compiling turns each source file into an object file containing machine code with unresolved references. The linker resolves those references, assigns every section an address according to a linker script, and produces an image. Most build problems are linker problems rather than compiler problems, and they are diagnosable because the linker will tell you exactly what it could not find and where everything ended up.

How it is built

  • Compilation is per file and independent. An object file contains code, data, a symbol table of what it defines, and relocations naming what it needs from elsewhere.
  • The linker matches every undefined symbol against a definition, places sections at addresses, and patches the relocations. An unresolved symbol means no object file or library defined it; a duplicate means two did.
  • The linker script assigns memory regions and section placement. It is where flash and RAM origins and lengths are stated, and where the stack top and heap boundaries come from.
  • Library order matters with static libraries: the linker processes them left to right and takes only the members resolving symbols outstanding at that point. A library listed before the code needing it contributes nothing.
  • The map file reports every section's address and size and which object contributed each symbol. It is the definitive answer to what is taking space and why something was included.
  • Incremental builds depend on correct dependencies. A header changed without the objects depending on it being rebuilt produces a mismatch between what the compiler assumed and what is actually linked - and the symptom is a crash with no source explanation.

Design procedure

  1. Read the error text carefully: 'undefined reference' names what is missing, and 'multiple definition' names what was defined twice and where.
  2. For an undefined symbol, check the definition exists, is not static, and that its object or library is actually on the link line - and after the code that needs it.
  3. For a section overflow, open the map file and find what grew. It is nearly always one large table or a library pulled in by a single call.
  4. Verify the linker script's memory regions match the actual part. A script copied from a similar device with less flash produces a puzzling overflow.
  5. When behaviour makes no sense after an edit, do a clean build. A stale object is the explanation for an impressive proportion of impossible bugs.
  6. Check the map for unexpectedly large contributors - printf and floating-point support are the classic ones and are pulled in by a single format specifier.

Key terms

Object file
Compiled code with a symbol table and unresolved relocations.
Symbol
A named definition. Undefined means nothing provided it.
Relocation
A reference the linker patches once addresses are assigned.
Linker script
States memory regions and section placement. Where the stack top comes from.
Map file
Every section's address and size, and which object contributed each symbol.
Library order
Left to right. A library before the code needing it contributes nothing.
Section
.text, .data, .bss, .rodata - the groups the linker places.
Stale object
An object not rebuilt after a header change. Source of impossible bugs.

Worked example

A project overflows flash by 6 kB after adding one debug line. The map file shows printf and its floating-point formatting pulled in, together about 8 kB, because the new line used a %f specifier. Switching to an integer format or a lightweight printf removes it entirely. Without the map file this looks like the project having outgrown the part; with it, the cause is one character in one format string.

Common pitfalls

More in Foundations

  • PointersComplete visual pointer laboratory: addresses, dereferencing, pointer arithmetic, arrays and decay, double pointers, dynamic memory, function pointers, const/volatile, MMIO, lifetime bugs — with a step-through memory simulator, 100+ interview questions and a mastery exam.
  • Computer Systems 0 → 100A beginner-first path from what a computer is through bits, CPUs, addresses, memory hierarchy, SRAM, DRAM, ROM, flash, SSDs, buses, PCIe, SATA, AHCI, NVMe, M.2, boot and performance—with comparisons and interview practice.
  • Cache Coherency 0 → 100A first-principles course from cache lines and the coherence problem through MSI, MESI, MOESI, snooping, directories, memory ordering, atomics, false sharing, DMA, NUMA, measurement and verification—with a live protocol engine and code lab.
  • Number RepresentationBases and why hex is the one you read, two's complement and the asymmetry that makes abs(INT_MIN) undefined, the bit-manipulation idioms with the edge case in each, and byte order - which matters in exactly three places and in none of the arithmetic.
  • FoundationsComputer systems from first principles—memory, storage and interconnects through cache coherence, ordering, atomics and DMA—then the firmware foundations of numbers, linking, interrupts and pointers.

References and further reading