Panel For Example Panel For Example Panel For Example

Deep Dive into MDK Scatter-Loading Files

Author : Adrian September 17, 2025

MDK scatter-loading guide

Introduction

This article provides an in-depth analysis of MDK scatter-loading files and the program loading process to clarify this topic.

Specifying Stack and Heap with the Scatter File

The ARM C library provides multiple implementations of the function __user_setup_stackheap(), and the correct implementation can be selected automatically based on information in the scatter file.

To select a two-region memory model, define two special execution regions in the scatter file named ARM_LIB_HEAP and ARM_LIB_STACK. Both regions should have the EMPTY attribute, which causes the library to select a non-default implementation of __user_setup_stackheap() that uses the following symbol names:

Image$$ARM_LIB_STACK$$Base Image$$ARM_LIB_STACK$$ZI$$Limit Image$$ARM_LIB_HEAP$$Base Image$$ARM_LIB_HEAP$$ZI$$Limit

Only one ARM_LIB_STACK or ARM_LIB_HEAP region can be specified, and each must include a size. For example:

ARM_LIB_HEAP 0x20100000 EMPTY 0x100000-0x8000 ; Heap starts at 1MB ARM_LIB_STACK 0x20200000 EMPTY -0x8000 ; Stack space starts at the end

You can use a combined stack and heap region by defining a single execution region named ARM_LIB_STACKHEAP with the EMPTY attribute. This makes __user_setup_stackheap() use the symbols Image$$ARM_LIB_STACKHEAP$$Base and Image$$ARM_LIB_STACKHEAP$$ZI$$Limit. Note that redefining __user_setup_stackheap() overrides the library implementations.

Creating a Root Execution Region

To mark a region as a root region in the scatter file, you can:

  • Specify

    ABSOLUTE as the region attribute (explicitly or by leaving the default), and use the same address for the first execution region and the enclosing load region. To make an execution region's address match the load region's address, either:

    • Specify the same numeric value for the execution region base and the load region base; or
    • Use an offset of

      +0 for the first execution region in the load region. If

      +0 is used for all subsequent execution regions in the load region, then all execution regions that do not follow a region containing

      ZI are also root regions.

The following example shows an implicitly defined root region:

LR_1 0x040000 ; load region starts at 0x40000 { ER_RO 0x040000 ; load address = execution address { * (+RO) ; all RO sections (must include section with ; initial entry point) } ... ; rest of scatter-loading description }

Using the FIXED Attribute

Using the FIXED execution region attribute ensures that a region's load address and execution address are the same. You can place any execution region at a specific address in ROM using FIXED. For example, the following map shows a fixed execution region:

LR_1 0x040000 ; load region starts at 0x40000 { ER_RO 0x040000 ; load address = execution address { * (+RO) ; RO sections other than those in init.o } ER_INIT 0x080000 FIXED ; load address and execution address of this ; execution region are fixed at 0x80000 { init.o(+RO) ; all RO sections from init.o } ... ; rest of scatter-loading description }

The same scatter description is shown above to illustrate placement.

Examples of Misusing the FIXED Attribute

The following example shows a common incorrect usage:

LR1 0x8000 { ER_LOW +0 0x1000 { *(+RO) } ; At this point the next available Load and Execution address is 0x8000 + size of ; contents of ER_LOW. The maximum size is limited to 0x1000 so the next available Load ; and Execution address is at most 0x9000 ER_HIGH 0xF0000000 FIXED { *(+RW+ZI) } ; The required execution address and load address is 0xF0000000. The linker inserts ; 0xF0000000 - (0x8000 + size of(ER_LOW)) bytes of padding so that load address matches ; execution address }

Creating Root Regions with FIXED

You can use the FIXED attribute in an execution region to create regions that load and execute at fixed addresses. FIXED is useful in single load-region devices, e.g., ROM, to place functions or data blocks (constant tables, checksums) at specific ROM addresses for easy pointer-based access.

If you place initialization code at the start of ROM and a checksum at the end, some memory between them may be unused. Use * or .ANY module selectors to fill the gap between those blocks.

For maintainability and ease of debugging, minimize explicit layout specifications in the scatter file and leave detailed function/data layout to the linker.

Partially Linked Component Objects

You cannot reference component object names that were discarded by partial linking. For example, if obj1.oobj2.o, and a partially linked obj3.o are combined into obj_all.o, the component object names are discarded in the resulting object. You cannot reference obj1.o by name; you must reference obj_all.o.

When FIXED Is Not Appropriate

In some cases, using

FIXED within a single load region is inappropriate. Alternatives include:

  • Use multiple load regions if your bootloader supports them, placing RO code or data in a dedicated load region.
  • If a function or data does not need to reside at a fixed ROM address, use

    ABSOLUTE instead of

    FIXED. The loader will copy data from the load region to RAM at the specified execution address.

    ABSOLUTE is the default attribute.

  • To place data structures at memory-mapped I/O addresses, use two load regions and specify

    UNINIT.

    UNINIT ensures the memory location is not zero-initialized.

Placing Functions and Data at Specific Addresses

Compilers typically produce RO, RW, and ZI sections from a single source file. To place a single function or data item at a fixed address, the linker must be able to treat that function/data separately from the rest of the inputs.

The linker supports two methods for placing sections at specific addresses:

  • Define an execution region in the scatter file at the desired address with a section selector that picks only the target section.
  • Use specially named sections from the section name to obtain placement; these are the

    __at sections.

To place a function or variable at a specific address, it must reside in its own section. Methods include:

  • Place the function or data item in its own source file.
  • Use an

    __attribute__((at(address))) to place a variable at an absolute address.

  • Use

    __attribute__((section("name"))) to place a function or variable in a named section.

  • Use the assembler

    AREA directive; in assembly the minimal relocatable unit is an

    AREA.

  • Use the compiler option

    --split_sections to generate one ELF section per function in a source file. This can slightly increase some function sizes but, combined with

    armlink --remove, allows the linker to discard unused functions and may reduce final image size.

Example: Placing a Variable at a Specific Address without a Scatter File

  1. Create

    main.c:

    #include <stdio.h> extern int sqr(int n1); int gSquared __attribute__((at(0x5000))); // Place at 0x5000 int main() { gSquared = sqr(3); printf("Value squared is: %d\n", gSquared); }

  2. Create

    function.c:

    int sqr(int n1) { return n1 * n1; }

  3. Compile and link:

    armcc -c -g function.c armcc -c -g main.c armlink --map function.o main.o -o squared.axf

    The

    --map option generates the memory map (.map) file.

    --autoat is the default.

In this example, __attribute__((at(0x5000))) places the global variable gSquared at absolute address 0x00005000. The variable is placed in execution region ER$$.ARM.__AT_0x00005000 and load region LR$$.ARM.__AT_0x00005000.

Example: Using a Scatter File to Place a Variable in a Specific Section

  1. Create

    main.c:

    #include <stdio.h> extern int sqr(int n1); int gSquared __attribute__((section("foo"))); // Place in section foo int main() { gSquared = sqr(3); printf("Value squared is: %d\n", gSquared); }

  2. Create

    function.c:

    int sqr(int n1) { return n1 * n1; }

  3. Create

    scatter.scat:

    LR1 0x0000 0x20000 { ER1 0x0 0x2000 { *(+RO) ; rest of code and read-only data } ER2 0x8000 0x2000 { main.o } ER3 0x10000 0x2000 { function.o *(foo) ; Place gSquared in ER3 } RAM 0x200000 (0x1FF00-0x2000) ; RW & ZI data to be placed at 0x200000 { *(+RW, +ZI) } ARM_LIB_STACK 0x800000 EMPTY -0x10000 { } ARM_LIB_HEAP +0 EMPTY 0x10000 { } }

  4. Compile and link:

    armcc -c -g function.c armcc -c -g main.c armlink --map --scatter=scatter.scat function.o main.o -o squared.axf

The memory map shows the foo section placed in ER3 at address 0x00010000.

Note: if *(foo) is omitted from the scatter file, that section will be placed in the default region of the same type (in this example, the RAM region).

Example: Using a Scatter File to Place a Variable at a Specific Address

  1. Create

    main.c:

    #include <stdio.h> extern int sqr(int n1); // Place at address 0x10000 const int gValue __attribute__((section(".ARM.__at_0x10000"))) = 3; int main() { int squared; squared = sqr(gValue); printf("Value squared is: %d\n", squared); }

  2. Create

    function.c:

    int sqr(int n1) { return n1 * n1; }

  3. Create

    scatter.scat:

    LR1 0x0 { ER1 0x0 { *(+RO) ; rest of code and read-only data } ER2 +0 { function.o *(.ARM.__at_0x10000) ; Place gValue at 0x10000 } RAM 0x200000 (0x1FF00-0x2000) ; RW & ZI data to be placed at 0x200000 { *(+RW, +ZI) } ARM_LIB_STACK 0x800000 EMPTY -0x10000 { } ARM_LIB_HEAP +0 EMPTY 0x10000 { } }

  4. Compile and link:

    armcc -c -g function.c armcc -c -g main.c armlink --no_autoat --scatter=scatter.scat --map function.o main.o -o squared.axf

The memory map shows the variable placed in ER2 at address 0x00010000. In this example ER1 has an unknown size, so gValue could be placed in ER1 or ER2. To ensure placement in ER2, include the corresponding selector and link with --no_autoat. If --no_autoat is omitted, gValue will be placed in a separate load region LR$$.ARM.__AT_0x00010000 containing execution region ER$$.ARM.__AT_0x00020000.

Variable Section Specification Examples

Method 1:

int variable __attribute__((section("foo"))) = 10;

Method 2:

// place variable1 in a section called .ARM.__at_0x00008000 int variable1 __attribute__((at(0x8000))) = 10; // place variable2 in a section called .ARM.__at_0x8000 int variable2 __attribute__((section(".ARM.__at_0x8000"))) = 10;

Specifying Function Addresses

int sqr(int n1) __attribute__((section(".ARM.__at_0x20000"))); int sqr(int n1) { return n1 * n1; }

Note: If no scatter-loading file is used, such sections are placed in the default ER_RW execution region of LR_1. If the source uses an undefined section name (not present in the scatter file), that section is placed in the defined RW execution region. The --autoat or --no_autoat options do not affect placement of named sections.

Explicitly Placing Named Sections Using Scatter Loading

The following example shows explicit placement of named sections with a scatter file:

LR1 0x0 0x10000 { ER1 0x0 0x2000 ; Root Region, containing init code { init.o(INIT, +FIRST) ; place init code at exactly 0x0 *(+RO) ; rest of code and read-only data } RAM_RW 0x400000 (0x1FF00-0x2000) ; RW & ZI data to be placed at 0x400000 { *(+RW) } RAM_ZI +0 { *(+ZI) } DATABLOCK 0x1FF00 0xFF ; execution region at 0x1FF00 { data.o(+RO-DATA) ; place RO data between 0x1FF00 and 0x1FFFF } }

This scatter description places:

  • Initialization code in the

    INIT section of

    init.o, starting at 0x0, followed by the remaining RO code and RO data from other objects.

  • All global RW variables in RAM at 0x400000.
  • All

    RO-DATA from

    data.o at 0x1FF00.

Placing Unassigned Sections with .ANY

The linker attempts to place input sections into specific execution regions. For any input sections that cannot be resolved and whose placement is unimportant, use the .ANY module selector in the scatter file.

In most cases, a single .ANY selector is equivalent to using the * selector. The difference is that you may specify .ANY in multiple execution regions.

Default Rules for Unassigned Sections

By default, the linker places unassigned sections as follows:

  • Place an unassigned section into the execution region that currently has the most available space. You can specify the maximum space allowed for unassigned sections in an execution region with the

    ANY_SIZE attribute.

  • Sort sections by descending size.

Placement Rules When Multiple .ANY Selectors Exist

When multiple .ANY selectors appear in the scatter file, the linker takes the largest unassigned section and assigns it to the most specific .ANY execution region that has sufficient space. For example, .ANY(.text) is considered more specific than .ANY(+RO).

If multiple execution regions share the same specificity, the section is assigned to the execution region with the most available remaining space.

Examples:

  • If you have two equally specific execution regions where one has a size limit of

    0x2000 and the other has no limit, all sections are allocated to the unbounded

    .ANY region.

  • If two equally specific execution regions have limits

    0x2000 and

    0x3000, the largest sections are allocated to the larger (0x3000) region until its remaining space drops to 0x2000; then assignment alternates between the two regions.

.ANY Priority

You can give multiple .ANY selectors a priority by appending a number. Higher integers indicate higher priority. Example using .ANYnum:

lr1 0x8000 1024 { er1 +0 512 { .ANY1(+RO) ; evenly distributed with er3 } er2 +0 256 { .ANY2(+RO) ; Highest priority, so filled first } er3 +0 256 { .ANY1(+RO) ; evenly distributed with er1 } }

Controlling Placement for Multiple .ANY Selectors

You can modify how the linker places unassigned input sections when multiple selectors exist by choosing a placement algorithm or sort order. Available command-line options:

  • --any_placement=algorithm where

    algorithm is one of

    first_fit,

    worst_fit,

    best_fit, or

    next_fit.

  • --any_sort_order=order where

    order is

    cmdline or

    descending_size.

Use first_fit to fill regions sequentially, best_fit to maximize fill, worst_fit to distribute evenly, and next_fit for more deterministic patterns.

If the linker attempts to fill a region to its limit (common with first_fit and best_fit), it might overfill due to unknown generated content (padding, tables). In that case you may see:

Error: L6220E: Execution region regionname size (size bytes) exceeds limit (limit bytes).

The --any_contingency option prevents the linker from filling a region to its maximum by reserving a portion of the region for linker-generated content, and only using it if other regions have no space. This is enabled by default for first_fit and best_fit.

Specifying Maximum Size for Unassigned Sections

The execution-region attribute ANY_SIZE max_size controls the maximum amount of space armlink can fill with unassigned sections.

Limitations:

  • max_size must be less than or equal to the region size.

  • ANY_SIZE may be used on a region without a

    .ANY selector, but

    armlink will ignore it.

When ANY_SIZE is present, armlink:

  • Does not override a given

    .ANY size to add more sections later.

  • Never recalculates contingency.
  • Never assigns sections into contingency space.

Using __at to Place Peripheral Registers

To place an uninitialized variable at a peripheral register address, use a __at section. For example, to use a register at 0x10000000:

int foo __attribute__((section(".ARM.__at_0x10000000"), zero_init)); ER_PERIPHERAL 0x10000000 UNINIT { *(.ARM.__at_0x10000000) }

Using automatic placement, and assuming there is no nearby execution region, the linker will create an UNINIT execution region at 0x10000000. The UNINIT attribute indicates the region contains uninitialized data or memory-mapped I/O.

Reserving an Empty Region

Use the EMPTY attribute in an execution region to reserve memory for stacks or other virtual ZI regions. The reserved block is not part of the load region and is only allocated at execution. Since it is created as a virtual ZI region, the linker provides the following symbols:

Image$$region_name$$ZI$$Base Image$$region_name$$ZI$$Limit Image$$region_name$$ZI$$Length

If the length is negative, the address is treated as the region end address and must be an absolute address, not relative.

Example:

LR_1 0x80000 ; load region starts at 0x80000 { STACK 0x800000 EMPTY -0x10000 ; region ends at 0x800000 because of the ; negative length. The start of the region ; is calculated using the length. { ; Empty region used to place stack } HEAP +0 EMPTY 0x10000 ; region starts at the end of previous ; region. End of region calculated using ; positive length { ; Empty region used to place heap } ... ; rest of scatter-loading description... }

Note: Virtual ZI regions created by EMPTY are not zero-initialized at runtime.

If an address is relative (+offset) and the length is negative, the linker will report an error.

For the example above, the linker generates:

Image$$STACK$$ZI$$Base = 0x7f0000 Image$$STACK$$ZI$$Limit = 0x800000 Image$$STACK$$ZI$$Length = 0x10000

The EMPTY attribute applies only to execution regions. The linker warns and ignores an EMPTY attribute used in a load region definition. The linker also checks that the address range of the EMPTY region does not conflict with any other execution region.

Using Preprocessing in Scatter Files

You can pass scatter files through the C preprocessor. This enables use of preprocessor features. Specify the preprocessor command on the first line of the scatter file:

#! preprocessor [pre_processor_flags]

A common directive is #! armcc -E, which runs the scatter file through the armcc preprocessor.

Examples of uses:

  • Add preprocessor directives at the top of the scatter file.
  • Use simple expression evaluation within the scatter file.

Example scatter file using preprocessing:

#! armcc -E #define ADDRESS 0x20000000 #include "include_file_1.h" lr1 ADDRESS { ... }

The linker parses the preprocessed scatter file and treats the preprocessor directives as comments.

You can also use the linker --predefine option. For example, remove the directive from file.scat and place #define ADDRESS 0x20000000 via the command line:

armlink --predefine="-DAADDRESS=0x20000000" --scatter=file.scat

Avoiding Padding in Scatter Files Using Expressions

Using ALIGNALIGNALL, or FIXED in scatter file attributes can introduce significant padding in the image. To avoid this padding, compute start addresses using expressions. The built-in function AlignExpr helps specify address expressions.

Example that generates padding:

LR1 0x4000 { ER1 +0 ALIGN 0x8000 { ... } }

The ALIGN keyword aligns ER1 on a 0x8000 boundary in both load and execution views, forcing the linker to insert 0x4000 bytes of padding in the load view.

Equivalent scatter file that avoids padding:

LR1 0x4000 { ER1 AlignExpr(+0, 0x8000) { ... } }

Using AlignExpr(+0, 0x8000) aligns the execution address to 0x8000 while keeping the load address at 0x4000, avoiding padding in the image.