|
| 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