7. Image layout
At its core, the linker is responsible for transforming compiled object files into a final image that is ready to run on the device. This process includes resolving symbols, relocating sections, and most importantly, laying out the image in memory.
Image layout refers to the linker’s assignment of addresses to code, data and metadata determining their arrangement in memory when the program runs, and to the placement of contents within the final output image.
In this document, we will take a deep dive into the linker’s image layout process, examining the factors that influence the layout and the reasoning behind the specific choices the linker makes. Understanding why the image layout looks the way it does is challenging, but it’s invaluable for diagnosing and fixing subtle, hard-to-decode layout issues.
But why is the image layout important?
The image layout is critical for correctness, performance and security. Among other things, it ensures alignment compliance, enables cache-friendly placement of code and data, and enhances security by enforcing memory protection policies such as preventing any executable page from having write permissions. In embedded systems, image layout is especially critical due to tight memory constraints, real-time performance needs, and hardware-specific placement requirements.
Before we dive deep into defining and understanding how the linker does image layout, it would be useful to understand the distinction between some of the terminology used in the linker.
7.1. Linker terminology
7.1.1. Input Sections
A section holds content that can be code, data and metadata (Example: .text
, .data
, .bss
, .comment
). The linker may reorder, merge
or discard whole sections but it treats each section as an indivisible unit.
The section header table describes each section’s name, type, virtual address, file offset,
alignment and more. The section header table and the sections are used by the linker during
linking. These come from object files (.o) and libraries (.a) compiled from source code.
Examples:
.text — code
.data — initialized data
.bss — uninitialized data
.rodata — read-only data
7.1.2. Output sections
These are the merged and organized sections in the final binary, created by the linker.
The linker collects input sections of the same type and merges them into output sections.
You can rely on the default rules and built-in heuristics provided by the linker.
You can also control this mapping by using a linker script.
7.1.3. Segments
A segment is composed of one or more output sections and describes how the program should be loaded into memory at runtime. In ELF, The program header table describes each segment’s type, virtual and physical address, file size, memory size, permissions, and alignment requirements. A loader only looks at the program header table to load an executable into memory. It does not care about the section header table.
Key Points:
Segments are containers for output sections
They define memory permissions (read, write, execute)
Used by the runtime loader
7.1.3.1. Segment alignment
7.1.3.1.1. Empty segments
Loadable segments all have segment align set to the page size set at link time
Non loadable segments have the segment alignment set to the minimum word size of the output ELF file
7.1.3.1.2. Non Empty segments
Loadable non empty segments have the segment alignment set to the page size
Non loadable segments are set to the maximum section alignment of the containing sections in the segment
Now let’s get started with understanding the image layout created by eld.
7.2. Image layout
There are two main approaches of defining the image layout:
Using default behavior
Using custom linker scripts
7.2.1. Using default behavior
When a linker performs layout without an explicit linker script, it relies on default rules and built-in heuristics provided by the linker implementation.
The linker has a default memory layout that defines:
The order of sections (e.g., .text, .data, .bss)
Default starting addresses (e.g., code starts at 0x08000000 for embedded systems or 0x400000 for ELF binaries on Linux)
Alignment requirements for each section
Default page alignment
Whether program headers are loaded or not loaded
7.2.2. linker scripts
Note
When using eld with a custom linker script, all default assumptions and behaviors built into the linker are overridden. The script provides complete control over the memory layout and section placement, effectively replacing the linker’s internal defaults.
The linker script primary job is to define the image layout requirements. It achieves
this with the SECTIONS
, PHDRS
, and the MEMORY
commands.
SECTIONS
command specifies the output section properties and the mapping of input sections to the output sections.PHDRS
specifies which program headers (also known as segments) should be created by the linker. IfPHDRS
is not specified, then the linker creates sensible PHDRs based on some default rules.MEMORY
command specifies the available memory regions. The output sections can then be assigned to particular memory regions. It provides a convenient way of arranging the output sections into memory.
Linker Script describes these commands in more detail.
Here we will only highlight, or in some cases reiterate, some of the important and/or subtle layout-related features and behavior.
7.2.2.1. Output section contents
An output section is composed of input sections. If no SECTIONS
command
is provided, the linker applies sensible default rules to match the input
sections to the output sections.
When a SECTIONS
command is present, the linker script input section description,
commonly referred to as linker script rule or just rule, control the input-output section mapping.
Input sections that match no rule are called orphans sections. Each orphan section is placed
into an output section with the same name; if no such output section exists, then the linker
creates a new one.
7.2.2.2. Image base address (starting address)
The default image base address depend upon the target, the linking type (whether the link is creating an executable or a shared library), and whether the link contains a linker script. The address 0 is used as the starting address if a linker script is present or if the link is creating a shared library.
The default image base value can be overridden by using --image-base
command-line
option.
7.2.2.3. Output section virtual memory address (VMA)
Multiple mechanisms can assign or influence the addresses of output sections.
A linker script can specify addresses explicitly; command-line options can set
section start addresses; and script directives such as MEMORY
allows
to conveniently arrange sections without hard-coding absolute addresses. Let’s
examine all these methods, detail their behavior, and clarify their precedence.
7.2.2.3.1. Brief review of the methods
7.2.2.3.1.1. 1) No address/memory-region specified
If no explicit address or MEMORY region is specified for an output section,
it is placed at the current value of the location counter (.`
). The location counter
is initialized to the image base, and is automatically incremented whenever a content is added.
For example, if the value of the location counter is 0x1000 before assigning address to
.foo
output section (size 0x200), then foo
is placed at 0x1000
and
the location counter advances to 0x1200.
The location counter can be explicitly incremented with a linker script assignment:
. = . + ALIGN(0x8);
If an output section has an explicit address (or a memory region), then the location counter is set to the address (or the valid address in the memory region) before placing the output section.
There is an exception to this, orphan sections whose name ends with @<address>
are placed
exactly at the specified address.
7.2.2.3.1.2. 2) Explicit linker script VMA address
Linker script can specify an explicit address for an output section:
section [address] :
{
output-section-command
...
}
Let’s see this in action with the help of an example:
// 1.c
int foo() { return 1; }
int bar() { return 3; }
// script.t
SECTIONS {
.foo (0x1000) : { *(.text.foo) }
.bar (0x2000) { *(.text.bar) }
}
The linker assigns the exact addresses as specified in the linker script. .foo
is assigned the VMA 0x1000
and bar
is assigned the VMA 0x2000
.
The linker assigns the exact addresses even if the addresses are not exactly aligned.
For the below example, .foo
will get VMA assigned to 0x1001
and .bar
will get VMA assigned to 0x2002
even though they do not satisfy the alignment
requirements. In this case, the output section contains alignment padding at the beginning
such that the alignment requirements for the actual content (input sections) is satisfied.
// 1.c
int foo() { return 1; }
int bar() { return 3; }
// script.t
SECTIONS {
.foo (0x1001) : { *(.text.foo) }
.bar : AT(0x2002) { *(.text.bar) }
}
However, if ALIGN
is also specified, then the explicit VMA is aligned to the
alignment specified in ALIGN
.
7.2.2.3.1.3. 3) Command-line options for setting section addresses
There are various linker command-line options for setting output section
VMA: -Tbss
, -Tdata
, -Ttext
and
--section-start
.
When both the linker script and the command line specify an output-section address, the command-line option takes precedence and overrides the script’s explicit address.
7.2.2.3.1.4. 4) Memory region
Assigning an output section to a MEMORY region places it at the region’s next available address, subject to alignment and size constraints.
Let’s understand this with the help of an example:
// 1.c int a; int u = 11; int v = 13; int foo() { return 1; } int bar() { return 3; }
// script.t
MEMORY {
RAM : ORIGIN = 0x1000, LENGTH = 0x1000
}
SECTIONS {
.data : { *(.data*) } >RAM
.foo : { *(.text.foo) } >RAM
.bss : {
*(.bss*)
} >RAM
.bar : { *(.text.bar) } >RAM
}
For the example above, the virtual addresses of the output section are shown below, along with the assumed size and alignment.
.data
(size: 0x8; alignment: 0x8): 0x1000.foo
(size: 0x8; alignment: 0x8): 0x1008.bss
(size: 0x4; alignment: 0x4): 0x1010.bar
(size: 0x8; alignment: 0x8): 0x1014
7.2.2.3.2. Precedence of virtual address assignment methods
The precedence is:
Command-line options for setting section addresses
Explicit linker script VMA address
Memory region
No address/memory-region specified
7.2.2.4. Segment creation when PHDRS
is not specified
eld uses a set of heuristics to decide when to start a new segment. Before detailing those heuristics, we need to note how eld walks the layout:
eld processes output sections in the order specified by the linker script; if no script is provided, then it uses the order in which the linker has created default output sections.
We use the below terms to describe when eld creates a segment:
Previous output section to refer to the output section that immediately precedes the current one in the traversal order.
Previous allocatable output section to refer to the nearest preceding allocatable output section.
Previous LOAD segment to refer to the segment of the nearest preceding allocatable output section.
With this traversal model and the terms in mind, we can now describe the segment-creation heuristics.
A new load segment is created when:
There is no previous LOAD segment.
Explicit output section address has been set using linker command-line options such as:
-Ttext
,--section-start
, …The segment flags required for the current output section is incompatible with the previous LOAD segment. By default, the segment flags
R
andRE
are compatible, whereasRW
is incompatible withR
andRW
.--rosegment
and--omagic
influence which sections can be part of the same segment.The memory region of the current output section is different than the memory region of the previous allocatable output section.
The previous output section type is NOBITS and the current output section type is PROGBITS.
The previous output section virtual memory address is greater than the current output section virtual memory address.
The VMA difference between the previous allocatable output section is greater than the segment alignment.
Other segment types:
A
PT_TLS
segment is created when an input file contains.tdata
/.tbss
.A
PT_DYNAMIC
segment is created when a shared library or a dynamic executable is getting built.A
PT_GNU_EH_FRAME
segment is created when the output containseh_frame_hdr
section.
7.2.2.5. Segment creation when PHDRS
is specified
When the PHDRS
is specified in the linker script, then the linker only
creates the section specified in the PHDRS
command. No additional segments
are created.
7.2.2.6. Linker script assignment evaluation
Linker script assignment evaluation can influence the image layout as the location counter value can be modified using a linker script assignment and the location counter controls where the next content would be placed.
eld does not support lazy expression evaluation and forward references in expressions.
In eld, the linker script assignments outside the SECTIONS
command are evaluated
before the linker script assignments inside the SECTIONS
command. The linker script
assignments order is:
First, all the linker script assignments outside the
SECTIONS
command are evaluated in the specified order.Then all the linker script assignments inside the
SECTIONS
command are evaluated in the specified order.
The linker script assignments specified using --defsym
are considered as
outside-SECTIONS
linker script assignments.
7.2.3. Linker options that affect layout
7.2.3.1. --rosegment
By default, readonly non-executable (R
) sections such as .rodata
sections and
executable sections (RX
) such as .text
can be part of the same segment. When
--rosegment
is specified, a different segment is created for readonly non-executable
segments.
7.2.3.2. --omagic
If --omagic
is specified, then readonly non-executable (R
),
executable (RX
), and read-write (RW
) sections can be part of the same segment.
Moreover, the segment alignment is set to the maximum section alignment instead of the page
alignment.
7.2.3.3. --align-segments
This option cannot be used with linker scripts. When used, the addresses of segments (both virtual and physical addresses) are aligned to the page boundaries.
7.2.4. Advanced image layout control using linker plugins
More finer-grained control over the image layout can be achieved using linker plugins.
Custom Layout Logic: Plugins can override default section placement logic to optimize for cache locality or hardware-specific constraints.
Dynamic Behavior: Unlike static scripts, plugins can make layout decisions based on runtime metadata or build-time heuristics.
Programmatic Layout: Developers can write plugins (e.g., LayoutOptimizer) that programmatically define how sections are arranged, enabling more flexible and performance-tuned binaries
This approach is especially useful in embedded systems where:
Memory is constrained and fragmented.
Performance depends on precise placement of code/data.
Debugging requires reproducible and traceable layouts.