use crate::dwarf::attach_dwarf_frames;
use crate::native::{
collect_sections, collect_text_symbols, collect_undefined_imports, symbol_fingerprint,
};
use crate::x86::X86_NORMALIZATION_VERSION;
use crate::{
ArtifactBackend, ArtifactCall, ArtifactCapabilities, ArtifactError, ArtifactFingerprint,
ArtifactFormat, ArtifactIr, ArtifactSymbol, UnresolvedCall,
};
use iced_x86::{Decoder, DecoderOptions, Mnemonic, OpKind};
use object::{
Architecture, Endianness, Object, ObjectKind, ObjectSection, RelocationKind, RelocationTarget,
SectionKind,
};
use std::collections::{BTreeSet, HashMap};
#[derive(Debug, Default, Clone, Copy)]
pub struct ElfBackend;
pub const ELF_NORMALIZATION_VERSION: &str = X86_NORMALIZATION_VERSION;
impl ArtifactBackend for ElfBackend {
fn format(&self) -> ArtifactFormat {
ArtifactFormat::Elf
}
fn detects(&self, bytes: &[u8]) -> bool {
bytes.starts_with(b"\x7fELF")
}
fn parse(&self, bytes: &[u8]) -> Result<ArtifactIr, ArtifactError> {
self.parse_with_debug_companion(bytes, None)
}
fn capabilities(&self) -> ArtifactCapabilities {
ArtifactCapabilities {
symbols: true,
call_graph: true,
source_mapping: false,
debug_info_unreadable: false,
normalized_duplicates: false,
independent_data_segments: false,
relocations: false,
data_segments: true,
}
}
}
impl ElfBackend {
#[allow(
clippy::too_many_lines,
reason = "parsing one artifact keeps all fallible format reads in one transaction"
)]
pub fn parse_with_debug_companion(
&self,
bytes: &[u8],
debug_companion: Option<&[u8]>,
) -> Result<ArtifactIr, ArtifactError> {
if !self.detects(bytes) {
return Err(ArtifactError::WrongFormat {
expected: ArtifactFormat::Elf,
});
}
let file = object::File::parse(bytes).map_err(|error| malformed(error.to_string()))?;
let debug_file = debug_companion
.map(|companion| {
let companion =
object::File::parse(companion).map_err(|error| malformed(error.to_string()))?;
if !matching_build_id(&file, &companion) {
return Err(malformed(
"external debug companion does not have the artifact's build ID".to_owned(),
));
}
Ok(companion)
})
.transpose()?;
let mut ir = ArtifactIr::empty(ArtifactFormat::Elf, bytes);
let mut symbol_fingerprints = HashMap::new();
let mut symbol_addresses = HashMap::new();
let mut symbol_addresses_by_section = HashMap::new();
let mut symbol_addresses_by_fingerprint = HashMap::new();
collect_sections(&file, &mut ir).map_err(|error| malformed(error.to_string()))?;
collect_undefined_imports(file.symbols().chain(file.dynamic_symbols()), &mut ir);
let supports_global_address_join = file.kind() != ObjectKind::Relocatable;
for symbol in
collect_text_symbols(&file, &mut ir).map_err(|error| malformed(error.to_string()))?
{
symbol_fingerprints.insert(symbol.index, Some(symbol.fingerprint));
symbol_addresses_by_section
.insert((symbol.section, symbol.address), symbol.fingerprint);
if supports_global_address_join {
symbol_addresses
.entry(symbol.address)
.or_insert(symbol.fingerprint);
}
symbol_addresses_by_fingerprint
.insert(symbol.fingerprint, (symbol.address, symbol.size));
}
if ir.symbols.is_empty() {
infer_text_regions(&file, &mut ir)?;
}
record_entry_point(file.entry(), &symbol_addresses, &mut ir);
record_init_fini_roots(&file, &symbol_fingerprints, &symbol_addresses, &mut ir);
ir.calls = x86_direct_calls(
&file,
&ir.symbols,
&symbol_fingerprints,
&symbol_addresses_by_section,
);
attach_dwarf_frames(
debug_file.as_ref().unwrap_or(&file),
&symbol_addresses_by_fingerprint,
&mut ir,
);
ir.capabilities = ArtifactCapabilities {
symbols: !ir.symbols.is_empty(),
call_graph: !ir.calls.is_empty(),
source_mapping: !ir.source_mappings.is_empty(),
debug_info_unreadable: ir.capabilities.debug_info_unreadable,
normalized_duplicates: crate::x86::supports_normalized_duplicates(file.architecture()),
independent_data_segments: false,
relocations: !ir.relocations.is_empty(),
data_segments: !ir.data_segments.is_empty(),
};
Ok(ir)
}
}
fn matching_build_id(artifact: &object::File<'_>, companion: &object::File<'_>) -> bool {
let Ok(Some(artifact_id)) = artifact.build_id() else {
return false;
};
let Ok(Some(companion_id)) = companion.build_id() else {
return false;
};
artifact_id == companion_id
}
fn record_entry_point(
entry_address: u64,
addresses: &HashMap<u64, ArtifactFingerprint>,
ir: &mut ArtifactIr,
) {
if entry_address != 0 {
if let Some(fingerprint) = addresses.get(&entry_address) {
ir.entry_points.push(*fingerprint);
}
}
}
fn record_init_fini_roots(
file: &object::File<'_>,
fingerprints: &HashMap<object::SymbolIndex, Option<ArtifactFingerprint>>,
addresses: &HashMap<u64, ArtifactFingerprint>,
ir: &mut ArtifactIr,
) {
let mut roots = BTreeSet::new();
for section in file.sections() {
if !matches!(section.name().ok(), Some(".init_array" | ".fini_array")) {
continue;
}
for (_, relocation) in section.relocations() {
if let RelocationTarget::Symbol(index) = relocation.target() {
if let Some(Some(fingerprint)) = fingerprints.get(&index) {
roots.insert(*fingerprint);
}
}
}
if let Ok(data) = section.data() {
roots.extend(pointer_roots(
data,
file.is_64(),
file.endianness(),
addresses,
));
}
}
let existing: BTreeSet<_> = ir.entry_points.iter().copied().collect();
ir.entry_points.extend(
roots
.into_iter()
.filter(|fingerprint| !existing.contains(fingerprint)),
);
}
fn pointer_roots(
bytes: &[u8],
is_64: bool,
endianness: Endianness,
addresses: &HashMap<u64, ArtifactFingerprint>,
) -> BTreeSet<ArtifactFingerprint> {
let width = if is_64 { 8 } else { 4 };
bytes
.chunks_exact(width)
.filter_map(|chunk| pointer_value(chunk, endianness))
.filter_map(|address| addresses.get(&address).copied())
.collect()
}
fn pointer_value(bytes: &[u8], endianness: Endianness) -> Option<u64> {
match bytes.len() {
4 => {
let bytes: [u8; 4] = bytes.try_into().ok()?;
Some(match endianness {
Endianness::Little => u64::from(u32::from_le_bytes(bytes)),
Endianness::Big => u64::from(u32::from_be_bytes(bytes)),
})
}
8 => {
let bytes: [u8; 8] = bytes.try_into().ok()?;
Some(match endianness {
Endianness::Little => u64::from_le_bytes(bytes),
Endianness::Big => u64::from_be_bytes(bytes),
})
}
_ => None,
}
}
fn infer_text_regions(file: &object::File<'_>, ir: &mut ArtifactIr) -> Result<(), ArtifactError> {
crate::native::infer_text_regions(file, ir, |section, normalized, data| {
symbol_fingerprint(None, section, normalized, data)
})
.map_err(|error| malformed(error.to_string()))?;
Ok(())
}
fn x86_direct_calls(
file: &object::File<'_>,
symbols: &[ArtifactSymbol],
fingerprints: &HashMap<object::SymbolIndex, Option<ArtifactFingerprint>>,
addresses: &HashMap<(object::SectionIndex, u64), ArtifactFingerprint>,
) -> Vec<ArtifactCall> {
let bitness = match file.architecture() {
Architecture::I386 => 32,
Architecture::X86_64 => 64,
_ => return Vec::new(),
};
if symbols.is_empty() {
return Vec::new();
}
let mut calls = Vec::new();
for section in file
.sections()
.filter(|section| section.kind() == SectionKind::Text)
{
let (section_offset, _) = section.file_range().unwrap_or((0, 0));
let section_index = u32::try_from(section.index().0).ok();
let mut relocation_targets = HashMap::new();
for (offset, relocation) in section.relocations() {
if !matches!(
relocation.kind(),
RelocationKind::Relative | RelocationKind::PltRelative
) {
continue;
}
relocation_targets.insert(offset, relocation.target());
}
for caller in symbols
.iter()
.filter(|symbol| symbol.section == section_index)
{
let Some(relative) = caller.offset.checked_sub(section_offset) else {
continue;
};
let Some(ip) = section.address().checked_add(relative) else {
continue;
};
let mut decoder = Decoder::with_ip(bitness, &caller.code, ip, DecoderOptions::NONE);
while decoder.can_decode() {
let instruction = decoder.decode();
if instruction.is_invalid()
|| instruction.mnemonic() != Mnemonic::Call
|| !matches!(
instruction.op0_kind(),
OpKind::NearBranch16 | OpKind::NearBranch32 | OpKind::NearBranch64
)
{
continue;
}
let Some(operand_offset) = instruction
.ip()
.checked_sub(section.address())
.and_then(|offset| offset.checked_add(1))
else {
continue;
};
let (target, unresolved) = relocation_targets.get(&operand_offset).map_or_else(
|| {
let target = addresses
.get(&(section.index(), instruction.near_branch_target()))
.copied();
(
target,
target
.is_none()
.then_some(UnresolvedCall::MissingRelocation),
)
},
|relocation_target| match relocation_target {
RelocationTarget::Symbol(index) => fingerprints
.get(index)
.and_then(|value| *value)
.map_or((None, Some(UnresolvedCall::ExternalImport)), |target| {
(Some(target), None)
}),
RelocationTarget::Section(_) | RelocationTarget::Absolute => {
(None, Some(UnresolvedCall::MissingRelocation))
}
_ => (None, Some(UnresolvedCall::MissingRelocation)),
},
);
calls.push(ArtifactCall {
caller: caller.fingerprint,
target,
unresolved,
});
}
}
}
calls
}
const fn malformed(message: String) -> ArtifactError {
ArtifactError::Malformed {
format: ArtifactFormat::Elf,
message,
}
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
mod tests;