Skip to main content

st_mem/
elf.rs

1use std::fs;
2use std::path::Path;
3
4use serde::Serialize;
5
6use crate::memory::MemoryConfig;
7
8/// Memory region type for a section.
9#[derive(Debug, Clone, PartialEq, Serialize)]
10pub enum RegionType {
11    Flash,
12    Ram,
13    Other,
14}
15
16/// Information about a single ELF section.
17#[derive(Debug, Clone, Serialize)]
18pub struct SectionInfo {
19    /// Section name from the ELF header.
20    pub name: String,
21    /// Virtual address where the section is loaded.
22    pub address: u32,
23    /// Section size in bytes.
24    pub size: u32,
25    /// Section flags (SHF_ALLOC, SHF_WRITE, SHF_EXECINSTR, etc.)
26    pub flags: u32,
27    /// Which memory region this section belongs to.
28    pub region: RegionType,
29}
30
31impl SectionInfo {
32    pub fn is_alloc(&self) -> bool {
33        self.flags & 0x2 != 0
34    }
35    pub fn is_writable(&self) -> bool {
36        self.flags & 0x1 != 0
37    }
38    pub fn is_executable(&self) -> bool {
39        self.flags & 0x4 != 0
40    }
41}
42
43/// Firmware memory usage analysis result.
44#[derive(Debug, Clone, Serialize)]
45pub struct FirmwareUsage {
46    /// FLASH bytes used by the firmware.
47    pub flash_used: u64,
48    /// RAM bytes used by the firmware.
49    pub ram_used: u64,
50    /// Total FLASH available (from memory.x).
51    pub flash_total: u64,
52    /// Total RAM available (from memory.x).
53    pub ram_total: u64,
54}
55
56impl FirmwareUsage {
57    /// FLASH usage as a percentage (0.0 - 100.0).
58    pub fn flash_percent(&self) -> f64 {
59        if self.flash_total == 0 {
60            0.0
61        } else {
62            self.flash_used as f64 * 100.0 / self.flash_total as f64
63        }
64    }
65
66    /// RAM usage as a percentage (0.0 - 100.0).
67    pub fn ram_percent(&self) -> f64 {
68        if self.ram_total == 0 {
69            0.0
70        } else {
71            self.ram_used as f64 * 100.0 / self.ram_total as f64
72        }
73    }
74
75    /// Remaining FLASH bytes.
76    pub fn flash_free(&self) -> u64 {
77        self.flash_total.saturating_sub(self.flash_used)
78    }
79
80    /// Remaining RAM bytes.
81    pub fn ram_free(&self) -> u64 {
82        self.ram_total.saturating_sub(self.ram_used)
83    }
84}
85
86/// Parsed ELF analysis result containing usage and section breakdown.
87#[derive(Debug, Clone, Serialize)]
88pub struct ElfAnalysis {
89    /// Overall firmware usage.
90    pub usage: FirmwareUsage,
91    /// Per-section breakdown (allocated sections only).
92    pub sections: Vec<SectionInfo>,
93}
94
95/// Analyze an ELF firmware binary and compute memory usage.
96pub fn analyze_elf<P: AsRef<Path>>(elf_path: P, config: &MemoryConfig) -> Result<FirmwareUsage, String> {
97    let path = elf_path.as_ref();
98    let data = fs::read(path)
99        .map_err(|e| format!("Failed to read ELF file {}: {}", path.display(), e))?;
100
101    let analysis = analyze_elf_bytes(&data, config)?;
102    Ok(analysis.usage)
103}
104
105/// Analyze an ELF firmware binary and return both usage and section breakdown.
106pub fn analyze_elf_detailed<P: AsRef<Path>>(elf_path: P, config: &MemoryConfig) -> Result<ElfAnalysis, String> {
107    let path = elf_path.as_ref();
108    let data = fs::read(path)
109        .map_err(|e| format!("Failed to read ELF file {}: {}", path.display(), e))?;
110
111    analyze_elf_bytes(&data, config)
112}
113
114/// Analyze ELF binary from a byte slice.
115pub fn analyze_elf_bytes(data: &[u8], config: &MemoryConfig) -> Result<ElfAnalysis, String> {
116    if data.len() < 52 {
117        return Err("File too small to be a valid 32-bit ELF".to_string());
118    }
119
120    // Validate ELF magic
121    if &data[0..4] != b"\x7fELF" {
122        return Err("Not a valid ELF file (bad magic)".to_string());
123    }
124
125    // ELF class: 4 = 32-bit, 8 = 64-bit
126    let elf_class = data[4];
127    if elf_class != 1 {
128        return Err("Only 32-bit ELF files are supported".to_string());
129    }
130
131    let flash_region = config.flash();
132    let ram_region = config.ram();
133
134    let flash_origin = flash_region.map(|r| r.origin as u32).unwrap_or(0x0800_0000);
135    let flash_length = flash_region.map(|r| r.length).unwrap_or(64 * 1024);
136    let flash_end = flash_origin as u64 + flash_length;
137
138    let ram_origin = ram_region.map(|r| r.origin as u32).unwrap_or(0x2000_0000);
139    let ram_length = ram_region.map(|r| r.length).unwrap_or(20 * 1024);
140    let ram_end = ram_origin as u64 + ram_length;
141
142    // Parse ELF32 header
143    let e_shoff = u32::from_le_bytes(data[0x20..0x24].try_into().unwrap()) as usize;
144    let e_shentsize = u16::from_le_bytes(data[0x2E..0x30].try_into().unwrap()) as usize;
145    let e_shnum = u16::from_le_bytes(data[0x30..0x32].try_into().unwrap()) as usize;
146    let e_shstrndx = u16::from_le_bytes(data[0x32..0x34].try_into().unwrap()) as usize;
147
148    if e_shentsize == 0 || e_shnum == 0 {
149        return Ok(ElfAnalysis {
150            usage: FirmwareUsage {
151                flash_used: 0,
152                ram_used: 0,
153                flash_total: flash_length,
154                ram_total: ram_length,
155            },
156            sections: Vec::new(),
157        });
158    }
159
160    // Read section name string table
161    let shstrtab = read_section_data(data, e_shoff, e_shstrndx, e_shentsize);
162
163    let mut flash_used: u64 = 0;
164    let mut ram_used: u64 = 0;
165    let mut sections = Vec::new();
166
167    for i in 0..e_shnum {
168        let sh_off = e_shoff + i * e_shentsize;
169        if sh_off + 40 > data.len() {
170            break;
171        }
172
173        let sh_name_idx = u32::from_le_bytes(data[sh_off..sh_off + 4].try_into().unwrap()) as usize;
174        let sh_type = u32::from_le_bytes(data[sh_off + 4..sh_off + 8].try_into().unwrap());
175        let sh_flags = u32::from_le_bytes(data[sh_off + 8..sh_off + 12].try_into().unwrap());
176        let sh_addr = u32::from_le_bytes(data[sh_off + 12..sh_off + 16].try_into().unwrap());
177        let sh_size = u32::from_le_bytes(data[sh_off + 20..sh_off + 24].try_into().unwrap());
178
179        // SHT_NULL = 0, skip null sections
180        if sh_type == 0 {
181            continue;
182        }
183
184        let name = read_string(&shstrtab, sh_name_idx);
185
186        // SHF_ALLOC = 0x2: section occupies memory during process execution
187        if sh_flags & 0x2 != 0 {
188            let addr = sh_addr as u64;
189            let region = if addr >= ram_origin as u64 && addr < ram_end {
190                ram_used += sh_size as u64;
191                RegionType::Ram
192            } else if addr >= flash_origin as u64 && addr < flash_end {
193                flash_used += sh_size as u64;
194                RegionType::Flash
195            } else {
196                RegionType::Other
197            };
198
199            sections.push(SectionInfo {
200                name,
201                address: sh_addr,
202                size: sh_size,
203                flags: sh_flags,
204                region,
205            });
206        }
207    }
208
209    // Sort sections: Flash first, then Ram, then Other; within each group by size descending
210    sections.sort_by(|a, b| {
211        let ra = match a.region { RegionType::Flash => 0, RegionType::Ram => 1, RegionType::Other => 2 };
212        let rb = match b.region { RegionType::Flash => 0, RegionType::Ram => 1, RegionType::Other => 2 };
213        ra.cmp(&rb).then_with(|| b.size.cmp(&a.size))
214    });
215
216    Ok(ElfAnalysis {
217        usage: FirmwareUsage {
218            flash_used,
219            ram_used,
220            flash_total: flash_length,
221            ram_total: ram_length,
222        },
223        sections,
224    })
225}
226
227/// Read raw section data bytes from the ELF file for a given section index.
228fn read_section_data(data: &[u8], e_shoff: usize, index: usize, e_shentsize: usize) -> Vec<u8> {
229    let sh_off = e_shoff + index * e_shentsize;
230    if sh_off + 40 > data.len() {
231        return Vec::new();
232    }
233    let sh_offset = u32::from_le_bytes(data[sh_off + 16..sh_off + 20].try_into().unwrap()) as usize;
234    let sh_size = u32::from_le_bytes(data[sh_off + 20..sh_off + 24].try_into().unwrap()) as usize;
235    if sh_offset + sh_size > data.len() {
236        return Vec::new();
237    }
238    data[sh_offset..sh_offset + sh_size].to_vec()
239}
240
241/// Read a null-terminated string from a byte slice at the given offset.
242fn read_string(strtab: &[u8], offset: usize) -> String {
243    if offset >= strtab.len() {
244        return format!("<unknown@{}>", offset);
245    }
246    let end = strtab[offset..]
247        .iter()
248        .position(|&b| b == 0)
249        .unwrap_or(strtab.len() - offset);
250    String::from_utf8_lossy(&strtab[offset..offset + end]).into_owned()
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256    use crate::memory::MemoryConfig;
257
258    fn test_config() -> MemoryConfig {
259        MemoryConfig::parse(
260            "MEMORY {\n  FLASH : ORIGIN = 0x08000000, LENGTH = 64K\n  RAM : ORIGIN = 0x20000000, LENGTH = 20K\n}"
261        ).unwrap()
262    }
263
264    #[test]
265    fn test_invalid_data() {
266        let config = test_config();
267        assert!(analyze_elf_bytes(b"too small", &config).is_err());
268        assert!(analyze_elf_bytes(b"NOT_ELF_DATA_HERE_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", &config).is_err());
269    }
270
271    #[test]
272    fn test_analyze_stm32dome() {
273        let config = crate::memory::MemoryConfig::from_file("memory.x").unwrap();
274        let analysis = analyze_elf_detailed("stm32dome", &config).unwrap();
275
276        assert!(analysis.usage.flash_used > 0, "FLASH should have used bytes");
277        assert!(analysis.usage.flash_total > 0);
278        assert!(!analysis.sections.is_empty(), "Should have sections");
279
280        // Verify sections are sorted: Flash before Ram
281        let mut seen_ram = false;
282        for sec in &analysis.sections {
283            if sec.region == RegionType::Ram {
284                seen_ram = true;
285            }
286            if seen_ram && sec.region == RegionType::Flash {
287                panic!("Flash section found after Ram section - sort order broken");
288            }
289        }
290    }
291}