Skip to content

Fix NOBITS sections inflating flash image and RAM accounting#104

Draft
AkshathaUdayashankar wants to merge 1 commit into
tock:masterfrom
AkshathaUdayashankar:fix-nobits-flash-inflation
Draft

Fix NOBITS sections inflating flash image and RAM accounting#104
AkshathaUdayashankar wants to merge 1 commit into
tock:masterfrom
AkshathaUdayashankar:fix-nobits-flash-inflation

Conversation

@AkshathaUdayashankar

Copy link
Copy Markdown

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:

  1. Flash bloat: NOBITS data (zeros) is embedded in the TBF image
  2. RAM double-counting: minimum_ram_size includes NOBITS via p_memsz and again via stack_len

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:

  • Skip segments containing only NOBITS sections, even if FileSiz > 0
  • Trim leading/trailing NOBITS from mixed NOBITS+PROGBITS segments
  • Collapse the LMA gap left by trimmed NOBITS to prevent inter-segment padding from re-embedding the removed data
  • Use PROGBITS section sizes instead of p_memsz for RAM accounting to prevent double-counting with stack_len

Impact on existing platforms:

None. On ARM/RISC-V where NOBITS segments already have FileSiz == 0, the new code does not trigger

@bradjc

bradjc commented May 19, 2026

Copy link
Copy Markdown
Contributor

Cool. Linkers are...interesting.

Can you share something like readelf -lS <elf> so we have an example of what requires this?

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.

@AkshathaUdayashankar

AkshathaUdayashankar commented May 21, 2026

Copy link
Copy Markdown
Author

Cool. Linkers are...interesting.

Can you share something like readelf -lS <elf> so we have an example of what requires this?

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:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .start            PROGBITS        0006c060 001060 0000a8 00  AX  0   0  1
  [ 2] .text             PROGBITS        0006c108 001108 007d2d 00  AX  0   0  1
  [ 3] .rodata           PROGBITS        00073e38 008e38 002610 00 AMS  0   0  4
  [13] .stack            NOBITS          00010000 00c000 009000 00  WA  0   0  1
  [14] .data             PROGBITS        00019000 015000 000064 00  WA  0   0  4
  [15] .bss              NOBITS          00019064 015064 0000d4 00  WA  0   0  4

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  LOAD           0x001060 0x0006c060 0x0006c060 0x07dd5 0x07dd5 R E 0x1000
  LOAD           0x008e38 0x00073e38 0x00073e38 0x02619 0x02619 R   0x1000
  LOAD           0x00c000 0x00010000 0x00076451 0x09064 0x09138 RW  0x1000
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0

 Section to Segment mapping:
  Segment Sections...
   00     .start .text
   01     .rodata ...
   02     .stack .data .bss
   03

(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.
@AkshathaUdayashankar AkshathaUdayashankar force-pushed the fix-nobits-flash-inflation branch from 88811cf to 87a3153 Compare June 17, 2026 23:54

@bradjc bradjc left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/convert.rs
Comment on lines +63 to +80
/// 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// 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.

Comment thread src/convert.rs
Comment on lines +748 to +753
// 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;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Comment thread src/convert.rs
Comment on lines +780 to +784
if collapsed_paddr >= prev_end as u64 {
segment.p_paddr = collapsed_paddr;
} else {
segment.p_paddr = prev_end as u64;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this if block do? It seems like shifting back to effectively remove the trimmed section shouldn't depend on the previous segment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants