How the IAR Linker Resolves Function Calls Across Files in ARM Cortex-M

In embedded systems — especially with microcontrollers like the STM32H5 (Cortex-M33) — linking is not just a toolchain step. It represents the final integration point where independently compiled code modules, memory layout constraints, and hardware-specific expectations converge into a working binary.

Yet, many engineers treat the linker as a black box — something that “just works.” But when your firmware refuses to jump to the correct function, your vector table doesn’t initialize properly, or the build system throws undefined symbol errors, it becomes painfully clear: you must understand what the linker actually does.

 

When you are building embedded firmware with multiple source files, the compiler translates each .c file into .o (object files). But it is the linker that truly weaves the system together — resolving cross-references, applying memory layout, and ensuring that function calls between modules work seamlessly.

A common scenario looks like this:

  • main.c contains main() and calls a function blink()
  • blink() is defined in another file, say led.c
  • Both files are compiled separately, and finally linked together into a firmware image

But how does this actually work under the hood?
How does the linker know where blink() is?

Let’s break this down — not like a tutorial, but like an engineer who has worked with linkers at the machine level.

 

🧱 Setup: A Simple but Deeply Insightful Example:

Let’s start with the simplest multi-file embedded program. It has:

A main.c file that calls blink():

#include "led.h"

int main() 
{
    blink();  // Function call — not defined here
    while (1);
}

A led.c file that defines blink():

void blink() 
{
    // Toggle an LED
}

A led.h file that declares blink():

#ifndef LED_H
#define LED_H

void blink(void);  // Declaration only

#endif

In the above example catch is that: main.c uses blink(), but doesn’t know what it does. led.c implements blink().

So how do they connect?

 

📦 Compilation Creates Object Files With Symbols:

When each .c file is compiled, it is turned into an object file (.o), which contains:

  • Machine instructions (like a partial .text section)
  • Data (e.g., global variables)
  • A symbol table

The symbol table contains two types of symbols:

Symbol Type Meaning
Exported (defined) Functions or variables this file provides
Imported (undefined) Functions or variables this file uses but doesn’t define

In our case:

From main.c (→ main.o):

  • Exports: main
  • Imports: blink (calls it, but doesn’t define it)

From led.c (→ led.o):

  • Exports: blink
  • Imports: (none — assuming it’s self-contained)

So far, each file is isolated. It knows only its own world. Now comes the linker’s job.

 

🔗 Linking: The Symbol Matchmaker

The linker (in IAR, this is ilinkarm) takes these .o files and performs a multi-phase process:

1. Symbol Table Construction:

The linker reads all input object files and builds a global list of:

  • All exported (defined) symbols
  • All imported (undefined) symbols

It tracks which file provides each function, and which file depends on it.

In our example:

Symbol Defined In Used In
main main.o
blink led.o main.o

 

2. Symbol Resolution:

Once the tables are complete, the linker resolves every undefined symbol by matching it to a defined one. This is like completing a circuit.

In our case:

  • main.o references blink → linker finds blink is defined in led.o
  • Match successful → the function call is now resolvable

If there were no match (say, led.o wasn’t compiled or linked), this would result in a linker error: the symbol is unresolved.

 

3. Relocation and Code Binding:

Now the linker has to “wire up” the pieces. In main.o, the compiler left a placeholder at the location where blink() is called. The linker replaces this placeholder with the actual memory address of blink() as defined in led.o.

This step is called relocation.

In simpler terms:

  • Before: “Call blink() — address unknown”
  • After: “Call blink() — now known to be at 0x08001234”

This address is determined after section merging and memory layout decisions.

 

🧠 How Import and Export Work Per .o File

To fully appreciate linking, you must realize that each object file is a contract:

  • “Here are the functions I offer” (exports)
  • “Here are the functions I need” (imports)

The linker is the broker that ensures every need is matched with an offer.

Summary Per File:

File Exports Imports
main.o main blink
led.o blink (none)

These are resolved statically — there’s no runtime dynamic loading like in PC environments. In embedded systems, everything must be known and resolved at build time.

 

🗃️ What if There’s a Conflict?

Imagine you had two files, both defining blink().

This would cause a multiple definition error. The linker doesn’t allow ambiguity.

To prevent this:

  • Use static for private functions (file-local)
  • Follow strict modular design
  • Resolve duplicate names before build

 

🚫 What if the Definition Is Missing?

If main.c references blink(), but no file defines it:

  • The linker halts and reports an undefined symbol
  • No firmware image is generated
  • You must include the correct object/library file that defines blink()

 

📍 Section Merging and Final Memory Layout

After symbol resolution, the linker merges sections (.text, .data, .bss, etc.) and assigns physical memory addresses.

In embedded systems like STM32H5:

  • .text (code) usually goes to Flash (0x08000000)
  • .data, .bss, and stack go to RAM (0x20000000)
  • The vector table must be placed at the beginning of Flash

These placements are governed by the IAR Linker Configuration File (.icf), which is the linker’s roadmap.

 

🧩 Putting It All Together

Here’s how the linker stitches the program:

  • Compiles each file to .o, tagging exports/imports
  • Builds a unified symbol table
  • Resolves references (e.g., main.o needs blink, led.o provides it)
  • Patches function calls to jump to correct addresses
  • Merges code and data into physical sections (Flash/RAM)
  • Emits a final ELF or HEX file ready for flashing.

 

✅ Key Takeaways:

  • Object files import and export symbols
  • Imported symbols must be resolved during linking
  • The linker matches undefined references to defined implementations
  • Function calls across files are resolved and relocated statically
  • Errors occur when a required symbol is missing or duplicated
  • Proper modular design ensures clean linking