read what the binary says about itself: names, signatures, prototypes; gen v2
This commit is contained in:
parent
54ef572202
commit
c458b4cb50
34 changed files with 58363 additions and 192 deletions
83
src/abi.rs
83
src/abi.rs
|
|
@ -17,6 +17,10 @@
|
|||
//! (a rebuild doesn't change which arguments a function takes) and moves precisely when the prototype
|
||||
//! does — so comparing it across builds flags exactly the prototype changes the byte-sig misses.
|
||||
//!
|
||||
//! A `call` is modelled as clobbering every argument register (all 14 are caller-saved), so a value
|
||||
//! read after one can never be mistaken for an incoming argument — that is what keeps the count a lower
|
||||
//! bound rather than an occasional over-count.
|
||||
//!
|
||||
//! Known limits (all bias toward UNDER-counting = a missed flag, never a false one): a pure forwarding
|
||||
//! thunk (`jmp Helper`) reads no arg register of its own, so it shapes as `(0,0)`; an argument used
|
||||
//! only inside a jump-table (indirect-branch) case isn't followed, so it can be missed. Both stay
|
||||
|
|
@ -24,6 +28,10 @@
|
|||
//! diff's `int==0` low-confidence bucket also absorbs the thunk case. `int_args` is the OBSERVABLE
|
||||
//! footprint = a lower bound on the declared prototype (a constant-returner reads nothing → `int=0`);
|
||||
//! that too is stable per function, so the cross-build diff still works.
|
||||
//!
|
||||
//! The lower-bound property is MEASURED, not assumed: Valve's entity-IO datadesc declares hundreds of
|
||||
//! independent handlers to one fixed `void(CEntityInstance*, InputData_t&)` prototype, and every one of
|
||||
//! them measures within it (see `pipeline::within_io_prototype`). That oracle runs on each derive.
|
||||
|
||||
use crate::elf::CodeImage;
|
||||
use iced_x86::{
|
||||
|
|
@ -36,6 +44,8 @@ use std::collections::HashMap;
|
|||
/// bitmask over these 14 slots is a function's live-in argument set.
|
||||
const N_INT: usize = 6;
|
||||
const N_XMM: usize = 8;
|
||||
/// All 14 argument slots — the set a call clobbers wholesale (every one is caller-saved).
|
||||
const ARG_SLOTS: u16 = (1 << (N_INT + N_XMM)) - 1;
|
||||
|
||||
/// A function's recovered ABI shape: how many integer/pointer and floating arguments it reads, plus
|
||||
/// whether it also loads arguments off the stack (a 7th+ integer / 9th+ float argument, or a large
|
||||
|
|
@ -54,8 +64,15 @@ pub struct AbiShape {
|
|||
/// footprint: a change here (int↔float↔by-value) is a prototype change the arg counts alone miss, and
|
||||
/// `ByValue` marks the RVO/sret functions that are UNSAFE to blind-call — the caller must pass an
|
||||
/// output-buffer pointer in RDI, so calling with the object there makes the function WRITE into it
|
||||
/// (the `CSwapTeams::GetDisplayString` sret trap). Best-effort, with an explicit
|
||||
/// `Unknown` when the return path doesn't decode — so it only ever adds a signal, never a false one.
|
||||
/// (the `CSwapTeams::GetDisplayString` sret trap).
|
||||
///
|
||||
/// UNLIKE the argument footprint, this is NOT a conservative bound, and it is not evidence about the
|
||||
/// DECLARED return type. A callee cannot tell whether its caller reads the result register, so a `void`
|
||||
/// function that merely uses RAX or XMM0 as scratch reads back as `Int`/`Float`: measured against the
|
||||
/// entity-IO datadesc, whose handlers are all declared `void`, only ~12% classify as [`RetClass::Void`].
|
||||
/// What it IS good for is the two things it is used for — the `ByValue` blind-call safety flag (no false
|
||||
/// positive appeared across that same set), and cross-build DIFFING, where the classification is stable
|
||||
/// per function so a change really does mean the function changed.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Default, Debug)]
|
||||
pub enum RetClass {
|
||||
/// No decodable return path (a forwarding thunk / tail call / undecoded) — no signal.
|
||||
|
|
@ -290,6 +307,19 @@ fn insn_effect(factory: &mut InstructionInfoFactory, insn: &Instruction) -> (u16
|
|||
use_m &= !(1 << slot);
|
||||
def_m |= 1 << slot;
|
||||
}
|
||||
// A CALL clobbers every caller-saved register, and all 14 argument registers are caller-saved —
|
||||
// only RBX/RBP/R12-R15 survive one. So nothing read AFTER a call can be an incoming argument: the
|
||||
// value must have been produced since, and anything the callee needed to outlive the call was
|
||||
// already copied somewhere safe (a read this analysis sees BEFORE the call). Modelling the clobber
|
||||
// is what keeps the footprint a lower bound; without it a float RETURNED by a callee and used
|
||||
// afterwards propagates back to the entry as a phantom float argument. Applied after `use_m` is
|
||||
// computed, so a register the call instruction itself reads (`call rdi`) still counts.
|
||||
if matches!(
|
||||
insn.flow_control(),
|
||||
FlowControl::Call | FlowControl::IndirectCall
|
||||
) {
|
||||
def_m = ARG_SLOTS;
|
||||
}
|
||||
(use_m, def_m, stack)
|
||||
}
|
||||
|
||||
|
|
@ -456,7 +486,16 @@ fn decode_region(img: &CodeImage, entry: u64) -> Option<(Vec<Insn>, bool, bool)>
|
|||
let next = start + insn.len() as u64;
|
||||
let mut succ = Vec::new();
|
||||
match insn.flow_control() {
|
||||
FlowControl::Return | FlowControl::IndirectBranch => {}
|
||||
// No successor. `Exception`/`Interrupt` (`ud2`, `int3`) are terminal here for the same reason
|
||||
// `Return` is: control does not continue to the next instruction, which is inter-function
|
||||
// padding. Following it would walk into the NEXT function and back-propagate ITS argument
|
||||
// reads into this one's live-in set — an over-count, the failure direction this module
|
||||
// promises not to have. Treating a hypothetical resuming `INT n` as terminal can only
|
||||
// under-count, which is the accepted direction.
|
||||
FlowControl::Return
|
||||
| FlowControl::IndirectBranch
|
||||
| FlowControl::Exception
|
||||
| FlowControl::Interrupt => {}
|
||||
FlowControl::UnconditionalBranch => {
|
||||
let t = insn.near_branch_target();
|
||||
if in_span(t) {
|
||||
|
|
@ -470,7 +509,9 @@ fn decode_region(img: &CodeImage, entry: u64) -> Option<(Vec<Insn>, bool, bool)>
|
|||
succ.push(t);
|
||||
}
|
||||
}
|
||||
_ => succ.push(next), // fall-through (incl. call/indirect-call: the call reads no arg regs)
|
||||
// Fall-through, including a call: control resumes at the next instruction, but the call has
|
||||
// already killed every argument register in `insn_effect`.
|
||||
_ => succ.push(next),
|
||||
}
|
||||
for &s in &succ {
|
||||
if !recs.contains_key(&s) {
|
||||
|
|
@ -689,6 +730,40 @@ mod tests {
|
|||
assert_eq!(shape_of(&[0xF2, 0x0F, 0x51, 0xD9, 0xC3]).key(), (0, 2));
|
||||
}
|
||||
|
||||
// --- a call clobbers every argument register (all 14 are caller-saved) ---
|
||||
|
||||
#[test]
|
||||
fn value_read_after_a_call_is_not_an_argument() {
|
||||
// call +0 ; movaps xmm1, xmm0 ; ret — XMM0 here holds the CALLEE's float result, not an
|
||||
// incoming argument. Without the clobber this back-propagates to the entry as a phantom
|
||||
// float arg, which is how a `void(ptr, ref)` entity-IO handler measured as taking floats.
|
||||
assert_eq!(
|
||||
shape_of(&[0xE8, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x28, 0xC8, 0xC3]).key(),
|
||||
(0, 0)
|
||||
);
|
||||
// call +0 ; mov rax, rsi ; ret — same on the integer side.
|
||||
assert_eq!(
|
||||
shape_of(&[0xE8, 0x00, 0x00, 0x00, 0x00, 0x48, 0x89, 0xF0, 0xC3]).key(),
|
||||
(0, 0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_register_the_call_itself_reads_still_counts() {
|
||||
// call rdi ; ret — the clobber must not swallow the call instruction's OWN operand read.
|
||||
assert_eq!(shape_of(&[0xFF, 0xD7, 0xC3]).key(), (1, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_read_before_the_call_still_counts() {
|
||||
// mov rbx, rsi ; call +0 ; ret — RSI is copied to a callee-saved register BEFORE the call,
|
||||
// which is exactly how a real argument survives one, so it is still an argument.
|
||||
assert_eq!(
|
||||
shape_of(&[0x48, 0x89, 0xF3, 0xE8, 0x00, 0x00, 0x00, 0x00, 0xC3]).key(),
|
||||
(2, 0)
|
||||
);
|
||||
}
|
||||
|
||||
// --- return class ---
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue