Skip to content

Commit ea3240e

Browse files
Fix NOBITS sections inflating flash image and RAM accounting
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.
1 parent 06b05fd commit ea3240e

1 file changed

Lines changed: 144 additions & 1 deletion

File tree

src/convert.rs

Lines changed: 144 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,94 @@ fn section_exists_in_segment(
3737
false
3838
}
3939

40+
/// Helper function to determine if a segment contains at least one section
41+
/// with actual data (PROGBITS), as opposed to only containing NOBITS sections
42+
/// (such as .stack or .bss).
43+
///
44+
/// This is used to skip segments that some linkers give a nonzero FileSiz even
45+
/// though they only contain NOBITS sections (e.g., a `.stack` placed in flash
46+
/// via `AT > FLASH`). Such segments hold only zero or uninitialized data and
47+
/// should not be included in flash.
48+
fn segment_has_progbits_section(
49+
shdrs: &[(String, elf::section::SectionHeader)],
50+
segment: &elf::segment::ProgramHeader,
51+
) -> bool {
52+
for (_, shdr) in shdrs.iter() {
53+
if shdr.sh_size > 0
54+
&& shdr.sh_type != elf::abi::SHT_NOBITS
55+
&& section_in_segment(shdr, segment)
56+
{
57+
return true;
58+
}
59+
}
60+
false
61+
}
62+
63+
/// Trim a segment to exclude leading and trailing NOBITS sections.
64+
///
65+
/// Some linkers may merge NOBITS sections (.stack, .bss) with PROGBITS sections
66+
/// (.data) into a single PT_LOAD segment when they share the same load address
67+
/// region (e.g., via `AT > FLASH` in the linker script). This causes the
68+
/// segment's FileSiz to include padding bytes (0x00s) for the NOBITS regions,
69+
/// wasting flash space when elf2tab copies the segment into the TBF.
70+
///
71+
/// This function finds the file offset range covered by PROGBITS sections
72+
/// within the segment and adjusts `p_offset`, `p_paddr`, `p_vaddr`, `p_filesz`,
73+
/// and `p_memsz` so the segment covers only that range. Leading NOBITS bytes
74+
/// (e.g., `.stack` before `.data`) and trailing NOBITS bytes (e.g., `.bss`
75+
/// after `.data`) are excluded.
76+
///
77+
/// Note: `p_paddr` is advanced by the same amount as `p_offset`. The caller
78+
/// is responsible for collapsing the resulting LMA gap (between this segment
79+
/// and the previous one) to prevent inter-segment padding from reintroducing
80+
/// the trimmed NOBITS space.
81+
fn trim_nobits_from_segment(
82+
shdrs: &[(String, elf::section::SectionHeader)],
83+
segment: &mut elf::segment::ProgramHeader,
84+
) {
85+
let mut first_progbits_offset: Option<u64> = None;
86+
let mut last_progbits_end: Option<u64> = None;
87+
88+
for (_, shdr) in shdrs.iter() {
89+
if shdr.sh_size > 0
90+
&& shdr.sh_type != elf::abi::SHT_NOBITS
91+
&& section_in_segment(shdr, segment)
92+
{
93+
let sec_offset = shdr.sh_offset;
94+
let sec_end = shdr.sh_offset + shdr.sh_size;
95+
96+
first_progbits_offset = Some(match first_progbits_offset {
97+
Some(prev) => std::cmp::min(prev, sec_offset),
98+
None => sec_offset,
99+
});
100+
last_progbits_end = Some(match last_progbits_end {
101+
Some(prev) => std::cmp::max(prev, sec_end),
102+
None => sec_end,
103+
});
104+
}
105+
}
106+
107+
if let (Some(first_offset), Some(last_end)) = (first_progbits_offset, last_progbits_end) {
108+
// Trim leading NOBITS: advance the segment start to the first
109+
// PROGBITS section's file offset.
110+
let leading_nobits = first_offset.saturating_sub(segment.p_offset);
111+
if leading_nobits > 0 {
112+
segment.p_offset = first_offset;
113+
segment.p_paddr += leading_nobits;
114+
segment.p_vaddr += leading_nobits;
115+
segment.p_filesz -= leading_nobits;
116+
segment.p_memsz -= leading_nobits;
117+
}
118+
119+
// Trim trailing NOBITS: shrink filesz to end at the last PROGBITS
120+
// section's end.
121+
let new_filesz = last_end - segment.p_offset;
122+
if new_filesz < segment.p_filesz {
123+
segment.p_filesz = new_filesz;
124+
}
125+
}
126+
}
127+
40128
/// Helper function to determine if a section is within a specific segment.
41129
///
42130
/// Based on the function `section_in_segment` in
@@ -231,6 +319,15 @@ pub fn elf_to_tbf(
231319
// These are set in the linker file to consume memory, and we need to
232320
// account for them when we set the minimum amount of memory this app
233321
// requires.
322+
//
323+
// We count the full in-memory size of each such segment (`p_memsz`), which
324+
// covers initialized data (.data) and zero-initialized data (.bss) the app
325+
// needs RAM for. The one exception is the stack: some linkers merge the
326+
// `.stack` NOBITS section into this segment when it shares the flash load
327+
// region (`AT > FLASH`). Because the stack is also accounted for separately
328+
// via `stack_len` below, we subtract any `.stack` section here to avoid
329+
// double-counting it. `.bss` is not tracked elsewhere and so must remain
330+
// counted.
234331
for segment in &elf_phdrs {
235332
// To filter, we need segments that are:
236333
// - Set to be LOADed.
@@ -243,7 +340,19 @@ pub fn elf_to_tbf(
243340
&& segment.p_memsz > 0
244341
&& ((segment.p_flags & elf::abi::PF_W) > 0)
245342
{
246-
minimum_ram_size += segment.p_memsz as u32;
343+
// Subtract any `.stack` section merged into this segment, since the
344+
// stack is accounted for separately via `stack_len`. Everything
345+
// else in the segment (notably `.data` and `.bss`) must be counted.
346+
let stack_in_segment: u64 = elf_sections
347+
.iter()
348+
.filter(|(name, shdr)| {
349+
name == ".stack"
350+
&& shdr.sh_type == elf::abi::SHT_NOBITS
351+
&& section_in_segment(shdr, segment)
352+
})
353+
.map(|(_, shdr)| shdr.sh_size)
354+
.sum();
355+
minimum_ram_size += (segment.p_memsz - stack_in_segment) as u32;
247356
}
248357
}
249358
if verbose {
@@ -636,6 +745,40 @@ pub fn elf_to_tbf(
636745
continue;
637746
}
638747

748+
// Skip segments that only contain NOBITS sections (e.g., .stack, .bss).
749+
// See `segment_has_progbits_section` for why such segments can have a
750+
// nonzero FileSiz. Their data is just zeros and should not occupy flash.
751+
if !segment_has_progbits_section(&elf_sections, segment) {
752+
continue;
753+
}
754+
755+
// Trim leading/trailing NOBITS sections from mixed NOBITS+PROGBITS
756+
// segments so only the PROGBITS file range is copied into flash. See
757+
// `trim_nobits_from_segment`.
758+
let paddr_before_trim = segment.p_paddr;
759+
trim_nobits_from_segment(&elf_sections, segment);
760+
761+
// After trimming, the segment may be empty if all PROGBITS sections
762+
// had zero size. Skip it in that case.
763+
if segment.p_filesz == 0 {
764+
continue;
765+
}
766+
767+
// Collapse the LMA gap left by trimmed leading NOBITS sections.
768+
//
769+
// trim_nobits_from_segment advances p_paddr to keep the offset-to-
770+
// address mapping within the segment, which leaves an LMA gap between
771+
// the previous segment's end and this segment's new p_paddr. The
772+
// inter-segment padding logic below would fill that gap with zeros,
773+
// re-embedding the NOBITS data we just trimmed. To prevent this, we
774+
// move p_paddr back to where it was before trimming, clamped to the
775+
// previous segment's end so the restored address cannot overlap it.
776+
if let Some(prev_end) = last_segment_address_end {
777+
if segment.p_paddr > paddr_before_trim {
778+
segment.p_paddr = cmp::max(paddr_before_trim, prev_end as u64);
779+
}
780+
}
781+
639782
// Check if the segment starts entirely before the start of flash. If
640783
// so, skip this segment.
641784
if let Some(flash_address) = fixed_address_flash {

0 commit comments

Comments
 (0)