Skip to main content

datui_lib/formats/
elf.rs

1//! ELF files as their symbol table: what uses flash and RAM.
2//!
3//! An ELF file (a firmware image, an executable, a library, an object file) opens as
4//! one row per symbol: its name (Rust names demangled), address, size, kind, binding,
5//! section, and the region its section sits in: `flash` for what is loaded and not
6//! written (code, constants), `ram` for what is written (`.data`, `.bss`). Sorted by
7//! size and grouped by section, it says what fills each. Its sections are a second
8//! table, `--table sections`.
9//!
10//! Only the headers and the symbol and string tables are read, through a map of the
11//! file, by the `object` crate; the table is small beside the file.
12
13use color_eyre::Result;
14
15use std::sync::Arc;
16
17use crate::formats::columns::{Builder, Cell, Kind};
18use object::{Object, ObjectSection, ObjectSymbol, SectionFlags, SymbolFlags, SymbolSection};
19use polars::prelude::*;
20
21use crate::formats::model_files::MetaValue;
22use crate::formats::sqlite::Table;
23use crate::formats::text_formats::Detail;
24
25/// What datui does with an ELF file: see [`crate::formats::readers`].
26pub(crate) const READER: crate::formats::readers::Reader = crate::formats::readers::Reader {
27    scan,
28    // Never in a listing, which would list every executable.
29    signatures: &[crate::formats::readers::Signature {
30        says: |head, _| looks_like(head),
31        kind: crate::formats::readers::Kind::Magic,
32        trusted: crate::formats::readers::Trusted {
33            listing: false,
34            tables: true,
35            ..crate::formats::readers::EVERYWHERE
36        },
37    }],
38    tables: Some(|_| Ok(tables())),
39    ..crate::formats::readers::BASE
40};
41
42/// The first four bytes of every ELF file.
43pub const MAGIC: &[u8; 4] = b"\x7fELF";
44
45/// The table an ELF file opens on, and the other one.
46pub const SYMBOLS: &str = "symbols";
47pub const SECTIONS: &str = "sections";
48
49// `sh_flags` bits.
50const SHF_WRITE: u64 = 0x1;
51const SHF_ALLOC: u64 = 0x2;
52
53/// Whether `head`, the first bytes of a file, begins an ELF file.
54pub fn looks_like(head: &[u8]) -> bool {
55    head.starts_with(MAGIC)
56}
57
58/// The tables of an ELF file, for the home screen and `--table`.
59pub fn tables() -> Vec<Table> {
60    let symbols = ["name", "addr", "size", "kind", "bind", "section", "region"];
61    let sections = ["name", "addr", "size", "flags", "kind", "region"];
62    vec![
63        Table::plain(SYMBOLS, "table", symbols),
64        Table::plain(SECTIONS, "table", sections),
65    ]
66}
67
68/// The region a section with `sh_flags` sits in: what is written is RAM, what is only
69/// loaded is flash; a section not loaded is in neither.
70fn region(flags: u64) -> Option<&'static str> {
71    match (flags & SHF_ALLOC != 0, flags & SHF_WRITE != 0) {
72        (false, _) => None,
73        (true, true) => Some("ram"),
74        (true, false) => Some("flash"),
75    }
76}
77
78/// `sh_flags` as `readelf` writes them: `WAX` for a writable, allocated, executable
79/// section.
80fn flags_text(flags: u64) -> String {
81    const LETTERS: [(u64, char); 11] = [
82        (0x1, 'W'),
83        (0x2, 'A'),
84        (0x4, 'X'),
85        (0x10, 'M'),
86        (0x20, 'S'),
87        (0x40, 'I'),
88        (0x80, 'L'),
89        (0x100, 'O'),
90        (0x200, 'G'),
91        (0x400, 'T'),
92        (0x800, 'C'),
93    ];
94    LETTERS
95        .iter()
96        .filter(|(bit, _)| flags & bit != 0)
97        .map(|(_, c)| *c)
98        .collect()
99}
100
101fn sh_flags(flags: SectionFlags) -> u64 {
102    match flags {
103        SectionFlags::Elf { sh_flags } => sh_flags,
104        _ => 0,
105    }
106}
107
108/// A symbol's type, from the low half of `st_info`.
109fn symbol_kind(st_info: u8) -> &'static str {
110    match st_info & 0xf {
111        0 => "notype",
112        1 => "object",
113        2 => "func",
114        3 => "section",
115        4 => "file",
116        5 => "common",
117        6 => "tls",
118        10 => "ifunc",
119        _ => "other",
120    }
121}
122
123/// A symbol's binding, from the high half of `st_info`.
124fn symbol_bind(st_info: u8) -> &'static str {
125    match st_info >> 4 {
126        0 => "local",
127        1 => "global",
128        2 => "weak",
129        10 => "unique",
130        _ => "other",
131    }
132}
133
134/// A Rust symbol's name demangled, without its hash; any other name as it is. C++
135/// names stay mangled: no C++ demangler is in the tree.
136pub fn demangle(name: &str) -> String {
137    match rustc_demangle::try_demangle(name) {
138        Ok(demangled) => format!("{demangled:#}"),
139        Err(_) => name.to_string(),
140    }
141}
142
143/// What an ELF file's tables hold.
144pub struct Elf {
145    pub symbols: DataFrame,
146    pub sections: DataFrame,
147    pub detail: Detail,
148    /// Symbols past `limits.elf_symbols`, left out.
149    pub left_out: usize,
150}
151
152/// Read the symbol and section tables of the ELF file in `data`.
153pub fn read(data: &[u8]) -> std::result::Result<Elf, String> {
154    if !looks_like(data) {
155        return Err("not an ELF file: no \\x7fELF at the start".into());
156    }
157    let file = object::File::parse(data).map_err(|e| format!("not a readable ELF file: {e}"))?;
158
159    // Each section's name, shared by its symbols rather than copied to each.
160    let mut section_names: Vec<Option<Arc<str>>> = Vec::new();
161    let mut section_flags: Vec<u64> = Vec::new();
162    let mut sections = Builder::new(&[
163        ("name", Kind::Str),
164        ("addr", Kind::U64),
165        ("size", Kind::U64),
166        ("flags", Kind::Str),
167        ("kind", Kind::Str),
168        ("region", Kind::Label),
169    ]);
170    let (mut flash, mut ram) = (0u64, 0u64);
171    for section in file.sections() {
172        let index = section.index().0;
173        if section_names.len() <= index {
174            section_names.resize(index + 1, Some(Arc::from("")));
175            section_flags.resize(index + 1, 0);
176        }
177        let name = section.name().unwrap_or_default();
178        let flags = sh_flags(section.flags());
179        let place = region(flags);
180        match place {
181            Some("ram") => ram = ram.saturating_add(section.size()),
182            Some(_) => flash = flash.saturating_add(section.size()),
183            None => {}
184        }
185        section_names[index] = Some(name.into());
186        section_flags[index] = flags;
187        sections.push([
188            Cell::Str(Some(name.to_string())),
189            Cell::U64(Some(section.address())),
190            Cell::U64(Some(section.size())),
191            Cell::Str(Some(flags_text(flags))),
192            Cell::Str(Some(format!("{:?}", section.kind()).to_ascii_lowercase())),
193            Cell::Label(place),
194        ]);
195    }
196
197    // The static symbol table, or the dynamic one of a stripped library.
198    let mut symbols: Vec<_> = file.symbols().collect();
199    if symbols.is_empty() {
200        symbols = file.dynamic_symbols().collect();
201    }
202    let total = symbols.len();
203    symbols.truncate(crate::limits::get().elf_symbols);
204    let rows = symbols.len();
205    let mut table = Builder::new(&[
206        ("name", Kind::Str),
207        ("addr", Kind::U64),
208        ("size", Kind::U64),
209        ("kind", Kind::Label),
210        ("bind", Kind::Label),
211        ("section", Kind::Shared),
212        ("region", Kind::Label),
213    ]);
214    let pseudo = |name: &str| Some(Arc::<str>::from(name));
215    for symbol in &symbols {
216        let st_info = match symbol.flags() {
217            SymbolFlags::Elf { st_info, .. } => st_info,
218            _ => 0,
219        };
220        let (in_section, in_region) = match symbol.section() {
221            SymbolSection::Section(index) => (
222                section_names.get(index.0).cloned().flatten(),
223                section_flags.get(index.0).copied().and_then(region),
224            ),
225            SymbolSection::Undefined => (pseudo("UND"), None),
226            SymbolSection::Absolute => (pseudo("ABS"), None),
227            SymbolSection::Common => (pseudo("COMMON"), None),
228            _ => (None, None),
229        };
230        table.push([
231            Cell::Str(Some(demangle(symbol.name().unwrap_or_default()))),
232            Cell::U64(Some(symbol.address())),
233            Cell::U64(Some(symbol.size())),
234            Cell::Label(Some(symbol_kind(st_info))),
235            Cell::Label(Some(symbol_bind(st_info))),
236            Cell::Shared(in_section),
237            Cell::Label(in_region),
238        ]);
239    }
240    let symbols = table.take().map_err(|e| e.to_string())?;
241    let sections = sections.take().map_err(|e| e.to_string())?;
242
243    let group = crate::numfmt::group_chrome;
244    let mut lines = vec![
245        format!(
246            "Class: {}, {} endian",
247            if file.is_64() { "64-bit" } else { "32-bit" },
248            if file.is_little_endian() {
249                "little"
250            } else {
251                "big"
252            }
253        ),
254        format!("Machine: {:?}", file.architecture()),
255        format!("Type: {:?}", file.kind()),
256        format!("Entry: 0x{:x}", file.entry()),
257        format!(
258            "Flash: {} bytes in loaded, unwritten sections",
259            group(usize::try_from(flash).unwrap_or(usize::MAX))
260        ),
261        format!(
262            "RAM: {} bytes in written sections",
263            group(usize::try_from(ram).unwrap_or(usize::MAX))
264        ),
265        format!("Symbols: {}", group(total)),
266    ];
267    if total > rows {
268        lines.push(format!("The first {} symbols are read.", group(rows)));
269    }
270    let list = crate::formats::text_formats::capped_list(
271        (0..sections.height()).map(|i| {
272            let get = |c: &str| sections.column(c).ok().and_then(|c| c.get(i).ok());
273            let text = format!(
274                "0x{:x}  {} bytes  {}",
275                get("addr")
276                    .and_then(|v| v.extract::<u64>())
277                    .unwrap_or_default(),
278                get("size")
279                    .and_then(|v| v.extract::<u64>())
280                    .unwrap_or_default(),
281                get("flags")
282                    .and_then(|v| v.get_str().map(str::to_string))
283                    .unwrap_or_default()
284            );
285            let name = get("name")
286                .and_then(|v| v.get_str().map(str::to_string))
287                .unwrap_or_default();
288            (name, MetaValue::Text(text))
289        }),
290        sections.height(),
291    );
292    Ok(Elf {
293        symbols,
294        sections,
295        detail: Detail {
296            tab: crate::formats::text_formats::tab(crate::FileFormat::Elf),
297            lines,
298            list_title: "Sections",
299            list,
300            first: false,
301            ..Default::default()
302        },
303        left_out: total - rows,
304    })
305}
306
307/// The scan of an ELF file: the table `--table` names, its symbols by default.
308fn scan(input: crate::formats::readers::ScanIn<'_>) -> Result<crate::loading::scan::Scan> {
309    let path = input.path();
310    let wanted = input.options.table.as_deref();
311    let tables = tables();
312    let picked = match wanted {
313        None => SYMBOLS.to_string(),
314        Some(_) => match crate::formats::members::pick(tables.clone(), wanted, path, "")? {
315            crate::formats::sqlite::Pick::One(table) => table.name,
316            crate::formats::sqlite::Pick::Several(_) => SYMBOLS.to_string(),
317        },
318    };
319    let bytes = crate::formats::fixed_records::Bytes::map(path)?;
320    let elf = read(bytes.as_slice()).map_err(|e| color_eyre::eyre::eyre!(e))?;
321    let mut notes = Vec::new();
322    if elf.left_out > 0 {
323        notes.push(crate::limits::left_out(
324            &format!("{} symbols", crate::numfmt::group_chrome(elf.left_out)),
325            crate::limits::get().elf_symbols,
326            "elf_symbols",
327        ));
328    }
329    let df = if picked == SECTIONS {
330        elf.sections
331    } else {
332        elf.symbols
333    };
334    let opened = crate::formats::members::Opened::for_table(
335        elf.detail,
336        &tables,
337        &picked,
338        notes,
339        "the symbol table",
340    );
341    Ok(opened.scan(input, df.lazy()))
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347
348    /// A file that is not ELF names itself, in the one shape.
349    #[test]
350    fn errors_name_the_file() {
351        crate::formats::readers::bad_input::each_names_its_file(
352            crate::FileFormat::Elf,
353            &[
354                ("text.elf", b"hello there", "Not an ELF file"),
355                (
356                    "cut.elf",
357                    b"\x7fELF\x02\x01\x01\0",
358                    "Not a readable ELF file",
359                ),
360            ],
361        );
362    }
363
364    #[test]
365    fn symbols_with_their_section_and_region() {
366        let elf = read(&crate::tests::fixtures::elf()).unwrap();
367        let s = &elf.symbols;
368        let text = |c: &str| -> Vec<Option<String>> {
369            s.column(c)
370                .unwrap()
371                .str()
372                .unwrap()
373                .iter()
374                .map(|v| v.map(str::to_string))
375                .collect()
376        };
377        let names = text("name");
378        let at = |n: &str| names.iter().position(|v| v.as_deref() == Some(n)).unwrap();
379        assert!(names.contains(&Some("core::fmt::write".to_string())));
380        let region = text("region");
381        let section = text("section");
382        let kind = text("kind");
383        let bind = text("bind");
384        assert_eq!(region[at("main")].as_deref(), Some("flash"));
385        assert_eq!(region[at("TABLE")].as_deref(), Some("flash"));
386        assert_eq!(region[at("counter")].as_deref(), Some("ram"));
387        assert_eq!(section[at("buffer")].as_deref(), Some(".bss"));
388        assert_eq!(region[at("buffer")].as_deref(), Some("ram"));
389        assert_eq!(kind[at("main")].as_deref(), Some("func"));
390        assert_eq!(bind[at("buffer")].as_deref(), Some("local"));
391        assert_eq!(bind[at("weak_hook")].as_deref(), Some("weak"));
392        let sizes = s.column("size").unwrap().u64().unwrap();
393        assert_eq!(sizes.get(at("buffer")), Some(1024));
394        let flags: Vec<_> = elf
395            .sections
396            .column("flags")
397            .unwrap()
398            .str()
399            .unwrap()
400            .iter()
401            .map(|v| v.unwrap_or_default().to_string())
402            .collect();
403        assert!(flags.contains(&"AX".to_string()) && flags.contains(&"WA".to_string()));
404        assert!(
405            elf.detail
406                .lines
407                .iter()
408                .any(|l| l.starts_with("RAM: 1,040 bytes")),
409            "{:?}",
410            elf.detail.lines
411        );
412    }
413
414    #[test]
415    fn garbage_is_refused() {
416        assert!(read(b"\x7fELF\x02\x01\x01").is_err());
417        assert!(read(b"MZ").is_err());
418        let mut cut = crate::tests::fixtures::elf();
419        cut.truncate(cut.len() - 100);
420        assert!(read(&cut).is_err());
421    }
422}