use std::collections::HashMap;
use crate::common::VAddr;
use crate::format::elf::{Elf64File, SHT_DYNSYM, SHT_RELA};
use crate::analysis::function_map::{Function, FunctionSource};
const RELA_SIZE: usize = 24;
const SYM_SIZE: usize = 24;
const R_X86_64_JUMP_SLOT: u32 = 7;
#[derive(Debug, thiserror::Error)]
pub enum PltError {
#[error("`.rela.plt` size {size} is not a multiple of {entry}")]
BadRelaSize { size: usize, entry: usize },
#[error("`.rela.plt` references invalid `.dynsym` index {idx}")]
BadDynsymIndex { idx: usize },
}
#[allow(clippy::missing_errors_doc)]
pub fn discover_plt_thunks(elf: &Elf64File) -> Result<Vec<Function>, PltError> {
let Some(slot_to_name) = build_jump_slot_name_map(elf)? else {
return Ok(Vec::new());
};
let mut out = Vec::new();
for section_name in [".plt", ".plt.got", ".plt.sec"] {
let Some((_, shdr, data)) = elf.section_by_name(section_name) else {
continue;
};
let entry_size = effective_plt_entry_size(shdr.sh_entsize);
let skip_first = section_name == ".plt"; scan_plt_section(
shdr.sh_addr,
data,
entry_size,
skip_first,
&slot_to_name,
&mut out,
);
}
Ok(out)
}
fn effective_plt_entry_size(sh_entsize: u64) -> usize {
if sh_entsize == 0 {
16
} else {
sh_entsize as usize
}
}
fn scan_plt_section(
base_addr: u64,
data: &[u8],
entry_size: usize,
skip_first: bool,
slot_to_name: &HashMap<u64, String>,
out: &mut Vec<Function>,
) {
if entry_size == 0 || data.len() < entry_size {
return;
}
let mut offset = if skip_first { entry_size } else { 0 };
while offset + 6 <= data.len() {
let entry_addr = base_addr.saturating_add(offset as u64);
if let Some(slot_addr) = decode_plt_entry_target(data, offset, entry_addr)
&& let Some(name) = slot_to_name.get(&slot_addr)
{
out.push(Function {
addr: VAddr(entry_addr),
size: entry_size as u64,
name: name.clone(),
sources: vec![FunctionSource::Plt],
});
}
offset += entry_size;
}
}
fn decode_plt_entry_target(data: &[u8], offset: usize, entry_addr: u64) -> Option<u64> {
if data.len() >= offset + 10
&& data[offset..offset + 4] == [0xf3, 0x0f, 0x1e, 0xfa]
&& data[offset + 4] == 0xff
&& data[offset + 5] == 0x25
{
return rip_relative_target(
entry_addr.saturating_add(10),
i32::from_le_bytes(data[offset + 6..offset + 10].try_into().unwrap()),
);
}
let bytes = data.get(offset..offset + 6)?;
if bytes[0] == 0xff && bytes[1] == 0x25 {
return rip_relative_target(
entry_addr.saturating_add(6),
i32::from_le_bytes([bytes[2], bytes[3], bytes[4], bytes[5]]),
);
}
None
}
fn rip_relative_target(rip_after: u64, disp: i32) -> Option<u64> {
if disp >= 0 {
#[allow(clippy::cast_sign_loss)]
let disp_u = disp as u32;
rip_after.checked_add(u64::from(disp_u))
} else {
rip_after.checked_sub(u64::from(disp.unsigned_abs()))
}
}
fn build_jump_slot_name_map(elf: &Elf64File) -> Result<Option<HashMap<u64, String>>, PltError> {
let mut slot_to_name: HashMap<u64, String> = HashMap::new();
let mut saw_rela_for_dynsym = false;
for (_idx, shdr, data) in elf.sections() {
if shdr.sh_type != SHT_RELA {
continue;
}
let dynsym_idx = shdr.sh_link as usize;
let dynsym_shdr = match elf.shdrs.get(dynsym_idx) {
Some(s) if s.sh_type == SHT_DYNSYM => s,
_ => continue, };
let dynsym_data = elf.section_data(dynsym_idx).unwrap_or(&[]);
let strtab_idx = dynsym_shdr.sh_link as usize;
let Some(strtab) = elf.section_data(strtab_idx) else {
continue;
};
saw_rela_for_dynsym = true;
if data.len() % RELA_SIZE != 0 {
return Err(PltError::BadRelaSize {
size: data.len(),
entry: RELA_SIZE,
});
}
for chunk in data.as_chunks::<RELA_SIZE>().0 {
let r_offset = u64::from_le_bytes(chunk[0..8].try_into().unwrap());
let r_info = u64::from_le_bytes(chunk[8..16].try_into().unwrap());
#[allow(clippy::cast_possible_truncation)]
let r_type = r_info as u32;
let sym_idx = (r_info >> 32) as usize;
if r_type != R_X86_64_JUMP_SLOT {
continue;
}
let Some(name) = read_dynsym_name(dynsym_data, sym_idx, strtab) else {
continue;
};
slot_to_name.insert(r_offset, name);
}
}
Ok(if saw_rela_for_dynsym {
Some(slot_to_name)
} else {
None
})
}
fn read_dynsym_name(dynsym_data: &[u8], idx: usize, strtab: &[u8]) -> Option<String> {
let off = idx.checked_mul(SYM_SIZE)?;
let chunk = dynsym_data.get(off..off + SYM_SIZE)?;
let st_name = u32::from_le_bytes(chunk[0..4].try_into().unwrap()) as usize;
let tail = strtab.get(st_name..)?;
let nul = tail.iter().position(|&b| b == 0)?;
let s = std::str::from_utf8(&tail[..nul]).ok()?;
if s.is_empty() {
None
} else {
Some(s.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decode_plt_target_handles_positive_disp() {
let bytes = [0xff, 0x25, 0x10, 0x00, 0x00, 0x00];
assert_eq!(decode_plt_entry_target(&bytes, 0, 0x1000), Some(0x1016));
}
#[test]
fn decode_plt_target_handles_negative_disp() {
let bytes = [0xff, 0x25, 0xf0, 0xff, 0xff, 0xff];
assert_eq!(decode_plt_entry_target(&bytes, 0, 0x1000), Some(0xff6));
}
#[test]
fn decode_plt_target_rejects_other_opcodes() {
let bytes = [0x90, 0x90, 0x90, 0x90, 0x90, 0x90];
assert!(decode_plt_entry_target(&bytes, 0, 0x1000).is_none());
}
#[test]
fn decode_plt_target_handles_ibt_endbr_prefix() {
let bytes = [
0xf3, 0x0f, 0x1e, 0xfa, 0xff, 0x25, 0x10, 0x00, 0x00, 0x00, ];
assert_eq!(decode_plt_entry_target(&bytes, 0, 0x1070), Some(0x108a));
}
}