The puzzle
Two messages every firmware developer meets in the first week: undefined reference to `gpio_put' and multiple definition of `led_count'. Both come from the linker, after every file has compiled without complaint. And a third, stranger fact: a project whose startup file defines SysTick_Handler as a function that loops forever still runs your own SysTick_Handler instead, with no error about two definitions. What rules is the linker applying, and what exactly does it do to the bytes the assembler produced?
STEP 1
Two jobs
An object file carries three things the linker needs:
- Sections: blocks of bytes with a name and attributes, such as
.text(code),.rodata,.dataand.bss(unit 3 lesson 5). - A symbol table: every name this file defines (with its section and offset) and every name it uses but does not define (undefined, “U”).
- Relocations: for each placeholder in a section, its offset, the symbol it refers to, and a relocation type that says how to compute the value.
With those, the linker does two things. Symbol resolution: pair every undefined use with exactly one definition in some other input. Relocation: lay out all the sections at final addresses, then visit every relocation and patch the placeholder with the value its formula gives. Resolution produces the errors; relocation produces the bytes.
STEP 2
Resolution and its failures
↑ This step uses the figure at the top of the page.
Step through the scenarios. When every U finds a definition, the link succeeds and each name is bound to one address. Leave out gpio.o and led.o’s use of gpio_put has nothing to bind to: undefined reference. Usual causes: a source file not in the build, a library not linked (lesson 3), a function declared in a header but never written, or a name spelled differently (C++ name mangling, or static on a function that another file needs).
Define led_count in two files and the linker has two candidates and no rule to choose: multiple definition. The usual cause is a definition in a header, uint32_t led_count;, instead of a declaration, extern uint32_t led_count;, so every file that includes the header defines it. Older GCC versions hid this by placing such uninitialised globals in a “common” block that the linker merged silently; current GCC defaults to -fno-common, which puts them in .bss and makes the mistake an error, as the manual explains.
STEP 3
Weak definitions
A definition is normally strong. GCC’s weak attribute makes it weak: a definition to use only if no strong one exists. Startup code relies on this. Arm’s reference startup file for the Cortex-M0+ declares its exception and interrupt handlers (all except HardFault, which gets its own weak loop) like this:
void Default_Handler(void) { while (1) { } }
void SysTick_Handler(void) __attribute__ ((weak, alias("Default_Handler")));
The vector table (unit 3 lesson 6) can then name SysTick_Handler without your program having to define one. If you write your own void SysTick_Handler(void) { … }, that strong definition wins with no error; if you do not, the weak alias points the vector at Default_Handler. The figure’s last two scenarios show both outcomes.
The flexibility cuts both ways. Misspell your handler, SysTick_handler, and nothing complains: your function is simply never called, the weak default takes the interrupt, and the program hangs in Default_Handler’s loop.
STEP 4
Relocation: patching the bytes
The linker knows, after layout, the final address of every symbol and every section. Each relocation type in the ELF for the Arm Architecture specification is a small formula in these terms:
- : the address of the symbol being referred to;
- : the addend, a constant adjustment (on Arm, usually stored in the placeholder itself);
- : the address of the place being patched;
- : 1 if the target is a Thumb function, otherwise 0.
Two relocations cover most embedded code. R_ARM_ABS32, for an absolute 32-bit address in data (a literal-pool word, a pointer in a table, a vector-table entry):
and R_ARM_THM_CALL, for a Thumb BL:
The Thumb bit is how Arm marks code addresses: a Thumb function’s symbol value has bit 0 set, and anything that jumps through a stored address (a function pointer, a vector-table entry) needs that bit set so the core stays in Thumb state. For the BL itself the bit is dropped when the offset is encoded.
The object file’s BL holds a placeholder whose field encodes the initial addend A = −4. The ELF for the Arm Architecture defines R_ARM_THM_CALL as X = ((S + A) | T) − P, where S is the target’s address, P the address being patched and T = 1 for a Thumb target. The linker encodes X into the instruction; a BL can only reach ±16 MiB, so a target further away needs a linker-generated stub.
STEP 5
Worked example: two patches in led_on
Use the layout from lesson 1: led_on at 0x1000_0234, gpio_put at 0x1000_0410, led_count (a variable) at 0x2000_0010.
The call. The BL is at = 0x1000_023A. Its placeholder f7ff fffe encodes . gpio_put is Thumb code, so :
Bit 0 is the Thumb bit; the encoded branch offset is 0x1D2 = 466 bytes, the same number lesson 1 computed as , and the instruction becomes f000 f8e9.
The address of the variable. The literal-pool word at 0x1000_0248 holds a placeholder 0x0000_0000 () with R_ARM_ABS32 against led_count. A variable is not a Thumb function, so and = 0x2000_0010. The ldr r2, [pc, #8] instruction loads that word, and the next instructions increment the variable through it.
Had the table held a pointer to a function, &led_on, the same relocation would store 0x1000_0235: the address with the Thumb bit set.
STEP 6
When the target is too far
A BL’s offset is a signed, even, 25-bit number (24 encoded bits plus an implicit zero), so it reaches ±16 MiB. On most microcontrollers all code fits in that range, but not always: copying a time-critical function into RAM (0x2000_0000 on a Cortex-M) puts it hundreds of megabytes away from flash code. The GNU linker for Arm then automatically inserts a small stub (or veneer) within reach of the call, which loads the full 32-bit address and branches to it. The call still works; it costs a few bytes and cycles, and the stub shows up in the map file (lesson 5). Select “gpio_put copied to RAM” in the figure to see the offset exceed the limit.
MYTHS AND FACTS
Common misconceptions
The linker just concatenates object files
It also resolves every symbol and patches every relocation; concatenation is the easy part.
A multiple-definition error means a function was written twice
Most often a variable was defined in a header that several files include. Declare it extern in the header and define it in one .c file.
Weak symbols are an optimisation
They are a linking rule: use this definition only if no strong one exists. A typo in a handler name silently selects the weak default.
The address of a Thumb function is even
Its symbol value, and any pointer to it, has bit 0 set to mark Thumb state; the instruction itself is at the even address.
BL can reach any address
Only ±16 MiB. Further calls go through linker-generated stubs.
Relocation happens when the program is loaded
In the usual (not position-independent) bare-metal build, everything is relocated at link time; the image in flash already holds final addresses.
Check yourself
Answer in your head, then open the card.
main.c calls uart_init(), declared in uart.h. The build reports “undefined reference to uart_init”. List three possible causes.
uart.c is not compiled or not passed to the linker; the library containing it is missing or in the wrong order (lesson 3); the function is defined with a different name or as static, so no external definition exists.
A table of handlers holds &button_isr, a Thumb function at 0x0800_1A40. What value does R_ARM_ABS32 store?
with , : 0x0800_1A41.
A BL at 0x0800_0200 targets a Thumb function at 0x0800_0100 with the usual addend −4. Compute X and the branch offset.
. Clearing the Thumb bit gives −260, which is = 0x100 − 0x204.
Your SysTick interrupt never runs your handler, and the debugger shows the CPU spinning in Default_Handler. What is the likely cause?
Your handler’s name does not exactly match the weak symbol the vector table uses (a typo, wrong case, or C++ name mangling without extern "C"), so the weak alias to Default_Handler was used.
Sources (6)
- Arm, ELF for the Arm Architecture (AAELF32), “Relocation codes” table and “Symbol Values” — R_ARM_ABS32 is (S + A) | T; R_ARM_THM_CALL and R_ARM_THM_JUMP24 are ((S + A) | T) – P; T is 1 if the target symbol is a Thumb function; a Thumb function symbol’s value has bit zero set, and relocation uses st_value & ~1 as the address
- GNU ld manual, “Command-line Options”: -l, -( … -), --gc-sections, -Map — how ld reads object files and archives, and the options that control what it keeps
- GNU ld manual, “ld and the ARM family” — “The linker will automatically generate and insert small sequences of code into a linked ARM ELF executable whenever an attempt is made to perform a function call to a symbol that is too far away”; these stubs are placed under the control of --stub-group-size
- GCC manual, “Common Function Attributes”: weak, alias — “The weak attribute causes a declaration of an external symbol to be emitted as a weak symbol rather than a global. This is primarily useful in defining library functions that can be overridden in user code”
- Arm, Cortex_DFP, Device/ARMCM0plus/Source/startup_ARMCM0plus.c — Arm’s reference Cortex-M0+ startup file: every handler except HardFault_Handler (weak, with its own loop) is declared __attribute__ ((weak, alias("Default_Handler")))
- GCC manual, “Options for Code Generation Conventions”: -fcommon / -fno-common — “The default is -fno-common, which specifies that the compiler places uninitialized global variables in the BSS section … so you get a multiple-definition error if the same variable is accidentally defined in more than one compilation unit”