Skip to main content

datui_lib/
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 std::path::Path;
14use std::sync::Arc;
15
16use color_eyre::Result;
17
18use crate::error_display::{FileError, in_file};
19use object::{Object, ObjectSection, ObjectSymbol, SectionFlags, SymbolFlags, SymbolSection};
20use polars::prelude::*;
21
22use crate::model_files::MetaValue;
23use crate::sqlite::Table;
24use crate::text_formats::Detail;
25
26/// What datui does with an ELF file: see [`crate::readers`].
27pub(crate) const READER: crate::readers::Reader = crate::readers::Reader {
28    scan,
29    // Never in a listing, which would list every executable.
30    signatures: &[crate::readers::Signature {
31        says: |head, _| looks_like(head),
32        kind: crate::readers::Kind::Magic,
33        trusted: crate::readers::Trusted {
34            listing: false,
35            tables: true,
36            ..crate::readers::EVERYWHERE
37        },
38    }],
39    tables: Some(|_| Ok(tables())),
40    ..crate::readers::BASE
41};
42
43/// The first four bytes of every ELF file.
44pub const MAGIC: &[u8; 4] = b"\x7fELF";
45
46/// The table an ELF file opens on, and the other one.
47pub const SYMBOLS: &str = "symbols";
48pub const SECTIONS: &str = "sections";
49
50/// Symbols read; a file of more says how many were left out.
51const MAX_SYMBOLS: usize = 10_000_000;
52
53// `sh_flags` bits.
54const SHF_WRITE: u64 = 0x1;
55const SHF_ALLOC: u64 = 0x2;
56
57/// Whether `head`, the first bytes of a file, begins an ELF file.
58pub fn looks_like(head: &[u8]) -> bool {
59    head.starts_with(MAGIC)
60}
61
62/// The tables of an ELF file, for the home screen and `--table`.
63pub fn tables() -> Vec<Table> {
64    let table = |name: &str, columns: &[&str]| Table {
65        name: name.to_string(),
66        kind: "table".to_string(),
67        internal: false,
68        columns: columns
69            .iter()
70            .map(|c| (c.to_string(), String::new()))
71            .collect(),
72    };
73    vec![
74        table(
75            SYMBOLS,
76            &["name", "addr", "size", "kind", "bind", "section", "region"],
77        ),
78        table(
79            SECTIONS,
80            &["name", "addr", "size", "flags", "kind", "region"],
81        ),
82    ]
83}
84
85/// The region a section with `sh_flags` sits in: what is written is RAM, what is only
86/// loaded is flash; a section not loaded is in neither.
87fn region(flags: u64) -> Option<&'static str> {
88    match (flags & SHF_ALLOC != 0, flags & SHF_WRITE != 0) {
89        (false, _) => None,
90        (true, true) => Some("ram"),
91        (true, false) => Some("flash"),
92    }
93}
94
95/// `sh_flags` as `readelf` writes them: `WAX` for a writable, allocated, executable
96/// section.
97fn flags_text(flags: u64) -> String {
98    const LETTERS: [(u64, char); 11] = [
99        (0x1, 'W'),
100        (0x2, 'A'),
101        (0x4, 'X'),
102        (0x10, 'M'),
103        (0x20, 'S'),
104        (0x40, 'I'),
105        (0x80, 'L'),
106        (0x100, 'O'),
107        (0x200, 'G'),
108        (0x400, 'T'),
109        (0x800, 'C'),
110    ];
111    LETTERS
112        .iter()
113        .filter(|(bit, _)| flags & bit != 0)
114        .map(|(_, c)| *c)
115        .collect()
116}
117
118fn sh_flags(flags: SectionFlags) -> u64 {
119    match flags {
120        SectionFlags::Elf { sh_flags } => sh_flags,
121        _ => 0,
122    }
123}
124
125/// A symbol's type, from the low half of `st_info`.
126fn symbol_kind(st_info: u8) -> &'static str {
127    match st_info & 0xf {
128        0 => "notype",
129        1 => "object",
130        2 => "func",
131        3 => "section",
132        4 => "file",
133        5 => "common",
134        6 => "tls",
135        10 => "ifunc",
136        _ => "other",
137    }
138}
139
140/// A symbol's binding, from the high half of `st_info`.
141fn symbol_bind(st_info: u8) -> &'static str {
142    match st_info >> 4 {
143        0 => "local",
144        1 => "global",
145        2 => "weak",
146        10 => "unique",
147        _ => "other",
148    }
149}
150
151/// A Rust symbol's name demangled, without its hash; any other name as it is. C++
152/// names stay mangled: no C++ demangler is in the tree.
153pub fn demangle(name: &str) -> String {
154    match rustc_demangle::try_demangle(name) {
155        Ok(demangled) => format!("{demangled:#}"),
156        Err(_) => name.to_string(),
157    }
158}
159
160/// What an ELF file's tables hold.
161pub struct Elf {
162    pub symbols: DataFrame,
163    pub sections: DataFrame,
164    pub detail: Detail,
165    /// Symbols past [`MAX_SYMBOLS`], left out.
166    pub left_out: usize,
167}
168
169/// Read the symbol and section tables of the ELF file in `data`.
170pub fn read(data: &[u8]) -> std::result::Result<Elf, String> {
171    if !looks_like(data) {
172        return Err("not an ELF file: no \\x7fELF at the start".into());
173    }
174    let file = object::File::parse(data).map_err(|e| format!("not a readable ELF file: {e}"))?;
175
176    let mut section_names: Vec<String> = Vec::new();
177    let mut section_flags: Vec<u64> = Vec::new();
178    let (mut s_name, mut s_addr, mut s_size, mut s_flags, mut s_kind, mut s_region) = (
179        Vec::new(),
180        Vec::new(),
181        Vec::new(),
182        Vec::new(),
183        Vec::new(),
184        Vec::new(),
185    );
186    let (mut flash, mut ram) = (0u64, 0u64);
187    for section in file.sections() {
188        let index = section.index().0;
189        if section_names.len() <= index {
190            section_names.resize(index + 1, String::new());
191            section_flags.resize(index + 1, 0);
192        }
193        let name = section.name().unwrap_or_default().to_string();
194        let flags = sh_flags(section.flags());
195        let place = region(flags);
196        match place {
197            Some("ram") => ram = ram.saturating_add(section.size()),
198            Some(_) => flash = flash.saturating_add(section.size()),
199            None => {}
200        }
201        section_names[index] = name.clone();
202        section_flags[index] = flags;
203        s_name.push(name);
204        s_addr.push(section.address());
205        s_size.push(section.size());
206        s_flags.push(flags_text(flags));
207        s_kind.push(format!("{:?}", section.kind()).to_ascii_lowercase());
208        s_region.push(place);
209    }
210
211    // The static symbol table, or the dynamic one of a stripped library.
212    let mut symbols: Vec<_> = file.symbols().collect();
213    if symbols.is_empty() {
214        symbols = file.dynamic_symbols().collect();
215    }
216    let total = symbols.len();
217    symbols.truncate(MAX_SYMBOLS);
218    let rows = symbols.len();
219    let (mut name, mut addr, mut size, mut kind, mut bind, mut section, mut place) = (
220        Vec::with_capacity(rows),
221        Vec::with_capacity(rows),
222        Vec::with_capacity(rows),
223        Vec::with_capacity(rows),
224        Vec::with_capacity(rows),
225        Vec::with_capacity(rows),
226        Vec::with_capacity(rows),
227    );
228    for symbol in &symbols {
229        let st_info = match symbol.flags() {
230            SymbolFlags::Elf { st_info, .. } => st_info,
231            _ => 0,
232        };
233        name.push(demangle(symbol.name().unwrap_or_default()));
234        addr.push(symbol.address());
235        size.push(symbol.size());
236        kind.push(symbol_kind(st_info));
237        bind.push(symbol_bind(st_info));
238        let (in_section, in_region) = match symbol.section() {
239            SymbolSection::Section(index) => (
240                section_names.get(index.0).cloned(),
241                section_flags.get(index.0).copied().and_then(region),
242            ),
243            SymbolSection::Undefined => (Some("UND".to_string()), None),
244            SymbolSection::Absolute => (Some("ABS".to_string()), None),
245            SymbolSection::Common => (Some("COMMON".to_string()), None),
246            _ => (None, None),
247        };
248        section.push(in_section);
249        place.push(in_region);
250    }
251    let symbols = df!(
252        "name" => name,
253        "addr" => addr,
254        "size" => size,
255        "kind" => kind,
256        "bind" => bind,
257        "section" => section,
258        "region" => place,
259    )
260    .map_err(|e| e.to_string())?;
261    let sections = df!(
262        "name" => s_name,
263        "addr" => s_addr,
264        "size" => s_size,
265        "flags" => s_flags,
266        "kind" => s_kind,
267        "region" => s_region,
268    )
269    .map_err(|e| e.to_string())?;
270
271    let group = crate::numfmt::group_chrome;
272    let mut lines = vec![
273        format!(
274            "Class: {}, {} endian",
275            if file.is_64() { "64-bit" } else { "32-bit" },
276            if file.is_little_endian() {
277                "little"
278            } else {
279                "big"
280            }
281        ),
282        format!("Machine: {:?}", file.architecture()),
283        format!("Type: {:?}", file.kind()),
284        format!("Entry: 0x{:x}", file.entry()),
285        format!(
286            "Flash: {} bytes in loaded, unwritten sections",
287            group(usize::try_from(flash).unwrap_or(usize::MAX))
288        ),
289        format!(
290            "RAM: {} bytes in written sections",
291            group(usize::try_from(ram).unwrap_or(usize::MAX))
292        ),
293        format!("Symbols: {}", group(total)),
294    ];
295    if total > rows {
296        lines.push(format!("The first {} symbols are read.", group(rows)));
297    }
298    let list = crate::text_formats::capped_list(
299        (0..sections.height()).map(|i| {
300            let get = |c: &str| sections.column(c).ok().and_then(|c| c.get(i).ok());
301            let text = format!(
302                "0x{:x}  {} bytes  {}",
303                get("addr")
304                    .and_then(|v| v.extract::<u64>())
305                    .unwrap_or_default(),
306                get("size")
307                    .and_then(|v| v.extract::<u64>())
308                    .unwrap_or_default(),
309                get("flags")
310                    .and_then(|v| v.get_str().map(str::to_string))
311                    .unwrap_or_default()
312            );
313            let name = get("name")
314                .and_then(|v| v.get_str().map(str::to_string))
315                .unwrap_or_default();
316            (name, MetaValue::Text(text))
317        }),
318        sections.height(),
319    );
320    Ok(Elf {
321        symbols,
322        sections,
323        detail: Detail {
324            tab: crate::text_formats::tab(crate::FileFormat::Elf),
325            lines,
326            list_title: "Sections",
327            list,
328            first: false,
329            ..Default::default()
330        },
331        left_out: total - rows,
332    })
333}
334
335/// Open the ELF file at `path` as the table `wanted` names: its symbols unless
336/// `--table sections` says otherwise.
337pub fn open(path: &Path, wanted: Option<&str>) -> Result<(LazyFrame, crate::members::Opened)> {
338    let tables = tables();
339    let picked = match wanted {
340        None => SYMBOLS.to_string(),
341        Some(_) => match crate::members::pick(tables.clone(), wanted, path, "")? {
342            crate::sqlite::Pick::One(table) => table.name,
343            crate::sqlite::Pick::Several(_) => SYMBOLS.to_string(),
344        },
345    };
346    let bytes = crate::fixed_records::Bytes::map(path).map_err(|e| in_file(path, e.into()))?;
347    let elf = read(bytes.as_slice()).map_err(|e| FileError::new(path, e))?;
348    let mut notes = Vec::new();
349    if elf.left_out > 0 {
350        notes.push(crate::text_formats::note(
351            format!(
352                "{} symbols left out: past the first {}",
353                crate::numfmt::group_chrome(elf.left_out),
354                crate::numfmt::group_chrome(MAX_SYMBOLS)
355            ),
356            "the symbol table".to_string(),
357        ));
358    }
359    let df = if picked == SECTIONS {
360        elf.sections
361    } else {
362        elf.symbols
363    };
364    Ok((
365        df.lazy(),
366        crate::members::Opened {
367            window: None,
368            detail: Some(Arc::new(elf.detail)),
369            other_tables: crate::members::others(&tables, &picked),
370            notes,
371            units: Vec::new(),
372            indexing: None,
373            numbering: None,
374        },
375    ))
376}
377
378/// The scan of an ELF file: the table `--table` names, its symbols by default.
379fn scan(input: crate::readers::ScanIn<'_>) -> Result<crate::scan::Scan> {
380    let (lf, opened) = open(input.path(), input.options.table.as_deref())?;
381    input.report.opened = Some(Arc::new(opened));
382    Ok(lf.into())
383}
384
385#[cfg(test)]
386pub(crate) mod tests {
387    use super::*;
388
389    /// A file that is not ELF names itself, in the one shape.
390    #[test]
391    fn errors_name_the_file() {
392        crate::readers::bad_input::each_names_its_file(
393            crate::FileFormat::Elf,
394            &[
395                ("text.elf", b"hello there", "Not an ELF file"),
396                (
397                    "cut.elf",
398                    b"\x7fELF\x02\x01\x01\0",
399                    "Not a readable ELF file",
400                ),
401            ],
402        );
403    }
404
405    /// name, type, flags, addr, offset, size, link, info, align, entsize.
406    type SectionHeader = (u32, u32, u64, u64, u64, u64, u32, u32, u64, u64);
407
408    /// A tiny 64-bit little-endian ELF executable: `.text` (AX), `.rodata` (A),
409    /// `.data` (WA), `.bss` (WA, no bits), `.symtab`, `.strtab`, `.shstrtab`, and
410    /// symbols in each, one of them a mangled Rust name.
411    pub(crate) fn tiny() -> Vec<u8> {
412        let shstr = b"\0.text\0.rodata\0.data\0.bss\0.symtab\0.strtab\0.shstrtab\0";
413        let name_at = |n: &[u8]| {
414            shstr
415                .windows(n.len())
416                .position(|w| w == n)
417                .expect("a section name") as u32
418        };
419        let strtab =
420            b"\0main\0TABLE\0counter\0buffer\0_ZN4core3fmt5write17h0123456789abcdefE\0weak_hook\0";
421        let str_at = |n: &[u8]| {
422            strtab
423                .windows(n.len())
424                .position(|w| w == n)
425                .expect("a symbol name") as u32
426        };
427        // (name, value, size, info, shndx)
428        let syms: Vec<(u32, u64, u64, u8, u16)> = vec![
429            (0, 0, 0, 0, 0),
430            (str_at(b"main\0"), 0x1000, 64, 0x12, 1),
431            (str_at(b"TABLE\0"), 0x2000, 256, 0x11, 2),
432            (str_at(b"counter\0"), 0x3000, 4, 0x11, 3),
433            (str_at(b"buffer\0"), 0x3010, 1024, 0x01, 4),
434            (
435                str_at(b"_ZN4core3fmt5write17h0123456789abcdefE\0"),
436                0x1040,
437                128,
438                0x12,
439                1,
440            ),
441            (str_at(b"weak_hook\0"), 0x10c0, 8, 0x22, 1),
442        ];
443        let mut symtab = Vec::new();
444        for (name, value, size, info, shndx) in &syms {
445            symtab.extend(name.to_le_bytes());
446            symtab.push(*info);
447            symtab.push(0);
448            symtab.extend(shndx.to_le_bytes());
449            symtab.extend(value.to_le_bytes());
450            symtab.extend(size.to_le_bytes());
451        }
452        let text = vec![0xc3u8; 0xc8];
453        let rodata = vec![1u8; 256];
454        let data = vec![2u8; 16];
455        // Section contents after the 64-byte header.
456        let mut body = Vec::new();
457        let mut place = |bytes: &[u8]| {
458            let at = 64 + body.len() as u64;
459            body.extend_from_slice(bytes);
460            while body.len() % 8 != 0 {
461                body.push(0);
462            }
463            at
464        };
465        let text_at = place(&text);
466        let rodata_at = place(&rodata);
467        let data_at = place(&data);
468        let symtab_at = place(&symtab);
469        let strtab_at = place(strtab);
470        let shstr_at = place(shstr);
471        let shoff = 64 + body.len() as u64;
472        let sections: Vec<SectionHeader> = vec![
473            (0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
474            (
475                name_at(b".text\0"),
476                1,
477                0x6,
478                0x1000,
479                text_at,
480                text.len() as u64,
481                0,
482                0,
483                16,
484                0,
485            ),
486            (
487                name_at(b".rodata\0"),
488                1,
489                0x2,
490                0x2000,
491                rodata_at,
492                256,
493                0,
494                0,
495                8,
496                0,
497            ),
498            (name_at(b".data\0"), 1, 0x3, 0x3000, data_at, 16, 0, 0, 8, 0),
499            (
500                name_at(b".bss\0"),
501                8,
502                0x3,
503                0x3010,
504                data_at + 16,
505                1024,
506                0,
507                0,
508                8,
509                0,
510            ),
511            (
512                name_at(b".symtab\0"),
513                2,
514                0,
515                0,
516                symtab_at,
517                symtab.len() as u64,
518                6,
519                1,
520                8,
521                24,
522            ),
523            (
524                name_at(b".strtab\0"),
525                3,
526                0,
527                0,
528                strtab_at,
529                strtab.len() as u64,
530                0,
531                0,
532                1,
533                0,
534            ),
535            (
536                name_at(b".shstrtab\0"),
537                3,
538                0,
539                0,
540                shstr_at,
541                shstr.len() as u64,
542                0,
543                0,
544                1,
545                0,
546            ),
547        ];
548        let mut out = Vec::new();
549        out.extend(MAGIC);
550        out.extend([2, 1, 1, 0]);
551        out.extend([0u8; 8]);
552        out.extend(2u16.to_le_bytes()); // ET_EXEC
553        out.extend(62u16.to_le_bytes()); // x86-64
554        out.extend(1u32.to_le_bytes());
555        out.extend(0x1000u64.to_le_bytes()); // entry
556        out.extend(0u64.to_le_bytes()); // phoff
557        out.extend(shoff.to_le_bytes());
558        out.extend(0u32.to_le_bytes());
559        out.extend(64u16.to_le_bytes());
560        out.extend(56u16.to_le_bytes());
561        out.extend(0u16.to_le_bytes());
562        out.extend(64u16.to_le_bytes());
563        out.extend((sections.len() as u16).to_le_bytes());
564        out.extend(7u16.to_le_bytes()); // shstrndx
565        out.extend(body);
566        for (name, kind, flags, addr, offset, size, link, info, align, entsize) in sections {
567            out.extend(name.to_le_bytes());
568            out.extend(kind.to_le_bytes());
569            out.extend(flags.to_le_bytes());
570            out.extend(addr.to_le_bytes());
571            out.extend(offset.to_le_bytes());
572            out.extend(size.to_le_bytes());
573            out.extend(link.to_le_bytes());
574            out.extend(info.to_le_bytes());
575            out.extend(align.to_le_bytes());
576            out.extend(entsize.to_le_bytes());
577        }
578        out
579    }
580
581    #[test]
582    fn symbols_with_their_section_and_region() {
583        let elf = read(&tiny()).unwrap();
584        let s = &elf.symbols;
585        let text = |c: &str| -> Vec<Option<String>> {
586            s.column(c)
587                .unwrap()
588                .str()
589                .unwrap()
590                .iter()
591                .map(|v| v.map(str::to_string))
592                .collect()
593        };
594        let names = text("name");
595        let at = |n: &str| names.iter().position(|v| v.as_deref() == Some(n)).unwrap();
596        assert!(names.contains(&Some("core::fmt::write".to_string())));
597        let region = text("region");
598        let section = text("section");
599        let kind = text("kind");
600        let bind = text("bind");
601        assert_eq!(region[at("main")].as_deref(), Some("flash"));
602        assert_eq!(region[at("TABLE")].as_deref(), Some("flash"));
603        assert_eq!(region[at("counter")].as_deref(), Some("ram"));
604        assert_eq!(section[at("buffer")].as_deref(), Some(".bss"));
605        assert_eq!(region[at("buffer")].as_deref(), Some("ram"));
606        assert_eq!(kind[at("main")].as_deref(), Some("func"));
607        assert_eq!(bind[at("buffer")].as_deref(), Some("local"));
608        assert_eq!(bind[at("weak_hook")].as_deref(), Some("weak"));
609        let sizes = s.column("size").unwrap().u64().unwrap();
610        assert_eq!(sizes.get(at("buffer")), Some(1024));
611        let flags: Vec<_> = elf
612            .sections
613            .column("flags")
614            .unwrap()
615            .str()
616            .unwrap()
617            .iter()
618            .map(|v| v.unwrap_or_default().to_string())
619            .collect();
620        assert!(flags.contains(&"AX".to_string()) && flags.contains(&"WA".to_string()));
621        assert!(
622            elf.detail
623                .lines
624                .iter()
625                .any(|l| l.starts_with("RAM: 1,040 bytes")),
626            "{:?}",
627            elf.detail.lines
628        );
629    }
630
631    #[test]
632    fn garbage_is_refused() {
633        assert!(read(b"\x7fELF\x02\x01\x01").is_err());
634        assert!(read(b"MZ").is_err());
635        let mut cut = tiny();
636        cut.truncate(cut.len() - 100);
637        assert!(read(&cut).is_err());
638    }
639}