Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 64

Custom Memory Placement with Linker Scripts

Recommended resources

Introduction

Welcome back to our module on low-level system control. In our previous lessons, we've established a solid foundation:

  1. We know the linker script acts as a blueprint, defining memory regions (FLASH, RAM) and mapping standard code/data sections (.text, .data, .bss) into them.
  2. We know the startup file is the runtime executor of that blueprint, copying the initial values for the .data section from Flash to RAM and zeroing out the .bss section before main() is ever called.

Now, it's time to move from interpreting the toolchain's work to directing it yourself. In this lesson, we will learn how to modify a linker script to place a specific function or variable in a custom memory location.

This isn't just an academic exercise; it's a powerful technique used by professional firmware engineers to:

  • Place calibration data or configuration settings at a fixed, known address in Flash.
  • Put performance-critical functions or data in faster memory regions (like CCM RAM on more advanced MCUs).
  • Create shared memory regions for communication between a bootloader and the main application.
  • Ensure a variable is placed at a specific address required by a hardware peripheral, like a DMA controller.

By the end of this lesson, you will have the tools to precisely control your application's memory map.


1. The Core Mechanism: Attributes and Linker Rules

The process of placing an object in a custom location involves two coordinated steps:

  1. In your C code: You "tag" a variable or function with a special attribute, telling the compiler to place it in a uniquely named input section instead of the default (.data, .text, etc.).
  2. In your linker script: You create a new rule that tells the linker exactly where to place this new, uniquely named section in the final memory map.

Let's start with a practical example: placing a constant configuration value at a specific, fixed address in Flash memory. This is common for storing things like serial numbers, calibration constants, or firmware version information that other tools might need to read directly from the binary file.

Step 1: Tagging a Variable in C

With the GCC toolchain, you can use the __attribute__ keyword to specify a section for a variable or function.

Let's declare a constant variable and place it in a custom section we'll name .config_section:




#include <stdint.h>

// This variable will NOT go into the default .rodata section.
// Instead, the compiler will place it in its own section called ".config_section".
const uint32_t FIRMWARE_VERSION __attribute__((section(".config_section"))) = 0x01020003; // v1.2.0.3

// This attribute tells the compiler not to optimize this variable away,
// even if it appears unused in the C code.
__attribute__((used))

// ... main function and other code

The __attribute__((used)) is also important. If you declare a variable but never reference it in your code, the linker is smart enough to discard it to save space. used tells the compiler to emit a directive to the linker to prevent this.

The following video introduces this concept of using attributes to mark C code for special linker handling.

ARM - M: Build process, role of linker and Linker scripts! | Embedded Systems podcast, in Pyjama!

This video from 'inpyjama' explains how C code can be marked with attributes to place it into custom-named sections, which are then handled by the linker script.

Watch from 01:01:53 to 01:05:53. Focus on understanding the __attribute__((section("section_name"))) syntax and the role of the used attribute. The example uses it for an interrupt vector table, but the principle is identical for any variable or function.

Step 2: Modifying the Linker Script

Now that our variable is tagged, we must tell the linker what to do with every .config_section it finds in the object files.

Open your STM32F103C8TX_FLASH.ld linker script. We will add a new output section definition inside the SECTIONS block. A good place is after the .rodata section.

/* Inside the SECTIONS { ... } block */

  /* Read-only data */
  .rodata :
  {
    . = ALIGN(4);
    *(.rodata)
    *(.rodata*)
    . = ALIGN(4);
  } >FLASH
  
  /* =================================================================== */
  /* NEW: Custom section for configuration data at a fixed address       */
  /* Place this section at address 0x0801F000 in Flash.                  */
  /* =================================================================== */
  .config_section 0x0801F000 :
  {
    . = ALIGN(4);
    KEEP(*(.config_section)) /* Find all .config_section inputs and KEEP them */
    . = ALIGN(4);
  } >FLASH

Let's break this down:

  • .config_section 0x0801F000 :: This defines a new output section named .config_section. By providing an address (0x0801F000), we are explicitly telling the linker to place this section at that specific location. This address is within the 64KB Flash of our Blue Pill, but near the end.
  • KEEP(*(.config_section)): This is the core instruction.
    • The * is a wildcard, so *(.config_section) means "find all input sections named .config_section from all object files and place them here."
    • KEEP() is a directive that prevents the linker from discarding this section during dead code elimination, even if FIRMWARE_VERSION isn't referenced anywhere. It's a stronger alternative to the __attribute__((used)) we saw earlier.
  • >FLASH: This ensures the section is placed in the FLASH memory region defined at the top of the script.

Step 3: Verification

After compiling your project, the best way to verify your work is to inspect the map file (.map). Open it and search for FIRMWARE_VERSION. You should see an entry confirming its address:

.config_section
                0x0801f000        0x4
 .config_section
                0x0801f000        0x4 your_project/Debug/../Core/Src/main.o
                0x0801f000                FIRMWARE_VERSION

This confirms that our variable is exactly where we intended it to be.

The following tutorial provides a concise, step-by-step guide to this entire process.

Customizing Memory Layout of Embedded Programs with GNU Linker Scripts

This VisualGDB tutorial is a fantastic, hands-on walkthrough of the exact process we just discussed. It demonstrates adding a variable, using the 'section' attribute, and modifying the linker script to place it at a specific address.

Read through steps 11, 12, and 13. This will reinforce the C attribute syntax, the linker script modification, and how to verify the result in the map file.


2. The Professional Challenge: Placing Initialized Data in RAM

Placing a constant in Flash is straightforward. A more complex and common scenario is placing a modifiable, initialized variable at a specific address in RAM.

Imagine you need a data buffer at a fixed RAM address to be shared with a DMA peripheral.
int dma_buffer[64] __attribute__((section(".dma_sec"))) = {1, 2, 3, ...};

This creates a challenge:

  • The variable dma_buffer must reside in RAM so it can be modified at runtime. This is its Virtual Memory Address (VMA).
  • Its initial values ({1, 2, 3, ...}) must be stored in Flash, because RAM is volatile and loses its contents on power-off. This is its Load Memory Address (LMA).

This is the exact same problem the linker solves for the standard .data section, and we must replicate the solution for our custom section.

Step 1: Update the Linker Script with VMA and LMA

We need to tell the linker that our section's runtime address (VMA) is in RAM, but its load-time address (LMA) is in Flash. We use the > symbol for VMA and AT> for LMA.

We'll also create symbols to mark the start and end of our section in both Flash and RAM, so the startup code can find it.

/* Add this to the end of the SECTIONS block */

  /*
   * The LMA of the .dma_sec will be after the LMA of the last loaded section (.data)
   * We store this address in a new symbol, _lma_dma_sec.
   */
  _lma_dma_sec = LOADADDR(.data) + SIZEOF(.data);

  /*
   * The VMA of the .dma_sec will be at a fixed address in RAM.
   */
  .dma_sec 0x20001000 : AT (_lma_dma_sec)
  {
    . = ALIGN(4);
    _s_dma_sec = .; /* Create a symbol for the start of the VMA */
    KEEP(*(.dma_sec))
    . = ALIGN(4);
    _e_dma_sec = .; /* Create a symbol for the end of the VMA */
  } >RAM

Here's the breakdown:

  • _lma_dma_sec = ...: We calculate the LMA for our new section. We place it in Flash immediately after the .data section's content. LOADADDR() gets the LMA and SIZEOF() gets the size.
  • .dma_sec 0x20001000: We define our output section and fix its VMA to 0x20001000 in RAM.
  • : AT (_lma_dma_sec): This is the crucial part. It tells the linker that the contents of this section should be loaded at the Flash address we calculated.
  • >RAM: This confirms that the VMA is in the RAM region.
  • _s_dma_sec / _e_dma_sec: We create symbols that mark the start and end addresses of our section in RAM. The startup code will use these as the destination for the copy.
STM32F410RB Memory Map and Startup Process
This diagram illustrates the separation of VMA and LMA. Our custom `.dma_sec` is conceptually similar to the `.data` section: its initial values are stored in Flash (LMA) and copied to RAM (VMA) at startup.

Step 2: Update the Startup Code

The default startup file (startup_stm32f103xb.s) only knows how to initialize .data and .bss. It has no idea our new .dma_sec exists. We must add code to copy its initial values from Flash to RAM.

You would need to modify the assembly in the Reset_Handler function. The logic is identical to the loop that copies the .data section. You need to:

  1. Load the LMA (_lma_dma_sec) into a register (source address).
  2. Load the VMA (_s_dma_sec) into another register (destination address).
  3. Load the end VMA (_e_dma_sec) into a third register (loop boundary).
  4. Loop, copying words from the source to the destination until the destination pointer reaches the end address.

While we won't edit the assembly file in this lesson, understanding this step is what separates a novice from a professional. When you take control of the memory map, you also take on the responsibility of ensuring it's initialized correctly.

The following resource provides an excellent explanation of this VMA/LMA problem and the need for a manual copy.

Customizing Memory Layout of Embedded Programs with GNU Linker Scripts

Let's return to the VisualGDB tutorial. This section shows the consequences of incorrectly placing an initialized variable in a new RAM region and then demonstrates the correct solution using separate VMA/LMA and a manual copy function.

Please read steps 21 through 25. Pay close attention to the explanation in step 21 about why simply placing the variable fails. Then, study the linker script modification in step 23 and the C helper function in step 25 that performs the manual copy. This is the complete, professional solution.


Conclusion

Today, you've gained a powerful new skill: the ability to command the linker to place code and data with surgical precision. This is a fundamental building block for many advanced embedded systems features.

Key Takeaways:

  • You can place a variable or function in a custom input section using the GCC __attribute__((section("..."))).
  • You must define a rule in the linker script's SECTIONS block to handle this custom section.
  • To place an object at a fixed address, you specify the address in the output section definition (e.g., .my_section 0xADDRESS :).
  • The KEEP(*(...)) directive is essential to prevent the linker from optimizing away your custom section if it's unreferenced.
  • For initialized data in a custom RAM section, you must define both a VMA (in RAM) and an LMA (in Flash) using the > RAM AT > FLASH syntax.
  • Crucially, when you define a custom initialized RAM section, you become responsible for adding the logic to the startup code to copy its data from Flash to RAM.

In our next and final lesson of this module, we will explore the startup file in greater depth and discuss how to debug low-level issues like stack overflows using your newfound knowledge of memory maps and linker scripts.