minor tweaks
This commit is contained in:
parent
fb652c39ae
commit
22ab973f0c
13 changed files with 223 additions and 144 deletions
93
src/pulse.rs
93
src/pulse.rs
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue