112 lines
5.4 KiB
Rust
112 lines
5.4 KiB
Rust
//! Locating primitives: where a library file lives ON DISK, and where the function entries live
|
||
//! INSIDE an image.
|
||
//!
|
||
//! On disk: `find_file` resolves a lib by name under a build tree (nearest-depth-wins, so a Metamod
|
||
//! shim can't shadow the real engine lib) and `load_lib` turns a build dir *or* a bare `.so` into a
|
||
//! loaded [`CodeImage`]. Both are leaf primitives (they touch only `elf` + the filesystem), so the
|
||
//! low-level readers — `schema` especially — depend on THIS module rather than up on the engine.
|
||
//!
|
||
//! In an image: [`candidate_entries`] enumerates plausible function starts without symbols — relocation
|
||
//! values that point into code (vtable slots + function pointers — covers virtual functions) unioned
|
||
//! with the targets of direct near `call`s found by a linear sweep. `xref` unions this with `.eh_frame`
|
||
//! starts to index the whole binary.
|
||
|
||
use crate::elf::CodeImage;
|
||
use anyhow::{Context, Result};
|
||
use iced_x86::{Decoder, DecoderOptions, FlowControl, OpKind};
|
||
use std::collections::BTreeSet;
|
||
use std::path::{Path, PathBuf};
|
||
|
||
/// Every plausible function ENTRY in the image, sorted and deduped: relocation code-pointers (every vtable
|
||
/// slot, every stored function pointer) ∪ decoded `call` targets ∪ `.eh_frame` FDE starts.
|
||
///
|
||
/// The union is the point, and it is why this is one function rather than four lines repeated. CS2 strips
|
||
/// `.eh_frame` from the game code — the FDE list covers the statically-linked runtime tail, roughly 8,327
|
||
/// of libserver's ~70,000 functions — so an FDE-only list misses the entire gameplay region, while a
|
||
/// relocation/call-target-only list misses the runtime tail that has no code pointer taken. Six callers
|
||
/// need exactly this set: the xref index, the ConVar and VScript readers, the change digest, and both
|
||
/// anchor passes. A fork adding PLT or ifunc entries edits here, once.
|
||
pub fn function_entries(img: &CodeImage) -> Vec<u64> {
|
||
let mut entries = candidate_entries(img);
|
||
entries.extend(img.eh_frame_functions().into_iter().map(|(s, _)| s));
|
||
entries.sort_unstable();
|
||
entries.dedup();
|
||
entries
|
||
}
|
||
|
||
/// Every plausible function entry address in `img`: relocation values that point into code, plus
|
||
/// the targets of direct near `call`s found by a linear sweep. Sorted, de-duplicated.
|
||
pub fn candidate_entries(img: &CodeImage) -> Vec<u64> {
|
||
let mut set: BTreeSet<u64> = img.code_pointer_targets().into_iter().collect();
|
||
for (va, code) in img.exec_blocks() {
|
||
let mut dec = Decoder::with_ip(64, code, va, DecoderOptions::NONE);
|
||
while dec.can_decode() {
|
||
let insn = dec.decode(); // iced advances one byte on invalid, so the sweep self-resyncs
|
||
if insn.flow_control() == FlowControl::Call
|
||
&& matches!(
|
||
insn.op0_kind(),
|
||
OpKind::NearBranch16 | OpKind::NearBranch32 | OpKind::NearBranch64
|
||
)
|
||
{
|
||
let t = insn.near_branch_target();
|
||
if img.is_code(t) {
|
||
set.insert(t);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
set.into_iter().collect()
|
||
}
|
||
|
||
/// Shallowest file named `name` under `dir` (bounded depth), ties broken by sorted path.
|
||
///
|
||
/// NEAREST-DEPTH-WINS, not first-`read_dir`-hit: a game install legitimately holds several files of the same
|
||
/// basename, and the shallowest is the real one. A CS2 tree has the engine's own
|
||
/// `csgo/bin/linuxsteamrt64/libserver.so` at depth 3 and Metamod's ~300 KB loader shim of the SAME name at
|
||
/// `csgo/addons/metamod/bin/linuxsteamrt64/libserver.so` (depth 5, plus any `bin.*.bak` siblings). Depth-first
|
||
/// order made which one you derive from a property of directory-entry order — deriving against the shim would
|
||
/// yield garbage — and plain sorting is WORSE, since `addons` sorts before `bin`. Sorting is only the tie-break
|
||
/// among equally-shallow candidates, so the result never depends on filesystem enumeration order.
|
||
pub(crate) fn find_file(dir: &Path, name: &str, depth: usize) -> Option<PathBuf> {
|
||
let mut level = vec![dir.to_path_buf()];
|
||
for _ in 0..depth {
|
||
let (mut hits, mut next) = (Vec::new(), Vec::new());
|
||
for d in &level {
|
||
let Ok(rd) = std::fs::read_dir(d) else {
|
||
continue;
|
||
};
|
||
for e in rd.flatten() {
|
||
let p = e.path();
|
||
if p.is_dir() {
|
||
next.push(p);
|
||
} else if p.file_name().and_then(|s| s.to_str()) == Some(name) {
|
||
hits.push(p);
|
||
}
|
||
}
|
||
}
|
||
if !hits.is_empty() {
|
||
hits.sort();
|
||
return hits.into_iter().next();
|
||
}
|
||
if next.is_empty() {
|
||
return None;
|
||
}
|
||
next.sort();
|
||
level = next;
|
||
}
|
||
None
|
||
}
|
||
|
||
/// Locate `lib` under `dir` (depth 8) and load it as a `CodeImage` — the `find_file` + load pattern the
|
||
/// command entry points share.
|
||
pub(crate) fn load_lib(path: &Path, lib: &str) -> Result<CodeImage> {
|
||
// Accept a build DIR (find `lib` within, depth 8) or a direct `.so` FILE (load as-is), so callers can
|
||
// pass `path/to/build_dir` or `path/to/libserver.so` interchangeably (e.g. `classify-change --prev`).
|
||
let file = if path.is_file() {
|
||
path.to_path_buf()
|
||
} else {
|
||
find_file(path, lib, 8)
|
||
.with_context(|| format!("{lib} not found under {}", path.display()))?
|
||
};
|
||
CodeImage::load(&file)
|
||
}
|