mirror of
https://github.com/HeyPuter/gex.git
synced 2026-11-13 20:12:43 -10:00
integ: merge merge/unity-seed 0ddc9390
# Conflicts: # web/src/runtime.js
This commit is contained in:
commit
ff69f7e446
27 files changed
+1300
-40
No files matched your search
@@ -3,8 +3,10 @@
|
||||
//!
|
||||
//! * `GEX_CGEN_RECOVERY_INPUTS`: what decides an image's recovery (its GIR
|
||||
//! and summaries) -- the IR, the lifters and decoders, gex-xlate's recovery
|
||||
//! (everything in it but the runtime translator's `lib.rs` and the
|
||||
//! profile and validation code), gex-abi, and gex-ld (placing the image);
|
||||
//! (everything in it but the runtime translator's `lib.rs` and
|
||||
//! `fjit.rs` -- the function JIT drives recovery at run time, a unit
|
||||
//! never calls it -- and the profile and validation code), gex-abi, and
|
||||
//! gex-ld (placing the image);
|
||||
//! * `GEX_CGEN_UNIT_INPUTS`: what decides the C and the module from a
|
||||
//! recovery -- this crate (and its runtime). What the C takes from gex-abi
|
||||
//! is hashed at run time (`image::abi_fingerprint`: the header and every
|
||||
@@ -55,7 +57,7 @@ fn main() {
|
||||
rec.extend(src(c));
|
||||
}
|
||||
rec.extend(src("gex-xlate").into_iter().filter(|p| {
|
||||
!matches!(p.file_name().and_then(|n| n.to_str()), Some("lib.rs" | "profile.rs" | "validate.rs"))
|
||||
!matches!(p.file_name().and_then(|n| n.to_str()), Some("lib.rs" | "profile.rs" | "validate.rs" | "fjit.rs"))
|
||||
}));
|
||||
// gex-abi is not hashed here: what the C takes from it is the generated
|
||||
// header and the values `image::abi_fingerprint` lists, which the unit
|
||||
|
||||
@@ -272,6 +272,10 @@ pub trait Translator: Send + Sync {
|
||||
fn image(&self, _process: u64, _program: &gex_ld::Program, _bytes: Option<&[u8]>) {}
|
||||
/// Guest process `process` is gone (its last reference dropped).
|
||||
fn forget(&self, _process: u64) {}
|
||||
/// Guest process `process` unmapped or reprotected the code in
|
||||
/// `ranges` (`Kernel::retired_code`): what the translator learned of
|
||||
/// those addresses no longer describes them.
|
||||
fn retire(&self, _process: u64, _ranges: &[(u64, u64)]) {}
|
||||
}
|
||||
|
||||
static TRANSLATOR: std::sync::OnceLock<Box<dyn Translator>> = std::sync::OnceLock::new();
|
||||
|
||||
@@ -87,6 +87,29 @@ impl Jit {
|
||||
self.add_image_in(process, image, elf);
|
||||
}
|
||||
|
||||
/// Process `process` unmapped or reprotected the code in `ranges`:
|
||||
/// what was learned there (function starts, never-returning functions,
|
||||
/// functions translated, read-only ranges) described the code that was
|
||||
/// there, and a `dlclose` and a later `dlopen` put another library at
|
||||
/// the same addresses. Kept, the old library's starts split the new
|
||||
/// one's functions where it had none: HK's SDL loads libdbus, unloads
|
||||
/// it, and maps libxcb over it; a libdbus start ended libxcb's
|
||||
/// `_xcb_in_read_block` before its `cmovle` as a tail call, flags dead,
|
||||
/// and it read the X setup reply out of an empty queue.
|
||||
pub fn retire(&self, process: u64, ranges: &[(u64, u64)]) {
|
||||
let mut st = self.st.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let Some(l) = st.procs.get_mut(&process) else { return };
|
||||
for &(lo, hi) in ranges.iter().filter(|r| r.1 > r.0) {
|
||||
let gone: Vec<u64> = l.starts.range(lo..hi).map(|(&a, _)| a).collect();
|
||||
for a in gone {
|
||||
l.starts.remove(&a);
|
||||
}
|
||||
l.noreturn.retain(|&a| a < lo || a >= hi);
|
||||
l.translated.retain(|&a| a < lo || a >= hi);
|
||||
l.readonly.retain(|&(s, e)| e <= lo || s >= hi);
|
||||
}
|
||||
}
|
||||
|
||||
/// Process `process` is gone.
|
||||
pub fn forget(&self, process: u64) {
|
||||
let mut st = self.st.lock().unwrap_or_else(|e| e.into_inner());
|
||||
@@ -107,18 +130,21 @@ impl Jit {
|
||||
/// starts (symbols, `.eh_frame`, init arrays, relocations; `elf` is the
|
||||
/// file, for `.eh_frame` and ELF32 symbols), with their extents, which
|
||||
/// bound functions and computed-goto tables as they do ahead of time.
|
||||
pub fn add_image(&self, image: &gex_ld::Image, elf: Option<&[u8]>) {
|
||||
self.add_image_in(0, image, elf);
|
||||
/// Returns how many function starts it knows of the image.
|
||||
pub fn add_image(&self, image: &gex_ld::Image, elf: Option<&[u8]>) -> usize {
|
||||
self.add_image_in(0, image, elf)
|
||||
}
|
||||
|
||||
fn add_image_in(&self, process: u64, image: &gex_ld::Image, elf: Option<&[u8]>) {
|
||||
fn add_image_in(&self, process: u64, image: &gex_ld::Image, elf: Option<&[u8]>) -> usize {
|
||||
let code = Code::with_elf(image, elf);
|
||||
let (starts, _) = func::seeds(&code, elf, false);
|
||||
let mut guard = self.st.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let st = guard.procs.entry(process).or_default();
|
||||
let mut n = 0;
|
||||
for (a, info) in starts {
|
||||
if info.strong() {
|
||||
st.starts.insert(a, info);
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
st.readonly.extend(
|
||||
@@ -128,6 +154,7 @@ impl Jit {
|
||||
.filter(|s| !s.writable)
|
||||
.map(|s| (s.vaddr, s.vaddr + s.bytes.len() as u64)),
|
||||
);
|
||||
n
|
||||
}
|
||||
|
||||
/// A module for the function at `addr` (see the module documentation),
|
||||
@@ -159,6 +186,28 @@ impl Jit {
|
||||
volatile: &dyn Fn(u64) -> bool,
|
||||
page_gen: &dyn Fn(u64) -> u32,
|
||||
class32: bool,
|
||||
) -> Option<Vec<u8>> {
|
||||
self.translate_skipping(process, mem, addr, executable, &|_| false, read_only, volatile, page_gen, class32)
|
||||
}
|
||||
|
||||
/// [`Jit::translate_in`], leaving out of the batch the callees `skip`
|
||||
/// names (served by another tier: the call reaches them through the
|
||||
/// table). `skip` bounds the batch only, never instruction fetch: a
|
||||
/// function another tier serves can have an entry in the middle of the
|
||||
/// one being recovered, and fetch taking its bytes for non-code decodes
|
||||
/// them as illegal.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn translate_skipping(
|
||||
&self,
|
||||
process: u64,
|
||||
mem: &[u8],
|
||||
addr: u64,
|
||||
executable: &dyn Fn(u64) -> bool,
|
||||
skip: &dyn Fn(u64) -> bool,
|
||||
read_only: &dyn Fn(u64, u64) -> bool,
|
||||
volatile: &dyn Fn(u64) -> bool,
|
||||
page_gen: &dyn Fn(u64) -> u32,
|
||||
class32: bool,
|
||||
) -> Option<Vec<u8>> {
|
||||
if gex_abi::PPC || (class32 && !gex_abi::X86) {
|
||||
return None;
|
||||
@@ -188,11 +237,31 @@ impl Jit {
|
||||
if batch.len() >= limit {
|
||||
break;
|
||||
}
|
||||
if batch.contains_key(&a) || (a != addr && st.translated.contains(&a)) || !executable(a) {
|
||||
if batch.contains_key(&a) || (a != addr && (st.translated.contains(&a) || skip(a))) || !executable(a) {
|
||||
continue;
|
||||
}
|
||||
let Learned { starts, noreturn, .. } = &mut *st;
|
||||
// Recovery records `a` as a function start for good, which bounds
|
||||
// every function recovered after it: a later recovery of the
|
||||
// function around `a` ends there as at a tail call, with the
|
||||
// flags dead. A root the table missed at can be the middle of a
|
||||
// function (a return or a signal's resume through dispatch, a
|
||||
// computed goto's label, a guessed slot target), so `a` stays a
|
||||
// start only if something already made it one (a call, the
|
||||
// image's symbols). The page's JIT, with no images to seed it,
|
||||
// split libxcb's `_xcb_in_read_block` before a `cmovle` this way.
|
||||
let before = starts.get(&a).cloned();
|
||||
let f = func::recover_one(&code, a, starts, noreturn, &opts);
|
||||
if !before.as_ref().is_some_and(|b| b.strong()) {
|
||||
match before {
|
||||
Some(b) => {
|
||||
starts.insert(a, b);
|
||||
}
|
||||
None => {
|
||||
starts.remove(&a);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Err(e) = f.verify().and_then(|_| block_local(&f)) {
|
||||
if std::env::var_os("GEX_FJIT_STATS").is_some() {
|
||||
eprintln!("[fjit] {a:#x}: recovered malformed ({e}); left to the fallback");
|
||||
|
||||
@@ -580,6 +580,10 @@ impl gex_kernel::ftab::Translator for FjitTranslator {
|
||||
fn forget(&self, process: u64) {
|
||||
self.0.forget(process);
|
||||
}
|
||||
|
||||
fn retire(&self, process: u64, ranges: &[(u64, u64)]) {
|
||||
self.0.retire(process, ranges);
|
||||
}
|
||||
}
|
||||
|
||||
/// The guest memory for this run (docs/mem64.md §5): `--mem64`/`--mem32`,
|
||||
|
||||
+9
-9
@@ -51,21 +51,21 @@ push** (rebased onto upstream wink 0dd84167; nothing pushed):
|
||||
- replay it with `replay-run.mjs gpu TRACE --query readback=1`;
|
||||
- bisect the shader variants with `shaders.mjs`;
|
||||
- then re-apply e204fa1b's two web files.
|
||||
2. *Grass is still dots* at GL 4.0 after the -0.0 fix (U mp27; the sea is
|
||||
back). Next: the geometry-shader emulation's output for the grass program
|
||||
(its source is in the Muck trace; `shaders.mjs`). It may resolve with
|
||||
item 1 at GL 4.1+, where Unity's 410 source compiles natively.
|
||||
2. *Grass:* fixed at GL 4.2 by GL 4.1's texture limits with mesa-wink
|
||||
60202246 (U mpg3: blades, 36.6-38.1 presents/s). gex's pin is 60202246
|
||||
and `?gllimits=41` is the default since U's pin commit on merge/unity
|
||||
(`?gllimits=0` turns it off; 43 stays per run, item 1).
|
||||
3. *Noita's world fps* is bound by its job hand-off, not by GL: the main
|
||||
thread is parked about 37% waiting on job threads, and KLock is 0.14%
|
||||
(stream F's area). The page's GL submit is no longer the limit
|
||||
(CLIENT_ARRAY: 55.5 vs 51.1 world fps on the old path, pewasm 47.2).
|
||||
4. Remaining E1: GL draw/pointer calls are on the fast path, but the page
|
||||
gain is below noise.
|
||||
5. *The 4-slot bind-group repro on the rebased tree* (`$G/urep5.sh`, mods
|
||||
tinyu) timed out at 1200 s with no output. Rerun it the way trep2.sh ran
|
||||
the earlier ones (full trace, `--query stream=1`, timeout 1500, no
|
||||
maxframes) before moving gex's pin; expect 0 validation errors from
|
||||
frame 1343 on.
|
||||
5. *The 4-slot bind-group repro on the rebased tree:* done (U,
|
||||
2026-10-09, `scratch/unity/urep-pin.sh` on edger, run like trep2.sh:
|
||||
full trace, `--query stream=1`, timeout 1500): mods/tinyu replays all
|
||||
2,777 frames (3,403 batches) of Noita's trace with 0 validation errors
|
||||
(5f5572e6 and +6d3a4cfd had 2,668 and 2,720 from frame 1343).
|
||||
|
||||
Tooling (unchanged):
|
||||
|
||||
|
||||
@@ -1801,3 +1801,364 @@ Not yet:
|
||||
applies the browser's flat-layout cuts natively, to test that.
|
||||
- HK's UnityPlayer maps at 0x11752000 in the page. A unit for that
|
||||
base is building on gooner (`scratch/unity/units/build-hk-page.log`).
|
||||
- **Bisecting the HK fjit black screen** (`?fjitlibs=+name,-name`,
|
||||
731faf46; `scratch/unity/page/hk-bis.sh NAME LIBS` gives the last
|
||||
readback's mean brightness: the menu ~37-40, the failure 0):
|
||||
|
||||
| run | fjitlibs | result |
|
||||
|---|---|---|
|
||||
| hb1 | -UnityPlayer.so | black |
|
||||
| hb2 | -UnityPlayer.so,-libc.so.6 | black |
|
||||
| hb3 | +none.so (JIT on, no object) | menu |
|
||||
| hb4 | +libxcb.so.1,+libX11.so.6 | menu (lit 59k from ~210 s; the last readback only black) |
|
||||
|
||||
The compositor's lit-pixel count (every 30 s in the log; ~5179 is
|
||||
the title bar alone, ~59k the menu) separates two things the last
|
||||
readback's brightness mixed up: hb1, hb2 and hp5 are never lit,
|
||||
while hb4 draws the menu and only its final frame is dark. So the X
|
||||
libraries are not the cause; hp1's libxcb crash (`_xcb_in_read_block`
|
||||
copying `queue_len` = -8 bytes) stays open.
|
||||
- **These rules leaked:** `?fjitlibs=` was checked only on tier-1
|
||||
dispatch misses (`serve`), not on `gex.lookup` misses, so JIT code
|
||||
calling through a pointer or a PLT slot translated any object
|
||||
(hb1 and hb2 still JIT-compiled UnityPlayer and libc that way).
|
||||
841b380b checks the rules at every translation and takes
|
||||
`name@lo-hi` (hex offsets into the mapping, as traps print them).
|
||||
- Next with the strict rules: hb6 `+libGL.so.1` (the guest GL;
|
||||
rendering black while presents continue points at the GL stream),
|
||||
hb7 `-libGL.so.1`. `hk-bis.sh` now prints the lit timeline too.
|
||||
- Native xbig (`scratch/unity/xbn/run.sh`, big X replies across
|
||||
many socket reads) passes with `--fjit`, `--fjit --mem64` and
|
||||
`GEX_CGEN_EXP=WASMFLAT`; the page runs of it (xb1, xb2) never
|
||||
printed (the old xbig printed only at the end; it now reports
|
||||
progress).
|
||||
- **hp6 (no fjit, `elfunits=UnityPlayer.so`) did not use the unit:**
|
||||
UnityPlayer mapped at 0x10752000, not 0x11752000. The function
|
||||
JIT's heap spans come from the arena cursor the guest's mappings
|
||||
use, so its 16 MiB chunk moved every later mapping in hp1. hp6 is a
|
||||
no-unit run: first frame 108 s, 100 presents 183 s, menu 15.7-16.0
|
||||
presents/s (hp4: 108 s, 185 s, 15.2-15.6). 841b380b adds
|
||||
`?pins=path@base+span` (the closure pins, `gex_pin_object`).
|
||||
- **hp7 (no fjit, the UnityPlayer unit pinned at 0x11752000,
|
||||
`?cgen=1&elfunits=UnityPlayer.so&pins=UnityPlayer.so@11752000+2674000`):**
|
||||
first frame 55 s (hp6 108 s), 100 presents at 100 s (hp6 183 s),
|
||||
the menu at 24.5-27.6 presents/s from ~180 s (hp6 15.7-16.0), lit
|
||||
from ~150 s; readback `shots/hp7/c017.png` is the real menu. Native
|
||||
gexc on edger (integ/2026-10-08d gate leg): 30.59 presents/s, menu
|
||||
ready at 406.5 s. So the page with the unit is about 14% slower
|
||||
than native at the menu (26.3 vs 30.59) and reaches it sooner.
|
||||
`hk-page.sh` runs want the pin whenever `elfunits` is given.
|
||||
- hb6 (`+libGL.so.1`, strict) died at ~133 s: thread 24 in
|
||||
UnityPlayer (+0x1dcbe60, tier-1) branched to 0x13450100, a mono.wasm
|
||||
export with no bridge (SIGSEGV, status 139). Not seen in any other
|
||||
run; hb10 repeats it.
|
||||
- The page keeps the guest's output (`shots/NAME/out.txt`), and
|
||||
hk-page.sh passes `-logFile -`, so HK's player log is in it.
|
||||
- The HK UnityPlayer unit for 0x11752000 (55,439 functions, 165 MiB
|
||||
wasm, key 81cfd177) is what hp7 ran with.
|
||||
- **The fjit crash (hb6-hb21, strict `?fjitlibs=`).** Every run that
|
||||
JIT-compiles libGL dies at 120-250 s, branching to the provider's
|
||||
return address 0x13450100 (`code_page + 0x100`, depth 0). It shows up
|
||||
on thread 24 (hb6/13/16/20) or thread 0 (hb21). hb10/17 instead had
|
||||
thread 0's poll return to a heap address. `+libxcb,+libX11` (hb4) and
|
||||
`+none.so` (hb3) reach the menu.
|
||||
- Block 0x12c17e60 is UnityPlayer's `syscall@plt`. The diagnostic's
|
||||
`.so +X` is relative to the text mapping, so UnityPlayer's base
|
||||
there is 0x10752000. The caller is Unity's futex lock slow path at
|
||||
+0xdf1ecd (FUTEX_WAIT_PRIVATE through `syscall()`).
|
||||
- At the fault rax is still 0xca (futex, never answered), rcx is
|
||||
after libc syscall()'s `syscall`, and rsp is syscall()'s entry rsp.
|
||||
So the futex trapped out to wait, and the thread resumed at the
|
||||
provider's ra instead of the saved pc (0x12ee34f9).
|
||||
- Every provider guest call in the ring is 0x10d90 deeper than that
|
||||
sp, every call-out is closed, and "provider guest call open" is
|
||||
false.
|
||||
- `provider_try` resets GUEST_CALLS on any exception, not only its
|
||||
raise tag, and rethrows. So a wasm trap that unwound a provider's
|
||||
frames would look exactly like this.
|
||||
- hb21 (`+libGL.so.1` only; since 7e23c82a the rules also keep other
|
||||
objects' callees out of a module) crashed the same way at 248 s. So
|
||||
JIT'd libc is not needed for it.
|
||||
- Diagnostics, f823644f: `[gex] a trap left through N provider guest
|
||||
call(s)` (from provider_try, with the JS stack) and `... reached the
|
||||
run loop with N open`. The wild-branch report now also lists the
|
||||
Worker's last 16 set_resume calls (lib.rs line, pc).
|
||||
- hb23 and hb24 (`+libGL.so.1,provcheck=1`) crashed the same way.
|
||||
provcheck said every provider guest call ended at its slot + 8, and
|
||||
the dump showed the "slot" at 0x57f4830 holding 0x12d7a280: no
|
||||
`ret` read 0x13450100. hb22 (`+libc.so.6`) instead ran a libc
|
||||
continuation (+0x8ea87, after `call *0x378(%r12)` into ld.so's
|
||||
`_dl_catch_error`) with Unity's registers and called a heap address.
|
||||
- **Cause: a stale resume (fixed in e2f06c87).** The dispatch miss
|
||||
that recognises a call-out's return (`cgen::returned`) sets the
|
||||
resume and traps when it cannot leave by returning, which is the
|
||||
case with JIT bodies on the stack. `#callOut`'s catch returned on
|
||||
`gex_callout_hit()` without reading the resume. The next time the
|
||||
thread blocked with nothing else to run (`schedule()` returning 2;
|
||||
here Unity's futex), the run loop took the stale resume. That was
|
||||
the provider's ra 0x13450100 (hb21/23/24, the last resume in the
|
||||
ring) or 0x12e63a87 (hb22). The catch now drops the resume when it
|
||||
ends its own call-out.
|
||||
- hb25 (`+libGL.so.1`, the fix): no crash; lit 59-60k from ~150 s;
|
||||
the last readback (`shots/hb25/c013.png`) is the menu; 15.0
|
||||
presents/s at 392 s (no unit). hb26 (the full function JIT) and hp8
|
||||
(`--fjit` with the pinned unit, the target configuration) are
|
||||
queued (`q16.sh`, `q17.sh`).
|
||||
- **HK's start-up in the page (hp7, the unit):** first frame 55 s;
|
||||
"Performing automatic level start" by 122 s; 18 presents/s by 152 s;
|
||||
the menu is up at about 2.5 min (native gate: menu ready 406.5 s).
|
||||
- **After the fix (2026-10-08 evening; `hk-bis.sh`, `hk-bisu.sh` for
|
||||
the unit runs; lit = the compositor's lit pixels, ~59k the menu):**
|
||||
|
||||
| run | config | result |
|
||||
|---|---|---|
|
||||
| hb26 | full fjit, no unit | never lit, 59.5 presents/s; player log: "The selected window backend is (null)", "Video subsystem has not been initialized", "Desktop is 0 x 0" |
|
||||
| hb27 | +UnityPlayer.so | menu (lit from ~270 s) |
|
||||
| hb28 | +libc.so.6 | menu (lit from ~210 s) |
|
||||
| hb29 | -UnityPlayer.so,-libc.so.6 | never lit; SDL's X11 video init fails as in hb26 |
|
||||
| hp8 | full fjit + the pinned unit | died at 32 s: p_memcpy OOB from libxcb `_xcb_in_read_block` (+0xf16f), `queue_len` negative (as hp1) |
|
||||
| hu1 | +libxcb.so.1 + the unit | menu at 393 s, 8.9 presents/s |
|
||||
| hu2 | -libxcb.so.1 + the unit | **menu, 41.6 presents/s** (first frame 128 s, 100 presents 166 s, 41.1-41.7 from 302 s) |
|
||||
|
||||
- hu2 against hp7 (the unit, no fjit): 41.6 vs 26.3 presents/s at
|
||||
the menu (58% more), and against native gexc's 30.59 (36% more);
|
||||
but the first frame is later (128 vs 55 s), as the JIT compiles.
|
||||
- libxcb crashes only with other objects JIT-compiled too, and alone
|
||||
it is fine (hu1). `queue_len` can only go negative if two threads
|
||||
run `read_packet` at once, so the suspect is the iolock: libxcb
|
||||
drops it around `poll()` in `_xcb_conn_wait` and takes it again
|
||||
after. A wrong resume after the blocking poll (the class of the
|
||||
stale-resume bug) would skip the re-lock.
|
||||
- The black screen is in SDL's X11 video init and needs neither
|
||||
UnityPlayer nor libc JIT-compiled (hb29). 3-minute strict runs,
|
||||
judged by the player log's "window backend is x11|(null)":
|
||||
|
||||
| run | fjitlibs | backend |
|
||||
|---|---|---|
|
||||
| hb30 | +libX11,+libxcb | x11 |
|
||||
| hb31 | +the 12 X extension/aux libraries (Xext, Xrandr, Xi, Xinerama, Xcursor, Xss, Xxf86vm, Xfixes, Xrender, X11-xcb, Xau, Xdmcp) | x11 |
|
||||
| hb32 | +ld.so, libdl, libpthread, libm, libstdc++, libgcc_s, librt | x11 |
|
||||
| hb33 | everything but UnityPlayer, libc and all 14 X libraries | x11 |
|
||||
| hb34 | everything but UnityPlayer, libc, ld.so, libdl, libpthread, libm, libgcc_s, librt | (null) |
|
||||
|
||||
So it takes the X libraries together with something outside them.
|
||||
(A `+` in `?fjitlibs=` must be `%2B` inside a name: hb34 meant to
|
||||
drop libstdc++ too and did not.) `q24.sh`: X plus libstdc++
|
||||
(hb35), plus the GL libraries (hb36), plus audio/system (hb37).
|
||||
- **xbig hung in the page (xb3 without fjit, xb4 with; fixed in
|
||||
895a3b8c).** xb5 (per-step times): round 0 (300 KB property, 480 KB
|
||||
image, 50 atoms) done at 1.7 s, then the second property read never
|
||||
returned. The X server is the host-side wasm Xwayland, and its
|
||||
`gex_poll` (web/xwayland/gexos.c) set POLLOUT on every descriptor
|
||||
but returned only for POLLIN: a reply that did not fit the guest's
|
||||
kernel socket (`SOCK_BUF`, 256 KiB) waited for input that never came,
|
||||
since the client was waiting for the reply. POLLOUT now comes from
|
||||
the socket's room (`k_sock_writable`, `hostx.js` `writable`) and
|
||||
counts. Any X client reading more than 256 KiB at once was exposed
|
||||
(Unity's XGetImage/property reads, Wine). Rebuilding Xwayland.wasm on
|
||||
gooner needed xorgproto 2024.1, util-macros, libxshmfence's header,
|
||||
wayland-protocols and W's host wayland-scanner staged by hand
|
||||
(`scratch/unity/xwl/`, `PKG_CONFIG_PATH`/`ACLOCAL_PATH`).
|
||||
- lockpoll (libxcb's unlock/poll/lock on 4 threads, 80,000 entries,
|
||||
`scratch/unity/page/lockpoll`) passes in the page with and without
|
||||
fjit (lp1/lp2) and natively, so the hp8 libxcb race is not that
|
||||
pattern on its own.
|
||||
- **Muck's grass is fixed by `?gllimits=41` with GL's rebased Mesa
|
||||
(mods/gpu, mesa-wink gl/perf 60202246):** mpg3 (fjit + the unit):
|
||||
the context is GL 4.2, no "Shader Unsupported" for the grass, and
|
||||
`shots/mpg3/c013.png` shows the grass blades and the full 3D menu
|
||||
scene; 36.6-38.1 presents/s. mpg1/mpg2 (mods/gpu at GL 4.0) were
|
||||
dots. With the site Mesa (mpg4) the 3D scene is never drawn (lit
|
||||
5179; "WebGPU validation: Bind group layout ..."), so it needs both.
|
||||
mp26's black scene was at GL 4.6 (the compute limits too); 4.1's
|
||||
texture limits alone do not cause it.
|
||||
- **The black screen bisect, continued (3-minute runs, X = the 14 X
|
||||
libraries, judged by the player log's window backend):**
|
||||
|
||||
| run | fjitlibs | backend |
|
||||
|---|---|---|
|
||||
| hb35 | X + libstdc++ | x11 |
|
||||
| hb36 | X + the GL libraries | x11 |
|
||||
| hb37 | X + audio/system | (null) |
|
||||
| hb39 | X + libbsd, libz, liblzma and the other small ones | x11 |
|
||||
| hb40 | X + audio, dbus, systemd, udev, steam | (null) |
|
||||
| hb43 | X + dbus, systemd, udev | (null) |
|
||||
| hb44 | X + libsteam_api | x11 |
|
||||
| hb45 | X + libdbus-1 | **(null)** |
|
||||
| hb46 | X + libsystemd | x11 |
|
||||
| hb47 | X + libudev | x11 |
|
||||
|
||||
So the X libraries JIT-compiled with libdbus. `LD_DEBUG` (hb41 fails,
|
||||
hb42 passes): after `SDL_X11_LoadSymbols` the failing run unloads
|
||||
the X libraries at once (`X11_CreateDevice` failed) and has opened
|
||||
one X connection, the passing run two. libdbus does not touch X: it
|
||||
only changes which functions land in which batch, so this looks like
|
||||
the same libxcb miscompile as hp8's crash. hs1/hs2 (`?strace=1`, X +
|
||||
libdbus and X + steam) record the syscalls around the connect.
|
||||
- **hp8's libxcb crash, bisected inside libxcb** (the unit + fjit;
|
||||
`name@lo-hi` excludes an offset range of the object's text):
|
||||
|
||||
| run | fjitlibs | result |
|
||||
|---|---|---|
|
||||
| hu3 | +xcb,+X11 | pass |
|
||||
| hu4 | +xcb,+libc | pass |
|
||||
| hu11 | +xcb,+ld.so | pass |
|
||||
| hu12 | +xcb,+libc,+ld.so | pass |
|
||||
| hu6 | everything, xcb 0-9f00 excluded | pass |
|
||||
| hu7 | everything, xcb 9f00-14000 excluded | crash |
|
||||
| hu8 | everything, only xcb 3120-32a0 (`_xcb_in_read_block`) excluded | pass |
|
||||
| hu9 | everything, xcb 0-3120 (the PLT, `xcb_connect_to_fd`'s callers) excluded | pass |
|
||||
| hu13 | everything, `watch=` on `queue_len` (c+0x1074) | queue_len stays 0 to the crash |
|
||||
|
||||
It is deterministic (~30-34 s, libxcb +0x316f). At the crash the
|
||||
registers hold `memmove(queue, queue+8, -8)` with done = 8 while
|
||||
`queue_len` is 0: the `min(queue_len, len)` select came out wrong in
|
||||
the page's code, not a race (the iolock theory above is wrong;
|
||||
lockpoll agrees). The offline harness (`scratch/unity/fjitrepro`:
|
||||
libxcb alone, and with libc and the glibc_mem providers, eager and
|
||||
lazy binding) emits a correct select, so it depends on the page's
|
||||
batch: PLT stubs inlined into it (`gex-fjit` inline.rs) or a callee
|
||||
summary. `?fjitlibs=dumpmod=ADDR` (fjit.rs) dumps the module whose
|
||||
batch root is ADDR as base64 `[fjitdump]` lines; hu14
|
||||
(`dumpmod=1a79d120`) captures the crashing module.
|
||||
- **Start-up in the page (unit + fjit, `-libxcb.so.1`):** ht1
|
||||
(`?attachtrace=1`): thread Workers attach ~0.1 s after their post, and
|
||||
posts are ~0.25 s apart, so spawning threads costs little. ht2 (a
|
||||
Chrome profile of the main Worker, 70-120 s): 64% (33 of 52 s) in
|
||||
`fjit::translate`: recovery 27.6 s (discover/recover_one 16.8, lift
|
||||
10), analyze 6.4, jump tables 4.4, emit 5.5, build_runtime 5.7, and
|
||||
7 s in `lib_wanted`/`object_file_off` (only with `?fjitlibs=` rules).
|
||||
ht3 (`byobj=1`, counts by object up to the menu,
|
||||
translations/functions/wasm): UnityPlayer.so 750 / 3,667 / 36 MB
|
||||
although the unit serves it, then libasound 7.9 MB, libc 4.7 MB,
|
||||
libcrypto 3.5 MB, libX11 2.1 MB; first frame 125 s. A miss in
|
||||
UnityPlayer (a function the unit lacks) pulled up to 63 of the unit's
|
||||
own functions into its batch. 1cf4e76f keeps callees the unit serves
|
||||
out of the batch (`cgen::index_of`).
|
||||
- **Root cause of both: the JIT kept a dlclose'd library's function
|
||||
starts (fixed in 763a1538).** `?fjitlibs=dumpmod=` lines must be
|
||||
under ~4,000 characters (the page console cuts them; ae44c162 uses
|
||||
3,000; `dumpmod=OBJ@OFF` names a function by object and text-mapping
|
||||
offset). hu16's module has `_xcb_in_read_block` ending after its `cmp`
|
||||
at +0x2d with a tail call to +0x30, the `cmovle`, and no flags stored
|
||||
before it: +0x30 was a strong start, so recovery ended the function
|
||||
there as at a tail call, with the flags dead, and the `cmovle` read
|
||||
stale flags: `min(queue_len, len)` gave `len` (hp8's memmove of -8
|
||||
bytes). hs1/hs3 (X + libdbus) show the first X connection shut right
|
||||
after the setup `writev`, with no read: `read_setup`'s
|
||||
`_xcb_in_read_block` took the reply from an empty queue (the black
|
||||
screen). hs4 (`dumpmod=libxcb.so.1@3150`) found no batch holding
|
||||
+0xf150 as a function, and hs5 (`scanconst=0x1310d150`, which reports
|
||||
every module mentioning an address) found it in libdbus's modules:
|
||||
SDL dlopens libdbus, dlcloses it, and libxcb is mapped over the same
|
||||
range, while the JIT's learned starts (and its set of translated
|
||||
functions) are keyed by address and outlived the unmap. That is why
|
||||
hb45 needed libdbus JIT-compiled. 763a1538 keys the learned state by
|
||||
an epoch that `retire_unmapped_code` moves on (`fjit::on_retire`,
|
||||
logged as "code unmapped ... learning afresh"; hb50 had 10 epochs by
|
||||
151 s, six from one libasound plugin range). hb50 (full fjit, no
|
||||
unit): "window backend is x11", where hb26/hb48/hb49 had "(null)".
|
||||
- Also (a12da54a, 6bc61569): a miss strictly inside an `.eh_frame` FDE
|
||||
(vfprintf's computed-goto labels, the code after `clone`'s syscall,
|
||||
return addresses reached through dispatch) is left to the fallback,
|
||||
since `recover_one` records every root as a start for good and would
|
||||
split the enclosing function the same way; `.plt`'s single FDE is
|
||||
exempt (its stubs are entries). Natively the same holes exist (the
|
||||
learned state is per process, kept across dlclose; mid-function
|
||||
roots become starts), masked by the images' seeded starts.
|
||||
- 1cf4e76f's first form used `cgen::index_of`, which also answers for
|
||||
the JIT's own entries, and `executable` is what instruction fetch
|
||||
asks of every byte: hu15 decoded the `cmovle` at 0x1a79d150 (a JIT
|
||||
entry by then) as illegal (SIGILL, status 132). 5ec43e82 counts unit
|
||||
entries only (`cgen::unit_serves`).
|
||||
- **Results after the fixes (2026-10-09, HK in the page on edger;
|
||||
presents/s at the menu, from the last minutes of 7-minute runs):**
|
||||
|
||||
| run | config | shim | first frame | 100 presents | menu presents/s |
|
||||
|---|---|---|---|---|---|
|
||||
| hp7 | unit, no fjit | before | 55 s | - | 26.3 |
|
||||
| hu2 | unit + fjit `-libxcb` | before | 128 s | 166 s | 41.6 |
|
||||
| ht3 | unit + fjit `-libxcb` | before (byobj) | 125 s | - | - |
|
||||
| ht5 | unit + fjit `-libxcb` | 5ec43e82 (unit callees out of batches) | 113 s | - | 40.2 |
|
||||
| ht6 | unit + fjit `-libxcb` | a12da54a (+ mid-function misses to the fallback) | 78 s | 96 s | 39.6-39.7 |
|
||||
| hp9/hp10 | full fjit + unit | a12da54a/6bc61569 | - | - | crash at 23.6 s (memmove after writev: the stale-start split) |
|
||||
| hp11 | full fjit + unit | 763a1538 (epochs) | 131 s | 161 s | 40.7-42.0 |
|
||||
| hp12 | full fjit + unit | 58e95157 (epochs only where translated) | 128 s | 158 s | 42.4-42.7 |
|
||||
| **hp13** | **full fjit + unit** | **977a2236 (current JIT callees out of batches too)** | **87 s** | **119 s** | **39.6-39.9** |
|
||||
| hb51 | full fjit, no unit | 58e95157 | 222 s | 270 s | 33.9 at 392 s, rising (x11 backend; was black) |
|
||||
| hb52 | full fjit, no unit | merge/unity-rekey d146c248 | - | - | SIGILL at start-up (status 132) |
|
||||
| hb53 | full fjit, no unit | merge/unity-rekey 64efd78e | 178 s | 225 s | 35.7 at 392 s (x11 backend) |
|
||||
|
||||
Native gexc's gate (unit, no fjit): menu at 406.5 s, 30.59
|
||||
presents/s. hp13 against hp7: 52% more presents/s (39.9 vs 26.3),
|
||||
first frame 32 s later (87 vs 55 s). hp13's menu rate is 6% below
|
||||
hp12's (39.9 vs 42.4): callees served by older modules are called
|
||||
through the table instead of directly, which may cost that much, but
|
||||
the menu rate ranges 39.7-42.7 across these runs. By the menu hp13's
|
||||
JIT has made libasound 20 MiB of wasm (947 functions), libc 4.3 MiB,
|
||||
libudev 3.2 MiB, libcrypto 2.9 MiB; UnityPlayer is out of the top 12.
|
||||
- **merge/unity-rekey (64efd78e, one commit on 4ed5cad3, for its own
|
||||
round):** the native JIT's two holes fixed in gex-xlate
|
||||
(`Jit::retire(process, ranges)` from both hosts' on_retire; a miss's
|
||||
root stays a start only if it was a strong one already) and fjit.rs out
|
||||
of GEX_CGEN_RECOVERY_INPUTS (key c6014ffc... with and without an
|
||||
fjit.rs edit, 855acf6a... after a func.rs edit). hb52 (d146c248) died
|
||||
with SIGILL: with misses no longer starts, recovery walked over a JIT
|
||||
entry inside the function, and the page's "served" test was part of
|
||||
`executable`, which fetch asks of every byte, so the bytes decoded as
|
||||
illegal. 64efd78e passes it as `skip` (`Jit::translate_skipping`), the
|
||||
batch bound only. hb53: menu, x11, first frame 178 s, 35.7 presents/s
|
||||
(hb51 on the epoch shim: 222 s, 33.9). Native: tests/fjit-remap (the
|
||||
Oct 8 gexc gets 99,800 of 200,000 wrong under --fjit), fjit-procs,
|
||||
x86-programs (82 ok), embed PASS, gex-xlate tests. The page's unit cache
|
||||
(scratch/unity/units/cgen-page) must be rebuilt after it lands.
|
||||
- **Mesa pin (1d5b26d9): mesa-wink 60202246 and `gllimits=41` by
|
||||
default.** Replays gpu vs this build: Noita 92f095c5, HK 9eece5c5, 0
|
||||
rejects and 0 validation errors each; GL's 4-slot repro (tinyu) 2,777
|
||||
frames with 0 validation errors; browser-gui --gl x86 and x64 12 ok
|
||||
each; HK hp14 (merge/unity shim, this Mesa) menu at 41.8 presents/s.
|
||||
Muck with the default and no `?gllimits` (mk2, the seed shim, no unit):
|
||||
"OpenGL 4.2" context, no "Shader Unsupported", grass blades in
|
||||
`shots/mk2/c013.png`, 36.9 presents/s at 393 s.
|
||||
- **Muck dies with SIGILL at ~25 s on merge/unity's shim (977a2236,
|
||||
4ed5cad3)**, with the unit (mpg5, mk1) and without (mk4). It does not
|
||||
with 64efd78e (mk3, 4 minutes, no unit) or 1bf5ba09 (mk2). The page asked
|
||||
"served by the unit or the JIT" of instruction fetch (part of
|
||||
`executable`); with epochs a JIT entry the current epoch has no start
|
||||
for is read as non-code inside the function being recovered and decoded
|
||||
as illegal. 64efd78e passes it as `skip` (batch bound only).
|
||||
- **Start-up (decision 3).** hp14's main-Worker profile, 10-90 s (unit +
|
||||
fjit): 61% (49.8 of 81 s) in `fjit::translate`: recover_one 27.2 s
|
||||
(jump tables 16.9, discover 9.2, lift 6.9), summaries 17.7, build_runtime
|
||||
8.8, emit 8.4, prepare 5.5; the per-byte `executable` 5.9 (2.3 in
|
||||
elfprov::provided's locks). Audio: the page has no audio device; FMOD's
|
||||
output init fails ("Unknown PCM default", tried twice), after pulse
|
||||
fails to connect. libasound's 20 MiB was mostly copies: the page's JIT
|
||||
had no function starts but those calls name, so a tail jump absorbed the
|
||||
function it reached, and libasound's exported wrappers each carried one.
|
||||
Offline (fjitrepro, all 1,698 exports): 57.2 MiB / 16.4 s unseeded, 9.7
|
||||
MiB / 7.1 s seeded. ALSA with no config (hp15, ALSA_CONFIG_PATH=/dev/null)
|
||||
cut libasound to 11.2 MiB but not the first frame (112 s against hp14's
|
||||
110 s, both profiled), so it is not the path to cut.
|
||||
merge/unity-seed 1bf5ba09 (on 64efd78e) seeds every mapped object's
|
||||
starts (`Jit::add_image`) before a translation and takes the fetch
|
||||
checks once per translation: no unit, hb53 (64efd78e) 178 s first
|
||||
frame, 225 s 100 presents, 35.7 presents/s; hb54 (seeding) 121 s, 171 s,
|
||||
39.1; hb55 (both) 121 s, 166 s, 40.1; libasound 26.0 -> 3.7 MiB.
|
||||
The HK unit re-keyed for 64efd78e (key 7bd87731, byte-identical c.wasm,
|
||||
1,941 s on gooner) is on edger in `units/cgen-page-rk`; Muck's is
|
||||
rebuilt too (4e58f35b, 884 s), also in `cgen-page-rk`.
|
||||
With the unit (target configuration, seed shim 1bf5ba09):
|
||||
- hp16 (HK): first frame 56 s (hp13 87 s), 100 presents 85 s (119 s),
|
||||
41.5 presents/s at 392 s (39.9).
|
||||
- hp17 (HK, profiled 10-90 s): `fjit::translate` 20.7 s of 81 s (25.5%;
|
||||
hp14 49.8 s, 61.5%); the main Worker now waits on its threads 31.6 s.
|
||||
Profiled runs' first frame (110 s both) is held by the profile.
|
||||
- mk5 (Muck): first frame 82 s, 100 presents 95 s, 34.0 presents/s at
|
||||
392 s, OpenGL 4.2, no "Shader Unsupported", grass blades
|
||||
(`shots/mk5/c013.png`).
|
||||
Branches: merge/unity-rekey 64efd78e (own round), merge/unity-seed
|
||||
1bf5ba09 on it (worktree wt/unity-rk); merge/unity 1d5b26d9 (pin).
|
||||
Natively the JIT seeds only the main image too (gexc's
|
||||
FjitTranslator::image); a Translator hook per mapped object would do
|
||||
the same there.
|
||||
@@ -806,6 +806,8 @@ export class Gex {
|
||||
static wasmDlls = '';
|
||||
/** `name,...` (the page's `?elfunits=`): shared objects that get function-tier units. */
|
||||
static elfUnits = '';
|
||||
/** `+name,-name` (the page's `?fjitlibs=`): objects the function JIT is limited to or kept from. */
|
||||
static fjitLibs = '';
|
||||
static #whySlot = 0;
|
||||
|
||||
#bytes() {
|
||||
@@ -2574,6 +2576,13 @@ export class Gex {
|
||||
* providers (cgen/libprov.rs). */
|
||||
#libprovConfig() {
|
||||
const e = this.#shim.exports;
|
||||
if (Gex.fjitLibs && e.gex_fjit_libs) {
|
||||
const b = new TextEncoder().encode(Gex.fjitLibs);
|
||||
const p = e.gex_alloc(Math.max(8, b.length)) >>> 0;
|
||||
this.#bytes().set(b, p);
|
||||
e.gex_fjit_libs(p, b.length);
|
||||
e.gex_free?.(p, Math.max(8, b.length));
|
||||
}
|
||||
if (Gex.elfUnits && e.gex_cgen_elf_units) {
|
||||
const b = new TextEncoder().encode(Gex.elfUnits);
|
||||
const p = e.gex_alloc(Math.max(8, b.length)) >>> 0;
|
||||
@@ -2821,6 +2830,8 @@ export class Gex {
|
||||
* exception, so code the provider publishes at run time (its JIT, which imports it)
|
||||
* can catch it with `try_table`, and `provider_try` takes it when nothing did. */
|
||||
#provTag = null;
|
||||
/** Traps seen unwinding provider guest calls (reported up to 16). */
|
||||
#provLeaks = 0;
|
||||
#providerTag() {
|
||||
this.#provTag ??= new WebAssembly.Tag({ parameters: ['i32'] });
|
||||
return this.#provTag;
|
||||
@@ -2841,8 +2852,16 @@ export class Gex {
|
||||
ftab.get(f)(arg);
|
||||
return 0;
|
||||
} catch (err) {
|
||||
const open = e.gex_libprov_guest_calls();
|
||||
e.gex_libprov_guest_calls_set(calls);
|
||||
if (err instanceof WebAssembly.Exception && err.is(tag)) return err.getArg(tag, 0);
|
||||
// Anything else that unwinds a provider's guest calls abandons
|
||||
// frames the guest will return into (their return addresses stay
|
||||
// on its stack): say which trap, where it resumes, and from where.
|
||||
if (open > calls && (this.#provLeaks = (this.#provLeaks ?? 0) + 1) <= 16) {
|
||||
console.error(`[gex] a trap left through ${open - calls} provider guest call(s): ${err?.message ?? err}; ` +
|
||||
`resume ${BigInt.asUintN(64, BigInt(e.gex_resume_pc())).toString(16)}\n${err?.stack ?? ''}`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
@@ -3675,6 +3694,15 @@ export class Gex {
|
||||
// alone it slid into the guest's stack and overwrote saved registers.
|
||||
this.#shim.exports.__stack_pointer.value = addrVal(this.#workerStack());
|
||||
const e = this.#shim.exports;
|
||||
// A trap that reached the run loop with a provider's guest call
|
||||
// still open abandoned the provider's frames (see provider_try).
|
||||
if ((e.gex_libprov_guest_calls?.() ?? 0) > 0) {
|
||||
if ((this.#provLeaks = (this.#provLeaks ?? 0) + 1) <= 16) {
|
||||
console.error(`[gex] a trap reached the run loop with ${e.gex_libprov_guest_calls()} provider guest call(s) open: ${err?.message ?? err}; ` +
|
||||
`resume ${BigInt.asUintN(64, BigInt(e.gex_resume_pc())).toString(16)}\n${err?.stack ?? ''}`);
|
||||
}
|
||||
e.gex_libprov_guest_calls_set(0);
|
||||
}
|
||||
// A slot the shim wants written before the guest goes back to it: its
|
||||
// module is instantiated with passive segments, so the code is present
|
||||
// and simply not in the table. Filling here costs one segment; at
|
||||
@@ -4342,7 +4370,21 @@ export class Gex {
|
||||
e.__stack_pointer.value = sp0;
|
||||
const sig = this.#calloutSignal;
|
||||
this.#calloutSignal = null;
|
||||
if (sig?.kind === 'return') return sig.i === me ? 1 : 0;
|
||||
// A call-out ended here was the trap's whole business: the resume
|
||||
// the miss path set at its return (`cgen::returned`) is spent, and
|
||||
// left in place it is where the run loop next goes -- the next
|
||||
// time this thread blocks with nothing else to run, it "resumed"
|
||||
// at the provider's return address (HK under ?fjit, docs §12 34).
|
||||
// (Only this one's: an outer call-out's hit returns through the
|
||||
// frames between, which leave as before.)
|
||||
const ended = (v) => {
|
||||
if (v === 1) {
|
||||
e.gex_resume_pc();
|
||||
e.gex_resume_ctx();
|
||||
}
|
||||
return v;
|
||||
};
|
||||
if (sig?.kind === 'return') return ended(sig.i === me ? 1 : 0);
|
||||
if (sig?.kind === 'bypass') {
|
||||
if (!sig.f) throw new Error('gex: a watched dispatch entry with nothing to run');
|
||||
next = sig.f;
|
||||
@@ -4359,7 +4401,7 @@ export class Gex {
|
||||
throw err;
|
||||
}
|
||||
const hit = e.gex_callout_hit();
|
||||
if (hit >= 0) return hit === me ? 1 : 0;
|
||||
if (hit >= 0) return ended(hit === me ? 1 : 0);
|
||||
// gex_nr::NATIVE_LEFT (gexrt saw the guest leave a native caller's
|
||||
// frames): a native caller's call-out leaves, at the context's PC
|
||||
// (the entry the guest makes again); anything else carries on.
|
||||
|
||||
@@ -416,6 +416,7 @@ async function handle(msg) {
|
||||
if (msg.mem64) globalThis.GEX_MEM64 = 1;
|
||||
if (msg.wasmDlls) Gex.wasmDlls = msg.wasmDlls;
|
||||
if (msg.elfUnits) Gex.elfUnits = msg.elfUnits;
|
||||
if (msg.fjitLibs) Gex.fjitLibs = msg.fjitLibs;
|
||||
if (msg.calloutTrace) globalThis.GEX_CALLOUT_TRACE = 1;
|
||||
if (msg.lazyThread) globalThis.GEX_LAZY_THREAD = 1;
|
||||
// `kernelStack` is this Worker's slice of the kernel's stack array. Absent for
|
||||
|
||||
@@ -1065,6 +1065,20 @@ pub(crate) fn index_of(addr: u64) -> Option<u32> {
|
||||
ftab(|m| m.get(addr).map(|f| f.idx))
|
||||
}
|
||||
|
||||
/// Whether a unit built ahead of time (or a provider) serves the function at
|
||||
/// `addr`: not the JIT's own functions, whose resume points are published
|
||||
/// at addresses inside functions.
|
||||
pub(crate) fn unit_serves(addr: u64) -> bool {
|
||||
ftab(|m| m.get(addr).is_some_and(|f| m.install(f.install).tier == Tier::Unit))
|
||||
}
|
||||
|
||||
/// Whether a current JIT function serves `addr` (its pages unchanged since).
|
||||
/// After the JIT forgets (`fjit::on_retire`) this is what still knows
|
||||
/// which functions it has made.
|
||||
pub(crate) fn jit_serves(addr: u64) -> bool {
|
||||
ftab(|m| m.get(addr).is_some_and(|f| m.install(f.install).tier == Tier::Jit && crate::fjit::current(f.install)))
|
||||
}
|
||||
|
||||
/// How many units are published: a Worker installs up to this many.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gex_cgen_count() -> u32 {
|
||||
@@ -1298,9 +1312,27 @@ pub extern "C" fn gex_ftab_fallback(ctx: i32, _depth: i32, _need: i32, ret: i64)
|
||||
c.len() - 1
|
||||
});
|
||||
CALLOUT_N.fetch_add(1, Ordering::Relaxed);
|
||||
// Opened (result -1): one that never closes was abandoned by a throw.
|
||||
CALLOUT_RING.with(|x| {
|
||||
let mut x = x.borrow_mut();
|
||||
if x.len() >= 24 {
|
||||
x.remove(0);
|
||||
}
|
||||
x.push((t, ret as u64, want, depth as u32, -1, sp));
|
||||
});
|
||||
// SAFETY: a host import taking scalars; nothing here holds a lock.
|
||||
let r = unsafe { call_out(ctx, t as i64, ret, want as i64) };
|
||||
CALLOUTS.with(|c| c.borrow_mut().truncate(depth));
|
||||
{
|
||||
let after = GuestMem::new(crate::guest_bytes()).read_u64(c + gex_abi::ctx::x(sp_index()) as u64).unwrap_or(0);
|
||||
CALLOUT_RING.with(|x| {
|
||||
let mut x = x.borrow_mut();
|
||||
if x.len() >= 24 {
|
||||
x.remove(0);
|
||||
}
|
||||
x.push((t, ret as u64, want, depth as u32, r, after));
|
||||
});
|
||||
}
|
||||
if r == gex_abi::ftab::RET_LEFT {
|
||||
CALLOUT_LEFT.fetch_add(1, Ordering::Relaxed);
|
||||
if libprov::provider_return(ret as u64) {
|
||||
@@ -1424,6 +1456,14 @@ thread_local! {
|
||||
static LOOKUP_RING: std::cell::RefCell<Vec<(u64, u32)>> = const { std::cell::RefCell::new(Vec::new()) };
|
||||
/// The last stateful-provider guest calls that reached the fallback: (target, return, sp).
|
||||
static PROV_RING: std::cell::RefCell<Vec<(u64, u64, u64)>> = const { std::cell::RefCell::new(Vec::new()) };
|
||||
/// This Worker's last finished call-outs: (target, return, sp wanted,
|
||||
/// depth, result, sp after), for the wild-branch diagnostic.
|
||||
static CALLOUT_RING: std::cell::RefCell<Vec<(u64, u64, u64, u32, i32, u64)>> = const { std::cell::RefCell::new(Vec::new()) };
|
||||
}
|
||||
|
||||
/// [`CALLOUT_RING`], for a diagnostic.
|
||||
pub(crate) fn callouts_recent() -> Vec<(u64, u64, u64, u32, i32, u64)> {
|
||||
CALLOUT_RING.with(|r| r.borrow().clone())
|
||||
}
|
||||
|
||||
/// Whether a stateful provider's guest call is open on this Worker: the guest
|
||||
|
||||
@@ -49,6 +49,14 @@ pub extern "C" fn gex_elfprov_off() {
|
||||
OFF.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Every address `provided` holds, at once (the function JIT asks of each
|
||||
/// byte it fetches; three locks a byte cost HK's start-up 2.3 s, hp14).
|
||||
pub(crate) fn provided_set() -> std::collections::HashSet<u64> {
|
||||
let mut s: std::collections::HashSet<u64> = lock(&BOUND).iter().flat_map(|b| b.addrs.iter().copied()).collect();
|
||||
s.extend(super::libprov::bound_addrs());
|
||||
s
|
||||
}
|
||||
|
||||
/// Whether an ELF provider holds `addr` (the function JIT leaves it alone).
|
||||
pub(crate) fn provided(addr: u64) -> bool {
|
||||
lock(&BOUND).iter().any(|b| b.addrs.contains(&addr)) || super::libprov::bound(addr)
|
||||
|
||||
@@ -76,6 +76,16 @@ pub(crate) fn replaced(addr: u64) -> bool {
|
||||
lock(&REPLACED).iter().any(|&(lo, hi)| addr >= lo && addr < hi)
|
||||
}
|
||||
|
||||
/// The addresses replaced libraries' providers are bound at.
|
||||
pub(crate) fn bound_addrs() -> Vec<u64> {
|
||||
lock(&LIBS).iter().flat_map(|l| l.pairs.iter().map(|p| p.0)).collect()
|
||||
}
|
||||
|
||||
/// The ranges of replaced libraries' own text.
|
||||
pub(crate) fn replaced_ranges() -> Vec<(u64, u64)> {
|
||||
lock(&REPLACED).clone()
|
||||
}
|
||||
|
||||
/// Whether `addr` is one a replaced library's provider is bound at (its
|
||||
/// pairs are sorted by address).
|
||||
pub(crate) fn bound(addr: u64) -> bool {
|
||||
@@ -587,10 +597,51 @@ thread_local! {
|
||||
static GUEST_CALLS: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
/// `?fjitlibs=provcheck=1`: [`gex_libprov_guest_call`] checks that each
|
||||
/// provider guest call ends with the guest's stack just past the return
|
||||
/// address the call pushed (its SP when the call opens), and says when not.
|
||||
pub(crate) static PROV_CHECK: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||
|
||||
thread_local! {
|
||||
/// The open guest calls' return-address slots, for [`PROV_CHECK`].
|
||||
static PROV_SLOTS: std::cell::RefCell<Vec<u64>> = const { std::cell::RefCell::new(Vec::new()) };
|
||||
}
|
||||
|
||||
/// `gex.provider_guest_call(delta)`.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gex_libprov_guest_call(delta: i32) {
|
||||
GUEST_CALLS.with(|c| c.set(c.get() + delta));
|
||||
if PROV_CHECK.load(Ordering::Relaxed) {
|
||||
prov_check(delta);
|
||||
}
|
||||
}
|
||||
|
||||
fn prov_check(delta: i32) {
|
||||
let ctx = crate::with_host(|h| h.threads[crate::cur()].kstate.ctx_addr);
|
||||
let gm = crate::GuestMem::new(crate::guest_bytes());
|
||||
let reg = |o: usize| gm.read_u64(ctx + o as u64).unwrap_or(0);
|
||||
let sp = reg(gex_abi::ctx::x(4));
|
||||
if delta > 0 {
|
||||
PROV_SLOTS.with(|s| s.borrow_mut().push(sp));
|
||||
return;
|
||||
}
|
||||
let Some(slot) = PROV_SLOTS.with(|s| s.borrow_mut().pop()) else { return };
|
||||
if sp == slot + 8 {
|
||||
return;
|
||||
}
|
||||
static SAID: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
|
||||
if SAID.fetch_add(1, Ordering::Relaxed) >= 32 {
|
||||
return;
|
||||
}
|
||||
crate::warn_line(&format!(
|
||||
"[gex] provcheck: a provider guest call ended with sp {sp:#x}, but its return address was at {slot:#x} ([slot] now {:#x}); \
|
||||
pc {:#x}, last target {:#x}, {} more open; last call-outs {:x?}\n",
|
||||
gm.read_u64(slot).unwrap_or(0),
|
||||
reg(gex_abi::ctx::PC),
|
||||
reg(gex_abi::ctx::LAST_TARGET),
|
||||
PROV_SLOTS.with(|s| s.borrow().len()),
|
||||
crate::cgen::callouts_recent().iter().rev().take(6).collect::<Vec<_>>(),
|
||||
));
|
||||
}
|
||||
|
||||
/// The count, which `provider_try` restores when a raise unwinds guest calls.
|
||||
@@ -602,6 +653,7 @@ pub extern "C" fn gex_libprov_guest_calls() -> i32 {
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gex_libprov_guest_calls_set(v: i32) {
|
||||
GUEST_CALLS.with(|c| c.set(v));
|
||||
PROV_SLOTS.with(|s| s.borrow_mut().truncate(v.max(0) as usize));
|
||||
}
|
||||
|
||||
/// Whether a provider's guest call is open on this Worker.
|
||||
|
||||
+423
-5
@@ -64,6 +64,111 @@ static CHECKS: Mutex<Option<HashMap<u32, Vec<(u64, u32)>>>> = Mutex::new(None);
|
||||
static TIER1_ONLY: Mutex<Option<HashSet<u64>>> = Mutex::new(None);
|
||||
|
||||
static TRANSLATED: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
/// Translations, functions and wasm bytes by object (`?fjitlibs=byobj=1`
|
||||
/// prints them every 256 translations): what the JIT spends start-up on.
|
||||
static BY_OBJECT: Mutex<Option<std::collections::BTreeMap<String, (u64, u64, u64)>>> = Mutex::new(None);
|
||||
static BY_OBJECT_ON: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
||||
/// `?fjitlibs=dumpmod=ADDR`: print (base64, in 3,000-character lines: the page console cuts
|
||||
/// a line at about 4,000) every JIT module
|
||||
/// whose functions include guest address ADDR, with its root, to rebuild the
|
||||
/// page's translation offline (scratch/unity/fjitrepro).
|
||||
static DUMP_MOD: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
|
||||
static DUMP_OBJ: Mutex<Option<(String, u64)>> = Mutex::new(None);
|
||||
/// `?fjitlibs=scanconst=ADDR`: say which translations mention guest address
|
||||
/// ADDR as an `i64.const` (a call or jump to it, or a pc it stores), and
|
||||
/// every translation asked for at ADDR: who made a mid-function address a
|
||||
/// function start.
|
||||
static SCAN_CONST: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
|
||||
fn scan_const(root: u64, funcs: &[u64], wasm: &[u8]) {
|
||||
let want = SCAN_CONST.load(Ordering::Relaxed);
|
||||
if want == 0 {
|
||||
return;
|
||||
}
|
||||
// `i64.const` (0x42) and its signed LEB128.
|
||||
let mut pat = vec![0x42u8];
|
||||
let mut v = want as i64;
|
||||
loop {
|
||||
let b = (v & 0x7f) as u8;
|
||||
v >>= 7;
|
||||
let done = (v == 0 && b & 0x40 == 0) || (v == -1 && b & 0x40 != 0);
|
||||
pat.push(if done { b } else { b | 0x80 });
|
||||
if done {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let hits = wasm.windows(pat.len()).filter(|w| *w == &pat[..]).count();
|
||||
if hits > 0 {
|
||||
let list: Vec<String> = funcs.iter().take(24).map(|a| format!("{a:x}")).collect();
|
||||
crate::warn_line(&format!(
|
||||
"[fjitscan] {want:#x} x{hits} in the module of root {root:#x} ({}), {} functions: {}\n",
|
||||
crate::object_off_str(root),
|
||||
funcs.len(),
|
||||
list.join(",")
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn dump_module(root: u64, funcs: &[u64], wasm: &[u8]) {
|
||||
let mut want = DUMP_MOD.load(Ordering::Relaxed);
|
||||
if want == 0 {
|
||||
let obj = lock(&DUMP_OBJ).clone();
|
||||
if let Some((name, off)) = obj {
|
||||
if let Some(&a) = funcs.iter().find(|&&a| crate::object_file_off(a) == (name.clone(), off)) {
|
||||
DUMP_MOD.store(a, Ordering::Relaxed);
|
||||
*lock(&DUMP_OBJ) = None;
|
||||
crate::warn_line(&format!("[fjitdump] {name}@{off:x} is {a:#x}\n"));
|
||||
want = a;
|
||||
}
|
||||
}
|
||||
}
|
||||
if want == 0 || !funcs.contains(&want) {
|
||||
return;
|
||||
}
|
||||
const T: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
let mut b = String::with_capacity(wasm.len() * 4 / 3 + 4);
|
||||
for c in wasm.chunks(3) {
|
||||
let n = (c[0] as u32) << 16 | (*c.get(1).unwrap_or(&0) as u32) << 8 | *c.get(2).unwrap_or(&0) as u32;
|
||||
for k in 0..4 {
|
||||
if k <= c.len() {
|
||||
b.push(T[(n >> (18 - 6 * k)) as usize & 63] as char);
|
||||
} else {
|
||||
b.push('=');
|
||||
}
|
||||
}
|
||||
}
|
||||
const W: usize = 3000;
|
||||
let lines = b.len().div_ceil(W);
|
||||
crate::warn_line(&format!("[fjitdump] root {root:#x} funcs {} bytes {} lines {lines}\n", funcs.len(), wasm.len()));
|
||||
for (i, fs) in funcs.chunks(32).enumerate() {
|
||||
let list: Vec<String> = fs.iter().map(|a| format!("{a:x}")).collect();
|
||||
crate::warn_line(&format!("[fjitdump] {root:x} funcs {i}: {}\n", list.join(",")));
|
||||
}
|
||||
for (i, ch) in b.as_bytes().chunks(W).enumerate() {
|
||||
crate::warn_line(&format!("[fjitdump] {root:x} {i} {}\n", std::str::from_utf8(ch).unwrap_or("")));
|
||||
}
|
||||
}
|
||||
|
||||
fn note_object(addr: u64, funcs: usize, bytes: usize) {
|
||||
if !BY_OBJECT_ON.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
let (name, _) = crate::object_file_off(addr);
|
||||
let mut g = lock(&BY_OBJECT);
|
||||
let m = g.get_or_insert_with(Default::default);
|
||||
let e = m.entry(name).or_default();
|
||||
e.0 += 1;
|
||||
e.1 += funcs as u64;
|
||||
e.2 += bytes as u64;
|
||||
let total: u64 = m.values().map(|e| e.0).sum();
|
||||
if total % 256 == 0 {
|
||||
let mut v: Vec<_> = m.iter().collect();
|
||||
v.sort_by(|a, b| b.1 .2.cmp(&a.1 .2));
|
||||
let list: Vec<String> = v.iter().take(12).map(|(n, e)| format!("{n} {}/{}/{}K", e.0, e.1, e.2 / 1024)).collect();
|
||||
crate::warn_line(&format!("[fjit] {total} translations by object (translations/functions/wasm): {}\n", list.join(", ")));
|
||||
}
|
||||
}
|
||||
static DECLINED: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
|
||||
fn lock<T>(m: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
|
||||
@@ -133,10 +238,167 @@ fn page_gen(page: u64, fresh: &mut Vec<(u64, u32)>) -> u32 {
|
||||
})
|
||||
}
|
||||
|
||||
/// Each object's `.eh_frame` function ranges (unbiased, sorted by start)
|
||||
/// and `PT_LOAD` headers (file offset, vaddr, file size), by file path;
|
||||
/// empty when it has none or cannot be read.
|
||||
type ObjFdes = (Vec<(u64, u64)>, Vec<(u64, u64, u64)>);
|
||||
static FDES: Mutex<Option<std::collections::HashMap<String, std::sync::Arc<ObjFdes>>>> = Mutex::new(None);
|
||||
static MID_DECLINED: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
|
||||
/// An ELF's `PT_LOAD` headers: (file offset, vaddr, file size).
|
||||
fn loads(elf: &[u8]) -> Vec<(u64, u64, u64)> {
|
||||
let mut out = Vec::new();
|
||||
let Some(&class) = elf.get(4) else { return out };
|
||||
let wide = class == 2;
|
||||
let rd = |o: usize, n: usize| -> Option<u64> {
|
||||
let b = elf.get(o..o + n)?;
|
||||
Some(b.iter().rev().fold(0u64, |a, &x| a << 8 | x as u64))
|
||||
};
|
||||
let hdr = if wide { (rd(0x20, 8), rd(0x36, 2), rd(0x38, 2)) } else { (rd(0x1c, 4), rd(0x2a, 2), rd(0x2c, 2)) };
|
||||
let (Some(phoff), Some(phentsize), Some(phnum)) = hdr else { return out };
|
||||
for i in 0..phnum {
|
||||
let ph = (phoff + i * phentsize) as usize;
|
||||
if rd(ph, 4) != Some(1) {
|
||||
continue;
|
||||
}
|
||||
let f = if wide { (rd(ph + 8, 8), rd(ph + 16, 8), rd(ph + 32, 8)) } else { (rd(ph + 4, 4), rd(ph + 8, 4), rd(ph + 16, 4)) };
|
||||
if let (Some(o), Some(v), Some(n)) = f {
|
||||
out.push((o, v, n));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Whether `addr` lies strictly inside a function of its object, by the
|
||||
/// object's `.eh_frame`: a miss there is a resume into the middle of a
|
||||
/// function (a signal's return, a block boundary of the fallback's), not an
|
||||
/// entry. Translated as a root, `recover_one` records it as a function start
|
||||
/// for good, and every later translation of the function around it ends at
|
||||
/// it as at a tail call, with the flags dead across it: libxcb's
|
||||
/// `_xcb_in_read_block` lost its `cmp` before the `cmovle` at +0x30 and took
|
||||
/// `min(queue_len, len)` from stale flags (hp8, hu16: memmove of -8 bytes),
|
||||
/// and SDL's X11 connect failed the same way (hb26, hb45). The fallback runs
|
||||
/// it instead; the function's real entry still gets translated.
|
||||
fn mid_function(addr: u64) -> bool {
|
||||
let Some((path, off, start)) = crate::with_host(|h| {
|
||||
h.kernel.mem.vmas().iter().find(|v| addr >= v.start && addr < v.end).and_then(|v| v.code.clone().map(|(p, o)| (p, o, v.start)))
|
||||
}) else {
|
||||
return false;
|
||||
};
|
||||
let cached = lock(&FDES).get_or_insert_with(Default::default).get(&path).cloned();
|
||||
let obj = match cached {
|
||||
Some(o) => o,
|
||||
None => {
|
||||
let bytes = crate::with_host(|h| h.kernel.read_file(&path)).unwrap_or_default();
|
||||
// Not the PLT's: one FDE covers all of `.plt`, whose stubs are
|
||||
// each an entry (hp9 left them to the fallback, and died).
|
||||
let plt: Vec<(u64, u64)> = gex_xlate::ehframe::sections(&bytes)
|
||||
.into_iter()
|
||||
.filter(|s| s.name.starts_with(".plt"))
|
||||
.map(|s| (s.addr, s.addr + s.size))
|
||||
.collect();
|
||||
let mut f: Vec<(u64, u64)> = gex_xlate::ehframe::fdes(&bytes, 0)
|
||||
.into_iter()
|
||||
.filter(|f| !plt.iter().any(|&(lo, hi)| f.start >= lo && f.start < hi))
|
||||
.map(|f| (f.start, f.end))
|
||||
.collect();
|
||||
f.sort_unstable();
|
||||
let o = std::sync::Arc::new((f, loads(&bytes)));
|
||||
lock(&FDES).get_or_insert_with(Default::default).insert(path.clone(), o.clone());
|
||||
o
|
||||
}
|
||||
};
|
||||
let (fdes, loads) = &*obj;
|
||||
// The vma's file offset gives its vaddr in the file, hence the bias.
|
||||
let Some(base) = loads.iter().find(|&&(o, _, n)| off >= o && off < o + n.max(1)).map(|&(o, v, _)| v + (off - o)) else {
|
||||
return false;
|
||||
};
|
||||
let x = addr - start + base;
|
||||
let i = fdes.partition_point(|&(s, _)| s <= x);
|
||||
let mid = i > 0 && fdes[i - 1].0 < x && x < fdes[i - 1].1;
|
||||
if mid {
|
||||
let n = MID_DECLINED.fetch_add(1, Ordering::Relaxed);
|
||||
if n < 32 || (n + 1).is_power_of_two() {
|
||||
let name = path.rsplit('/').next().unwrap_or(&path);
|
||||
crate::warn_line(&format!("[fjit] {addr:#x} ({name} +{x:#x}) is inside a function: left to the fallback ({} so far)\n", n + 1));
|
||||
}
|
||||
}
|
||||
mid
|
||||
}
|
||||
|
||||
/// Code in `ranges` was unmapped or reprotected (`retire_unmapped_code`):
|
||||
/// what the JIT learned there goes (`Jit::retire`). HK's SDL loads libdbus,
|
||||
/// unloads it, and maps libxcb over the same range; with libdbus's starts
|
||||
/// kept, libxcb's `_xcb_in_read_block` ended at a libdbus function's address
|
||||
/// (+0x30, the `cmovle` of `min(queue_len, len)`) as at a tail call, flags
|
||||
/// dead, and read the setup reply out of an empty queue: X11 video init
|
||||
/// failed, the black screen (hb26, hb45; `scanconst=` in hs5 showed the
|
||||
/// start was libdbus's), and with the unit, a memmove of -8 bytes (hp8).
|
||||
pub(crate) fn on_retire(ranges: &[(u64, u64)]) {
|
||||
if let Some(jit) = JIT.get() {
|
||||
jit.retire(0, ranges);
|
||||
}
|
||||
lock(&SEEDED).retain(|&(_, lo, hi)| !ranges.iter().any(|&(s, e)| s < hi && lo < e));
|
||||
}
|
||||
|
||||
/// The objects whose function starts the JIT has (`seed_mapped`): path and
|
||||
/// the address range of their `PT_LOAD`s, dropped when code there retires.
|
||||
static SEEDED: Mutex<Vec<(String, u64, u64)>> = Mutex::new(Vec::new());
|
||||
|
||||
/// Give the JIT the function starts of every mapped object it lacks (its
|
||||
/// symbols, `.eh_frame`, init arrays and relocations, `Jit::add_image`),
|
||||
/// as natively ahead of time. Without them recovery knows only the starts
|
||||
/// calls name, and follows a tail jump into the function it reaches: each
|
||||
/// of libasound's exported wrappers carried a copy of the function it
|
||||
/// jumps to. Offline, translating each of libasound's 1,698 exports in
|
||||
/// turn made 57.2 MiB of wasm in 16.4 s without its starts and 9.7 MiB in
|
||||
/// 7.1 s with them (one function per module: 49.7 MiB and 14.1 s against
|
||||
/// 3.2 MiB and 1.7 s); HK's menu had 20 MiB of libasound wasm (hp13).
|
||||
fn seed_mapped(jit: &gex_xlate::fjit::Jit) {
|
||||
let maps: Vec<(u64, String, u64)> = crate::with_host(|h| {
|
||||
use gex_kernel::mem::prot;
|
||||
h.kernel.mem.vmas().iter().filter(|v| v.prot & prot::EXEC != 0).filter_map(|v| v.code.clone().map(|(p, o)| (v.start, p, o))).collect()
|
||||
});
|
||||
for (start, path, off) in maps {
|
||||
if lock(&SEEDED).iter().any(|(p, lo, hi)| *p == path && start >= *lo && start < *hi) {
|
||||
continue;
|
||||
}
|
||||
let bytes = crate::with_host(|h| h.kernel.read_file(&path)).unwrap_or_default();
|
||||
let lds = loads(&bytes);
|
||||
// The vma's file offset gives its vaddr in the file, hence the bias.
|
||||
let Some(bias) = lds.iter().find(|&&(o, _, n)| off >= o && off < o + n.max(1)).map(|&(o, v, _)| start.wrapping_sub(v + (off - o))) else {
|
||||
lock(&SEEDED).push((path, start, start + 1));
|
||||
continue;
|
||||
};
|
||||
let lo = lds.iter().map(|l| l.1).min().unwrap_or(0).wrapping_add(bias) & !0xfff;
|
||||
let hi = lds.iter().map(|l| l.1 + l.2).max().unwrap_or(0).wrapping_add(bias);
|
||||
let n = match gex_ld::elf::load(&bytes, bias) {
|
||||
Ok(mut image) => {
|
||||
gex_ld::elf::apply_own_relative(&mut image);
|
||||
jit.add_image(&image, Some(&bytes))
|
||||
}
|
||||
Err(_) => 0,
|
||||
};
|
||||
if BY_OBJECT_ON.load(Ordering::Relaxed) {
|
||||
let name = path.rsplit('/').next().unwrap_or(&path);
|
||||
crate::warn_line(&format!("[fjit] starts of {name} at {bias:#x}: {n}\n"));
|
||||
}
|
||||
lock(&SEEDED).push((path, lo, hi.max(lo + 1)));
|
||||
}
|
||||
}
|
||||
|
||||
/// Translate the function at `addr` and publish it (`gex.lookup`'s miss):
|
||||
/// its index in the function table, or `None` (the fallback runs it).
|
||||
pub(crate) fn translate(addr: u64) -> Option<u32> {
|
||||
if !on() || is_tier1_only(addr) {
|
||||
let r = translate_root(addr);
|
||||
if addr != 0 && addr == SCAN_CONST.load(Ordering::Relaxed) {
|
||||
crate::warn_line(&format!("[fjitscan] translation asked at {addr:#x}: {r:?}\n"));
|
||||
}
|
||||
r
|
||||
}
|
||||
|
||||
fn translate_root(addr: u64) -> Option<u32> {
|
||||
if !on() || is_tier1_only(addr) || !lib_wanted(addr) {
|
||||
return None;
|
||||
}
|
||||
let only = ONLY.load(Ordering::Relaxed);
|
||||
@@ -144,6 +406,7 @@ pub(crate) fn translate(addr: u64) -> Option<u32> {
|
||||
return None;
|
||||
}
|
||||
let jit = JIT.get()?;
|
||||
seed_mapped(jit);
|
||||
let (exec, ro, wx): (Vec<(u64, u64)>, Vec<(u64, u64)>, Vec<(u64, u64)>) = crate::with_host(|h| {
|
||||
use gex_kernel::mem::prot;
|
||||
let v = h.kernel.mem.vmas();
|
||||
@@ -156,8 +419,39 @@ pub(crate) fn translate(addr: u64) -> Option<u32> {
|
||||
if !exec.iter().any(|&(s, e)| addr >= s && addr < e) {
|
||||
return None;
|
||||
}
|
||||
if mid_function(addr) {
|
||||
return None;
|
||||
}
|
||||
// An address an ELF provider holds is not code to translate (cgen::elfprov).
|
||||
let executable = |a: u64| exec.iter().any(|&(s, e)| a >= s && a < e) && !crate::cgen::elfprov::provided(a) && !crate::cgen::libprov::replaced(a);
|
||||
// With `?fjitlibs=` rules, callees outside them are not brought in either.
|
||||
let rules = !lock(&LIBS).is_empty();
|
||||
// A callee a unit or the JIT already serves is not brought into the
|
||||
// batch: the call reaches it through the table. Without this, a miss in
|
||||
// UnityPlayer (a function its unit lacks) re-translated up to 63 of the
|
||||
// unit's own functions with it -- 3,667 functions and 36 MiB of wasm by
|
||||
// HK's menu (ht3), JIT code shadowing clang's -- and libasound's served
|
||||
// callees went into every batch (28 MiB of wasm by the menu, hp12).
|
||||
// `skip` bounds the batch only. Asked of instruction fetch (as part of
|
||||
// `executable`), an entry inside the function being recovered -- a
|
||||
// resume point, or a root the JIT took inside a function and, since
|
||||
// misses no longer make starts, does not stop recovery at -- read as
|
||||
// non-code and decoded as illegal: `cmovle` in libxcb's
|
||||
// `_xcb_in_read_block` (hu15), and SIGILL at HK's start-up without the
|
||||
// unit (hb52).
|
||||
let served_memo = std::cell::RefCell::new(std::collections::HashMap::<u64, bool>::new());
|
||||
let served = |a: u64| *served_memo.borrow_mut().entry(a).or_insert_with(|| crate::cgen::unit_serves(a) || crate::cgen::jit_serves(a));
|
||||
// Asked of every byte fetched: the providers' addresses and replaced
|
||||
// ranges taken once, and the range checks kept by 4 KiB page (exec,
|
||||
// replaced and object ranges are pages; `?fjitlibs=` ranges are not).
|
||||
let provided = crate::cgen::elfprov::provided_set();
|
||||
let replaced = crate::cgen::libprov::replaced_ranges();
|
||||
let page_memo = std::cell::RefCell::new(std::collections::HashMap::<u64, bool>::new());
|
||||
let ranged = rules && lock(&LIBS).iter().any(|r| r.range.is_some());
|
||||
let executable = |a: u64| {
|
||||
let page_ok = |a: u64| exec.iter().any(|&(s, e)| a >= s && a < e) && !replaced.iter().any(|&(lo, hi)| a >= lo && a < hi) && (!rules || lib_wanted(a));
|
||||
let ok = if ranged { page_ok(a) } else { *page_memo.borrow_mut().entry(a >> 12).or_insert_with(|| page_ok(a)) };
|
||||
ok && !provided.contains(&a)
|
||||
};
|
||||
let read_only = |a: u64, n: u64| ro.iter().any(|&(s, e)| a >= s && a.saturating_add(n) <= e);
|
||||
// Writable-and-executable code (a JIT's) stays tier-1, as natively.
|
||||
let volatile = |a: u64| wx.iter().any(|&(s, e)| a >= s && a < e);
|
||||
@@ -173,7 +467,7 @@ pub(crate) fn translate(addr: u64) -> Option<u32> {
|
||||
};
|
||||
let class32 = gex_abi::X86 && gex_xlate::arch::compat();
|
||||
trace(&format!("translate {addr:#x}"));
|
||||
let Some(wasm) = jit.translate(crate::guest_bytes(), addr, &executable, &read_only, &volatile, &pg, class32) else {
|
||||
let Some(wasm) = jit.translate_skipping(0, crate::guest_bytes(), addr, &executable, &served, &read_only, &volatile, &pg, class32) else {
|
||||
DECLINED.fetch_add(1, Ordering::Relaxed);
|
||||
trace(&format!("declined {addr:#x}"));
|
||||
return None;
|
||||
@@ -183,6 +477,9 @@ pub(crate) fn translate(addr: u64) -> Option<u32> {
|
||||
DECLINED.fetch_add(1, Ordering::Relaxed);
|
||||
return None;
|
||||
}
|
||||
note_object(addr, funcs.len(), wasm.len());
|
||||
dump_module(addr, &funcs, &wasm);
|
||||
scan_const(addr, &funcs, &wasm);
|
||||
let wasm = named(wasm);
|
||||
// Held until the module is in the list: a Worker replaying the
|
||||
// function table's log (`gex_fjit_ptr`, `gex_fjit_regs`) waits for it
|
||||
@@ -210,7 +507,10 @@ pub(crate) fn translate(addr: u64) -> Option<u32> {
|
||||
drop(modules);
|
||||
TRANSLATED.fetch_add(1, Ordering::Relaxed);
|
||||
let idx = crate::cgen::index_of(addr);
|
||||
trace(&format!("module {n}: {addr:#x} {} funcs, install {install}, index {idx:?}", funcs.len()));
|
||||
if TRACE.load(Ordering::Relaxed) {
|
||||
let names: Vec<String> = crate::with_host(|h| funcs.iter().map(|&a| format!("{a:#x} {}", crate::object_off_at(h, a))).collect());
|
||||
trace(&format!("module {n}: {addr:#x} {} funcs, install {install}, index {idx:?}: {}", funcs.len(), names.join(", ")));
|
||||
}
|
||||
idx
|
||||
}
|
||||
|
||||
@@ -233,7 +533,7 @@ fn named(mut wasm: Vec<u8>) -> Vec<u8> {
|
||||
/// make one)? Then its bridge reaches it once this Worker has installed
|
||||
/// what the table's log holds (the caller traps or installs in place).
|
||||
pub(crate) fn serve(pc: u64) -> bool {
|
||||
if !on() || is_tier1_only(pc) {
|
||||
if !on() || is_tier1_only(pc) || !lib_wanted(pc) {
|
||||
return false;
|
||||
}
|
||||
if crate::cgen::index_of(pc).is_some() {
|
||||
@@ -242,6 +542,124 @@ pub(crate) fn serve(pc: u64) -> bool {
|
||||
translate(pc).is_some()
|
||||
}
|
||||
|
||||
/// `+name,-name` (`?fjitlibs=`): the objects the JIT is limited to (any `+`)
|
||||
/// and kept from (`-`), by file name. `name@lo-hi` (hex) narrows a rule to
|
||||
/// the functions whose entry lies at those offsets into their mapping (the
|
||||
/// `name +0x..` a trap prints), for bisecting within an object. Checked at
|
||||
/// every translation, the table lookups' (`gex.lookup`) as well as tier-1's
|
||||
/// misses: JIT code calling through a pointer must not pull in objects the
|
||||
/// rules keep out.
|
||||
struct Rule {
|
||||
include: bool,
|
||||
name: String,
|
||||
range: Option<(u64, u64)>,
|
||||
}
|
||||
|
||||
static LIBS: Mutex<Vec<Rule>> = Mutex::new(Vec::new());
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gex_fjit_libs(ptr: u32, len: u32) {
|
||||
let spec = String::from_utf8_lossy(&crate::guest_bytes()[ptr as usize..ptr as usize + len as usize]).into_owned();
|
||||
let mut v = lock(&LIBS);
|
||||
v.clear();
|
||||
for w in spec.split(',').map(str::trim).filter(|w| !w.is_empty()) {
|
||||
// `watch=0xADDR` (a debugging aid riding on the same query): say when
|
||||
// the u64 there changes, at syscalls and safepoints (`watch_check`).
|
||||
if let Some(a) = w.strip_prefix("watch=") {
|
||||
if let Ok(a) = u64::from_str_radix(a.trim_start_matches("0x"), 16) {
|
||||
WATCH.store(a, Ordering::Relaxed);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// `callout=0|2`, `inplace=0`, `callouttrap=1`: the A/B switches of
|
||||
// the call-out and the in-place redispatch (bisection aids);
|
||||
// `provcheck=1`: check each provider guest call ends at its return;
|
||||
// `byobj=1`: count translations by object (`note_object`).
|
||||
if let Some(a) = w.strip_prefix("scanconst=") {
|
||||
if let Ok(a) = u64::from_str_radix(a.trim_start_matches("0x"), 16) {
|
||||
SCAN_CONST.store(a, Ordering::Relaxed);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if let Some(a) = w.strip_prefix("dumpmod=") {
|
||||
// `dumpmod=OBJ@OFF`: by the object's name and the hex offset from
|
||||
// its text mapping (as `object_off_at` prints), for a layout
|
||||
// whose addresses are not known ahead.
|
||||
if let Some((name, off)) = a.split_once('@') {
|
||||
if let Ok(off) = u64::from_str_radix(off.trim_start_matches("0x"), 16) {
|
||||
*lock(&DUMP_OBJ) = Some((name.to_string(), off));
|
||||
}
|
||||
} else if let Ok(a) = u64::from_str_radix(a.trim_start_matches("0x"), 16) {
|
||||
DUMP_MOD.store(a, Ordering::Relaxed);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if let Some((k, v)) = w.split_once('=') {
|
||||
let v: i32 = v.parse().unwrap_or(1);
|
||||
match k {
|
||||
"callout" => crate::cgen::gex_set_callout(v),
|
||||
"inplace" => crate::gex_set_in_place(v),
|
||||
"callouttrap" => crate::cgen::TRAP_CALLOUTS.store(v != 0, Ordering::Relaxed),
|
||||
"provcheck" => crate::cgen::libprov::PROV_CHECK.store(v != 0, Ordering::Relaxed),
|
||||
"byobj" => BY_OBJECT_ON.store(v != 0, Ordering::Relaxed),
|
||||
_ => crate::warn_line(&format!("gex: ?fjitlibs: no switch {k}\n")),
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let (include, w) = match w.strip_prefix('-') {
|
||||
Some(n) => (false, n),
|
||||
None => (true, w.trim_start_matches('+')),
|
||||
};
|
||||
let hex = |x: &str| u64::from_str_radix(x.trim_start_matches("0x"), 16).ok();
|
||||
let (name, range) = match w.split_once('@') {
|
||||
Some((n, r)) => (n, r.split_once('-').and_then(|(lo, hi)| Some((hex(lo)?, hex(hi)?)))),
|
||||
None => (w, None),
|
||||
};
|
||||
v.push(Rule { include, name: name.to_string(), range });
|
||||
}
|
||||
}
|
||||
|
||||
static WATCH: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||
static WATCH_LAST: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(u64::MAX);
|
||||
static WATCH_SAID: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
|
||||
|
||||
/// `?fjitlibs=watch=0xADDR`: the u64 at ADDR changed since the last look
|
||||
/// (any thread's): say where this thread is (`at`: syscall or safepoint).
|
||||
pub(crate) fn watch_check(pc: u64, at: &str) {
|
||||
let a = WATCH.load(Ordering::Relaxed);
|
||||
if a == 0 {
|
||||
return;
|
||||
}
|
||||
// Not before the memory reaches it (the slice spans the maximum).
|
||||
if a + 8 > crate::warch::memory_size(0) as u64 * 65536 {
|
||||
return;
|
||||
}
|
||||
let mem = crate::guest_bytes();
|
||||
let Some(b) = mem.get(a as usize..a as usize + 8) else { return };
|
||||
let v = u64::from_le_bytes(b.try_into().unwrap());
|
||||
let last = WATCH_LAST.swap(v, Ordering::Relaxed);
|
||||
if last != v && WATCH_SAID.fetch_add(1, Ordering::Relaxed) < 64 {
|
||||
let (thread, whose, vwhose) = crate::with_host(|h| (crate::cur(), crate::object_off_at(h, pc), crate::object_off_at(h, v)));
|
||||
crate::warn_line(&format!(
|
||||
"[watch] {a:#x}: {last:#x} -> {v:#x} ({vwhose}), seen by thread {thread} at {at} pc {pc:#x} ({whose}), {} JIT modules\n",
|
||||
lock(&MODULES).len()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn lib_wanted(pc: u64) -> bool {
|
||||
let rules = lock(&LIBS);
|
||||
if rules.is_empty() {
|
||||
return true;
|
||||
}
|
||||
let (name, off) = crate::object_file_off(pc);
|
||||
let hit = |r: &Rule| r.name == name && r.range.is_none_or(|(lo, hi)| off >= lo && off < hi);
|
||||
if rules.iter().any(|r| !r.include && hit(r)) {
|
||||
return false;
|
||||
}
|
||||
!rules.iter().any(|r| r.include) || rules.iter().any(|r| r.include && hit(r))
|
||||
}
|
||||
|
||||
/// JIT module `n`'s bytes.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gex_fjit_ptr(n: u32) -> u32 {
|
||||
|
||||
@@ -5959,6 +5959,7 @@ pub extern "C" fn gex_syscall(ctx: i32, pc: i64) {
|
||||
IN_PROVIDER.with(|c| c.set(provider));
|
||||
((pc as u64) & !PROVIDER_SYSCALL) as i64
|
||||
};
|
||||
fjit::watch_check(pc as u64, "syscall");
|
||||
// A guest thread in a syscall counts as stopped for a provider's
|
||||
// collection (cgen/libprov.rs); it parks on its way back if one runs.
|
||||
let stop_tracked = !IN_PROVIDER.with(|c| c.get()) && cgen::libprov::any_registered();
|
||||
@@ -7279,9 +7280,23 @@ fn resume() -> Option<u64> {
|
||||
RESUME.with(|c| c.get())
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
/// This Worker's last resumes set (the shim's line that set it, pc), for
|
||||
/// the wild-branch report: who sent the guest where it went.
|
||||
static RESUME_RING: std::cell::RefCell<Vec<(u32, u64)>> = const { std::cell::RefCell::new(Vec::new()) };
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn set_resume(v: u64) {
|
||||
RESUME.with(|c| c.set(Some(v)));
|
||||
let line = std::panic::Location::caller().line();
|
||||
RESUME_RING.with(|r| {
|
||||
let mut r = r.borrow_mut();
|
||||
if r.len() == 16 {
|
||||
r.remove(0);
|
||||
}
|
||||
r.push((line, v));
|
||||
});
|
||||
// GEX_NT_RESUME_LOG: each (call site, pc) a resume was set from, once:
|
||||
// where the guest leaves wasm and comes back (an AOT-only run needs a
|
||||
// unit or region entry at every such pc).
|
||||
@@ -10590,6 +10605,7 @@ pub extern "C" fn gex_cache_op(_ctx: i32, _pc: i64, op: i32, addr: i64) {
|
||||
/// preemption is the engine's job now; see the end of this function.)
|
||||
#[no_mangle]
|
||||
pub extern "C" fn gex_safepoint(ctx: i32, pc: i64) {
|
||||
fjit::watch_check(pc as u64, "safepoint");
|
||||
// A Windows process's clocks in KUSER_SHARED_DATA, which a guest polls
|
||||
// without a syscall: a loop that spins on them passes here.
|
||||
nt::tick();
|
||||
@@ -11086,6 +11102,16 @@ pub extern "C" fn gex_dispatch_miss(_ctx: i32, pc: i64) -> i32 {
|
||||
let stack: Vec<String> = (0..8)
|
||||
.map(|i| format!("{:#x}", gm.read_u64(sp + i * 8).unwrap_or(0)))
|
||||
.collect();
|
||||
// The stack around sp, named, and the registers (a bad return's frame).
|
||||
let around: Vec<String> = (0..24u64)
|
||||
.map(|i| {
|
||||
let a = sp.wrapping_sub(0x60).wrapping_add(i * 8);
|
||||
let v = gm.read_u64(a).unwrap_or(0);
|
||||
format!("{a:#x}={v:#x}{}", Some(object_off_at(h, v)).filter(|n| !n.is_empty()).map(|n| format!(" ({n})")).unwrap_or_default())
|
||||
})
|
||||
.collect();
|
||||
let regs: Vec<String> = (0..16u8).map(|r| format!("r{r}={:#x}", gm.read_u64(c + ctx::x(r) as u64).unwrap_or(0))).collect();
|
||||
warn_line(&format!("[gex] wild branch: stack {}; regs {}\n", around.join(", "), regs.join(" ")));
|
||||
warn_line(&format!(
|
||||
"[gex] pid {} thread {} branched to {pc:#x}, which is not executable guest memory (SIGSEGV); last indirect target {last:#x} ({}) from block {src:#x} ({}), sp={sp:#x} stack=[{}], last syscall {}\n",
|
||||
h.kernel.pid,
|
||||
@@ -11105,6 +11131,15 @@ pub extern "C" fn gex_dispatch_miss(_ctx: i32, pc: i64) -> i32 {
|
||||
cgen::provider_calls_recent()
|
||||
));
|
||||
warn_line(&format!("[gex] last lookups (address, index): {:x?}\n", cgen::lookups_recent()));
|
||||
warn_line(&format!(
|
||||
"[gex] last call-outs (target, return, sp wanted, depth, result, sp after): {:x?}; provider guest call open {}\n",
|
||||
cgen::callouts_recent(),
|
||||
cgen::libprov::guest_call_open()
|
||||
));
|
||||
warn_line(&format!(
|
||||
"[gex] last resumes set (lib.rs line, pc): {:x?}\n",
|
||||
RESUME_RING.with(|r| r.borrow().iter().map(|&(l, v)| (l, v)).collect::<Vec<_>>())
|
||||
));
|
||||
}
|
||||
set_exit(h, 128 + 11);
|
||||
});
|
||||
@@ -11893,6 +11928,7 @@ fn retire_unmapped_code() {
|
||||
let retired = std::mem::take(&mut h.kernel.retired_code);
|
||||
if !retired.is_empty() {
|
||||
crate::cgen::on_retire(&retired);
|
||||
crate::fjit::on_retire(&retired);
|
||||
retire_code(h, &retired);
|
||||
}
|
||||
});
|
||||
@@ -12511,12 +12547,30 @@ fn note_blocks(
|
||||
e.2 = e.2.max(a);
|
||||
}
|
||||
|
||||
/// The object file at `a` (by file name) and `a`'s offset into its mapping.
|
||||
pub(crate) fn object_file_off(a: u64) -> (String, u64) {
|
||||
with_host(|h| {
|
||||
h.kernel
|
||||
.mem
|
||||
.vmas()
|
||||
.iter()
|
||||
.find(|v| a >= v.start && a < v.end)
|
||||
.map(|v| (v.code.as_ref().map(|(p, _)| p.rsplit('/').next().unwrap_or(p).to_string()).unwrap_or_default(), a - v.start))
|
||||
.unwrap_or_default()
|
||||
})
|
||||
}
|
||||
|
||||
/// [`object_off_at`], taking the host itself.
|
||||
pub(crate) fn object_off_str(a: u64) -> String {
|
||||
with_host(|h| object_off_at(h, a))
|
||||
}
|
||||
|
||||
/// The short name of whatever object holds this address, for a counter. Takes
|
||||
/// the host it already has, since the caller is inside `with_host` and a second
|
||||
/// `RefCell` borrow would panic.
|
||||
/// Like `object_name_at` but with the `+offset` into the object, so a return
|
||||
/// address on a crash stack can be run through objdump to name the function.
|
||||
fn object_off_at(h: &Host, a: u64) -> String {
|
||||
pub(crate) fn object_off_at(h: &Host, a: u64) -> String {
|
||||
h.kernel
|
||||
.mem
|
||||
.vmas()
|
||||
|
||||
@@ -742,11 +742,47 @@ fn bisect_allows(addr: u64) -> bool {
|
||||
only.as_ref().is_none_or(inr) && !skip.as_ref().is_some_and(inr)
|
||||
}
|
||||
|
||||
/// `GEX_FJIT_LIBS=+name,-name` (file names, as the page's `?fjitlibs=`):
|
||||
/// the objects the function JIT is limited to (any `+`) and kept from (`-`).
|
||||
fn libs_allow(caller: &mut Caller<'_, HostState>, addr: u64) -> bool {
|
||||
static RULES: std::sync::OnceLock<Vec<(bool, String)>> = std::sync::OnceLock::new();
|
||||
let rules = RULES.get_or_init(|| {
|
||||
std::env::var("GEX_FJIT_LIBS")
|
||||
.unwrap_or_default()
|
||||
.split(',')
|
||||
.map(str::trim)
|
||||
.filter(|w| !w.is_empty())
|
||||
.map(|w| match w.strip_prefix('-') {
|
||||
Some(n) => (false, n.to_string()),
|
||||
None => (true, w.trim_start_matches('+').to_string()),
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
if rules.is_empty() {
|
||||
return true;
|
||||
}
|
||||
let name = caller.data_mut().with_kernel(|k| {
|
||||
k.mem
|
||||
.vmas()
|
||||
.iter()
|
||||
.find(|v| addr >= v.start && addr < v.end)
|
||||
.and_then(|v| v.code.as_ref().map(|(p, _)| p.rsplit('/').next().unwrap_or(p).to_string()))
|
||||
.unwrap_or_default()
|
||||
});
|
||||
if rules.iter().any(|(inc, n)| !inc && *n == name) {
|
||||
return false;
|
||||
}
|
||||
!rules.iter().any(|(inc, _)| *inc) || rules.iter().any(|(inc, n)| *inc && *n == name)
|
||||
}
|
||||
|
||||
fn job(caller: &mut Caller<'_, HostState>, sh: &Arc<Mutex<JitShared>>, memory: &gex_mem::GuestMemory, addr: u64) -> Option<Job> {
|
||||
gex_kernel::ftab::translator()?;
|
||||
if !bisect_allows(addr) {
|
||||
return None;
|
||||
}
|
||||
if !libs_allow(caller, addr) {
|
||||
return None;
|
||||
}
|
||||
let (exec, ro, wx): (Vec<(u64, u64)>, Vec<(u64, u64)>, Vec<(u64, u64)>) = caller.data_mut().with_kernel(|k| {
|
||||
let v = k.mem.vmas();
|
||||
(
|
||||
@@ -968,6 +1004,9 @@ pub(crate) fn install_bridges(mut cx: impl AsContextMut<Data = HostState>, slots
|
||||
/// sending callers there first.
|
||||
pub(crate) fn on_retire(sh: &mut JitShared, memory: &mut [u8], ranges: &[(u64, u64)]) {
|
||||
sh.ftab.epoch += 1;
|
||||
if let Some(t) = gex_kernel::ftab::translator() {
|
||||
t.retire(sh.ftab.process.0, ranges);
|
||||
}
|
||||
sh.ftab.queued.retain(|&a| !ranges.iter().any(|&(s, e)| a >= s && a < e));
|
||||
for &(start, end) in ranges {
|
||||
let hit: Vec<(u64, gex_kernel::ftab::Func)> = sh.ftab.map.range(start, end).collect();
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// The function JIT must forget what it learned of code that is unmapped: a
|
||||
// dlclose and a later dlopen put another library at the same addresses. Code
|
||||
// A has a function at +0x10; code B, mapped over it, has a min() whose cmp is
|
||||
// before +0x10 and whose cmovle is at it. Kept, A's start split B's function
|
||||
// there as at a tail call, with the flags dead (HK's libdbus and libxcb in the
|
||||
// page, docs/unity-dotnet.md). Output must match native.
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
|
||||
typedef int (*fn)(int, int);
|
||||
|
||||
static void *at;
|
||||
|
||||
// The first call picks the address; the second maps over the same one.
|
||||
static void *place(const unsigned char *code, size_t n) {
|
||||
void *p = mmap(at, 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | (at ? MAP_FIXED : 0), -1, 0);
|
||||
if (p == MAP_FAILED || (at && p != at)) return 0;
|
||||
at = p;
|
||||
memset(p, 0xcc, 4096);
|
||||
memcpy(p, code, n);
|
||||
if (mprotect(p, 4096, PROT_READ | PROT_EXEC)) return 0;
|
||||
return p;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
// A: +0x00 ret; +0x10 lea (%rdi,%rsi),%eax; ret
|
||||
unsigned char a[0x14] = {0xc3};
|
||||
memset(a + 1, 0x90, 0xf);
|
||||
a[0x10] = 0x8d; a[0x11] = 0x04; a[0x12] = 0x37; a[0x13] = 0xc3;
|
||||
char *p = place(a, sizeof a);
|
||||
if (!p) { puts("map A failed"); return 1; }
|
||||
volatile fn f = (fn)(p + 0x10);
|
||||
long s = 0;
|
||||
for (int i = 0; i < 200000; i++) s += f(i, 1);
|
||||
printf("A %ld\n", s);
|
||||
if (munmap(p, 4096)) { puts("munmap failed"); return 1; }
|
||||
// B: mov %esi,%eax; cmp %esi,%edi; nop x12; +0x10 cmovle %edi,%eax; ret
|
||||
unsigned char b[0x14] = {0x89, 0xf0, 0x39, 0xf7};
|
||||
memset(b + 4, 0x90, 0xc);
|
||||
b[0x10] = 0x0f; b[0x11] = 0x4e; b[0x12] = 0xc7; b[0x13] = 0xc3;
|
||||
p = place(b, sizeof b);
|
||||
if (!p) { puts("map B failed"); return 1; }
|
||||
volatile fn g = (fn)p;
|
||||
long bad = 0, t = 0;
|
||||
for (int i = 0; i < 200000; i++) {
|
||||
int x = (i * 7919) % 1000 - 500, y = (i * 104729) % 1000 - 500;
|
||||
int m = g(x, y);
|
||||
t += m;
|
||||
if (m != (x < y ? x : y)) bad++;
|
||||
}
|
||||
printf("B %ld bad %ld\n", t, bad);
|
||||
return 0;
|
||||
}
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
#!/bin/sh
|
||||
# Code unmapped and replaced at the same address (remap.c): output under gexc
|
||||
# --fjit must match native.
|
||||
#
|
||||
# tests/fjit-remap/run.sh [gexc] (needs zig for the musl target)
|
||||
set -u
|
||||
HERE=$(cd "$(dirname "$0")" && pwd)
|
||||
GEXC=${1:-target/x86/release/gexc}
|
||||
W=${WORK:-$(mktemp -d)}
|
||||
mkdir -p "$W"
|
||||
zig cc -target x86_64-linux-musl -O1 -static -o "$W/remap" "$HERE/remap.c" || exit 1
|
||||
native=$("$W/remap")
|
||||
fail=0
|
||||
for flags in "" "--fjit"; do
|
||||
gex=$("$GEXC" run $flags "$W/remap" 2>&1)
|
||||
if [ "$native" = "$gex" ]; then
|
||||
echo "ok remap ${flags:-t1}"
|
||||
else
|
||||
echo "FAIL remap ${flags:-t1}"; printf 'native:\n%s\ngex:\n%s\n' "$native" "$gex"; fail=1
|
||||
fi
|
||||
done
|
||||
exit $fail
|
||||
@@ -252,6 +252,8 @@ async function main() {
|
||||
writeFileSync(join(shotDir, `c${String(shots).padStart(3, '0')}.png`), encodePng(r.w, r.h, Buffer.from(r.b64, 'base64')));
|
||||
}
|
||||
}
|
||||
// The guest's output so far (a game's log with `-logFile -`), kept whole.
|
||||
if (shotDir && s?.out) writeFileSync(join(shotDir, 'out.txt'), s.out);
|
||||
const rate = window.length > 1 ? (n - window[0][1]) / ((Date.now() - window[0][0]) / 1000) : 0;
|
||||
log(`${((Date.now() - t0) / 1000).toFixed(0)} s: presents ${n}, ${rate.toFixed(1)}/s over the last minute; ${(s?.out ?? '').slice(-160).replace(/\n/g, ' | ')}`);
|
||||
// The compositor's own account: its windows, its notes (GL attachment
|
||||
|
||||
@@ -235,6 +235,7 @@ export async function openPage(query, { width = 1280, height = 900, shotDir = nu
|
||||
const { sessionId: page } = await browser.send('Target.attachToTarget', { targetId, flatten: true });
|
||||
const consoleLines = [];
|
||||
let printed = 0;
|
||||
let keepRe = null;
|
||||
// Every Worker's console too: each guest thread is one.
|
||||
browser.on('Runtime.consoleAPICalled', (p, sid) => {
|
||||
const text = p.args.map((a) => a.value ?? a.description ?? '').join(' ');
|
||||
@@ -246,7 +247,10 @@ export async function openPage(query, { width = 1280, height = 900, shotDir = nu
|
||||
const wanted = p.type === 'error'
|
||||
? !/^\[mesa-gl\] gex-gl: gl[A-Z]/.test(text)
|
||||
: p.type === 'warning' && /gex-nt|gex-wgpu|wine:|\[gex\]|panic|trap/i.test(text);
|
||||
if ((wanted || process.env.GEX_NT_BROWSER_VERBOSE) && printed++ < 20000) {
|
||||
// GEX_NT_BROWSER_KEEP=REGEX: also any line it matches (Mesa's compile
|
||||
// and link notes, say), without the verbose flood.
|
||||
keepRe ??= process.env.GEX_NT_BROWSER_KEEP ? new RegExp(process.env.GEX_NT_BROWSER_KEEP) : false;
|
||||
if ((wanted || process.env.GEX_NT_BROWSER_VERBOSE || (keepRe && keepRe.test(text))) && printed++ < 20000) {
|
||||
log(`console[${sid?.slice(0, 6)} @${((Date.now() - T0) / 1000).toFixed(1)}s] ${text.slice(0, 4000)}`);
|
||||
}
|
||||
});
|
||||
|
||||
Vendored
+1
-1
Submodule third_party/mesa updated: 5f5572e62f...6020224605.
+20
-1
@@ -20,7 +20,26 @@ else
|
||||
fi
|
||||
OUT=${1:-$DEFAULT_OUT}
|
||||
BUILD=$OUT/build
|
||||
EMDAWN=${GEX_EMDAWN_DIR:-$($ROOT/web/fetch-emdawnwebgpu.sh)}
|
||||
# emdawnwebgpu: a Mesa checkout that pins its own (the wink branch:
|
||||
# wink/lib/emdawn.sh, a release matched to its Tint and patched by
|
||||
# wink/tools/patch-emdawnwebgpu.py) supplies it; else gex's own fetch.
|
||||
if [ -z "${GEX_EMDAWN_DIR:-}" ] && [ -f "$MESA/wink/lib/emdawn.sh" ]; then
|
||||
WINK_CACHE=${GEX_MESA_WINK_CACHE:-${TMPDIR:-/tmp}/gex-mesa-wink-cache}
|
||||
EMSDK_ROOT=$(CDPATH='' cd -- "$(dirname "$(command -v emcc)")/../.." && pwd)
|
||||
emdawn_log=$(WINK_ALLOW_ANY_EMSDK=1 bash "$MESA/wink/build.sh" --variant=mt --until=emdawn \
|
||||
--cache="$WINK_CACHE" --build-dir="$WINK_CACHE/build" --emsdk="$EMSDK_ROOT" 2>&1) || {
|
||||
echo "$emdawn_log" | tail -20 >&2
|
||||
exit 1
|
||||
}
|
||||
EMDAWN=$(echo "$emdawn_log" | sed -n 's/^\[emdawn\] \(ready\|cached\) at //p' | tail -1)
|
||||
[ -f "$EMDAWN/webgpu/include/webgpu/webgpu.h" ] || {
|
||||
echo "$emdawn_log" | tail -20 >&2
|
||||
echo "no emdawnwebgpu from wink/build.sh" >&2
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
EMDAWN=${GEX_EMDAWN_DIR:-$($ROOT/web/fetch-emdawnwebgpu.sh)}
|
||||
fi
|
||||
TINT_OUT=${GEX_MESA_TINT:-${TMPDIR:-/tmp}/gex-mesa-wasm/mesa-tint-pthread}
|
||||
TINT_TMP=${GEX_MESA_TINT_TMP:-${TMPDIR:-/tmp}/gex-mesa-tint-build}
|
||||
|
||||
|
||||
@@ -1440,6 +1440,7 @@ self.onmessage = ({ data: m }) => {
|
||||
canvas = m.canvas;
|
||||
presentOverride = m.present ?? null;
|
||||
// The compositor draws with WebGPU on the device Mesa will use.
|
||||
if (m.glLimits != null) globalThis.GEX_GL_LIMITS = m.glLimits;
|
||||
requestGexDevice().then((gpu) => {
|
||||
comp = new Compositor(m.canvas, m.channel, m.stats ? new Int32Array(m.stats) : null, gpu);
|
||||
comp.setDirect(m.direct);
|
||||
|
||||
@@ -137,8 +137,13 @@ export class Compositor {
|
||||
// is the canvas, fitted to the viewport; compositing only when needed.
|
||||
const direct = pq?.get('direct') === '1';
|
||||
this.#pointerLock = pq?.get('pointerlock') === '1';
|
||||
// `?gllimits=40|41|43`: the device asks for WebGPU's default limits,
|
||||
// GL 4.1's texture limits (the default), or 4.3's compute ones too,
|
||||
// which decide the GL version Mesa's context earns (gpu-device.js). At
|
||||
// 4.6 Muck's 3D scene drew black, so 43 is only per run.
|
||||
const glLimits = pq?.has('gllimits') ? Number(pq.get('gllimits')) || 0 : null;
|
||||
worker.postMessage(
|
||||
{ kind: 'init', canvas: offscreen, channel, stats: this.#stats, present, direct },
|
||||
{ kind: 'init', canvas: offscreen, channel, stats: this.#stats, present, direct, glLimits },
|
||||
[offscreen],
|
||||
);
|
||||
await ready;
|
||||
|
||||
+21
-4
@@ -44,11 +44,28 @@ export function requestGexDevice() {
|
||||
maxStorageBuffersInVertexStage: 16,
|
||||
maxStorageBuffersInFragmentStage: 16,
|
||||
maxColorAttachmentBytesPerSample: 64,
|
||||
// Not yet: asking for GL 4.1's texture limits (16384 texels, 2048
|
||||
// layers) and GL 4.3's compute ones makes the context GL 4.6, and then
|
||||
// Unity's 4.x shader variants draw Muck's 3D scene black (U, mp26).
|
||||
// Back once that is fixed; see docs/gl-path.md.
|
||||
// GL 4.3's compute limits (with them the context is GL 4.6) drew
|
||||
// Muck's 3D scene black (U, mp26), so they are only per run; GL 4.1's
|
||||
// texture limits are the default, below.
|
||||
};
|
||||
// `?gllimits=40|41|43` (GEX_GL_LIMITS, set by the compositor worker;
|
||||
// 41 when unset): GL 4.1 wants 16384 texels a side and 2048 array
|
||||
// layers, GL 4.3's compute shaders 1024 invocations a workgroup and
|
||||
// 32 KiB of shared memory; WebGPU's defaults (8192, 256; 256, 16 KiB)
|
||||
// keep the context at GL 4.0. Unity drops shaders whose target the
|
||||
// context is below: at 4.0 Muck's grass geometry shader ("Shader
|
||||
// Unsupported ... All passes removed") left the grass as dots; at 4.2
|
||||
// with mesa-wink 60202246 it draws blades (U, mpg3).
|
||||
const glLimitsSet = globalThis.GEX_GL_LIMITS;
|
||||
const glLimits = glLimitsSet === undefined ? 41 : Number(glLimitsSet) || 0;
|
||||
if (glLimits >= 41) {
|
||||
Object.assign(want, { maxTextureDimension1D: 16384, maxTextureDimension2D: 16384,
|
||||
maxTextureDimension3D: 2048, maxTextureArrayLayers: 2048 });
|
||||
}
|
||||
if (glLimits >= 43) {
|
||||
Object.assign(want, { maxComputeInvocationsPerWorkgroup: 1024, maxComputeWorkgroupSizeX: 1024,
|
||||
maxComputeWorkgroupSizeY: 1024, maxComputeWorkgroupSizeZ: 64, maxComputeWorkgroupStorageSize: 32768 });
|
||||
}
|
||||
const requiredLimits = {};
|
||||
for (const [limit, wanted] of Object.entries(want)) {
|
||||
const available = adapter.limits[limit];
|
||||
|
||||
+10
-1
@@ -419,6 +419,15 @@ export class HostX {
|
||||
}
|
||||
},
|
||||
|
||||
/** Room to write: a guest's socket in the kernel fills (256 KiB); the
|
||||
* compositor's queues never do. */
|
||||
writable(fd) {
|
||||
const ep = self.#fds.get(fd);
|
||||
if (!ep) return -1;
|
||||
if (ep.kind === 'kernel') return self.#k.k_sock_writable(ep.id) ? 1 : 0;
|
||||
return 1;
|
||||
},
|
||||
|
||||
send(fd, buf, len, fds, nfds) {
|
||||
const ep = self.#fds.get(fd);
|
||||
if (!ep) return -EBADF;
|
||||
@@ -515,7 +524,7 @@ export class HostX {
|
||||
const wrapped = {};
|
||||
let left = this.#trace;
|
||||
for (const [name, fn] of Object.entries(raw)) {
|
||||
if (name === 'tick' || name === 'wait' || name === 'readable') {
|
||||
if (name === 'tick' || name === 'wait' || name === 'readable' || name === 'writable') {
|
||||
wrapped[name] = fn;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -119,6 +119,12 @@ async function main() {
|
||||
if (q.get('wasmdlls')) globalThis.GEX_WASM_DLLS = q.get('wasmdlls');
|
||||
// `?elfunits=name,...`: shared objects that get function-tier units (with `?cgen=1`).
|
||||
if (q.get('elfunits')) globalThis.GEX_ELF_UNITS = q.get('elfunits');
|
||||
// `?fjitlibs=+name,-name`: the function JIT only for (+) or not for (-) those objects (a bisection aid).
|
||||
if (q.get('fjitlibs')) globalThis.GEX_FJIT_LIBS = q.get('fjitlibs');
|
||||
// `?pins=path@base+span,...` (hex): place those shared objects at fixed bases, as a
|
||||
// closure's pins do, so a unit built for one base (`?elfunits=`) is used whatever
|
||||
// else (the function JIT's heap, say) moved the arena first.
|
||||
if (q.get('pins')) globalThis.GEX_PINS = q.get('pins');
|
||||
if (q.get('dense') === '0') globalThis.GEX_DENSE = 0;
|
||||
if (q.get('dense') === '2') globalThis.GEX_DENSE = 2;
|
||||
// cgen=1|2 (prefer|require units), in the unguarded flavor of i386 units
|
||||
@@ -133,6 +139,11 @@ async function main() {
|
||||
if (q.get('aot')) globalThis.GEX_AOT = 1;
|
||||
if (q.get('densedebug')) globalThis.GEX_DENSE_DEBUG = 1;
|
||||
if (q.get('tracemisses')) globalThis.GEX_TRACE_MISSES = 1;
|
||||
// `?attachtrace=1`: time each thread Worker from the guest's clone to its
|
||||
// start (host.mjs, gex.mjs `#catchUp`), as main.js does for the demos.
|
||||
if (q.get('attachtrace')) globalThis.GEX_ATTACH_TRACE = 1;
|
||||
// `?strace=1`: narrate every syscall (`gex_set_trace`), as main.js does.
|
||||
if (q.get('strace')) globalThis.GEX_STRACE = 1;
|
||||
const [w, h] = (q.get('size') || '1024x768').split('x').map(Number);
|
||||
const canvas = document.getElementById('out');
|
||||
canvas.width = w;
|
||||
|
||||
+15
-1
@@ -691,6 +691,8 @@ export async function runProgram(
|
||||
wasmDlls: globalThis.GEX_WASM_DLLS || undefined,
|
||||
// `?elfunits=name,...`: shared objects that get function-tier units (cgen/libprov.rs).
|
||||
elfUnits: globalThis.GEX_ELF_UNITS || undefined,
|
||||
// `?fjitlibs=+name,-name` (fjit.rs `gex_fjit_libs`).
|
||||
fjitLibs: globalThis.GEX_FJIT_LIBS || undefined,
|
||||
traceMisses: !!globalThis.GEX_TRACE_MISSES,
|
||||
// `?sharetrace=1`: say how compiled modules are shared between a process's Workers.
|
||||
shareTrace: flag('sharetrace') ? 1 : undefined,
|
||||
@@ -815,7 +817,7 @@ export async function runProgram(
|
||||
// out an address inside a pinned object and the image lands somewhere
|
||||
// its own translated code does not expect.
|
||||
opts: {
|
||||
cwd, tty, uid, recompileObjects, heartbeat, link, dirs, modes, hard, symlinks, env, pins: closure?.pins,
|
||||
cwd, tty, uid, recompileObjects, heartbeat, link, dirs, modes, hard, symlinks, env, pins: pinsFrom(closure?.pins, globalThis.GEX_PINS),
|
||||
// `?strace=1`, and every child inherits it: see `#opts` in gex.mjs.
|
||||
strace: globalThis.GEX_STRACE ? 1 : undefined,
|
||||
// `?stracepid=N`: narrate that one process, which is affordable.
|
||||
@@ -857,3 +859,15 @@ export async function runProgram(
|
||||
|
||||
export { TerminalEnv };
|
||||
export { loadKernel as globalKernel };
|
||||
|
||||
/** A closure's pins plus the page's `?pins=path@base+span,...` (hex; the query wins). */
|
||||
function pinsFrom(closurePins, spec) {
|
||||
if (!spec) return closurePins;
|
||||
const out = { ...(closurePins ?? {}) };
|
||||
for (const w of spec.split(',').map((x) => x.trim()).filter(Boolean)) {
|
||||
const m = /^(.+)@(?:0x)?([0-9a-f]+)(?:\+(?:0x)?([0-9a-f]+))?$/i.exec(w);
|
||||
if (!m) { console.error(`[gex] ?pins: cannot read ${w}`); continue; }
|
||||
out[m[1]] = { base: Number.parseInt(m[2], 16), span: m[3] ? Number.parseInt(m[3], 16) : 0 };
|
||||
}
|
||||
return out;
|
||||
}
|
||||
+13
-6
@@ -36,6 +36,7 @@ EM_JS(int, gexjs_connect, (int fd, const char *path, int len), { return Module.g
|
||||
EM_JS(int, gexjs_accept, (int fd), { return Module.gexos.accept(fd); });
|
||||
EM_JS(int, gexjs_close, (int fd), { return Module.gexos.close(fd); });
|
||||
EM_JS(int, gexjs_readable, (int fd), { return Module.gexos.readable(fd); });
|
||||
EM_JS(int, gexjs_writable, (int fd), { return Module.gexos.writable(fd); });
|
||||
EM_JS(int, gexjs_send, (int fd, const char *buf, int len, const int *fds, int nfds),
|
||||
{ return Module.gexos.send(fd, buf, len, fds, nfds); });
|
||||
EM_JS(int, gexjs_recv, (int fd, char *buf, int len, int *fds, int maxfds, int *nfds),
|
||||
@@ -510,7 +511,7 @@ gex_poll(struct pollfd *fds, nfds_t nfds, int timeout)
|
||||
fds[i].revents = 0;
|
||||
if (fds[i].fd < 0)
|
||||
continue;
|
||||
if (fds[i].events & POLLOUT)
|
||||
if ((fds[i].events & POLLOUT) && gexjs_writable(fds[i].fd) > 0)
|
||||
fds[i].revents |= POLLOUT;
|
||||
if (fds[i].events & POLLIN) {
|
||||
int r = gexjs_readable(fds[i].fd);
|
||||
@@ -519,11 +520,14 @@ gex_poll(struct pollfd *fds, nfds_t nfds, int timeout)
|
||||
else if (r < 0)
|
||||
fds[i].revents |= POLLNVAL;
|
||||
}
|
||||
if (fds[i].revents & (POLLIN | POLLNVAL))
|
||||
if (fds[i].revents)
|
||||
ready++;
|
||||
}
|
||||
/* POLLOUT alone is not worth waking for: the server asks for it while
|
||||
* it has a client's reply buffered, and a queue here is never full. */
|
||||
/* POLLOUT counts. The server asks for it when a client's socket in
|
||||
* the kernel is full (256 KiB) with a reply still buffered, and it
|
||||
* writes the rest only when poll says so: a poll that kept waiting
|
||||
* for POLLIN instead hung every reply bigger than the socket (the
|
||||
* client waits for the reply and sends nothing). */
|
||||
if (ready || timeout == 0)
|
||||
return ready;
|
||||
int slice = timeout < 0 ? 20 : (timeout - (int) deadline_slept > 20
|
||||
@@ -539,10 +543,13 @@ gex_poll(struct pollfd *fds, nfds_t nfds, int timeout)
|
||||
for (nfds_t i = 0; i < nfds; i++) {
|
||||
fds[i].revents = 0;
|
||||
if (fds[i].fd >= 0 && (fds[i].events & POLLIN) &&
|
||||
gexjs_readable(fds[i].fd) > 0) {
|
||||
gexjs_readable(fds[i].fd) > 0)
|
||||
fds[i].revents |= POLLIN;
|
||||
if (fds[i].fd >= 0 && (fds[i].events & POLLOUT) &&
|
||||
gexjs_writable(fds[i].fd) > 0)
|
||||
fds[i].revents |= POLLOUT;
|
||||
if (fds[i].revents)
|
||||
last++;
|
||||
}
|
||||
}
|
||||
return last;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user