Skip to content

Commit 54993f2

Browse files
committed
Add orphaned sections article
1 parent 5a5f31d commit 54993f2

File tree

1 file changed

+284
-0
lines changed

1 file changed

+284
-0
lines changed
Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
---
2+
title: "Linkers and orphaned sections"
3+
date: 2025-02-25T21:13:08+01:00
4+
author: Javier Alvarez
5+
layout: post
6+
tags:
7+
- ARM
8+
- Binutils
9+
- GNU
10+
- ld
11+
- lld
12+
- Linker Script
13+
- Microcontroller
14+
---
15+
16+
## Problem statement
17+
18+
I recently came across a situation in a project where I had the following code:
19+
20+
```cpp
21+
struct FaultInfo final {
22+
uint32_t r0;
23+
uint32_t r1;
24+
// And all the other register state of a Cortex-M0+ processor
25+
// ...
26+
27+
uint32_t crc;
28+
};
29+
30+
[[gnu::section(".uninit")]] volatile FaultInfo fault_data;
31+
```
32+
33+
I was using this static region of data to persist some fault information across
34+
reboots, to log it on the next boot after recovering from the fault.
35+
36+
After an MCU fault, it can be challenging to determine which hardware and software components are
37+
still functioning correctly. Thus, it's best to rely on a minimal set of hardware until the MCU is
38+
rebooted, after which fault logging can be performed to persistent storage or a communication
39+
interface once the system is stable.
40+
41+
Of course, this code went along with the following output section declaration
42+
in the linker script:
43+
44+
```
45+
.uninit ALIGN(4) (NOLOAD) : {
46+
KEEP(*(.uninit))
47+
} > RAM : uninit
48+
```
49+
50+
This worked well until one day, a refactor affected the linker script, and the `.uninit` output section
51+
was mistakenly removed.
52+
53+
You would think that now this fault data would be placed in `.bss` or `.data`,
54+
or maybe the linker would error out, right? Well, the answer is, it is complicated.
55+
56+
## Where did my uninitialized data go?
57+
58+
When the linker processes an input section in an object file that does not match any
59+
of the input section matchers defined in the linker script, the section becomes orphaned.
60+
61+
Orphaned sections are copied to the target ELF executable. So far, so good, because it
62+
looks like it keeps the same behavior we had with the linker script above. However,
63+
where in memory does this section go?
64+
65+
This is where things break down. Multiple linkers exhibit different behavior,
66+
and the rules seem to be pretty complex, depending on whether any PHDRS have
67+
been explicitly declared, and whether any MEMORY nodes are present in the linker
68+
script.
69+
70+
For a long time, the issue went unnoticed since the section was placed in RAM, which was the
71+
intended behavior. However, at some point, the section was placed in FLASH instead, causing the
72+
application to fail. That's how I realized this was an issue.
73+
74+
### Figuring out where the section is being placed
75+
76+
I used `readelf` to figure out where the section was being placed, as well as the corresponding segment
77+
in which the section was placed. To figure out the placement of the section you can run:
78+
79+
```
80+
$ arm-none-eabi-readelf -S $MY_ELF
81+
There are 26 section headers, starting at offset 0x488bc:
82+
83+
Section Headers:
84+
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
85+
[ 0] NULL 00000000 000000 000000 00 0 0 0
86+
[ 1] .root_section PROGBITS 08000000 010000 000040 00 A 0 0 4
87+
[ 2] .text PROGBITS 08000040 010040 00078a 00 AX 0 0 4
88+
[ 3] .rodata PROGBITS 080007cc 0107cc 0000d2 00 AMS 0 0 4
89+
[ 4] .data PROGBITS 20000000 020000 000010 00 WA 0 0 4
90+
[ 5] .uninit PROGBITS 20000010 020010 000020 00 WA 0 0 4
91+
[ 6] .bss NOBITS 20000030 020030 00047c 00 WA 0 0 4
92+
[ 7] .comment PROGBITS 00000000 020030 000029 01 MS 0 0 1
93+
[ 8] .symtab SYMTAB 00000000 02005c 000730 10 9 59 4
94+
[ 9] .strtab STRTAB 00000000 02078c 000907 00 0 0 1
95+
[10] .shstrtab STRTAB 00000000 021093 00012d 00 0 0 1
96+
[11] .debug_loclists PROGBITS 00000000 0211c0 0000b8 00 0 0 1
97+
[12] .debug_abbrev PROGBITS 00000000 021278 00177e 00 0 0 1
98+
[13] .debug_info PROGBITS 00000000 0229f6 013182 00 0 0 1
99+
[14] .debug_str_offset PROGBITS 00000000 035b78 0000a4 00 0 0 1
100+
[15] .debug_str PROGBITS 00000000 035c1c 00c3ab 01 MS 0 0 1
101+
[16] .debug_addr PROGBITS 00000000 041fc7 000028 00 0 0 1
102+
[17] .debug_frame PROGBITS 00000000 041ff0 000490 00 0 0 4
103+
[18] .debug_line PROGBITS 00000000 042480 0042b9 00 0 0 1
104+
[19] .debug_line_str PROGBITS 00000000 046739 0002af 01 MS 0 0 1
105+
[20] .debug_loc PROGBITS 00000000 0469e8 00172a 00 0 0 1
106+
[21] .debug_ranges PROGBITS 00000000 048112 000478 00 0 0 1
107+
[22] .debug_aranges PROGBITS 00000000 04858a 000020 00 0 0 1
108+
[23] .interned_strings PROGBITS 00000000 0485aa 0002f8 00 0 0 1
109+
[24] .postform_config PROGBITS 00000000 0488a4 000004 00 0 0 4
110+
[25] .postform_version PROGBITS 00000000 0488a8 000011 00 0 0 1
111+
Key to Flags:
112+
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
113+
L (link order), O (extra OS processing required), G (group), T (TLS),
114+
C (compressed), x (unknown), o (OS specific), E (exclude),
115+
y (purecode), p (processor specific)
116+
```
117+
118+
Here you can see the exact location of the section in memory, at `0x20000010`. The `PROGBITS` type
119+
indicates that the section does have initial data, which should not be the case, considering we do not
120+
plan on initializing the data, so there's also no point in storing it.
121+
122+
```
123+
$ arm-none-eabi-readelf -l $MY_ELF
124+
125+
Elf file type is EXEC (Executable file)
126+
Entry point 0x8000495
127+
There are 5 program headers, starting at offset 52
128+
129+
Program Headers:
130+
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
131+
LOAD 0x010000 0x08000000 0x08000000 0x00040 0x00040 R E 0x10000
132+
LOAD 0x010040 0x08000040 0x08000040 0x0078a 0x0078a R E 0x10000
133+
LOAD 0x0107cc 0x080007cc 0x080007cc 0x000d2 0x000d2 R 0x10000
134+
LOAD 0x020000 0x20000000 0x080008a0 0x00030 0x00030 RW 0x10000
135+
LOAD 0x020030 0x20000030 0x20000030 0x00000 0x0047c RW 0x10000
136+
137+
Section to Segment mapping:
138+
Segment Sections...
139+
00 .root_section
140+
01 .text
141+
02 .rodata
142+
03 .data .uninit
143+
04 .bss
144+
```
145+
146+
And here we see that the `.uninit` data section is just being bundled in the same data segment as
147+
the `.data` section. This is incorrect because an ELF loader will attempt to load .uninit from the
148+
ELF file into memory, which contradicts the intent of keeping it uninitialized. Fortunately, this
149+
isn't a problem in our case since this is a bare-metal application that doesn't use an ELF loader.
150+
But regardless of this, we are storing more data than we need into the ELF file, bloating its initial
151+
size. Ideally we want this section to be placed in a segment with `FileSiz` of 0, like the segment
152+
number 4, corresponding to the `.bss` section.
153+
154+
Now let's look at the fixed ELF, after the section is declared:
155+
156+
```
157+
$ arm-none-eabi-readelf -S $MY_ELF
158+
There are 26 section headers, starting at offset 0x48d18:
159+
160+
Section Headers:
161+
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
162+
[ 0] NULL 00000000 000000 000000 00 0 0 0
163+
[ 1] .root_section PROGBITS 08000000 010000 000040 00 A 0 0 4
164+
[ 2] .text PROGBITS 08000040 010040 00078a 00 AX 0 0 4
165+
[ 3] .rodata PROGBITS 080007cc 0107cc 0000d2 00 AMS 0 0 4
166+
[ 4] .data PROGBITS 20000000 020000 000010 00 WA 0 0 4
167+
[ 5] .bss NOBITS 20000010 020010 00047c 00 WA 0 0 4
168+
[ 6] .uninit NOBITS 2000048c 02048c 000020 00 WA 0 0 4
169+
[ 7] .comment PROGBITS 00000000 02048c 000029 01 MS 0 0 1
170+
[ 8] .symtab SYMTAB 00000000 0204b8 000730 10 9 59 4
171+
[ 9] .strtab STRTAB 00000000 020be8 000907 00 0 0 1
172+
[10] .shstrtab STRTAB 00000000 0214ef 00012d 00 0 0 1
173+
[11] .debug_loclists PROGBITS 00000000 02161c 0000b8 00 0 0 1
174+
[12] .debug_abbrev PROGBITS 00000000 0216d4 00177e 00 0 0 1
175+
[13] .debug_info PROGBITS 00000000 022e52 013182 00 0 0 1
176+
[14] .debug_str_offset PROGBITS 00000000 035fd4 0000a4 00 0 0 1
177+
[15] .debug_str PROGBITS 00000000 036078 00c3ab 01 MS 0 0 1
178+
[16] .debug_addr PROGBITS 00000000 042423 000028 00 0 0 1
179+
[17] .debug_frame PROGBITS 00000000 04244c 000490 00 0 0 4
180+
[18] .debug_line PROGBITS 00000000 0428dc 0042b9 00 0 0 1
181+
[19] .debug_line_str PROGBITS 00000000 046b95 0002af 01 MS 0 0 1
182+
[20] .debug_loc PROGBITS 00000000 046e44 00172a 00 0 0 1
183+
[21] .debug_ranges PROGBITS 00000000 04856e 000478 00 0 0 1
184+
[22] .debug_aranges PROGBITS 00000000 0489e6 000020 00 0 0 1
185+
[23] .interned_strings PROGBITS 00000000 048a06 0002f8 00 0 0 1
186+
[24] .postform_config PROGBITS 00000000 048d00 000004 00 0 0 4
187+
[25] .postform_version PROGBITS 00000000 048d04 000011 00 0 0 1
188+
Key to Flags:
189+
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
190+
L (link order), O (extra OS processing required), G (group), T (TLS),
191+
C (compressed), x (unknown), o (OS specific), E (exclude),
192+
y (purecode), p (processor specific)
193+
```
194+
195+
Now, the `.uninit` section is marked as NOBITS, meaning it will be allocated in memory but not stored
196+
in the ELF file, ensuring that no unnecessary data is included in the binary. Let's see the segments:
197+
198+
```
199+
$ arm-none-eabi-readelf -l $MY_ELF
200+
201+
Elf file type is EXEC (Executable file)
202+
Entry point 0x8000495
203+
There are 6 program headers, starting at offset 52
204+
205+
Program Headers:
206+
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
207+
LOAD 0x010000 0x08000000 0x08000000 0x00040 0x00040 R E 0x10000
208+
LOAD 0x010040 0x08000040 0x08000040 0x0078a 0x0078a R E 0x10000
209+
LOAD 0x0107cc 0x080007cc 0x080007cc 0x000d2 0x000d2 R 0x10000
210+
LOAD 0x020000 0x20000000 0x080008a0 0x00010 0x00010 RW 0x10000
211+
LOAD 0x020010 0x20000010 0x20000010 0x00000 0x0047c RW 0x10000
212+
LOAD 0x02048c 0x2000048c 0x2000048c 0x00000 0x00020 RW 0x10000
213+
214+
Section to Segment mapping:
215+
Segment Sections...
216+
00 .root_section
217+
01 .text
218+
02 .rodata
219+
03 .data
220+
04 .bss
221+
05 .uninit
222+
```
223+
224+
And now the ELF is fixed, and the `FileSiz` of section 5, in which the `.uninit`
225+
section is allocated, has the right size of 0.
226+
227+
## How can we make the linker script more robust?
228+
229+
After finding this issue, I looked for a way to detect this kind of problem,
230+
ideally at compile-time. I found the `--orphaned-handling` flag of the `ld` and `lld` linkers[^1].
231+
This flag allows us to specify what should be the behavior when an orphaned
232+
section is encountered. You have the following options:
233+
234+
- `place`: the worst option. Just silently ignores that this section is orphaned
235+
and places it somewhere in memory.
236+
- `warn`: same as place, but at least it emits a warning when linking.
237+
- `error`: trigger a link-time error when a section is orphaned.
238+
- `discard`: The data in the orphaned section is just dropped.
239+
240+
By setting `--orphan-handling=error`, we prevent silent misplacement of sections,
241+
ensuring a predictable memory layout. This serves as a safeguard against subtle
242+
and hard-to-diagnose issues in embedded applications.
243+
244+
However, setting the `--orphan-handling=error` flag means that the binary
245+
does no longer compile. The reason behind this is that I did NOT declare all
246+
output sections in the linker script, even after I added the `.uninit`
247+
section back.
248+
249+
The problem is that there is a set of sections that are always required for
250+
any [ELF](https://en.wikipedia.org/wiki/Executable_and_Linkable_Format) executable
251+
to be valid. And similarly, if you intend to use [Dwarf](https://dwarfstd.org/) debugging
252+
information (e.g. using `-gdwarf-2`), you will need to append some more output sections
253+
to your linker script.
254+
255+
```
256+
/* ELF Sections */
257+
.comment 0 : { *(.comment) }
258+
.symtab 0 : { *(.symtab) }
259+
.strtab 0 : { *(.strtab) }
260+
.shstrtab 0 : { *(.shstrtab) }
261+
262+
/* Dwarf Sections */
263+
.debug_loclists 0 : { *(.debug_loclists) }
264+
.debug_abbrev 0 : { *(.debug_abbrev) }
265+
.debug_info 0 : { *(.debug_info) }
266+
.debug_str_offsets 0 : { *(.debug_str_offsets) }
267+
.debug_str 0 : { *(.debug_str) }
268+
.debug_addr 0 : { *(.debug_addr) }
269+
.debug_frame 0 : { *(.debug_frame) }
270+
.debug_line 0 : { *(.debug_line) }
271+
.debug_line_str 0 : { *(.debug_line_str) }
272+
.debug_loc 0 : { *(.debug_loc) }
273+
.debug_ranges 0 : { *(.debug_ranges) }
274+
.debug_aranges 0 : { *(.debug_aranges) }
275+
276+
/* Exceptions are disabled, we don't need these sections */
277+
/DISCARD/ : {
278+
*(.ARM.exidx);
279+
*(.ARM.attributes);
280+
}
281+
```
282+
283+
[^1]: You can find the ld documentation for orphaned sections
284+
[here](https://sourceware.org/binutils/docs/ld/Orphan-Sections.html).

0 commit comments

Comments
 (0)