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