minor tweaks
All checks were successful
CI / lint (push) Successful in 16s
CI / fuzz (push) Successful in 2m3s
CI / test (push) Successful in 25s

This commit is contained in:
Kamal Tufekcic 2026-08-03 03:59:53 +03:00
commit 22ab973f0c
13 changed files with 223 additions and 144 deletions

View file

@ -24,6 +24,9 @@
//! declares. A layout change yields FEWER signatures, never wrong ones, and the profile floor turns
//! "fewer" into a failed release.
// Registers whose value a call destroys. The ONE list in `abi`, not a second copy of it — both loops
// below that invalidate across a call read it directly.
use crate::abi::CALLER_SAVED;
use crate::elf::CodeImage;
use iced_x86::{Decoder, DecoderOptions, FlowControl, Instruction, Mnemonic, OpKind, Register};
use std::collections::{BTreeMap, HashMap, HashSet};
@ -83,20 +86,6 @@ struct Trace {
ret: Option<(u64, u64)>,
}
/// Registers whose value a call destroys. Anything else the pass cannot evaluate is invalidated as the
/// instruction that writes it is seen, so the default is always "unknown" rather than "stale".
const CALLER_SAVED: [Register; 9] = [
Register::RAX,
Register::RCX,
Register::RDX,
Register::RSI,
Register::RDI,
Register::R8,
Register::R9,
Register::R10,
Register::R11,
];
fn full(r: Register) -> Register {
if r.is_gpr() { r.full_register() } else { r }
}
@ -362,26 +351,6 @@ fn record(img: &CodeImage, accessor: u64) -> Option<Record> {
})
}
/// Every CODE pointer an accessor's initializer stores into its record region, with the region base:
/// `(base, [(address written, code address written)])`.
///
/// A DIAGNOSTIC, and deliberately not part of any shipped artifact. The parameter records carry a
/// function pointer whose ROLE is not established — the record reader already has to look at these in
/// order to reject them as parameter names, so exposing them costs nothing and lets that question be
/// settled against evidence collected elsewhere (a runtime call-edge trace) rather than guessed. Nothing
/// here interprets them; they are raw measurements.
pub fn code_stores(img: &CodeImage, accessor: u64) -> Option<(u64, Vec<(u64, u64)>)> {
let r = record(img, accessor)?;
let stores =
r.t.writes
.iter()
.filter(|&(a, _)| *a >= r.base)
.filter(|&(_, p)| img.is_code(*p))
.map(|(&a, &p)| (a, p))
.collect();
Some((r.base, stores))
}
/// The spacings at which this record's `count` names could sit, given that element 0's name is at
/// `base + 8` and the array is contiguous. Usually one; a record carrying a second identifier-shaped
/// string of its own offers more, which is why the stride is settled per IMAGE and not per record.
@ -582,10 +551,10 @@ const SHIM_SLOTS: [Register; 6] = [
/// What an invocation shim was measured to read, and therefore what a caller has to supply.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ShimReads {
/// The argument slots actually read, named — `rcx`, `r8`, `stack0`.
/// The argument slots actually read, named — `rcx`, `r8`, `stack0`. The argument array (`r8`) is
/// stated here and nowhere else: reading it is the ordinary case and constrains a caller in no way,
/// so it needs no flag of its own beside the three that do.
pub reads: Vec<&'static str>,
/// Does it read the argument array (`r8`)?
pub args: bool,
/// Does it read the output sink (the first stack slot)? True for exactly the bindings that declare a
/// return, measured across both games with no exceptions.
pub sink: bool,
@ -634,6 +603,24 @@ pub fn record_region(img: &CodeImage, accessor: u64) -> Option<(u64, u64)> {
(r.base != 0).then_some((r.base, r.count))
}
/// What a read of one argument slot demands of a HOST caller.
///
/// The argument array (`r8`) demands nothing — the caller builds it, so reading it is the ordinary case
/// and `reads` already states it. The Pulse context (`rcx`) is VM-owned and cannot be supplied at all.
/// Everything else is a slot the caller would otherwise pass null.
///
/// A named arm rather than a fall-through for `r8` specifically: dropping it into the `_` catch-all would
/// mark every ordinary binding as needing a slot no host can fill, retiring the entire `args-only`
/// callable tier — a collapse that reads as "this build has no callable bindings", which is a legitimate
/// answer for a game and therefore invisible.
fn slot_need(r: Register, out: &mut ShimReads) {
match r {
Register::RCX => out.context = true,
Register::R8 => {}
_ => out.other = true,
}
}
/// Measure which of a shim's seven arguments it reads.
///
/// Reachable instructions in ADDRESS order, which needs two guards that cost real time to find:
@ -725,11 +712,7 @@ pub fn shim_reads(img: &CodeImage, entry: u64) -> Option<ShimReads> {
{
if live.contains_key(r) {
out.reads.push(name);
match *r {
Register::RCX => out.context = true,
Register::R8 => out.args = true,
_ => out.other = true,
}
slot_need(*r, &mut out);
}
}
if sink {
@ -802,26 +785,26 @@ mod tests {
let ctx = ShimReads {
context: true,
sink: true,
args: true,
reads: vec!["rcx", "r8", "stack0"],
..Default::default()
};
assert_eq!(ctx.needs(), "pulse-context");
let other = ShimReads {
other: true,
sink: true,
args: true,
reads: vec!["rdi", "r8", "stack0"],
..Default::default()
};
assert_eq!(other.needs(), "other-slots");
let sink = ShimReads {
sink: true,
args: true,
reads: vec!["r8", "stack0"],
..Default::default()
};
assert_eq!(sink.needs(), "output-sink");
// The callable tier: the argument array and nothing else.
let only = ShimReads {
args: true,
reads: vec!["r8"],
..Default::default()
};
assert_eq!(only.needs(), "args-only");
@ -829,6 +812,24 @@ mod tests {
assert_eq!(ShimReads::default().needs(), "args-only");
}
#[test]
fn reading_the_argument_array_leaves_a_shim_host_callable() {
// Asserted against the shipped rule rather than a copy of it. `r8` is the argument array the
// CALLER builds, so a read of it must impose nothing; the arm exists only to keep it out of the
// catch-all, where it would mark every ordinary binding uncallable at once.
let mut r8 = ShimReads::default();
slot_need(Register::R8, &mut r8);
assert_eq!(r8.needs(), "args-only");
let mut rcx = ShimReads::default();
slot_need(Register::RCX, &mut rcx);
assert_eq!(rcx.needs(), "pulse-context");
for r in [Register::RDI, Register::RSI, Register::RDX, Register::R9] {
let mut o = ShimReads::default();
slot_need(r, &mut o);
assert_eq!(o.needs(), "other-slots", "{r:?} is a slot a host must fill");
}
}
#[test]
fn pval_void_is_negative_one_and_still_a_type() {
assert!(valid_pval(0)); // PVAL_BOOL