Pantech.AI

Linker Script Explained: A Beginner to Advanced Guide for Embedded Systems

In embedded systems and low-level programming, a linker script plays a crucial role in defining how memory is organized and utilized. This guide takes you through everything you need to know—from basic concepts to advanced configurations—to write and customize linker scripts for your projects.

1. Introduction to Linker Scripts

What is a Linker Script?

A linker script is a file used by the linker to control how object files are combined into a final executable. It defines memory regions and dictates where different program sections (such as code, variables, stack, and heap) should be placed in memory.

Why Do We Need a Linker Script?

In embedded systems, memory is limited and needs to be precisely managed. A linker script allows us to:

  • Specify memory layout (Flash, RAM, Stack, Heap).
  • Place code and data in appropriate sections.
  • Define startup and execution locations of different program components.
  • Optimize memory utilization.

How Does a Linker Work?

The compiler generates object files (.o) from source code. The linker then combines these object files into a final executable (.elf or .bin) based on the linker script.

📌 Key Role of the Linker:

  • Resolves symbols (function and variable references).
  • Arranges sections (.text, .data, .bss, etc.).
  • Generates a final memory map for execution.

2. Understanding the Linker Process

Before diving into linker scripts, let’s quickly review the build process in embedded systems:

Step-by-Step Build Process

  1. Compilation: Converts C/C++ source files into assembly code.
  2. Assembly: Converts assembly code into machine code (object files).
  3. Linking: Combines object files and places them in memory as per the linker script.
  4. Conversion: Generates the final binary (.bin, .hex, .elf).

The linker script directly influences Step 3 by defining memory mapping.

3. Basic Structure of a Linker Script

A linker script consists of two primary components:

  1. MEMORY: Defines available memory regions.
  2. SECTIONS: Specifies how different parts of the program should be mapped to memory.

Example of a Simple Linker Script

Here’s a basic linker script for an ARM Cortex-M microcontroller

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

SECTIONS
{
    .text : 
    {
        *(.isr_vector) /* Interrupt vector table */
        *(.text*)      /* Program code */
        *(.rodata*)    /* Read-only data */
        . = ALIGN(4);
    } > FLASH

    .data :
    {
        *(.data*) /* Initialized data */
        . = ALIGN(4);
    } > RAM AT > FLASH

    .bss :
    {
        *(.bss*) /* Uninitialized data */
        . = ALIGN(4);
    } > RAM
}

📌 Explanation of Key Elements:

  • MEMORY: Defines FLASH (code storage) and RAM (runtime data storage).
  • SECTIONS: Maps .text (code), .data (initialized variables), and .bss (uninitialized variables) to appropriate regions.
  • ALIGN(4): Ensures memory alignment for better performance.

4. Memory Layout and Organization

How Memory is Organized in Embedded Systems

An embedded system typically has three main memory areas:

  1. Flash (ROM) → Stores code, constants, bootloader (non-volatile).
  2. RAM → Stores variables, stack, heap (volatile).
  3. Peripheral Registers → Controls hardware (e.g., GPIO, UART, ADC).

Linker Script’s Role in Memory Layout

A linker script defines:

  • Where code and data reside in memory.
  • The size and boundaries of stack and heap.
  • The memory sections (.text, .data, .bss, .heap, .stack).

5. Advanced Concepts in Linker Scripts

Custom Memory Regions

You can define custom memory blocks for specific variables, e.g., placing critical data in a backup RAM region:

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

SECTIONS
{
    .backup_data :
    {
        *(.backup_section) /* Custom backup variables */
    } > BACKUP_RAM
}

Placing Specific Variables in Custom Sections

You can assign variables to specific memory regions using GCC attributes:

__attribute__((section(".backup_section"))) int critical_data;

This ensures critical_data is stored in BACKUP_RAM instead of normal RAM.

Defining Stack and Heap

A linker script also defines stack and heap:

_estack = ORIGIN(RAM) + LENGTH(RAM); /* Top of RAM */
_min_heap_size = 0x400;  /* 1KB heap */
_min_stack_size = 0x800; /* 2KB stack */

6. Debugging and Optimizing Linker Scripts

Common Linker Errors and Solutions

ErrorCauseSolution
undefined referenceSymbol not foundCheck function/variable definitions
section overlapsMemory regions are exceeding limitsAdjust memory sizes in MEMORY
not enough memoryProgram too largeOptimize code size, remove unnecessary data

Techniques for Debugging

  1. Check the Linker Map File (.map)
    • Use arm-none-eabi-objdump -h yourfile.elf to inspect sections.
  2. Enable Linker Warnings
    • Add -Wl,--warn-section-align in your compiler options.
  3. Analyze Memory Usage
    • Use arm-none-eabi-size yourfile.elf to check section sizes.

7. Practical Examples and Use Cases

  • Writing a linker script for STM32 with FreeRTOS.
  • Creating a custom bootloader memory layout.
  • Placing critical firmware in a protected memory region.

8. Conclusion

A linker script is an essential part of embedded systems programming. By understanding its structure and functionality, you can:
✅ Customize memory layouts for efficient use of Flash and RAM.
✅ Optimize memory placement for performance and reliability.
✅ Debug and fix linking issues in embedded projects.

Leave a Comment

Your email address will not be published. Required fields are marked *