use std::error::Error;
use std::ops::Range;
use std::path::Path;
use gimli::{BaseAddresses, CieOrFde, EhFrame, LittleEndian, UnwindSection};
use object::{Object, ObjectSection, ObjectSegment, ObjectSymbol, ObjectSymbolTable, RelocationTarget, SymbolKind};
pub struct Segment {
pub va: u64,
pub offset: usize,
pub size: usize,
}
#[derive(Default)]
pub struct Image {
pub data: Vec<u8>,
pub segments: Vec<Segment>,
pub functions: Vec<Range<u64>>,
pub pointers: Vec<(u64, u64)>,
pub imports: Vec<(u64, String)>,
pub symbols: Vec<(u64, String)>,
pub metadata: crate::image_format::Metadata,
}
pub fn occurrences(haystack: &[u8], needle: &[u8]) -> Vec<usize> {
occurrence_iter(haystack, needle).collect()
}
pub fn occurrence_iter<'a>(haystack: &'a [u8], needle: &'a [u8]) -> impl Iterator<Item = usize> + 'a {
let mut from = 0;
std::iter::from_fn(move || {
let (&first, rest) = needle.split_first()?;
while let Some(index) = haystack[from..].iter().position(|byte| *byte == first) {
let at = from + index;
from = at + 1;
if haystack[at + 1..].starts_with(rest) {
return Some(at);
}
}
None
})
}
fn parse_eh_frame_section(
file: &object::File<'_>,
section: &object::Section<'_, '_>,
) -> Result<Vec<Range<u64>>, Box<dyn Error>> {
let eh_frame = EhFrame::new(section.data()?, LittleEndian);
let mut bases = BaseAddresses::default().set_eh_frame(section.address());
if let Some(text) = file.section_by_name(".text") {
bases = bases.set_text(text.address());
}
let mut functions = Vec::new();
let mut entries = eh_frame.entries(&bases);
while let Some(entry) = entries.next()? {
if let CieOrFde::Fde(partial) = entry {
let fde = partial.parse(EhFrame::cie_from_offset)?;
if fde.len() > 0 {
let end = fde.initial_address().checked_add(fde.len()).ok_or("function range overflow")?;
functions.push(fde.initial_address()..end);
}
}
}
functions.sort_by_key(|range| (range.start, range.end));
functions.dedup();
Ok(functions)
}
type UnwindFunctions = (Vec<Range<u64>>, Option<&'static str>);
fn function_ranges(file: &object::File<'_>, require_eh_frame: bool) -> Result<UnwindFunctions, Box<dyn Error>> {
let Some(section) = file.section_by_name(".eh_frame") else {
if require_eh_frame {
return Err("no .eh_frame section".into());
}
return Ok((Vec::new(), Some("missing_eh_frame")));
};
match parse_eh_frame_section(file, §ion) {
Ok(functions) => Ok((functions, None)),
Err(err) if require_eh_frame => Err(err),
Err(_) => Ok((Vec::new(), Some("invalid_eh_frame"))),
}
}
impl Image {
pub fn load(path: &Path) -> Result<Self, Box<dyn Error>> {
let data = std::fs::read(path)?;
Self::parse(data)
}
pub fn parse(data: Vec<u8>) -> Result<Self, Box<dyn Error>> {
Self::parse_inner(data, true)
}
pub fn parse_for_live(data: Vec<u8>) -> Result<Self, Box<dyn Error>> {
Self::parse_inner(data, false)
}
fn parse_inner(data: Vec<u8>, require_eh_frame: bool) -> Result<Self, Box<dyn Error>> {
let file = crate::image_format::parse(&data)?;
let segments = file
.segments()
.filter(|segment| segment.file_range().1 > 0)
.map(|segment| {
let (offset, size) = segment.file_range();
if offset.checked_add(size).is_none_or(|end| end > data.len() as u64)
|| segment.address().checked_add(size).is_none()
{
return Err("segment range overflow".into());
}
Ok(Segment { va: segment.address(), offset: usize::try_from(offset)?, size: usize::try_from(size)? })
})
.collect::<Result<Vec<_>, Box<dyn Error>>>()?;
let (functions, unwind_warning) = function_ranges(&file, require_eh_frame)?;
let mut metadata = crate::image_format::metadata(&file, &data, &segments, &functions)?;
if let Some(warning) = unwind_warning {
metadata.warn(warning);
}
let symbols = file.dynamic_symbol_table();
let mut pointers = Vec::new();
let mut imports = Vec::new();
for (place, relocation) in file.dynamic_relocations().into_iter().flatten() {
match relocation.target() {
RelocationTarget::Symbol(index) => {
let name = symbols.as_ref().and_then(|table| table.symbol_by_index(index).ok()?.name().ok());
imports.push((place, name.unwrap_or("?").to_owned()));
}
_ if matches!(relocation.flags(), object::RelocationFlags::Elf { r_type } if r_type == object::elf::R_X86_64_RELATIVE) =>
{
pointers.push((place, relocation.addend().cast_unsigned()));
}
_ => {}
}
}
if let object::File::MachO64(macho) = &file {
let linkage = crate::macho::linkage::collect(macho, &data, metadata.base)?;
imports = linkage.imports;
pointers = linkage.pointers;
metadata.exports.extend(linkage.exports);
if let Some(entry) = metadata.entry {
metadata.exports.insert(entry);
}
if !linkage.imports_complete {
metadata.warn("macho_import_metadata_incomplete");
}
if !linkage.fixups_complete {
metadata.warn("macho_fixup_metadata_incomplete");
}
if !linkage.exports_complete {
metadata.warn("macho_export_metadata_incomplete");
}
for warning in linkage.warnings {
metadata.warn(warning);
}
}
pointers.sort_unstable();
imports.sort_unstable();
let mut symbols: Vec<(u64, String)> = file
.symbols()
.filter(|symbol| symbol.kind() == SymbolKind::Text && symbol.address() != 0)
.filter_map(|symbol| Some((symbol.address(), symbol.name().ok()?.to_owned())))
.collect();
symbols.sort_unstable();
drop(file);
Ok(Self { data, segments, functions, pointers, imports, symbols, metadata })
}
pub fn read(&self, va: u64, len: usize) -> Option<&[u8]> {
let segment = self.segments.iter().find(|s| va >= s.va && va - s.va + len as u64 <= s.size as u64)?;
let start = segment.offset + usize::try_from(va - segment.va).ok()?;
self.data.get(start..start + len)
}
pub fn read_u64(&self, va: u64) -> Option<u64> {
Some(u64::from_le_bytes(self.read(va, 8)?.try_into().ok()?))
}
pub fn read_u32(&self, va: u64) -> Option<u32> {
Some(u32::from_le_bytes(self.read(va, 4)?.try_into().ok()?))
}
pub fn function_at(&self, addr: u64) -> Option<Range<u64>> {
let end = self.functions.partition_point(|range| range.start <= addr);
self.functions[..end].iter().rev().take(64).find(|range| range.contains(&addr)).cloned()
}
pub fn function_or_span(&self, addr: u64, to: Option<u64>) -> Option<Range<u64>> {
if let Some(f) = self.function_at(addr) {
return Some(f);
}
if self.metadata.kind == crate::image_format::Kind::MachO {
let section = self.metadata.code_ranges.iter().find(|r| r.contains(&addr))?;
let next = self.metadata.function_starts.iter().copied().find(|&s| s > addr).unwrap_or(section.end);
let end = next.min(section.end).min(addr.saturating_add(0x1000));
let end = to.unwrap_or(end).min(end);
return (addr < end).then_some(addr..end);
}
let seg = self.segments.iter().find(|s| addr >= s.va && addr < s.va + s.size as u64)?;
let seg_end = seg.va + seg.size as u64;
let next = self.functions.iter().find(|f| f.start > addr).map_or(seg_end, |f| f.start);
Some(addr..to.unwrap_or_else(|| next.min(seg_end).min(addr + 0x1000)))
}
pub fn code(&self, function: &Range<u64>) -> Option<&[u8]> {
self.read(function.start, usize::try_from(function.end - function.start).ok()?)
}
pub fn slot(&self, place: u64) -> Option<Slot<'_>> {
if let Ok(index) = self.imports.binary_search_by_key(&place, |(at, _)| *at) {
return Some(Slot::Import(&self.imports[index].1));
}
let index = self.pointers.binary_search_by_key(&place, |(at, _)| *at).ok()?;
Some(Slot::Pointer(self.pointers[index].1))
}
pub fn find(&self, needle: &[u8]) -> Vec<u64> {
let mut found = Vec::new();
for segment in self.searchable_segments() {
let Some(bytes) = self.data.get(segment.offset..segment.offset + segment.size) else { continue };
found.extend(occurrences(bytes, needle).into_iter().map(|at| segment.va + at as u64));
}
found.sort_unstable();
found.dedup();
found
}
pub fn searchable_segments(&self) -> impl Iterator<Item = &Segment> {
self.segments.iter().filter(|s| !self.metadata.search_excluded.iter().any(|r| r.contains(&s.va)))
}
pub fn rodata_preview(&self, va: u64) -> Option<(String, bool)> {
let bytes = self.read(va, 64).or_else(|| self.read(va, 8))?;
let printable = |slice: &[u8]| slice.iter().all(|b| (0x20..0x7f).contains(b));
let length = usize::from(bytes[0]);
if (2..bytes.len()).contains(&length)
&& printable(&bytes[1..=length])
&& (bytes[0] < 0x20 || bytes.get(1 + length).is_some_and(|&b| b < 0x20))
{
return Some((String::from_utf8_lossy(&bytes[1..=length]).into_owned(), true));
}
let run = bytes.iter().take_while(|b| (0x20..0x7f).contains(*b)).count();
(run >= 4).then(|| (String::from_utf8_lossy(&bytes[..run]).into_owned(), bytes.get(run) == Some(&0)))
}
pub fn preview(&self, va: u64) -> Option<String> {
self.rodata_preview(va).map(|(s, _)| s)
}
}
pub enum Slot<'a> {
Import(&'a str),
Pointer(u64),
}
#[cfg(test)]
mod tests {
use super::occurrences;
#[test]
fn occurrences_include_overlapping_matches() {
assert_eq!(occurrences(b"abababa", b"aba"), [0, 2, 4]);
assert_eq!(occurrences(b"some text\0some text", b"some text"), [0, 10]);
assert_eq!(occurrences(b"abc", b""), Vec::<usize>::new());
}
}