@specy/x86 2.4.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/index.d.mts +191 -5
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +370 -25
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -1112,6 +1112,17 @@ function emptyFpuStateBlock() {
|
|
|
1112
1112
|
}
|
|
1113
1113
|
//#endregion
|
|
1114
1114
|
//#region src/blink-runtime.ts
|
|
1115
|
+
/** The machine's page size: a page is contiguous in the host heap, the next page need not be. */
|
|
1116
|
+
const BLINK_PAGE_SIZE = 4096;
|
|
1117
|
+
/**
|
|
1118
|
+
* The shadow-memory range blink keeps for its own bookkeeping, which the
|
|
1119
|
+
* memory bridge refuses to read. Kept in step with `IsShadow` in
|
|
1120
|
+
* `blink/blinkenlib.c`, so the fast read path refuses exactly what the bridge
|
|
1121
|
+
* refuses.
|
|
1122
|
+
*/
|
|
1123
|
+
function isShadowAddress(address) {
|
|
1124
|
+
return address >= 2147450880n && address < 17594333528064n;
|
|
1125
|
+
}
|
|
1115
1126
|
const SIGNALS = {
|
|
1116
1127
|
SIGTRAP: 5,
|
|
1117
1128
|
SIGXCPU: 24
|
|
@@ -1191,6 +1202,8 @@ const SIGNAL_INFO = {
|
|
|
1191
1202
|
};
|
|
1192
1203
|
var BlinkRuntime = class BlinkRuntime {
|
|
1193
1204
|
module;
|
|
1205
|
+
/** Cached view over the wasm heap, for `spyMemoryBytes`; growth invalidates it. */
|
|
1206
|
+
heapView = null;
|
|
1194
1207
|
mode;
|
|
1195
1208
|
state = "NOT_READY";
|
|
1196
1209
|
stopReason = null;
|
|
@@ -1485,6 +1498,47 @@ var BlinkRuntime = class BlinkRuntime {
|
|
|
1485
1498
|
if (!result.ok) throw new Error(`${result.error}: 0x${address.toString(16)}`);
|
|
1486
1499
|
return Uint8Array.from(result.bytes);
|
|
1487
1500
|
}
|
|
1501
|
+
/**
|
|
1502
|
+
* The bytes at `address` read straight out of the machine's own pages,
|
|
1503
|
+
* or null when the machine cannot answer for the whole range.
|
|
1504
|
+
*
|
|
1505
|
+
* `blinkenlibReadMemoryBytes` looks each byte up and then builds a
|
|
1506
|
+
* JavaScript array inside the bridge, which costs about four times as much
|
|
1507
|
+
* as this does; recording a step reads only addresses the step just wrote,
|
|
1508
|
+
* so it takes this path and falls back to the bridge whenever this one
|
|
1509
|
+
* declines. The two agree byte for byte because they do the same lookup:
|
|
1510
|
+
* `blinkenlib_spy_address` IS the lookup `blinkenlib_read_memory_byte`
|
|
1511
|
+
* makes, and the shadow-memory range the bridge refuses is refused here
|
|
1512
|
+
* too. A page is contiguous in the host heap and the next page need not
|
|
1513
|
+
* be, so the read stops at every page boundary and looks the next one up.
|
|
1514
|
+
*/
|
|
1515
|
+
spyMemoryBytes(address, length) {
|
|
1516
|
+
const spy = this.module._blinkenlib_spy_address;
|
|
1517
|
+
if (!spy || !this.module.wasmExports?.memory) return null;
|
|
1518
|
+
if (!Number.isSafeInteger(length) || length <= 0) return null;
|
|
1519
|
+
if (this.state === "NOT_READY" || this.state === "READY") return null;
|
|
1520
|
+
const bytes = new Uint8Array(length);
|
|
1521
|
+
let offset = 0;
|
|
1522
|
+
while (offset < length) {
|
|
1523
|
+
const target = address + BigInt(offset);
|
|
1524
|
+
if (isShadowAddress(target)) return null;
|
|
1525
|
+
const pointer = spy.call(this.module, target) >>> 0;
|
|
1526
|
+
if (pointer === 0) return null;
|
|
1527
|
+
const take = Math.min(BLINK_PAGE_SIZE - Number(target & 4095n), length - offset);
|
|
1528
|
+
bytes.set(this.heapBytes().subarray(pointer, pointer + take), offset);
|
|
1529
|
+
offset += take;
|
|
1530
|
+
}
|
|
1531
|
+
return bytes;
|
|
1532
|
+
}
|
|
1533
|
+
/**
|
|
1534
|
+
* A view over the wasm heap, rebuilt only when growth has replaced the
|
|
1535
|
+
* buffer the last one was taken from.
|
|
1536
|
+
*/
|
|
1537
|
+
heapBytes() {
|
|
1538
|
+
const buffer = this.module.wasmExports.memory.buffer;
|
|
1539
|
+
if (!this.heapView || this.heapView.buffer !== buffer) this.heapView = new Uint8Array(buffer);
|
|
1540
|
+
return this.heapView;
|
|
1541
|
+
}
|
|
1488
1542
|
writeMemoryBytes(address, data) {
|
|
1489
1543
|
const result = this.module.blinkenlibWriteMemoryBytes(address, data);
|
|
1490
1544
|
if (!result.ok) throw new Error(`${result.error}: 0x${address.toString(16)}`);
|
|
@@ -1874,6 +1928,14 @@ var BaseEmulator = class {
|
|
|
1874
1928
|
};
|
|
1875
1929
|
//#endregion
|
|
1876
1930
|
//#region src/x86-emulator-utils.ts
|
|
1931
|
+
/**
|
|
1932
|
+
* Whether a native memory write is one a history entry can report in full:
|
|
1933
|
+
* the machine captured every byte it replaced. A write that overflowed the
|
|
1934
|
+
* native journal keeps its `Other` shape instead and stays out of undo.
|
|
1935
|
+
*/
|
|
1936
|
+
function isRecordableWrite(write) {
|
|
1937
|
+
return !write.truncated && write.old.length === write.size;
|
|
1938
|
+
}
|
|
1877
1939
|
var CircularHistory = class {
|
|
1878
1940
|
capacity;
|
|
1879
1941
|
entries;
|
|
@@ -1965,12 +2027,14 @@ function cloneCallStack(stack) {
|
|
|
1965
2027
|
}
|
|
1966
2028
|
function stripPrivateHistory(entry) {
|
|
1967
2029
|
return {
|
|
2030
|
+
kind: entry.kind,
|
|
1968
2031
|
mutations: entry.mutations,
|
|
1969
2032
|
pc: entry.pc,
|
|
1970
2033
|
old_ccr: entry.old_ccr,
|
|
1971
2034
|
new_ccr: entry.new_ccr,
|
|
1972
2035
|
line: entry.line,
|
|
1973
|
-
file: entry.file
|
|
2036
|
+
file: entry.file,
|
|
2037
|
+
...entry.writes ? { writes: entry.writes } : {}
|
|
1974
2038
|
};
|
|
1975
2039
|
}
|
|
1976
2040
|
function toHistoryPc(address) {
|
|
@@ -2003,6 +2067,9 @@ var X86Emulator = class X86Emulator extends BaseEmulator {
|
|
|
2003
2067
|
lastSourceCode = "";
|
|
2004
2068
|
history = new CircularHistory(0);
|
|
2005
2069
|
callStack = [];
|
|
2070
|
+
openPoke = null;
|
|
2071
|
+
/** True while an instruction is running, so a Poke cannot open on top of one. */
|
|
2072
|
+
executing = false;
|
|
2006
2073
|
constructor(runtime) {
|
|
2007
2074
|
super({
|
|
2008
2075
|
systemSize: 8,
|
|
@@ -2086,8 +2153,15 @@ var X86Emulator = class X86Emulator extends BaseEmulator {
|
|
|
2086
2153
|
this.runtime.loadElf(data);
|
|
2087
2154
|
}
|
|
2088
2155
|
async runUntilBlocked() {
|
|
2156
|
+
this.assertNoOpenPoke("run");
|
|
2089
2157
|
if (this.isTracingEnabled()) return this.run();
|
|
2090
|
-
|
|
2158
|
+
const wasExecuting = this.executing;
|
|
2159
|
+
this.executing = true;
|
|
2160
|
+
try {
|
|
2161
|
+
await this.runtime.runUntilBlocked();
|
|
2162
|
+
} finally {
|
|
2163
|
+
this.executing = wasExecuting;
|
|
2164
|
+
}
|
|
2091
2165
|
return this.getStatus();
|
|
2092
2166
|
}
|
|
2093
2167
|
provideInput(line) {
|
|
@@ -2172,11 +2246,115 @@ var X86Emulator = class X86Emulator extends BaseEmulator {
|
|
|
2172
2246
|
const entry = this.history.peekNewest();
|
|
2173
2247
|
return Boolean(entry?.reversible);
|
|
2174
2248
|
}
|
|
2249
|
+
/**
|
|
2250
|
+
* Opens a Poke: a register or memory value the HOST changes between two
|
|
2251
|
+
* instructions, recorded as one step of this history and undone like an
|
|
2252
|
+
* instruction.
|
|
2253
|
+
*
|
|
2254
|
+
* Everything `setRegisterValue`, `setFpuState` and `writeMemoryBytes`
|
|
2255
|
+
* change until `endPoke()` becomes one entry, however many calls it took.
|
|
2256
|
+
* Outside a transaction those setters stay exactly as direct as they were:
|
|
2257
|
+
* a Testcase presetting its starting values records nothing.
|
|
2258
|
+
*
|
|
2259
|
+
* Throws when a poke is already open, and when an instruction is running -
|
|
2260
|
+
* a step inside a transaction would hand the program's own writes to the
|
|
2261
|
+
* poke, and undoing it would then revert the instruction too.
|
|
2262
|
+
*/
|
|
2263
|
+
beginPoke() {
|
|
2264
|
+
if (this.openPoke) throw new Error("A poke is already open: end it before beginning another");
|
|
2265
|
+
if (this.executing) throw new Error("Cannot begin a poke while an instruction is executing");
|
|
2266
|
+
const snapshot = this.runtime.getRegisterSnapshot();
|
|
2267
|
+
this.openPoke = {
|
|
2268
|
+
registersBefore: cloneRegisterValues(snapshot),
|
|
2269
|
+
flagsBefore: snapshot.flags,
|
|
2270
|
+
fpuBefore: this.runtime.getFpuStateRaw(),
|
|
2271
|
+
callStackBefore: cloneCallStack(this.callStack),
|
|
2272
|
+
memoryWrites: []
|
|
2273
|
+
};
|
|
2274
|
+
}
|
|
2275
|
+
/**
|
|
2276
|
+
* Closes the open Poke and records it, returning whether anything was
|
|
2277
|
+
* recorded: a transaction that changed no value - because it wrote
|
|
2278
|
+
* nothing, or wrote the value that was already there - records nothing and
|
|
2279
|
+
* answers false, so the caller never shows an Undo step that reverts
|
|
2280
|
+
* nothing.
|
|
2281
|
+
*
|
|
2282
|
+
* Nothing here resumes the machine: the poke changed state, not why the
|
|
2283
|
+
* program stopped, and the next step resumes a paused machine itself.
|
|
2284
|
+
*
|
|
2285
|
+
* A poke takes one slot of the history, exactly as an instruction does, so
|
|
2286
|
+
* with `initialize(0)` it applies and answers true while the zero-capacity
|
|
2287
|
+
* history keeps it no more than it keeps an instruction.
|
|
2288
|
+
*
|
|
2289
|
+
* Throws when no poke is open.
|
|
2290
|
+
*/
|
|
2291
|
+
endPoke() {
|
|
2292
|
+
const poke = this.openPoke;
|
|
2293
|
+
if (!poke) throw new Error("No poke is open: begin one before ending it");
|
|
2294
|
+
this.openPoke = null;
|
|
2295
|
+
const after = this.runtime.getRegisterSnapshot();
|
|
2296
|
+
const registersAfter = cloneRegisterValues(after);
|
|
2297
|
+
const mutations = [];
|
|
2298
|
+
const writes = [];
|
|
2299
|
+
for (const register of X86_REGISTER_NAMES) {
|
|
2300
|
+
const old = poke.registersBefore[register];
|
|
2301
|
+
const value = registersAfter[register];
|
|
2302
|
+
if (old === value) continue;
|
|
2303
|
+
mutations.push({
|
|
2304
|
+
type: "WriteRegister",
|
|
2305
|
+
value: {
|
|
2306
|
+
register,
|
|
2307
|
+
old,
|
|
2308
|
+
new: value,
|
|
2309
|
+
size: 8
|
|
2310
|
+
}
|
|
2311
|
+
});
|
|
2312
|
+
writes.push({
|
|
2313
|
+
type: "register",
|
|
2314
|
+
name: register,
|
|
2315
|
+
old,
|
|
2316
|
+
new: value
|
|
2317
|
+
});
|
|
2318
|
+
}
|
|
2319
|
+
this.recordFpuMutations(poke.fpuBefore, this.runtime.getFpuStateRaw(), mutations, writes);
|
|
2320
|
+
const memoryWrites = this.recordPokeMemoryMutations(poke.memoryWrites, mutations, writes);
|
|
2321
|
+
if (writes.length === 0) return false;
|
|
2322
|
+
const location = this.runtime.getSourceLocationForAddress(after.pc);
|
|
2323
|
+
const entry = {
|
|
2324
|
+
kind: "poke",
|
|
2325
|
+
mutations,
|
|
2326
|
+
writes,
|
|
2327
|
+
pc: toHistoryPc(after.pc),
|
|
2328
|
+
old_ccr: { bits: poke.flagsBefore },
|
|
2329
|
+
new_ccr: { bits: after.flags },
|
|
2330
|
+
line: location?.line ?? -1,
|
|
2331
|
+
file: location?.path,
|
|
2332
|
+
registersBefore: poke.registersBefore,
|
|
2333
|
+
flagsBefore: poke.flagsBefore,
|
|
2334
|
+
fpuBefore: poke.fpuBefore,
|
|
2335
|
+
callStackBefore: cloneCallStack(this.callStack),
|
|
2336
|
+
memoryWrites,
|
|
2337
|
+
reversible: true
|
|
2338
|
+
};
|
|
2339
|
+
this.history.push(entry);
|
|
2340
|
+
return true;
|
|
2341
|
+
}
|
|
2342
|
+
/** True between `beginPoke()` and `endPoke()`. */
|
|
2343
|
+
isPokeOpen() {
|
|
2344
|
+
return this.openPoke !== null;
|
|
2345
|
+
}
|
|
2175
2346
|
async step() {
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2347
|
+
this.assertNoOpenPoke("step");
|
|
2348
|
+
const wasExecuting = this.executing;
|
|
2349
|
+
this.executing = true;
|
|
2350
|
+
try {
|
|
2351
|
+
if (this.isTracingEnabled()) this.captureStep();
|
|
2352
|
+
else {
|
|
2353
|
+
this.prepareOneInstructionRun();
|
|
2354
|
+
this.runtime.step();
|
|
2355
|
+
}
|
|
2356
|
+
} finally {
|
|
2357
|
+
this.executing = wasExecuting;
|
|
2180
2358
|
}
|
|
2181
2359
|
return { terminated: this.hasTerminated() };
|
|
2182
2360
|
}
|
|
@@ -2186,8 +2364,23 @@ var X86Emulator = class X86Emulator extends BaseEmulator {
|
|
|
2186
2364
|
if (this.runtime.state === "PROGRAM_STOPPED") return 0;
|
|
2187
2365
|
return 1;
|
|
2188
2366
|
}
|
|
2367
|
+
/**
|
|
2368
|
+
* Inside an open Poke the bytes this overwrites are journaled first, so
|
|
2369
|
+
* the transaction can put them back; outside one the write goes straight
|
|
2370
|
+
* into the machine and records nothing, as it always has.
|
|
2371
|
+
*/
|
|
2189
2372
|
writeMemoryBytes(address, data) {
|
|
2373
|
+
const poke = this.openPoke;
|
|
2374
|
+
if (!poke || data.length === 0) {
|
|
2375
|
+
this.runtime.writeMemoryBytes(address, data);
|
|
2376
|
+
return;
|
|
2377
|
+
}
|
|
2378
|
+
const old = [...this.runtime.readMemoryBytes(address, BigInt(data.length))];
|
|
2190
2379
|
this.runtime.writeMemoryBytes(address, data);
|
|
2380
|
+
poke.memoryWrites.push({
|
|
2381
|
+
address,
|
|
2382
|
+
old
|
|
2383
|
+
});
|
|
2191
2384
|
}
|
|
2192
2385
|
readMemoryBytes(address, length) {
|
|
2193
2386
|
return this.runtime.readMemoryBytes(address, length);
|
|
@@ -2286,15 +2479,25 @@ var X86Emulator = class X86Emulator extends BaseEmulator {
|
|
|
2286
2479
|
return this.runtime.state === "PROGRAM_STOPPED";
|
|
2287
2480
|
}
|
|
2288
2481
|
async run(limit, breakpoints = []) {
|
|
2482
|
+
this.assertNoOpenPoke("run");
|
|
2289
2483
|
this.validateRunLimit(limit);
|
|
2290
2484
|
const breakpointAddresses = this.resolveBreakpointAddresses(breakpoints);
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
limit,
|
|
2295
|
-
breakpointAddresses
|
|
2296
|
-
|
|
2297
|
-
|
|
2485
|
+
const wasExecuting = this.executing;
|
|
2486
|
+
this.executing = true;
|
|
2487
|
+
try {
|
|
2488
|
+
if (this.isTracingEnabled()) return await this.runWithHistory(limit, breakpointAddresses);
|
|
2489
|
+
if (breakpointAddresses.length) return await this.runWithBreakpoints(limit, breakpointAddresses);
|
|
2490
|
+
await this.runtime.runUntilBlocked({
|
|
2491
|
+
limit,
|
|
2492
|
+
breakpointAddresses
|
|
2493
|
+
});
|
|
2494
|
+
return this.getStatus();
|
|
2495
|
+
} finally {
|
|
2496
|
+
this.executing = wasExecuting;
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
assertNoOpenPoke(what) {
|
|
2500
|
+
if (this.openPoke) throw new Error(`Cannot ${what} while a poke is open: end it first`);
|
|
2298
2501
|
}
|
|
2299
2502
|
validateRunLimit(limit) {
|
|
2300
2503
|
if (limit === void 0) return;
|
|
@@ -2358,6 +2561,7 @@ var X86Emulator = class X86Emulator extends BaseEmulator {
|
|
|
2358
2561
|
clearExecutionTrace() {
|
|
2359
2562
|
this.history.clear();
|
|
2360
2563
|
this.callStack = [];
|
|
2564
|
+
this.openPoke = null;
|
|
2361
2565
|
}
|
|
2362
2566
|
prepareOneInstructionRun() {
|
|
2363
2567
|
if (this.runtime.state === "PROGRAM_LOADED" || this.runtime.state === "PROGRAM_STOPPED") {
|
|
@@ -2393,6 +2597,7 @@ var X86Emulator = class X86Emulator extends BaseEmulator {
|
|
|
2393
2597
|
value: {
|
|
2394
2598
|
register,
|
|
2395
2599
|
old: registersBefore[register],
|
|
2600
|
+
new: registersAfter[register],
|
|
2396
2601
|
size: 8
|
|
2397
2602
|
}
|
|
2398
2603
|
});
|
|
@@ -2402,6 +2607,7 @@ var X86Emulator = class X86Emulator extends BaseEmulator {
|
|
|
2402
2607
|
if (nativeStep.valid) this.recordControlFlowMutation(nativeStep, instruction, mutations);
|
|
2403
2608
|
const location = this.runtime.getSourceLocationForAddress(pcBefore);
|
|
2404
2609
|
const entry = {
|
|
2610
|
+
kind: "instruction",
|
|
2405
2611
|
mutations,
|
|
2406
2612
|
pc: toHistoryPc(pcBefore),
|
|
2407
2613
|
old_ccr: { bits: flagsBefore },
|
|
@@ -2428,7 +2634,7 @@ var X86Emulator = class X86Emulator extends BaseEmulator {
|
|
|
2428
2634
|
* exact either way, because it restores the whole block rather than
|
|
2429
2635
|
* replaying the mutations.
|
|
2430
2636
|
*/
|
|
2431
|
-
recordFpuMutations(fpuBefore, fpuAfter, mutations) {
|
|
2637
|
+
recordFpuMutations(fpuBefore, fpuAfter, mutations, writes) {
|
|
2432
2638
|
if (fpuStateBlocksEqual(fpuBefore, fpuAfter)) return;
|
|
2433
2639
|
const stateBefore = decodeFpuState(fpuBefore);
|
|
2434
2640
|
const stateAfter = decodeFpuState(fpuAfter);
|
|
@@ -2439,18 +2645,34 @@ var X86Emulator = class X86Emulator extends BaseEmulator {
|
|
|
2439
2645
|
value: {
|
|
2440
2646
|
register: X86_SSE_REGISTERS[index],
|
|
2441
2647
|
old: stateBefore.xmm[index],
|
|
2648
|
+
new: stateAfter.xmm[index],
|
|
2442
2649
|
size: 16
|
|
2443
2650
|
}
|
|
2444
2651
|
});
|
|
2652
|
+
writes?.push({
|
|
2653
|
+
type: "register",
|
|
2654
|
+
name: X86_SSE_REGISTERS[index],
|
|
2655
|
+
old: stateBefore.xmm[index],
|
|
2656
|
+
new: stateAfter.xmm[index]
|
|
2657
|
+
});
|
|
2445
2658
|
}
|
|
2446
|
-
if (stateBefore.mxcsr !== stateAfter.mxcsr)
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2659
|
+
if (stateBefore.mxcsr !== stateAfter.mxcsr) {
|
|
2660
|
+
mutations.push({
|
|
2661
|
+
type: "WriteRegister",
|
|
2662
|
+
value: {
|
|
2663
|
+
register: "mxcsr",
|
|
2664
|
+
old: BigInt(stateBefore.mxcsr),
|
|
2665
|
+
new: BigInt(stateAfter.mxcsr),
|
|
2666
|
+
size: 4
|
|
2667
|
+
}
|
|
2668
|
+
});
|
|
2669
|
+
writes?.push({
|
|
2670
|
+
type: "register",
|
|
2671
|
+
name: "mxcsr",
|
|
2450
2672
|
old: BigInt(stateBefore.mxcsr),
|
|
2451
|
-
|
|
2452
|
-
}
|
|
2453
|
-
}
|
|
2673
|
+
new: BigInt(stateAfter.mxcsr)
|
|
2674
|
+
});
|
|
2675
|
+
}
|
|
2454
2676
|
const stBitsBefore = readLogicalStBits(fpuBefore);
|
|
2455
2677
|
const stBitsAfter = readLogicalStBits(fpuAfter);
|
|
2456
2678
|
for (let index = 0; index < stBitsBefore.length; index += 1) {
|
|
@@ -2460,9 +2682,16 @@ var X86Emulator = class X86Emulator extends BaseEmulator {
|
|
|
2460
2682
|
value: {
|
|
2461
2683
|
register: X86_X87_REGISTERS[index],
|
|
2462
2684
|
old: stBitsBefore[index],
|
|
2685
|
+
new: stBitsAfter[index],
|
|
2463
2686
|
size: 8
|
|
2464
2687
|
}
|
|
2465
2688
|
});
|
|
2689
|
+
writes?.push({
|
|
2690
|
+
type: "register",
|
|
2691
|
+
name: X86_X87_REGISTERS[index],
|
|
2692
|
+
old: stBitsBefore[index],
|
|
2693
|
+
new: stBitsAfter[index]
|
|
2694
|
+
});
|
|
2466
2695
|
}
|
|
2467
2696
|
for (const word of [
|
|
2468
2697
|
"fctrl",
|
|
@@ -2475,15 +2704,106 @@ var X86Emulator = class X86Emulator extends BaseEmulator {
|
|
|
2475
2704
|
value: {
|
|
2476
2705
|
register: word,
|
|
2477
2706
|
old: BigInt(stateBefore[word]),
|
|
2707
|
+
new: BigInt(stateAfter[word]),
|
|
2478
2708
|
size: 2
|
|
2479
2709
|
}
|
|
2480
2710
|
});
|
|
2711
|
+
writes?.push({
|
|
2712
|
+
type: "register",
|
|
2713
|
+
name: word,
|
|
2714
|
+
old: BigInt(stateBefore[word]),
|
|
2715
|
+
new: BigInt(stateAfter[word])
|
|
2716
|
+
});
|
|
2481
2717
|
}
|
|
2482
2718
|
}
|
|
2719
|
+
/**
|
|
2720
|
+
* The memory a Poke actually changed. The journal is collapsed per byte
|
|
2721
|
+
* first - the EARLIEST `old` of every address, from the first write that
|
|
2722
|
+
* touched it - and only then compared against what memory holds now, so a
|
|
2723
|
+
* range written twice inside one transaction is diffed against what the
|
|
2724
|
+
* machine held before the poke and not against the intermediate value the
|
|
2725
|
+
* first write left. A range written away and back therefore leaves no
|
|
2726
|
+
* trace at all, and two differing writes to one range leave one entry
|
|
2727
|
+
* carrying the value the machine really had. What survives keeps the old
|
|
2728
|
+
* bytes the way an instruction's writes do, for undo to put back in
|
|
2729
|
+
* reverse order.
|
|
2730
|
+
*/
|
|
2731
|
+
recordPokeMemoryMutations(journaled, mutations, writes) {
|
|
2732
|
+
const earliest = /* @__PURE__ */ new Map();
|
|
2733
|
+
for (const write of journaled) write.old.forEach((byte, index) => {
|
|
2734
|
+
const address = write.address + BigInt(index);
|
|
2735
|
+
if (!earliest.has(address)) earliest.set(address, byte);
|
|
2736
|
+
});
|
|
2737
|
+
const addresses = [...earliest.keys()].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
2738
|
+
const ranges = [];
|
|
2739
|
+
for (const address of addresses) {
|
|
2740
|
+
const open = ranges[ranges.length - 1];
|
|
2741
|
+
const byte = earliest.get(address);
|
|
2742
|
+
if (open && open.address + BigInt(open.old.length) === address) {
|
|
2743
|
+
open.old.push(byte);
|
|
2744
|
+
continue;
|
|
2745
|
+
}
|
|
2746
|
+
ranges.push({
|
|
2747
|
+
address,
|
|
2748
|
+
old: [byte]
|
|
2749
|
+
});
|
|
2750
|
+
}
|
|
2751
|
+
const undoWrites = [];
|
|
2752
|
+
for (const range of ranges) {
|
|
2753
|
+
const current = [...this.runtime.readMemoryBytes(range.address, BigInt(range.old.length))];
|
|
2754
|
+
if (current.every((byte, index) => byte === range.old[index])) continue;
|
|
2755
|
+
undoWrites.push(range);
|
|
2756
|
+
mutations.push({
|
|
2757
|
+
type: "WriteMemoryBytes",
|
|
2758
|
+
value: {
|
|
2759
|
+
address: range.address,
|
|
2760
|
+
old: [...range.old],
|
|
2761
|
+
new: [...current]
|
|
2762
|
+
}
|
|
2763
|
+
});
|
|
2764
|
+
writes.push({
|
|
2765
|
+
type: "memory",
|
|
2766
|
+
address: range.address,
|
|
2767
|
+
old: [...range.old],
|
|
2768
|
+
new: current
|
|
2769
|
+
});
|
|
2770
|
+
}
|
|
2771
|
+
return undoWrites;
|
|
2772
|
+
}
|
|
2773
|
+
/**
|
|
2774
|
+
* The memory a step wrote. The native journal hands over the bytes each
|
|
2775
|
+
* store REPLACED, captured in the machine at that store; it records
|
|
2776
|
+
* nothing about what a store PUT there. So `old` is the machine's own
|
|
2777
|
+
* capture, taken at the write, while `new` is read out of the machine as
|
|
2778
|
+
* the entry is recorded - the step has finished and nothing has run since,
|
|
2779
|
+
* so those addresses still hold what the step left.
|
|
2780
|
+
*
|
|
2781
|
+
* The read costs one page lookup per store: one for the single store
|
|
2782
|
+
* almost every store-bearing instruction journals, and none at all for the
|
|
2783
|
+
* majority of instructions, which write no memory. It goes through the
|
|
2784
|
+
* machine's own page lookup rather than the byte-by-byte bridge (see
|
|
2785
|
+
* `BlinkRuntime.spyMemoryBytes`), which keeps it far below the cost of
|
|
2786
|
+
* tracing the step around it.
|
|
2787
|
+
*
|
|
2788
|
+
* What reading rather than journaling costs: were one step to store twice
|
|
2789
|
+
* over the same address, both entries would report the bytes the step
|
|
2790
|
+
* ENDED with rather than the bytes each store left. No x86 instruction
|
|
2791
|
+
* reachable here does that - blink coalesces a `rep` into one record, and
|
|
2792
|
+
* push, call, ret, enter, leave, `xchg` with memory and read-modify-write
|
|
2793
|
+
* arithmetic each journal a single store - so nothing observable turns on
|
|
2794
|
+
* it today. Only a post-image in the native journal would close it, and
|
|
2795
|
+
* that needs the wasm rebuilt, which this package does not do.
|
|
2796
|
+
*
|
|
2797
|
+
* A range the machine refuses to read back - a step can unmap what it
|
|
2798
|
+
* wrote - leaves that entry with no new bytes rather than a guess, and the
|
|
2799
|
+
* read never throws out of recording. Truncated writes keep their existing
|
|
2800
|
+
* `Other` shape and stay out of the undo journal.
|
|
2801
|
+
*/
|
|
2483
2802
|
recordMemoryMutations(writes, mutations) {
|
|
2484
2803
|
const undoWrites = [];
|
|
2485
|
-
|
|
2486
|
-
|
|
2804
|
+
const written = this.writtenBytes(writes);
|
|
2805
|
+
for (const [index, write] of writes.entries()) {
|
|
2806
|
+
if (!isRecordableWrite(write)) {
|
|
2487
2807
|
mutations.push({
|
|
2488
2808
|
type: "Other",
|
|
2489
2809
|
value: `Wrote ${write.size} bytes to 0x${write.address.toString(16)}`
|
|
@@ -2498,12 +2818,37 @@ var X86Emulator = class X86Emulator extends BaseEmulator {
|
|
|
2498
2818
|
type: "WriteMemoryBytes",
|
|
2499
2819
|
value: {
|
|
2500
2820
|
address: write.address,
|
|
2501
|
-
old: [...write.old]
|
|
2821
|
+
old: [...write.old],
|
|
2822
|
+
new: written[index] ?? []
|
|
2502
2823
|
}
|
|
2503
2824
|
});
|
|
2504
2825
|
}
|
|
2505
2826
|
return undoWrites;
|
|
2506
2827
|
}
|
|
2828
|
+
/**
|
|
2829
|
+
* The bytes each of a step's stores left, by the index of that store: what
|
|
2830
|
+
* the addresses that store wrote hold now, at the width of the write. One
|
|
2831
|
+
* lookup per store it can account for, and none for a store it cannot.
|
|
2832
|
+
*/
|
|
2833
|
+
writtenBytes(writes) {
|
|
2834
|
+
return writes.map((write) => isRecordableWrite(write) ? this.readBytes(write.address, write.size) : []);
|
|
2835
|
+
}
|
|
2836
|
+
/**
|
|
2837
|
+
* One read of the machine that answers with nothing rather than throwing,
|
|
2838
|
+
* so recording a step can never fail after the instruction already ran.
|
|
2839
|
+
* It takes the machine's own page lookup when that can answer for the
|
|
2840
|
+
* whole range and the byte-by-byte bridge otherwise; the two read the same
|
|
2841
|
+
* memory the same way.
|
|
2842
|
+
*/
|
|
2843
|
+
readBytes(address, length) {
|
|
2844
|
+
const spied = this.runtime.spyMemoryBytes(address, length);
|
|
2845
|
+
if (spied) return Array.from(spied);
|
|
2846
|
+
try {
|
|
2847
|
+
return [...this.runtime.readMemoryBytes(address, BigInt(length))];
|
|
2848
|
+
} catch {
|
|
2849
|
+
return [];
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2507
2852
|
recordControlFlowMutation(step, instruction, mutations) {
|
|
2508
2853
|
if (step.controlFlow === "call") {
|
|
2509
2854
|
const returnAddress = instruction ? instruction.address + BigInt(instruction.size) : step.pcBefore;
|
|
@@ -2546,6 +2891,6 @@ async function createX86Emulator(options = {}) {
|
|
|
2546
2891
|
return X86Emulator.create(options);
|
|
2547
2892
|
}
|
|
2548
2893
|
//#endregion
|
|
2549
|
-
export { BaseEmulator, BlinkRuntime, BlinkState, DEFAULT_ASSEMBLER_ID, EmulatorStatus, RegisterSize, X86Emulator, X86_FPU_STATE_CW_OFFSET, X86_FPU_STATE_DP_OFFSET, X86_FPU_STATE_IP_OFFSET, X86_FPU_STATE_MXCSR_OFFSET, X86_FPU_STATE_OP_OFFSET, X86_FPU_STATE_SIZE, X86_FPU_STATE_ST_OFFSET, X86_FPU_STATE_SW_OFFSET, X86_FPU_STATE_TW_OFFSET, X86_FPU_STATE_XMM_OFFSET, X86_PROJECT_ROOT, X86_REGISTER_NAMES, X86_SSE_REGISTERS, X86_X87_REGISTERS, assemblers, createX86Emulator, decodeFpuState, defaultResourceUrl, emptyFpuStateBlock, encodeFpuState, fasmDiagnostics, fpuStateBlocksEqual, gnuDiagnostics, isNodeRuntime, ldDiagnostics, locateDiagnosticColumn, locateDiagnosticSpan, nasmDiagnostics, observeCallbackResult, readLogicalStBits, readLogicalStTags, readResourceBytes, selectX86Source, stageX86Project, toResourceUrl, validateX86Project, x86ProjectSourcePath, x86ProjectText };
|
|
2894
|
+
export { BaseEmulator, BlinkRuntime, BlinkState, DEFAULT_ASSEMBLER_ID, EmulatorStatus, RegisterSize, X86Emulator, X86_FPU_STATE_CW_OFFSET, X86_FPU_STATE_DP_OFFSET, X86_FPU_STATE_IP_OFFSET, X86_FPU_STATE_MXCSR_OFFSET, X86_FPU_STATE_OP_OFFSET, X86_FPU_STATE_SIZE, X86_FPU_STATE_ST_OFFSET, X86_FPU_STATE_SW_OFFSET, X86_FPU_STATE_TW_OFFSET, X86_FPU_STATE_XMM_OFFSET, X86_PROJECT_ROOT, X86_REGISTER_NAMES, X86_SSE_REGISTERS, X86_X87_REGISTERS, assemblers, createX86Emulator, decodeFpuState, defaultResourceUrl, emptyFpuStateBlock, encodeFpuState, fasmDiagnostics, fpuStateBlocksEqual, gnuDiagnostics, isNodeRuntime, isShadowAddress, ldDiagnostics, locateDiagnosticColumn, locateDiagnosticSpan, nasmDiagnostics, observeCallbackResult, readLogicalStBits, readLogicalStTags, readResourceBytes, selectX86Source, stageX86Project, toResourceUrl, validateX86Project, x86ProjectSourcePath, x86ProjectText };
|
|
2550
2895
|
|
|
2551
2896
|
//# sourceMappingURL=index.mjs.map
|