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
- Compilation: Converts C/C++ source files into assembly code.
- Assembly: Converts assembly code into machine code (object files).
- Linking: Combines object files and places them in memory as per the linker script.
- 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:
- MEMORY: Defines available memory regions.
- 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
: DefinesFLASH
(code storage) andRAM
(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:
- Flash (ROM) → Stores code, constants, bootloader (non-volatile).
- RAM → Stores variables, stack, heap (volatile).
- 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
Error | Cause | Solution |
---|---|---|
undefined reference | Symbol not found | Check function/variable definitions |
section overlaps | Memory regions are exceeding limits | Adjust memory sizes in MEMORY |
not enough memory | Program too large | Optimize code size, remove unnecessary data |
Techniques for Debugging
- Check the Linker Map File (
.map
)- Use
arm-none-eabi-objdump -h yourfile.elf
to inspect sections.
- Use
- Enable Linker Warnings
- Add
-Wl,--warn-section-align
in your compiler options.
- Add
- Analyze Memory Usage
- Use
arm-none-eabi-size yourfile.elf
to check section sizes.
- Use
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.