mirror of
https://github.com/HeyPuter/gex.git
synced 2026-11-13 18:34:21 -10:00
record/native: NT entries without a unit, providers survive library rewrites, provider_try counts
- NtSession.entries: addresses in images that had a unit where code ran without one (tier-1 misses, resumes, fallback calls). The eject seeds recovery with them: the narrow, recorded stopgap until frames survive exceptions (R's nested dispatch). Noita: 5 outside lua51 (combase, sechost, ntdll). - Export providers are not retired when the library's bytes at their addresses are rewritten: those bytes do not run. Noita's mod sandbox overwrites ~120 lua51.dll entry points; pewasm ignores it the same way. - GEX_NT_STATS: provider_try / provider_throw (Lua protected calls). - The profile's temporary file is per writer: two threads harvesting at once raced on one name, and the loser failed its thread. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01LBBwErLgub3jGiXXNBCpzv
This commit is contained in:
7 files changed
+84
-4
No files matched your search
@@ -1041,6 +1041,10 @@ pub struct NtSession {
|
||||
/// Every image mapped, in mapping order: `(object id, guest path, base,
|
||||
/// preferred base)`.
|
||||
pub images: Vec<NtImage>,
|
||||
/// Addresses inside images that had function-tier units where code ran
|
||||
/// without one: tier-1 entries after a unit left, resumes, fallback
|
||||
/// calls. Code built ahead of time must have a function there.
|
||||
pub entries: Vec<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
@@ -1176,6 +1180,13 @@ impl Session {
|
||||
m.size
|
||||
);
|
||||
}
|
||||
s.push_str("],\n\"entries\":[");
|
||||
for (i, a) in nt.entries.iter().enumerate() {
|
||||
if i > 0 {
|
||||
s.push(',');
|
||||
}
|
||||
let _ = write!(s, "{a}");
|
||||
}
|
||||
s.push_str("]}");
|
||||
}
|
||||
s.push_str("}\n");
|
||||
@@ -1256,6 +1267,7 @@ impl Session {
|
||||
settings: strs("settings", nt),
|
||||
gl: nt.get("gl").and_then(Json::as_bool).unwrap_or(false),
|
||||
images,
|
||||
entries: nt.get("entries").and_then(Json::as_arr).unwrap_or(&[]).iter().filter_map(Json::as_u64).collect(),
|
||||
});
|
||||
}
|
||||
for f in v.get("files").and_then(Json::as_arr).unwrap_or(&[]) {
|
||||
|
||||
@@ -43,6 +43,12 @@ pub(crate) struct Shared {
|
||||
/// Stateful providers' installs (`libprov`), by module: their data's
|
||||
/// guest address.
|
||||
pub stateful: HashMap<u32, crate::libprov::Extras>,
|
||||
/// Installs that are export providers (`publish_pairs`): native code in
|
||||
/// place of a library's, so a rewrite of the library's bytes there does
|
||||
/// not retire them (the bytes are not what runs). Noita's mod sandbox
|
||||
/// overwrites ~120 lua51.dll entry points; pewasm, whose lua51 is
|
||||
/// native too, ignores those writes the same way.
|
||||
pub providers: std::collections::HashSet<u32>,
|
||||
}
|
||||
|
||||
/// One store's half, inside [`Jit`].
|
||||
@@ -349,7 +355,9 @@ pub(crate) fn publish_pairs(
|
||||
pairs: &[(u64, u32)],
|
||||
tier: Tier,
|
||||
) -> Result<u32> {
|
||||
Ok(publish_pairs_at(sh, memory, module, len, pairs, tier, Vec::new(), None)?.expect("an unconditional publish"))
|
||||
let n = publish_pairs_at(sh, memory, module, len, pairs, tier, Vec::new(), None)?.expect("an unconditional publish");
|
||||
sh.lock().unwrap_or_else(|e| e.into_inner()).ftab.providers.insert(n);
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
/// [`publish_pairs`] for a stateful provider (`libprov`): every store
|
||||
@@ -499,6 +507,9 @@ fn page_gen(sh: &Arc<Mutex<JitShared>>, memory: &gex_mem::GuestMemory, page: u64
|
||||
fn current(s: &JitShared, memory: &[u8], addr: u64, f: gex_kernel::ftab::Func) -> bool {
|
||||
let inst = s.ftab.map.install(f.install);
|
||||
let word = |off: u64| memory.get(off as usize..off as usize + 4).map(|b| u32::from_le_bytes(b.try_into().unwrap())).unwrap_or(0);
|
||||
if s.ftab.providers.contains(&f.install) {
|
||||
return true;
|
||||
}
|
||||
match inst.tier {
|
||||
Tier::Unit => {
|
||||
let page = addr / gex_abi::DISPATCH_PAGE;
|
||||
@@ -754,6 +765,9 @@ fn fallback(caller: &mut Caller<'_, HostState>, ctx: i32, ret: u64) -> Result<i3
|
||||
(true, true) => 4,
|
||||
};
|
||||
let _ = GuestMem::new(memory.bytes()).write_u64(c + ctx::PC as u64, target);
|
||||
if caller.data().nt.is_some() && crate::profile::active() {
|
||||
crate::profile::note_nt_entry(target);
|
||||
}
|
||||
if stats::FALLBACK_CALLS.fetch_add(1, std::sync::atomic::Ordering::Relaxed) & 1023 == 0 {
|
||||
*stats::FALLBACK_SITES.lock().unwrap_or_else(|e| e.into_inner()).get_or_insert_with(Default::default).entry(target).or_default() += 1;
|
||||
}
|
||||
|
||||
@@ -3435,6 +3435,9 @@ fn register_imports(linker: &mut Linker<HostState>) -> Result<()> {
|
||||
/// table, so freshly translated code shares state with everything already
|
||||
/// running and can branch into it through the table.
|
||||
fn translate_and_install(caller: &mut Caller<'_, HostState>, pc: u64) -> Result<bool> {
|
||||
if caller.data().nt.is_some() && profile::active() {
|
||||
profile::note_nt_entry(pc);
|
||||
}
|
||||
// With runtime translation off, a miss is still worth catching up on --
|
||||
// another thread or a freshly mapped object may already have covered this
|
||||
// address -- but nothing new is translated for it. A miss that survives
|
||||
@@ -4517,6 +4520,9 @@ fn classify_trap(e: anyhow::Error, store: &Store<HostState>) -> anyhow::Error {
|
||||
/// Record a taken resume's cause (set with `resume_at`) and target, under
|
||||
/// GEX_NT_STATS; `taker` names the loop that took it when no cause was set.
|
||||
pub(crate) fn note_resume(state: &mut HostState, target: u64, taker: &str) {
|
||||
if state.nt.is_some() && profile::active() {
|
||||
profile::note_nt_entry(target);
|
||||
}
|
||||
let cause = state.resume_cause.take();
|
||||
if crate::ftab::stats_on() {
|
||||
crate::ftab::stats::note_resume_cause(cause.unwrap_or_else(|| format!("untagged ({taker})")), target);
|
||||
|
||||
@@ -194,6 +194,12 @@ pub(crate) fn relocate_once(cx: &mut wasmtime::StoreContextMut<'_, HostState>, i
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `gex.provider_try` calls (a Lua protected call each: lua_pcall,
|
||||
/// lua_resume, lua_close's finalizers, the parser) and the throws they
|
||||
/// caught, for GEX_NT_STATS.
|
||||
pub static TRIES: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
pub static THROWS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
|
||||
/// A `gex.provider_throw` unwinding to its `gex.provider_try`.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Throw(pub i32);
|
||||
@@ -209,12 +215,16 @@ impl std::error::Error for Throw {}
|
||||
/// The host functions stateful providers import.
|
||||
pub(crate) fn register(linker: &mut Linker<HostState>) -> Result<()> {
|
||||
linker.func_wrap(IMPORT_MODULE, "provider_try", |mut caller: Caller<'_, HostState>, f: i32, arg: i32| -> Result<i32> {
|
||||
TRIES.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
let func = ftab::table_func(&mut caller, f as u32)?;
|
||||
let typed = func.typed::<i32, ()>(&caller).context("gex.provider_try: the function is not void(int)")?;
|
||||
match typed.call(&mut caller, arg) {
|
||||
Ok(()) => Ok(0),
|
||||
Err(e) => match e.downcast_ref::<Throw>() {
|
||||
Some(t) => Ok(t.0),
|
||||
Some(t) => {
|
||||
THROWS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
Ok(t.0)
|
||||
}
|
||||
None => Err(e),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -296,7 +296,12 @@ fn start_ticker(memory: gex_mem::GuestMemory, p: std::sync::Weak<nt::Process>, s
|
||||
let nest_exits = GuestMem::new(memory.bytes()).read_u64(gex_abi::NEST_EXITS).unwrap_or(0);
|
||||
let co = crate::CALLOUTS.load(std::sync::atomic::Ordering::Relaxed);
|
||||
let coa = crate::CALLOUT_ABORTS.load(std::sync::atomic::Ordering::Relaxed);
|
||||
let cg = format!("cgen nest_exits={nest_exits} callouts={co} callout_aborts={coa} {}", crate::ftab::stats::line());
|
||||
let cg = format!(
|
||||
"cgen nest_exits={nest_exits} callouts={co} callout_aborts={coa} provider_try={} provider_throw={} {}",
|
||||
crate::libprov::TRIES.load(std::sync::atomic::Ordering::Relaxed),
|
||||
crate::libprov::THROWS.load(std::sync::atomic::Ordering::Relaxed),
|
||||
crate::ftab::stats::line()
|
||||
);
|
||||
match value {
|
||||
Some(v) => eprintln!("gex-nt: stats t={} presents={presents} peek={v} layout_miss={} {cg} {}", start.elapsed().as_millis(), layout_misses(), crate::jit_stats::line()),
|
||||
None => eprintln!("gex-nt: stats t={} presents={presents} layout_miss={} {cg} {}", start.elapsed().as_millis(), layout_misses(), crate::jit_stats::line()),
|
||||
|
||||
@@ -260,6 +260,12 @@ pub fn prepare_trampolines(class32: bool, jobs: usize) -> Result<std::path::Path
|
||||
prepare_image(&image, &seeds, jobs, false)
|
||||
}
|
||||
|
||||
/// The guest addresses export providers take over in a PE image at `base`
|
||||
/// (its unit has no entry there: the provider serves them).
|
||||
pub fn provider_stubs(pe: &gex_ld::pe::PeImage, base: u64) -> Vec<u64> {
|
||||
provided(pe, base).stubs
|
||||
}
|
||||
|
||||
/// Build every export provider for a PE image (`gex_cgen::provider`) into the
|
||||
/// unit cache, as mapping it would; their names.
|
||||
pub fn prepare_providers(pe: &gex_ld::pe::PeImage) -> Result<Vec<String>> {
|
||||
@@ -454,6 +460,11 @@ pub(crate) fn on_retire(memory: &gex_mem::GuestMemory, ranges: &[(u64, u64)]) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether an installed unit covers the code page of `pc`.
|
||||
pub(crate) fn unit_covers(pc: u64) -> bool {
|
||||
UNIT_PAGES.lock().unwrap_or_else(|e| e.into_inner()).as_ref().is_some_and(|m| m.contains_key(&(pc / gex_abi::DISPATCH_PAGE)))
|
||||
}
|
||||
|
||||
/// Record a placed unit's pages as built, and clear their patched bits.
|
||||
fn note_pages(memory: &gex_mem::GuestMemory, pages: &[(u64, u64)]) {
|
||||
let memory = memory.bytes();
|
||||
|
||||
@@ -367,11 +367,30 @@ pub fn note_nt(setup: &crate::nt::NtSetup, gl: bool) {
|
||||
settings,
|
||||
gl,
|
||||
images: Vec::new(),
|
||||
entries: Vec::new(),
|
||||
};
|
||||
rec.state.lock().unwrap_or_else(|e| e.into_inner()).session.nt = Some(nt);
|
||||
gex_kernel::nt::file::set_file_hook(note_nt_file);
|
||||
}
|
||||
|
||||
/// Code at `pc` ran without a unit although its image has one (a tier-1
|
||||
/// entry after a unit left, a resume, a fallback call): an ejected build
|
||||
/// needs a function there.
|
||||
pub fn note_nt_entry(pc: u64) {
|
||||
let Some(rec) = recorder() else {
|
||||
return;
|
||||
};
|
||||
if !crate::nt_cgen::unit_covers(pc) {
|
||||
return;
|
||||
}
|
||||
let mut st = rec.state.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if let Some(nt) = st.session.nt.as_mut() {
|
||||
if !nt.entries.contains(&pc) {
|
||||
nt.entries.push(pc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A path the NT layer opened, queried or read (`nt::file::set_file_hook`),
|
||||
/// recorded as a Linux open is.
|
||||
fn note_nt_file(a: &gex_kernel::nt::file::FileAccess<'_>) {
|
||||
@@ -658,7 +677,10 @@ fn write(rec: &Recorder) -> Result<()> {
|
||||
// Write-then-rename, so a reader never sees half a file.
|
||||
for (name, text) in [("profile.json", profile), ("session.json", session)] {
|
||||
let path = rec.out.join(name);
|
||||
let tmp = rec.out.join(format!(".{name}.tmp"));
|
||||
// A name of this writer's own: two threads harvesting at once each
|
||||
// write their own temporary, and the last rename wins.
|
||||
static SEQ: AtomicU64 = AtomicU64::new(0);
|
||||
let tmp = rec.out.join(format!(".{name}.{}.tmp", SEQ.fetch_add(1, Ordering::Relaxed)));
|
||||
std::fs::write(&tmp, text).with_context(|| format!("writing {}", tmp.display()))?;
|
||||
std::fs::rename(&tmp, &path).with_context(|| format!("writing {}", path.display()))?;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user