Fix NOBITS sections inflating flash image and RAM accounting#104
Fix NOBITS sections inflating flash image and RAM accounting#104AkshathaUdayashankar wants to merge 2 commits into
Conversation
|
Cool. Linkers are...interesting. Can you share something like I'm a little confused why the linker is able to combine sections like stack into segments with data that actually needs to be loaded, but, at the end of the day elf2tab will handle whatever it needs to handle. |
Here's the readelf -lS output from a Rust usermode app built with LLD for an x86 (i386) Tock platform: (Some defmt metadata sections in segment 01 omitted for readability.) The key issue is segment 02: LLD merges .stack (NOBITS, 0x9000 = 36 KiB), .data (PROGBITS, 0x64 = 100 bytes), and .bss (NOBITS, 0xD4) into a single PT_LOAD with FileSiz = 0x9064. That's 0x9000 + 0x64 — the NOBITS .stack is materialized as 36 KiB of zeros in the file. On our platform, RAM sits below FLASH in the address space (RAM at 0x10000, FLASH at 0x6c000). The linker script uses AT > FLASH to assign load addresses for initialized data. LLD sees the contiguous LMA range and coalesces the NOBITS and PROGBITS sections into one segment, giving the NOBITS content a nonzero FileSiz. On ARM/RISC-V this doesn't happen because NOBITS segments already get FileSiz == 0 — the new code in this PR only triggers when it detects nonzero FileSiz on NOBITS sections. |
88811cf to
87a3153
Compare
bradjc
left a comment
There was a problem hiding this comment.
Thanks for the readelf output. I can't reproduce this with arm/riscv (the linker still puts the .stack section as its own segment even if I make it > SRAM AT > FLASH, but, I can see the benefit.
I have a couple questions about the code, but this generally looks good.
| /// Trim a segment to exclude leading and trailing NOBITS sections. | ||
| /// | ||
| /// Some linkers may merge NOBITS sections (.stack, .bss) with PROGBITS sections | ||
| /// (.data) into a single PT_LOAD segment when they share the same load address | ||
| /// region (e.g., via `AT > FLASH` in the linker script). This causes the | ||
| /// segment's FileSiz to include zero bytes for the NOBITS regions, wasting | ||
| /// flash space when elf2tab copies the segment into the TBF. | ||
| /// | ||
| /// This function finds the file offset range covered by PROGBITS sections | ||
| /// within the segment and adjusts `p_offset`, `p_paddr`, `p_vaddr`, `p_filesz`, | ||
| /// and `p_memsz` so the segment covers only that range. Leading NOBITS bytes | ||
| /// (e.g., `.stack` before `.data`) and trailing NOBITS bytes (e.g., `.bss` | ||
| /// after `.data`) are excluded. | ||
| /// | ||
| /// Note: `p_paddr` is advanced by the same amount as `p_offset`. The caller | ||
| /// is responsible for collapsing the resulting LMA gap (between this segment | ||
| /// and the previous one) to prevent inter-segment padding from reintroducing | ||
| /// the trimmed NOBITS space. |
There was a problem hiding this comment.
| /// Trim a segment to exclude leading and trailing NOBITS sections. | |
| /// | |
| /// Some linkers may merge NOBITS sections (.stack, .bss) with PROGBITS sections | |
| /// (.data) into a single PT_LOAD segment when they share the same load address | |
| /// region (e.g., via `AT > FLASH` in the linker script). This causes the | |
| /// segment's FileSiz to include zero bytes for the NOBITS regions, wasting | |
| /// flash space when elf2tab copies the segment into the TBF. | |
| /// | |
| /// This function finds the file offset range covered by PROGBITS sections | |
| /// within the segment and adjusts `p_offset`, `p_paddr`, `p_vaddr`, `p_filesz`, | |
| /// and `p_memsz` so the segment covers only that range. Leading NOBITS bytes | |
| /// (e.g., `.stack` before `.data`) and trailing NOBITS bytes (e.g., `.bss` | |
| /// after `.data`) are excluded. | |
| /// | |
| /// Note: `p_paddr` is advanced by the same amount as `p_offset`. The caller | |
| /// is responsible for collapsing the resulting LMA gap (between this segment | |
| /// and the previous one) to prevent inter-segment padding from reintroducing | |
| /// the trimmed NOBITS space. | |
| /// Trim a segment to exclude leading and trailing NOBITS sections. | |
| /// | |
| /// Some linkers may merge NOBITS sections (.stack, .bss) with PROGBITS sections | |
| /// (.data) into a single PT_LOAD segment when they share the same load address | |
| /// region (e.g., via `AT > FLASH` in the linker script). This causes the | |
| /// segment's FileSiz to include padding bytes (0x00s) for the NOBITS regions, wasting | |
| /// flash space when elf2tab copies the segment into the TBF. | |
| /// | |
| /// This function finds the file offset range covered by PROGBITS sections | |
| /// within the segment and adjusts `p_offset`, `p_paddr`, `p_vaddr`, `p_filesz`, | |
| /// and `p_memsz` so the segment covers only that range. Leading NOBITS bytes | |
| /// (e.g., `.stack` before `.data`) and trailing NOBITS bytes (e.g., `.bss` | |
| /// after `.data`) are excluded. | |
| /// | |
| /// Note: `p_paddr` is advanced by the same amount as `p_offset`. The caller | |
| /// is responsible for collapsing the resulting LMA gap (between this segment | |
| /// and the previous one) to prevent inter-segment padding from reintroducing | |
| /// the trimmed NOBITS space. |
There was a problem hiding this comment.
Good call — updated to "padding bytes (0x00s)."
| // Skip segments that only contain NOBITS sections (e.g., .stack, .bss). | ||
| // See `segment_has_progbits_section` for why such segments can have a | ||
| // nonzero FileSiz. Their data is just zeros and should not occupy flash. | ||
| if !segment_has_progbits_section(&elf_sections, segment) { | ||
| continue; | ||
| } |
There was a problem hiding this comment.
Is this not the same check as right about (if segment.p_filesz == 0 continue)?
If everything is NOBITS, then filesz must be 0, right?
There was a problem hiding this comment.
You're right for every case we can currently reproduce. The example segment contains .data (PROGBITS), so this check passes through and trim_nobits_from_segment does the work; afterward p_filesz is nonzero so the p_filesz == 0 guard is also a no-op there. The only case where this check isn't redundant is a segment that is entirely NOBITS yet still has FileSiz > 0 - there, trim_nobits_from_segment finds no PROGBITS, returns early leaving p_filesz at its original nonzero value, so the post-trim p_filesz == 0 guard wouldn't catch it either. I don't have a concrete ELF that produces that (every repro has .data merged in), so unless you know of a linker that emits a standalone .stack with nonzero FileSiz, I'll drop this check and rely on the p_filesz == 0 guard. Preference?
| if collapsed_paddr >= prev_end as u64 { | ||
| segment.p_paddr = collapsed_paddr; | ||
| } else { | ||
| segment.p_paddr = prev_end as u64; | ||
| } |
There was a problem hiding this comment.
What does this if block do? It seems like shifting back to effectively remove the trimmed section shouldn't depend on the previous segment.
There was a problem hiding this comment.
Agreed it was obfuscated. The arithmetic reduces to just restoring the pre-trim LMA, so I've simplified it to [if p_paddr > paddr_before_trim { p_paddr = max(paddr_before_trim, prev_end); (verified byte-identical output). On the prev_end dependence: it's a defensive clamp so the restored LMA can't overlap the previous segment — which may itself have been shifted by the flash-address truncation a few lines down. It doesn't trigger in normal non-overlapping layouts, but since that flash-truncation can move p_paddr, I'd rather keep the clamp than assert it's impossible to overlap. Open to dropping it if you'd prefer a debug_assert! instead.
Some linkers emit a nonzero FileSiz for segments that contain NOBITS sections (.stack, .bss), or merge NOBITS and PROGBITS sections into a single PT_LOAD segment when they share a flash load region (AT > FLASH). This caused two problems: 1. Flash bloat: NOBITS zero data was embedded in the TBF image. 2. RAM over-counting: the .stack NOBITS section was counted both via the segment p_memsz and again via stack_len. Changes: - Skip segments that contain only NOBITS sections, even when FileSiz > 0. - Trim leading/trailing NOBITS from mixed NOBITS+PROGBITS segments and collapse the resulting LMA gap so inter-segment padding does not re-embed the trimmed data. - For RAM accounting, count each segment's full p_memsz (covering .data and .bss) but subtract any merged .stack section to avoid double- counting it with stack_len. On ARM/RISC-V, where NOBITS segments already have FileSiz == 0 and the stack is not merged into a data segment, behavior is unchanged.
87a3153 to
ea3240e
Compare
Extract segment_ram_bytes and stack_bytes_in_segment so the minimum-RAM computation can be unit-tested, and add a test module covering: - segment_has_progbits_section detection of merged and pure-NOBITS segments - trim_nobits_from_segment offset/filesz/memsz math for leading and trailing NOBITS sections, and the PROGBITS-only no-op case - RAM accounting that subtracts .stack but keeps .bss, and that .bss is never mistaken for .stack The tests construct elf header types directly, so no production types change. The segment_ram_bytes and stack_bytes_in_segment extractions are behavior-preserving.
Description:
elf2tab currently filters segments based solely on FileSiz and PhysAddr, with no awareness of ELF section types. This causes two problems when linkers produce segments with nonzero FileSiz for NOBITS (.stack, .bss) sections:
This occurs on platforms where RAM < FLASH in the address space (e.g., x86). LLD 20+ merges NOBITS and PROGBITS sections into a single PT_LOAD segment when both use AT > FLASH in the linker script, materializing the NOBITS content as zeros with nonzero FileSiz.
Changes:
Impact on existing platforms:
None. On ARM/RISC-V where NOBITS segments already have FileSiz == 0, the new code does not trigger