Skip to main content

fnprint_loader/
lib.rs

1//! Load an ELF, hand back the loadable segments and a best-effort list of
2//! functions. x86-64 only for now (v0.1). We try symbols first, fall back to
3//! .eh_frame FDE ranges when the thing is stripped, which covers most release
4//! binaries since they keep unwind info even without a symtab.
5
6use anyhow::{bail, Context, Result};
7use goblin::elf::Elf;
8
9#[derive(Clone)]
10pub struct Segment {
11    pub vaddr: u64,
12    pub bytes: Vec<u8>,
13    pub exec: bool,
14    pub write: bool,
15}
16
17#[derive(Clone, Debug)]
18pub struct Func {
19    pub name: Option<String>,
20    pub entry: u64,
21    pub size: u64,
22    /// how we found it, handy for debugging discovery
23    pub source: FuncSource,
24}
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub enum FuncSource {
28    Symtab,
29    DynSym,
30    EhFrame,
31}
32
33pub struct Image {
34    pub segments: Vec<Segment>,
35    pub entry: u64,
36    pub is_pie: bool,
37}
38
39impl Image {
40    /// grab the code bytes for a function out of the mapped segments
41    pub fn code_at(&self, vaddr: u64, len: usize) -> Option<&[u8]> {
42        for s in &self.segments {
43            if vaddr >= s.vaddr && vaddr + len as u64 <= s.vaddr + s.bytes.len() as u64 {
44                let off = (vaddr - s.vaddr) as usize;
45                return Some(&s.bytes[off..off + len]);
46            }
47        }
48        None
49    }
50}
51
52pub struct Loaded {
53    pub image: Image,
54    pub funcs: Vec<Func>,
55}
56
57pub fn load(bytes: &[u8]) -> Result<Loaded> {
58    let elf = Elf::parse(bytes).context("not a valid elf")?;
59    if elf.header.e_machine != goblin::elf::header::EM_X86_64 {
60        bail!(
61            "only x86-64 is supported in this version (got e_machine {})",
62            elf.header.e_machine
63        );
64    }
65
66    let mut segments = Vec::new();
67    for ph in &elf.program_headers {
68        if ph.p_type != goblin::elf::program_header::PT_LOAD {
69            continue;
70        }
71        let start = ph.p_offset as usize;
72        let fsz = ph.p_filesz as usize;
73        let end = start.saturating_add(fsz).min(bytes.len());
74        let mut data = bytes[start..end].to_vec();
75        // bss: memsz > filesz, pad with zeros so reads there are defined
76        if (ph.p_memsz as usize) > data.len() {
77            data.resize(ph.p_memsz as usize, 0);
78        }
79        segments.push(Segment {
80            vaddr: ph.p_vaddr,
81            bytes: data,
82            exec: ph.is_executable(),
83            write: ph.is_write(),
84        });
85    }
86    if segments.is_empty() {
87        bail!("no PT_LOAD segments");
88    }
89
90    let is_pie = elf.header.e_type == goblin::elf::header::ET_DYN;
91
92    let mut funcs = discover(&elf, bytes)?;
93    // sort + dedup by entry, prefer named entries
94    funcs.sort_by(|a, b| {
95        a.entry
96            .cmp(&b.entry)
97            .then(b.name.is_some().cmp(&a.name.is_some()))
98    });
99    funcs.dedup_by_key(|f| f.entry);
100
101    Ok(Loaded {
102        image: Image {
103            segments,
104            entry: elf.header.e_entry,
105            is_pie,
106        },
107        funcs,
108    })
109}
110
111fn discover(elf: &Elf, raw: &[u8]) -> Result<Vec<Func>> {
112    let mut out = Vec::new();
113
114    for (sym, src) in elf
115        .syms
116        .iter()
117        .map(|s| (s, FuncSource::Symtab))
118        .chain(elf.dynsyms.iter().map(|s| (s, FuncSource::DynSym)))
119    {
120        if sym.st_type() != goblin::elf::sym::STT_FUNC {
121            continue;
122        }
123        if sym.st_value == 0 || sym.st_size == 0 {
124            continue; // imports / plt stubs with no body
125        }
126        let name = match src {
127            FuncSource::Symtab => elf.strtab.get_at(sym.st_name),
128            _ => elf.dynstrtab.get_at(sym.st_name),
129        }
130        .map(|s| s.to_string())
131        .filter(|s| !s.is_empty());
132        out.push(Func {
133            name,
134            entry: sym.st_value,
135            size: sym.st_size,
136            source: src,
137        });
138    }
139
140    // stripped? lean on unwind info.
141    if out.is_empty() {
142        if let Some(mut fdes) = eh_frame_funcs(elf, raw) {
143            out.append(&mut fdes);
144        }
145    }
146
147    Ok(out)
148}
149
150// pull function start+length out of every FDE in .eh_frame.
151fn eh_frame_funcs(elf: &Elf, raw: &[u8]) -> Option<Vec<Func>> {
152    use gimli::{BaseAddresses, CieOrFde, EhFrame, LittleEndian, UnwindSection};
153
154    let sh = elf
155        .section_headers
156        .iter()
157        .find(|s| elf.shdr_strtab.get_at(s.sh_name) == Some(".eh_frame"))?;
158    let start = sh.sh_offset as usize;
159    let data = raw.get(start..start + sh.sh_size as usize)?;
160
161    let eh = EhFrame::new(data, LittleEndian);
162    let bases = BaseAddresses::default().set_eh_frame(sh.sh_addr);
163
164    let mut entries = eh.entries(&bases);
165    let mut out = Vec::new();
166    loop {
167        match entries.next() {
168            Ok(Some(CieOrFde::Fde(partial))) => {
169                if let Ok(fde) = partial.parse(EhFrame::cie_from_offset) {
170                    let entry = fde.initial_address();
171                    let size = fde.len();
172                    if size > 0 {
173                        out.push(Func {
174                            name: None,
175                            entry,
176                            size,
177                            source: FuncSource::EhFrame,
178                        });
179                    }
180                }
181            }
182            Ok(Some(CieOrFde::Cie(_))) => {}
183            Ok(None) => break,
184            Err(_) => break,
185        }
186    }
187    if out.is_empty() {
188        None
189    } else {
190        Some(out)
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    #[test]
199    fn garbage_input_errors_not_panics() {
200        assert!(load(b"").is_err());
201        assert!(load(b"not an elf at all, just text here").is_err());
202        // elf magic then garbage bytes
203        let mut junk = vec![0x7f, b'E', b'L', b'F'];
204        junk.extend(std::iter::repeat_n(0x41u8, 400));
205        let _ = load(&junk); // must return Err/Ok, never panic
206    }
207
208    #[test]
209    fn truncated_header_does_not_panic() {
210        let mut hdr = vec![0x7f, b'E', b'L', b'F', 2, 1, 1, 0];
211        hdr.extend(std::iter::repeat_n(0u8, 48));
212        let _ = load(&hdr);
213    }
214}