@asmlift/core 0.1.0 → 0.3.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 +18 -23
- package/package.json +1 -1
- package/src/backend/cfamily.ts +30 -4
- package/src/contracts.ts +30 -0
- package/src/declare.ts +225 -0
- package/src/detect.ts +5 -2
- package/src/frontend/format.ts +11 -3
- package/src/frontend/frontend.ts +12 -2
- package/src/frontend/mips.ts +206 -2
- package/src/frontend/splat.ts +305 -0
- package/src/frontend/thumb.ts +119 -6
- package/src/l3/ast.ts +8 -2
- package/src/l3/symbol-refs.ts +61 -0
- package/src/l3/typing.ts +4 -0
- package/src/macros.ts +126 -0
- package/src/pipeline.ts +15 -4
- package/src/proto.ts +55 -0
- package/src/raise/magicdiv.ts +1 -1
- package/src/rank.ts +215 -76
- package/src/structure/structure.ts +462 -45
- package/src/symbols.ts +426 -0
- package/src/trace.ts +8 -2
package/src/frontend/thumb.ts
CHANGED
|
@@ -19,7 +19,9 @@ import type { Opcode } from '../ir/opcodes';
|
|
|
19
19
|
import { T } from '../ir/types';
|
|
20
20
|
import { type Prototypes, protoArity } from '../proto';
|
|
21
21
|
import { RUNTIME_HELPERS } from '../raise/softdiv';
|
|
22
|
+
import { type SymbolMap, lookupInterior, lookupSymbol } from '../symbols';
|
|
22
23
|
import type { TargetDescription } from '../target';
|
|
24
|
+
import type { AsmData } from './asmdata';
|
|
23
25
|
import { pushSwitchBr } from './emit';
|
|
24
26
|
import { FrontendUnsupportedError } from './errors';
|
|
25
27
|
import { assertInputFormat } from './format';
|
|
@@ -171,10 +173,15 @@ function splitOperands(s: string): string[] {
|
|
|
171
173
|
// Parse a Thumb memory addressing operand `[base]` or `[base, #off]` into base register +
|
|
172
174
|
// constant byte offset. (Register-scaled indices like `[base, r1, lsl #2]` are not handled
|
|
173
175
|
// yet — agbcc materialises those as explicit add/lsl before the load in the cases we target.)
|
|
174
|
-
function parseAddr(operand: string): { base: string; off: number } {
|
|
176
|
+
function parseAddr(operand: string): { base: string; off: number; regOff?: string } {
|
|
175
177
|
const inner = operand.replace(/[[\]]/g, '').trim();
|
|
176
178
|
const parts = inner.split(',').map((s) => s.trim());
|
|
177
179
|
const base = parts[0];
|
|
180
|
+
// `[rB, rX]` — REGISTER-offset addressing. Surfaced to the caller so load/store DECLINE
|
|
181
|
+
// loud: silently reading `[rB]` (the old behavior) dropped the index — a silent miscompile.
|
|
182
|
+
if (parts[1] !== undefined && !parts[1].startsWith('#')) {
|
|
183
|
+
return { base, off: 0, regOff: parts[1] };
|
|
184
|
+
}
|
|
178
185
|
const off = parts[1]?.startsWith('#') ? imm(parts[1]) : 0;
|
|
179
186
|
return { base, off };
|
|
180
187
|
}
|
|
@@ -724,6 +731,37 @@ function poolRef(operand: string, dataWords: Map<string, string[]>): PoolRef | n
|
|
|
724
731
|
return { kind: 'unmodelled', why: `pool word '${w}' is a symbol offset or code label` };
|
|
725
732
|
}
|
|
726
733
|
|
|
734
|
+
/** Does this function's literal pool name at least one EXTERNAL symbol?
|
|
735
|
+
*
|
|
736
|
+
* agbcc emits a pool word symbolically exactly when the source expression named a linker symbol,
|
|
737
|
+
* and numerically when it did not (`*(vu16 *)0x4000130`, an address-cast macro). So within one
|
|
738
|
+
* function, a numeric word sitting alongside a symbolic one is numeric *by the source's choice* —
|
|
739
|
+
* which is what lets {@link liftThumb} refuse to invent a name for it.
|
|
740
|
+
*
|
|
741
|
+
* The witness is required because that inference only holds for asm that KEPT its symbols. A
|
|
742
|
+
* linked-ROM disassembly resolves every relocation to a number, and there "numeric" says nothing
|
|
743
|
+
* about the source; vetoing on it would disable the map's naming for the users who need it most.
|
|
744
|
+
* A function whose pool names nothing external is therefore left alone (both spellings still
|
|
745
|
+
* enumerate, and the differ referees) rather than being read as evidence of anything.
|
|
746
|
+
*
|
|
747
|
+
* Labels DEFINED in this same asm — the jump-table pointer word, a pret-style `_08012358` pool
|
|
748
|
+
* label — are not external symbols and never witness: they survive disassembly whether or not
|
|
749
|
+
* relocations did. */
|
|
750
|
+
function poolNamesASymbol(dataWords: Map<string, string[]>, blockLabels: Set<string>): boolean {
|
|
751
|
+
for (const [, words] of dataWords) {
|
|
752
|
+
for (const raw of words) {
|
|
753
|
+
const w = raw.trim();
|
|
754
|
+
if (!/^[A-Za-z_]\w*$/.test(w) || w.startsWith('.L')) {
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
if (!dataWords.has(w) && !blockLabels.has(w)) {
|
|
758
|
+
return true;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
return false;
|
|
763
|
+
}
|
|
764
|
+
|
|
727
765
|
// Recover an agbcc Thumb jump-table dispatch. Given a dispatch block `disp` ending in `mov pc, rV`
|
|
728
766
|
// and its unique bounds predecessor `bounds` ending in `cmp rX,#(N-1); bhi DEF`, verify the exact
|
|
729
767
|
// idiom and read the inline table — else return null (→ the indirect-jump loud-fail fires). The
|
|
@@ -816,7 +854,14 @@ function recoverJumpTable(
|
|
|
816
854
|
/** Lift decoded asm → an L1 Fn with block-argument SSA. `prototypes` supplies each callee's
|
|
817
855
|
* declared parameter count (from the project's headers); it is authoritative for recovering
|
|
818
856
|
* how many argument registers a `bl` passes (falling back to a heuristic when absent). */
|
|
819
|
-
export function lift(
|
|
857
|
+
export function lift(
|
|
858
|
+
name: string,
|
|
859
|
+
asm: string,
|
|
860
|
+
target: TargetDescription,
|
|
861
|
+
prototypes: Prototypes = {},
|
|
862
|
+
_asmData?: AsmData,
|
|
863
|
+
symbols?: SymbolMap,
|
|
864
|
+
): Fn {
|
|
820
865
|
assertInputFormat('thumb', 'gnu-as', asm);
|
|
821
866
|
const { blocks: rawBlocks, dataWords } = decode(name, asm);
|
|
822
867
|
|
|
@@ -825,6 +870,9 @@ export function lift(name: string, asm: string, target: TargetDescription, proto
|
|
|
825
870
|
// dispatch block is ELIDED from the CFG. A `mov pc` that is NOT a recognised table falls through
|
|
826
871
|
// to the loud-fail below.
|
|
827
872
|
const blockLabels = new Set(rawBlocks.map((b) => b.label));
|
|
873
|
+
// Whether THIS asm preserves symbol names in its literal pools — the witness the numeric-pool
|
|
874
|
+
// naming veto needs (see poolNamesASymbol).
|
|
875
|
+
const poolNamesSymbols = poolNamesASymbol(dataWords, blockLabels);
|
|
828
876
|
// Any label referenced as a branch target (so we can tell if an elided dispatch block has a SECOND
|
|
829
877
|
// predecessor — a `b disp` from elsewhere — which would dangle after elision; decline if so).
|
|
830
878
|
const branchTargets = new Set<string>();
|
|
@@ -1278,6 +1326,54 @@ export function lift(name: string, asm: string, target: TargetDescription, proto
|
|
|
1278
1326
|
if (ins.mnemonic === 'ldr' && b !== undefined) {
|
|
1279
1327
|
const pr = poolRef(b, dataWords);
|
|
1280
1328
|
if (pr?.kind === 'const') {
|
|
1329
|
+
// Numeric-pool PROMOTION (symbols.ts): a pool-loaded word whose value the
|
|
1330
|
+
// project's symbol map knows becomes the NAMED global's address — the same
|
|
1331
|
+
// `gaddr` the symbol-pool path emits, so everything downstream is the existing
|
|
1332
|
+
// named-global machinery. Only pool-loaded words promote (an address built by
|
|
1333
|
+
// arithmetic never reaches here); a promoted `code` symbol carries `code: true`
|
|
1334
|
+
// so the structurer spells it `(u32)Name`, not `&Name`.
|
|
1335
|
+
//
|
|
1336
|
+
// VETOED when this asm's pool names other symbols (poolNamesASymbol): agbcc would
|
|
1337
|
+
// have emitted THIS word symbolically too had the source named it, so promoting it
|
|
1338
|
+
// spells a name the source did not use.
|
|
1339
|
+
//
|
|
1340
|
+
// …but the veto is really about RELOCATION, not about naming. An `extern` name makes
|
|
1341
|
+
// the compiler emit a relocated pool word, which contradicts the numeric word the
|
|
1342
|
+
// target shows. An address-cast MACRO expands to that same numeric literal, so it is
|
|
1343
|
+
// COMPATIBLE with the evidence by construction and is never vetoed — indeed it is
|
|
1344
|
+
// the spelling the numeric word is evidence FOR (klonoa's true source reaches these
|
|
1345
|
+
// cells through exactly such macros). Nothing is guessed in either case: a vetoed
|
|
1346
|
+
// word stays the raw constant the target says it is.
|
|
1347
|
+
const found = symbols ? lookupSymbol(symbols, pr.value) : null;
|
|
1348
|
+
const si = found && (!poolNamesSymbols || found.macroBody !== undefined) ? found : null;
|
|
1349
|
+
if (si) {
|
|
1350
|
+
const res = mkValue(T.unk(32));
|
|
1351
|
+
irb.ops.push(
|
|
1352
|
+
mkOp('gaddr', {
|
|
1353
|
+
results: [res],
|
|
1354
|
+
attrs: { sym: si.name, ...(si.kind === 'code' ? { code: true } : {}) },
|
|
1355
|
+
}),
|
|
1356
|
+
);
|
|
1357
|
+
writeVar(reg(a), bi, res);
|
|
1358
|
+
break;
|
|
1359
|
+
}
|
|
1360
|
+
// INTERIOR attribution: a value strictly inside a sized data symbol becomes
|
|
1361
|
+
// `gaddr sym + offset` — the `&gSym + K` tree structure.ts already lowers (and,
|
|
1362
|
+
// with a struct layout, spells as the named field). Sized symbols only; an
|
|
1363
|
+
// unattributed address stays a raw const — nothing guesses.
|
|
1364
|
+
// Interior attribution is always an `&gSym + K` spelling — extern-shaped, hence
|
|
1365
|
+
// relocated — so the veto applies to it without the macro exemption above.
|
|
1366
|
+
const interior = symbols && !poolNamesSymbols ? lookupInterior(symbols, pr.value) : null;
|
|
1367
|
+
if (interior) {
|
|
1368
|
+
const g = mkValue(T.unk(32));
|
|
1369
|
+
const k = mkValue(T.unk(32));
|
|
1370
|
+
const res = mkValue(T.unk(32));
|
|
1371
|
+
irb.ops.push(mkOp('gaddr', { results: [g], attrs: { sym: interior.info.name } }));
|
|
1372
|
+
irb.ops.push(mkOp('const', { results: [k], attrs: { value: interior.offset } }));
|
|
1373
|
+
irb.ops.push(mkOp('add', { operands: [g, k], results: [res] }));
|
|
1374
|
+
writeVar(reg(a), bi, res);
|
|
1375
|
+
break;
|
|
1376
|
+
}
|
|
1281
1377
|
const res = mkValue(T.unk(32));
|
|
1282
1378
|
irb.ops.push(mkOp('const', { results: [res], attrs: { value: pr.value } }));
|
|
1283
1379
|
writeVar(reg(a), bi, res);
|
|
@@ -1303,9 +1399,19 @@ export function lift(name: string, asm: string, target: TargetDescription, proto
|
|
|
1303
1399
|
}
|
|
1304
1400
|
const width = /b/.test(ins.mnemonic) ? 1 : /h/.test(ins.mnemonic) ? 2 : 4;
|
|
1305
1401
|
const signed = ins.mnemonic === 'ldr' || /s/.test(ins.mnemonic.slice(3));
|
|
1306
|
-
const { base, off } = parseAddr(b);
|
|
1402
|
+
const { base, off, regOff } = parseAddr(b);
|
|
1403
|
+
// `[rB, rX]` register-offset: lower EXACTLY as `rB + rX` then a load at offset 0 —
|
|
1404
|
+
// the same address arithmetic the encoding performs. (parseAddr used to silently
|
|
1405
|
+
// read `[rB]`, dropping the index — a silent miscompile; ldrsh exists ONLY in this
|
|
1406
|
+
// form in Thumb-1, so every ldrsh went through here.)
|
|
1407
|
+
let baseVal = readData(base, bi);
|
|
1408
|
+
if (regOff !== undefined) {
|
|
1409
|
+
const sum = mkValue(T.unk(32));
|
|
1410
|
+
irb.ops.push(mkOp('add', { operands: [baseVal, readData(regOff, bi)], results: [sum] }));
|
|
1411
|
+
baseVal = sum;
|
|
1412
|
+
}
|
|
1307
1413
|
const res = mkValue(T.unk(32));
|
|
1308
|
-
irb.ops.push(mkOp('load', { operands: [
|
|
1414
|
+
irb.ops.push(mkOp('load', { operands: [baseVal], results: [res], attrs: { off, width, signed } }));
|
|
1309
1415
|
writeVar(reg(a), bi, res);
|
|
1310
1416
|
break;
|
|
1311
1417
|
}
|
|
@@ -1318,8 +1424,15 @@ export function lift(name: string, asm: string, target: TargetDescription, proto
|
|
|
1318
1424
|
break;
|
|
1319
1425
|
}
|
|
1320
1426
|
const width = /b/.test(ins.mnemonic) ? 1 : /h/.test(ins.mnemonic) ? 2 : 4;
|
|
1321
|
-
const { base, off } = parseAddr(b);
|
|
1322
|
-
|
|
1427
|
+
const { base, off, regOff } = parseAddr(b);
|
|
1428
|
+
let storeBase = readData(base, bi);
|
|
1429
|
+
if (regOff !== undefined) {
|
|
1430
|
+
// register-offset store: same exact `rB + rX` lowering as the load path above
|
|
1431
|
+
const sum = mkValue(T.unk(32));
|
|
1432
|
+
irb.ops.push(mkOp('add', { operands: [storeBase, readData(regOff, bi)], results: [sum] }));
|
|
1433
|
+
storeBase = sum;
|
|
1434
|
+
}
|
|
1435
|
+
irb.ops.push(mkOp('store', { operands: [storeBase, readData(reg(a), bi)], attrs: { off, width } }));
|
|
1323
1436
|
break;
|
|
1324
1437
|
}
|
|
1325
1438
|
case 'bl':
|
package/src/l3/ast.ts
CHANGED
|
@@ -43,7 +43,7 @@ export type Expr =
|
|
|
43
43
|
// pointer, so the byte offset resolves to a named field instead of a scaled array index).
|
|
44
44
|
// Unlike `index`, this carries the field NAME (which encodes the byte offset, `field_<off>`),
|
|
45
45
|
// not a width-scaled number — the byte-offset-carrying member access cpp.ts's sub-word guard needs.
|
|
46
|
-
| { k: 'field'; base: Expr; name: string }
|
|
46
|
+
| { k: 'field'; base: Expr; name: string; dot?: true }
|
|
47
47
|
// A GAP MARKER — the annotate-mode (`onGap: "annotate"`) spelling of a value asmlift could not
|
|
48
48
|
// faithfully lift (an unmodelled instruction's `opaque` result, an unlowered transient op, a
|
|
49
49
|
// dropped def). Every backend spells it as a call to the UNDEFINED symbol `ASMLIFT_ERROR("reason",
|
|
@@ -111,6 +111,10 @@ export interface SFn {
|
|
|
111
111
|
name: string;
|
|
112
112
|
params: { name: string; type: IrType }[];
|
|
113
113
|
locals: { name: string; type: IrType }[]; // recovered locals, declared at function top
|
|
114
|
+
/** project globals referenced with a known declaration shape (symbol map) — typed for the
|
|
115
|
+
* legalization env (exprCType) but NEVER declared by a backend: the project's own headers
|
|
116
|
+
* declare them, exactly like every other global name asmlift emits. */
|
|
117
|
+
globals?: { name: string; type: IrType }[];
|
|
114
118
|
retType: IrType;
|
|
115
119
|
body: Stmt[];
|
|
116
120
|
/** Struct types this function's fields reference, declared above it by the backend. Empty
|
|
@@ -148,7 +152,9 @@ export function dotBase(f: Extract<Expr, { k: 'field' }>): Extract<Expr, { k: 'i
|
|
|
148
152
|
|
|
149
153
|
/** Boolean projection of `dotBase` for conditions that need no narrowing. */
|
|
150
154
|
export function fieldSpellsDot(f: Extract<Expr, { k: 'field' }>): boolean {
|
|
151
|
-
|
|
155
|
+
// dot also spells a STRUCT-VALUE global's field (`gSym.field`, the symbol-map layout path) —
|
|
156
|
+
// marked explicitly by the structurer via `dot: true` since the base is a `var`, not an index.
|
|
157
|
+
return dotBase(f) !== undefined || f.dot === true;
|
|
152
158
|
}
|
|
153
159
|
|
|
154
160
|
/** Structural equality of two expression trees. THE one copy of Expr deep-equal (like
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// asmlift — SELF-DECLARING CANDIDATES: the pure map-reference query
|
|
2
|
+
// (research/self-declaring-candidates-2026-07-26.md).
|
|
3
|
+
//
|
|
4
|
+
// `collectSymbolRefs` derives, from a FINAL structured tree, every map-derived symbol the body
|
|
5
|
+
// references in a VALUE context — the input to the scoring layer's declaration synthesis
|
|
6
|
+
// (@asmlift/cli declare.ts). It is a pure tree query with no pipeline state: the enumeration
|
|
7
|
+
// layer (rank.ts) calls it exactly once per candidate, on the tree the candidate's source was
|
|
8
|
+
// emitted from, at the moment the candidate is finalized. There is deliberately NO cached
|
|
9
|
+
// `symbolRefs` field on `SFn` — a carried field would oblige every future l3 pass to remember
|
|
10
|
+
// to recompute it (a dead-store DCE that drops a tree's only reference would otherwise leave a
|
|
11
|
+
// stale ref, transitively reintroducing the hazards the collector excludes). Deriving at the
|
|
12
|
+
// consumption point makes staleness impossible by construction.
|
|
13
|
+
import type { SymbolInfo } from '../symbols';
|
|
14
|
+
import { Expr, Stmt, exprChildren, stmtChildren, stmtExprs } from './ast';
|
|
15
|
+
|
|
16
|
+
/** One recorded map-symbol VALUE reference — a name the tree references plus its map facts. */
|
|
17
|
+
export interface SymbolRef {
|
|
18
|
+
name: string;
|
|
19
|
+
info: SymbolInfo;
|
|
20
|
+
/** NAME-ONLY symbols (no map shape): the bare off-0 access facts observed in the candidate's
|
|
21
|
+
* own IR — attached by the enumeration (rank.ts bareGlobalAccessFacts), consumed by the
|
|
22
|
+
* declaration synthesis (declare.ts) as the width/signedness authority for `extern T name;`. */
|
|
23
|
+
access?: { width: number; signed: boolean };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The map-derived symbols a structured body references in a VALUE context — the input to the
|
|
27
|
+
* scoring layer's declaration synthesis. A name counts when it appears as a `var`/`addr` leaf
|
|
28
|
+
* and the map knows it (bare `gSym`, `&gSym`, `(u32)Func`, a `field` base — all reduce to
|
|
29
|
+
* those leaves). A name that is ANY call's target is excluded entirely, even if also
|
|
30
|
+
* value-referenced: prototyping a called symbol `void F(void);` hard-errors under gcc-2.9
|
|
31
|
+
* when the call passes args, while leaving it undeclared keeps today's implicit-declaration
|
|
32
|
+
* behavior (the one honest option without arity knowledge). The function's OWN name
|
|
33
|
+
* (`selfName`) is excluded too — the candidate's definition IS its declaration, and a
|
|
34
|
+
* synthesized `void F(void);` above `s32 F(...)` is a conflicting-types hard error (a
|
|
35
|
+
* self-address reference resolves against the definition itself). */
|
|
36
|
+
export function collectSymbolRefs(body: Stmt[], symbols: Map<string, SymbolInfo>, selfName: string): SymbolRef[] {
|
|
37
|
+
const called = new Set<string>();
|
|
38
|
+
const valueRefs = new Set<string>();
|
|
39
|
+
const visitExpr = (e: Expr): void => {
|
|
40
|
+
if (e.k === 'call') {
|
|
41
|
+
called.add(e.fn);
|
|
42
|
+
} else if ((e.k === 'var' || e.k === 'addr') && symbols.has(e.name)) {
|
|
43
|
+
valueRefs.add(e.name);
|
|
44
|
+
}
|
|
45
|
+
exprChildren(e).forEach(visitExpr);
|
|
46
|
+
};
|
|
47
|
+
const visitStmt = (s: Stmt): void => {
|
|
48
|
+
// an `assign` carries its target as a NAME, not an Expr — a scalar global WRITE
|
|
49
|
+
// (`gSym = x;`) references the symbol every bit as much as a read does
|
|
50
|
+
if (s.k === 'assign' && symbols.has(s.name)) {
|
|
51
|
+
valueRefs.add(s.name);
|
|
52
|
+
}
|
|
53
|
+
stmtExprs(s).forEach(visitExpr);
|
|
54
|
+
stmtChildren(s).forEach(visitStmt);
|
|
55
|
+
};
|
|
56
|
+
body.forEach(visitStmt);
|
|
57
|
+
return [...valueRefs]
|
|
58
|
+
.filter((n) => !called.has(n) && n !== selfName)
|
|
59
|
+
.sort()
|
|
60
|
+
.map((n) => ({ name: n, info: symbols.get(n)! }));
|
|
61
|
+
}
|
package/src/l3/typing.ts
CHANGED
|
@@ -27,6 +27,10 @@ export type VarTypes = (name: string) => IrType | undefined;
|
|
|
27
27
|
|
|
28
28
|
export function declaredTypes(fn: SFn): VarTypes {
|
|
29
29
|
const m = new Map<string, IrType>();
|
|
30
|
+
// shape-known project globals first, so a (theoretical) local of the same name wins
|
|
31
|
+
for (const g of fn.globals ?? []) {
|
|
32
|
+
m.set(g.name, g.type);
|
|
33
|
+
}
|
|
30
34
|
for (const p of fn.params) {
|
|
31
35
|
m.set(p.name, p.type);
|
|
32
36
|
}
|
package/src/macros.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// asmlift — address-cast macros: the OTHER way a project names a fixed RAM cell.
|
|
2
|
+
//
|
|
3
|
+
// Some decomp projects declare `extern u16 gCounter;` and let the linker place it; others write
|
|
4
|
+
// `#define gCounter (*(u16 *)0x03001234)`. Both read the same cell, but they are NOT
|
|
5
|
+
// interchangeable in the bytes an old compiler emits: an `extern` produces a RELOCATED literal-pool
|
|
6
|
+
// word (`.word gCounter`), the macro a NUMERIC one (`.word 0x3001234`). A target that shows the
|
|
7
|
+
// numeric word can therefore only be matched by the macro spelling — a symtab name will not do,
|
|
8
|
+
// and no `.symtab` carries these names in the first place (a macro is not a symbol).
|
|
9
|
+
//
|
|
10
|
+
// This module is the PURE recognizer over preprocessor output (`cpp -dD`). Everything it accepts is
|
|
11
|
+
// a fact it can name exactly; everything else is refused, because a wrong width or a dropped
|
|
12
|
+
// `volatile` is the plausible-but-wrong class — see the guards on {@link addressCastMacros}.
|
|
13
|
+
|
|
14
|
+
/** One recognized address-cast macro. */
|
|
15
|
+
export interface AddressMacro {
|
|
16
|
+
name: string;
|
|
17
|
+
/** the cell's address — the map key this macro names */
|
|
18
|
+
address: number;
|
|
19
|
+
/** the macro body VERBATIM, as the declaration must reproduce it */
|
|
20
|
+
body: string;
|
|
21
|
+
/** the cast's byte width */
|
|
22
|
+
size: number;
|
|
23
|
+
/** the cast type's signedness */
|
|
24
|
+
signed: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The scalar type spellings a cast may use, and what each one means. Deliberately a CLOSED table:
|
|
28
|
+
* an unrecognized spelling (a project typedef, an enum, a struct) is refused rather than guessed,
|
|
29
|
+
* and every `volatile` alias is absent so it can never be silently dropped — the qualifier changes
|
|
30
|
+
* whether repeated reads may be folded, which is both a byte and a semantic difference. */
|
|
31
|
+
const SCALAR_TYPES: Record<string, { size: number; signed: boolean }> = {
|
|
32
|
+
u8: { size: 1, signed: false },
|
|
33
|
+
s8: { size: 1, signed: true },
|
|
34
|
+
u16: { size: 2, signed: false },
|
|
35
|
+
s16: { size: 2, signed: true },
|
|
36
|
+
u32: { size: 4, signed: false },
|
|
37
|
+
s32: { size: 4, signed: true },
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** `#define NAME (*(TYPE *)0xADDR)` — the ONE shape recognized. Anything else (a two-level
|
|
41
|
+
* indirection, an offset expression, a function-like macro, a bare integer constant) does not
|
|
42
|
+
* match and is therefore refused by construction. */
|
|
43
|
+
const ADDRESS_CAST = /^\s*#define\s+([A-Za-z_]\w*)\s+(\(\s*\*\s*\(\s*(\w+)\s*\*\s*\)\s*(0[xX][0-9A-Fa-f]+)\s*\))\s*$/;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Recognize the address-cast macros in `cpp -dD` output, keyed by the address each names.
|
|
47
|
+
*
|
|
48
|
+
* REFUSALS, all of them because the alternative is a plausible-but-wrong spelling:
|
|
49
|
+
* - a cast type outside {@link SCALAR_TYPES} — including every `volatile` alias (`vu16`), whose
|
|
50
|
+
* qualifier must not be silently dropped;
|
|
51
|
+
* - two macros naming the SAME address (`REG_VCOUNT`/`REG_VCOUNT_L`/`REG_VCOUNT_H` at 0x04000006
|
|
52
|
+
* differ in width, and picking wrong turns an `ldrh` into an `ldrb`) — both are dropped;
|
|
53
|
+
* - one name defined at two addresses, which no correct spelling can disambiguate.
|
|
54
|
+
*/
|
|
55
|
+
export function addressCastMacros(cppOutput: string): Map<number, AddressMacro> {
|
|
56
|
+
return addressCastMacrosFrom(cppOutput.split('\n'));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The same recognizer over already-split `#define NAME body` lines — what a DWARF
|
|
60
|
+
* `.debug_macinfo` reader produces once each definition is re-spelled as a directive. */
|
|
61
|
+
export function addressCastMacrosFrom(defineLines: readonly string[]): Map<number, AddressMacro> {
|
|
62
|
+
const byAddress = new Map<number, AddressMacro>();
|
|
63
|
+
const collided = new Set<number>();
|
|
64
|
+
const seenNames = new Map<string, number>();
|
|
65
|
+
for (const line of defineLines) {
|
|
66
|
+
const m = ADDRESS_CAST.exec(line);
|
|
67
|
+
if (!m) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const [, name, body, typeName, addrText] = m;
|
|
71
|
+
const type = SCALAR_TYPES[typeName];
|
|
72
|
+
if (!type) {
|
|
73
|
+
continue; // unknown or volatile-qualified spelling — refuse
|
|
74
|
+
}
|
|
75
|
+
const address = Number.parseInt(addrText, 16);
|
|
76
|
+
if (!Number.isFinite(address)) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const priorAddr = seenNames.get(name);
|
|
80
|
+
if (priorAddr !== undefined && priorAddr !== address) {
|
|
81
|
+
collided.add(priorAddr);
|
|
82
|
+
collided.add(address);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
seenNames.set(name, address);
|
|
86
|
+
const prior = byAddress.get(address);
|
|
87
|
+
if (prior && prior.name !== name) {
|
|
88
|
+
collided.add(address);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
byAddress.set(address, { name, address, body, size: type.size, signed: type.signed });
|
|
92
|
+
}
|
|
93
|
+
for (const addr of collided) {
|
|
94
|
+
byAddress.delete(addr);
|
|
95
|
+
}
|
|
96
|
+
return byAddress;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The `#define` lines for every address-cast macro in `symbols` that `source` actually names.
|
|
101
|
+
*
|
|
102
|
+
* A published source that spells `gCollisionMapPtr` only compiles where that macro is defined —
|
|
103
|
+
* and a REPRODUCTION of it must therefore carry the definition, or the script the benchmark
|
|
104
|
+
* publishes cannot build the very source it publishes. Selected by the names the source uses
|
|
105
|
+
* rather than by the whole map, so a reproduction context stays the size of what it needs.
|
|
106
|
+
*
|
|
107
|
+
* Name-sorted and deduplicated: the materialized context must be byte-stable across machines.
|
|
108
|
+
*/
|
|
109
|
+
export function macroDefinesUsedBy(
|
|
110
|
+
symbols: Map<number, { name: string; macroBody?: string }[]>,
|
|
111
|
+
source: string,
|
|
112
|
+
): string {
|
|
113
|
+
const used = new Map<string, string>();
|
|
114
|
+
for (const infos of symbols.values()) {
|
|
115
|
+
for (const info of infos) {
|
|
116
|
+
if (info.macroBody === undefined || used.has(info.name)) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (new RegExp(`\\b${info.name}\\b`).test(source)) {
|
|
120
|
+
used.set(info.name, info.macroBody);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const names = [...used.keys()].sort();
|
|
125
|
+
return names.length ? names.map((n) => `#define ${n} ${used.get(n)}`).join('\n') + '\n' : '';
|
|
126
|
+
}
|
package/src/pipeline.ts
CHANGED
|
@@ -13,12 +13,13 @@ import { Expr, LanguageBackend, SFn, Stmt, exprChildren, stmtChildren, stmtExprs
|
|
|
13
13
|
import { hoistReusedGlobalBases } from './l3/basecse';
|
|
14
14
|
import { eliminateDeadStores } from './l3/dce';
|
|
15
15
|
import { DEFAULT_IDIOM_PATTERNS, RewritePattern, applyPattern, dce, patternApplies } from './pattern/engine';
|
|
16
|
-
import type
|
|
16
|
+
import { type Prototypes, prototypesFromSymbols } from './proto';
|
|
17
17
|
import { RaiseUnsupportedError } from './raise/errors';
|
|
18
18
|
import { type PreRecoveryPass, runPreRecovery } from './raise/pre-recovery';
|
|
19
19
|
import { recoverTypes } from './raise/recover';
|
|
20
20
|
import { sinkReturns } from './raise/retsink';
|
|
21
21
|
import { StructureError, structure } from './structure/structure';
|
|
22
|
+
import { type SymbolMap, symbolsByName } from './symbols';
|
|
22
23
|
import { type TargetDescription, structureOptionsFor } from './target';
|
|
23
24
|
|
|
24
25
|
/** How a gap (a construct asmlift cannot faithfully model) degrades:
|
|
@@ -53,6 +54,10 @@ export interface DecompileOptions {
|
|
|
53
54
|
* MIPS/PPC switch declines/loud-fails; present ⇒ the frontend recovers the `switch_br`.
|
|
54
55
|
* Produced by `extractAsmData(obj, target)` from the scoring object. */
|
|
55
56
|
asmData?: AsmData;
|
|
57
|
+
/** OPTIONAL address→symbol map (symbols.ts) — the project's own names (ELF symtab) and
|
|
58
|
+
* declaration shapes (DWARF types-sidecar). Drives the Thumb numeric-pool promotion and the
|
|
59
|
+
* byte-sensitive global spellings. Absent ⇒ byte-identical to today. */
|
|
60
|
+
symbols?: SymbolMap;
|
|
56
61
|
/** gap policy — see `OnGap`. Default "strict". */
|
|
57
62
|
onGap?: OnGap;
|
|
58
63
|
}
|
|
@@ -97,9 +102,11 @@ function runTower(
|
|
|
97
102
|
onGap: OnGap,
|
|
98
103
|
): DecompileResult {
|
|
99
104
|
const backend = opts.backend ?? cBackend;
|
|
100
|
-
|
|
105
|
+
// The project's own DWARF signatures fill in what the caller did not state — in practice the
|
|
106
|
+
// CALLEES (a function still in assembly has none), which is what makes this transferable.
|
|
107
|
+
const prototypes = prototypesFromSymbols(opts.symbols, opts.prototypes ?? {});
|
|
101
108
|
// (1) lift: ISA frontend (resolved by target) → L1 with block-argument SSA
|
|
102
|
-
const fn = frontendFor(target).lift(name, asm, target, prototypes, opts.asmData);
|
|
109
|
+
const fn = frontendFor(target).lift(name, asm, target, prototypes, opts.asmData, opts.symbols);
|
|
103
110
|
verify(fn);
|
|
104
111
|
const raw = print(fn);
|
|
105
112
|
|
|
@@ -115,7 +122,11 @@ function runTower(
|
|
|
115
122
|
|
|
116
123
|
// (4) structure: IR → neutral AST; boundary contract: no unresolved value leaked (strict), or
|
|
117
124
|
// every unresolved value spelled as a loud ASMLIFT_ERROR marker (annotate).
|
|
118
|
-
const sfn = structureChecked(fn, {
|
|
125
|
+
const sfn = structureChecked(fn, {
|
|
126
|
+
...structureOptionsFor(target, prototypes[name]?.returnsVoid ?? false),
|
|
127
|
+
onGap,
|
|
128
|
+
...(opts.symbols ? { symbols: symbolsByName(opts.symbols) } : {}),
|
|
129
|
+
});
|
|
119
130
|
|
|
120
131
|
// (5) lower + print: neutral AST → target language
|
|
121
132
|
const source = backend.emit(sfn);
|
package/src/proto.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { SymbolMap } from './symbols';
|
|
2
|
+
|
|
1
3
|
// asmlift — function prototypes: the single carrier for the caller-supplied facts a
|
|
2
4
|
// matching-decomp project reads from its headers (arg counts, void-ness). One `Prototypes`
|
|
3
5
|
// map, keyed by symbol, is threaded through every entry point and resolved at the point of
|
|
@@ -40,3 +42,56 @@ export function protoArity(p: FnProto | undefined): number | undefined {
|
|
|
40
42
|
// fall back to the frontend's arg-register heuristic rather than misread a string's `.length`.
|
|
41
43
|
return undefined;
|
|
42
44
|
}
|
|
45
|
+
|
|
46
|
+
/** The C type spelling for one declared parameter/return, or null when the facts do not
|
|
47
|
+
* determine one. A pointer is `void *` — address-identical to any object pointer, and asmlift
|
|
48
|
+
* makes every stride explicit — so nothing is guessed about what it points at. */
|
|
49
|
+
function typeSpelling(t: { size: number | null; signed: boolean | null; pointer?: boolean }): ParamType | null {
|
|
50
|
+
if (t.pointer) {
|
|
51
|
+
return 'void *';
|
|
52
|
+
}
|
|
53
|
+
if (t.size === 1 || t.size === 2 || t.size === 4) {
|
|
54
|
+
// A signless 4-byte type is the C89 enum idiom (int); a signless NARROW one has no honest
|
|
55
|
+
// spelling, and the width alone would not fix its load, so it is refused.
|
|
56
|
+
if (t.signed === null) {
|
|
57
|
+
return t.size === 4 ? 's32' : null;
|
|
58
|
+
}
|
|
59
|
+
return `${t.signed ? 's' : 'u'}${t.size * 8}`;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Prototypes the project's own DWARF states, merged UNDER the caller's.
|
|
66
|
+
*
|
|
67
|
+
* A caller-supplied proto always wins: it comes from the user's headers or the benchmark
|
|
68
|
+
* manifest, and it is the thing a real user actually has for the function they are decompiling.
|
|
69
|
+
* The map fills the rest — in practice the CALLEES, since a function still written in assembly
|
|
70
|
+
* has no signature in its project's ELF (see SymbolSignature).
|
|
71
|
+
*
|
|
72
|
+
* Every parameter must spell faithfully or the whole entry is dropped: a partly-typed list would
|
|
73
|
+
* be read for its LENGTH and give the right arity with the wrong widths, which is worse than the
|
|
74
|
+
* arg-register heuristic it would replace.
|
|
75
|
+
*/
|
|
76
|
+
export function prototypesFromSymbols(symbols: SymbolMap | undefined, base: Prototypes = {}): Prototypes {
|
|
77
|
+
if (!symbols) {
|
|
78
|
+
return base;
|
|
79
|
+
}
|
|
80
|
+
const out: Prototypes = { ...base };
|
|
81
|
+
for (const infos of symbols.values()) {
|
|
82
|
+
for (const info of infos) {
|
|
83
|
+
if (info.kind !== 'code' || !info.signature || out[info.name] !== undefined) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const params = info.signature.params.map(typeSpelling);
|
|
87
|
+
if (params.some((p) => p === null)) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
out[info.name] = {
|
|
91
|
+
params: params as ParamType[],
|
|
92
|
+
...(info.signature.returns === null ? { returnsVoid: true } : {}),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
package/src/raise/magicdiv.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// asmlift — magic-number constant-division recovery (L1 recognition;
|
|
1
|
+
// asmlift — magic-number constant-division recovery (L1 recognition; gcc2.7.2kmc + mwcc_242_81).
|
|
2
2
|
//
|
|
3
3
|
// A compiler replaces `x / C` for a non-power-of-2 constant `C` with a HIGH-WORD MULTIPLY by a
|
|
4
4
|
// precomputed "magic" reciprocal `M`, a shift `s`, and a sign correction. The frontend lifts the
|