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 containingZI
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.o
, obj2.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 ofFIXED
. 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 anAREA
. - 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 witharmlink --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
- 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); }
- Create
function.c
:int sqr(int n1) { return n1 * n1; }
- 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
- 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); }
- Create
function.c
:int sqr(int n1) { return n1 * n1; }
- 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 { } }
- 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
- 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); }
- Create
function.c
:int sqr(int n1) { return n1 * n1; }
- 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 { } }
- 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 ofinit.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
fromdata.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
and0x3000
, 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
wherealgorithm
is one offirst_fit
,worst_fit
,best_fit
, ornext_fit
. -
--any_sort_order=order
whereorder
iscmdline
ordescending_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, butarmlink
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 ALIGN
, ALIGNALL
, 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.