UNIT 04 · LESSON 2 OF 6

Compiling, Assembling, and Linking

What rules is the linker applying, and what exactly does it do to the bytes the assembler produced?

INTERACTIVEHow the linker matches symbols
Symbol tables of several object files and the linker’s resolution resultmain.oT mainU led_onled.oT led_onB led_countU gpio_putgpio.oT gpio_putld: link succeededmain → main.o · led_on → led.o · led_count → led.o · gpio_put → gpio.o
Symbol tables of several object files and the linker’s resolution resultmain.oT mainU led_onled.oT led_onB led_countU gpio_putgpio.oT gpio_putld: link succeededmain → main.o · led_on → led.o · led_count →led.o · gpio_put → gpio.o

Try this

Scenario
Link succeeds: main from main.o, led_on from led.o, led_count from led.o, gpio_put from gpio.o.

Each object file lists the symbols it defines (T code, B zero-initialised data, W weak) and the symbols it needs (U undefined). The linker pairs every U with exactly one definition. A missing definition or two strong definitions of the same name stop the link; a weak definition is used only if no strong one exists.

What you will be able to do
  • Describe the two jobs of the linker, symbol resolution and relocation, and what information in an object file each one uses.
  • Diagnose “undefined reference” and “multiple definition” errors from the symbol tables involved.
  • Explain strong and weak definitions and how startup code uses weak aliases to provide default interrupt handlers.
  • Evaluate the R_ARM_ABS32 and R_ARM_THM_CALL relocation formulas for given addresses, including the Thumb bit.
  • Explain why a call to a distant address needs a linker-generated stub.
Before you start
  • The four build stages and the BL offset S−(P+4)S - (P + 4) (lesson 1).
  • Function pointers and addresses (unit 2, lesson 3).
Steps in this lesson
  1. Two jobs
  2. Resolution and its failures
  3. Weak definitions
  4. Relocation: patching the bytes
  5. Worked example: two patches in led_on
  6. When the target is too far
  7. Common misconceptions

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, .data and .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:

  • SS: the address of the symbol being referred to;
  • AA: the addend, a constant adjustment (on Arm, usually stored in the placeholder itself);
  • PP: the address of the place being patched;
  • TT: 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):

X=(S+A)∣TX = (S + A) \mid T

and R_ARM_THM_CALL, for a Thumb BL:

X=((S+A)∣T)−PX = ((S + A) \mid T) - P

The Thumb bit TT 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.

INTERACTIVEPatching a call with a relocation
The R_ARM_THM_CALL relocation computed for one callS (gpio_put)0x1000_0410P (the BL)0x1000_023AA (addend)−4T (Thumb)1X = ((S + A) | T) − P = 0x1D3bit 0 of X is the Thumb bit T; the BL field encodes X with bit 0 cleared, 0x1D2 =466 bytes from P + 4object file: f7ff fffe (placeholder)linked:       f000 f8e9Decoding the new halfwords gives an offset of 466 bytes from P + 4, which landsexactly on gpio_put.
The R_ARM_THM_CALL relocation computed for one callS (gpio_put)0x1000_0410P (the BL)0x1000_023AA (addend)−4T (Thumb)1X = ((S + A) | T) − P = 0x1D3bit 0 of X is the Thumb bit T; the BL field encodesX with bit 0 cleared, 0x1D2 = 466 bytes from P + 4object file: f7ff fffe (placeholder)linked:       f000 f8e9Decoding the new halfwords gives an offset of 466bytes from P + 4, which lands exactly on gpio_put.
Where gpio_put ends up
BL at 0x1000_023A to 0x1000_0410: X = 467 (bit 0 is the Thumb bit), so the branch offset is 466 bytes, encoded as f000 f8e9.

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 PP = 0x1000_023A. Its placeholder f7ff fffe encodes A=−4A = -4. gpio_put is Thumb code, so T=1T = 1:

X=((0x1000_0410−4)∣1)−0x1000_023A=0x1000_040D−0x1000_023A=0x1D3X = ((\text{0x1000\_0410} - 4) \mid 1) - \text{0x1000\_023A} = \text{0x1000\_040D} - \text{0x1000\_023A} = \text{0x1D3}

Bit 0 is the Thumb bit; the encoded branch offset is 0x1D2 = 466 bytes, the same number lesson 1 computed as S−(P+4)S - (P + 4), and the instruction becomes f000 f8e9.

The address of the variable. The literal-pool word at 0x1000_0248 holds a placeholder 0x0000_0000 (A=0A = 0) with R_ARM_ABS32 against led_count. A variable is not a Thumb function, so T=0T = 0 and XX = 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?

(S+A)∣T(S + A) \mid T with A=0A = 0, T=1T = 1: 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.

X=((0x0800_0100−4)∣1)−0x0800_0200=0x0800_00FD−0x0800_0200=−259X = ((\text{0x0800\_0100} - 4) \mid 1) - \text{0x0800\_0200} = \text{0x0800\_00FD} - \text{0x0800\_0200} = -259. Clearing the Thumb bit gives −260, which is S−(P+4)S - (P + 4) = 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)
  1. 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
  2. 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
  3. 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
  4. 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”
  5. 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")))
  6. 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”