Fix NOBITS sections inflating flash image and RAM accounting#104
Fix NOBITS sections inflating flash image and RAM accounting#104AkshathaUdayashankar wants to merge 1 commit 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. |
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.
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. |
| // 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?
| 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.
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