@asmlift/core 0.2.0 → 0.4.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 +5 -3
- package/package.json +1 -1
- package/src/backend/cfamily.ts +154 -5
- package/src/backend/cpp.ts +3 -1
- package/src/backend/pascal.ts +11 -0
- package/src/contracts.ts +37 -5
- package/src/declare.ts +251 -0
- package/src/frontend/frontend.ts +12 -2
- package/src/frontend/mips.ts +24 -23
- package/src/frontend/opaque.ts +39 -2
- package/src/frontend/ssa.ts +32 -53
- package/src/frontend/thumb.ts +420 -32
- package/src/ir/opcodes.ts +44 -0
- package/src/ir/simplify.ts +72 -0
- package/src/l3/argbase.ts +216 -0
- package/src/l3/ast.ts +126 -6
- package/src/l3/basecse.ts +3 -40
- package/src/l3/coalesce.ts +146 -0
- package/src/l3/dce.ts +2 -23
- package/src/l3/hoist.ts +65 -0
- package/src/l3/reindex.ts +7 -0
- package/src/l3/scopebase.ts +436 -0
- package/src/l3/symbol-refs.ts +61 -0
- package/src/l3/tailmerge.ts +120 -0
- package/src/l3/typing.ts +4 -0
- package/src/macros.ts +335 -0
- package/src/pattern/engine.ts +99 -6
- package/src/pipeline.ts +20 -6
- package/src/proto.ts +55 -0
- package/src/raise/divpow2.ts +226 -0
- package/src/raise/gvn.ts +141 -0
- package/src/raise/pre-recovery.ts +37 -3
- package/src/raise/recover.ts +24 -7
- package/src/raise/retsink.ts +36 -7
- package/src/raise/shortcircuit.ts +264 -22
- package/src/raise/structs.ts +12 -2
- package/src/rank.ts +370 -79
- package/src/structure/analysis.ts +42 -1
- package/src/structure/structure.ts +852 -67
- package/src/structure/switch-recover.ts +21 -3
- package/src/symbols.ts +541 -0
- package/src/target.ts +4 -2
- package/src/trace.ts +17 -2
package/src/macros.ts
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
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
|
+
/** the cast type was volatile-qualified (`vu16`) — the MMIO idiom. Load-bearing: a
|
|
26
|
+
* non-volatile spelling lets the compiler fold or reorder repeated accesses. */
|
|
27
|
+
volatile?: true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The scalar type spellings a cast may use, and what each one means. Deliberately a CLOSED table:
|
|
31
|
+
* an unrecognized spelling (a project typedef, an enum, a struct) is refused rather than guessed,
|
|
32
|
+
* and every `volatile` alias is absent so it can never be silently dropped — the qualifier changes
|
|
33
|
+
* whether repeated reads may be folded, which is both a byte and a semantic difference. */
|
|
34
|
+
const SCALAR_TYPES: Record<string, { size: number; signed: boolean; volatile?: true }> = {
|
|
35
|
+
u8: { size: 1, signed: false },
|
|
36
|
+
s8: { size: 1, signed: true },
|
|
37
|
+
u16: { size: 2, signed: false },
|
|
38
|
+
s16: { size: 2, signed: true },
|
|
39
|
+
u32: { size: 4, signed: false },
|
|
40
|
+
s32: { size: 4, signed: true },
|
|
41
|
+
// The `volatile` aliases. They were excluded so the qualifier could never be silently dropped;
|
|
42
|
+
// it is now CARRIED instead (`volatile: true`, reproduced by every spelling this feeds), which
|
|
43
|
+
// is the same guarantee without the cost — refusing them lost every MMIO register name a GBA
|
|
44
|
+
// project has, since those are exactly the cells one declares volatile.
|
|
45
|
+
vu8: { size: 1, signed: false, volatile: true },
|
|
46
|
+
vs8: { size: 1, signed: true, volatile: true },
|
|
47
|
+
vu16: { size: 2, signed: false, volatile: true },
|
|
48
|
+
vs16: { size: 2, signed: true, volatile: true },
|
|
49
|
+
vu32: { size: 4, signed: false, volatile: true },
|
|
50
|
+
vs32: { size: 4, signed: true, volatile: true },
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** A pointer cast inside an address expression (`(void *)0x4000000`). The VALUE is the integer it
|
|
54
|
+
* wraps: these headers spell a register base as a `void *` and add a byte offset to it, which is
|
|
55
|
+
* GCC's byte-arithmetic extension, so the cast contributes nothing to the address.
|
|
56
|
+
*
|
|
57
|
+
* BYTE-SIZED POINTEES ONLY, and that restriction is load-bearing rather than tidy. C pointer
|
|
58
|
+
* arithmetic SCALES by the pointee: `(vu16 *)0x4000000 + 5` is 0x400000A, not 0x4000005. Stripping
|
|
59
|
+
* a wider cast would fold the wrong address AND then republish it in a synthesized body that
|
|
60
|
+
* agrees with itself — so the candidate still byte-matches the numeric pool word it was looked up
|
|
61
|
+
* by, while naming a different register. A wrong name that survives the differ is the one failure
|
|
62
|
+
* this module cannot let through.
|
|
63
|
+
*
|
|
64
|
+
* A wider pointee is REFUSED EXPLICITLY below, not left to fall out of the token grammar further
|
|
65
|
+
* down — the enforcing line belongs next to the rule it enforces. The cost is named rather than
|
|
66
|
+
* hidden: a wider cast with NO arithmetic after it would fold correctly and is refused anyway,
|
|
67
|
+
* because the hazard is cast-THEN-add and this cannot tell which it is looking at. */
|
|
68
|
+
const PTR_CAST_ANY = /\(\s*(\w+)\s*\*\s*\)/g;
|
|
69
|
+
const BYTE_POINTEE = new Set(['void', 'u8', 's8', 'vu8', 'vs8']);
|
|
70
|
+
|
|
71
|
+
/** `src` with byte-sized pointer casts removed, or null if any cast SCALES. */
|
|
72
|
+
function stripPointerCasts(src: string): string | null {
|
|
73
|
+
let scaling = false;
|
|
74
|
+
const out = src.replace(PTR_CAST_ANY, (_m, pointee: string) => {
|
|
75
|
+
if (!BYTE_POINTEE.has(pointee)) {
|
|
76
|
+
scaling = true;
|
|
77
|
+
}
|
|
78
|
+
return ' ';
|
|
79
|
+
});
|
|
80
|
+
return scaling ? null : out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** An object-like `#define NAME body`, for the expansion table the address evaluator resolves
|
|
84
|
+
* identifiers against. Function-like macros (`NAME(x)`) are deliberately excluded: an address
|
|
85
|
+
* expression that calls one is refused, not expanded. */
|
|
86
|
+
const OBJECT_DEFINE = /^\s*#define\s+([A-Za-z_]\w*)\s+(\S.*?)\s*$/;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Evaluate a macro's ADDRESS operand to a number, or null when it is not a constant expression
|
|
90
|
+
* this module can be sure of.
|
|
91
|
+
*
|
|
92
|
+
* Real decomp headers rarely write the address as a literal. The Klonoa headers spell every
|
|
93
|
+
* register as `(*(vu16 *)REG_ADDR_BLDALPHA)` over `REG_ADDR_BLDALPHA = (REG_BASE +
|
|
94
|
+
* REG_OFFSET_BLDALPHA)`, `REG_BASE = (void *)0x4000000`, `REG_OFFSET_BLDALPHA = 0x52` — so a
|
|
95
|
+
* literal-only recognizer sees none of the 466 `REG_*` names, and reads every MMIO cell as a
|
|
96
|
+
* decimal address instead.
|
|
97
|
+
*
|
|
98
|
+
* The accepted language is deliberately tiny — integer literals, `+`, `-`, parentheses, pointer
|
|
99
|
+
* casts (see {@link PTR_CAST}), and identifiers that resolve to another object-like define. Any
|
|
100
|
+
* other token, an unknown identifier, a function-like macro, a cycle, or a negative result refuses
|
|
101
|
+
* the whole expression. Folding is done on the EXPANDED integer text, so an operand only ever
|
|
102
|
+
* evaluates to a number every step of which this module recognized.
|
|
103
|
+
*/
|
|
104
|
+
function evalAddressExpr(
|
|
105
|
+
src: string,
|
|
106
|
+
defines: ReadonlyMap<string, string>,
|
|
107
|
+
seen: ReadonlySet<string>,
|
|
108
|
+
memo: Map<string, number | null> = new Map(),
|
|
109
|
+
): number | null {
|
|
110
|
+
if (seen.size > 12) {
|
|
111
|
+
return null; // pathological nesting — refuse rather than walk further
|
|
112
|
+
}
|
|
113
|
+
const stripped = stripPointerCasts(src);
|
|
114
|
+
if (stripped === null) {
|
|
115
|
+
return null; // a scaling pointer cast — see PTR_CAST_ANY
|
|
116
|
+
}
|
|
117
|
+
const tokens = stripped.match(/[A-Za-z_]\w*|0[xX][0-9A-Fa-f]+|\d+|[()+-]/g);
|
|
118
|
+
// every character must belong to a token — anything else (`*`, `<<`, a comma) is out of language
|
|
119
|
+
if (!tokens || tokens.join('') !== stripped.replace(/\s+/g, '')) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
const expanded: string[] = [];
|
|
123
|
+
for (const tok of tokens) {
|
|
124
|
+
if (/^[A-Za-z_]/.test(tok)) {
|
|
125
|
+
const body = defines.get(tok);
|
|
126
|
+
if (body === undefined || seen.has(tok)) {
|
|
127
|
+
return null; // undefined name, or a cycle
|
|
128
|
+
}
|
|
129
|
+
// Memoized per NAME. The depth cap bounds nesting but not BRANCHING — a define mentioning k
|
|
130
|
+
// others re-evaluates the whole subtree k times, so a deep, wide table costs exponentially.
|
|
131
|
+
//
|
|
132
|
+
// A name's result CAN depend on the path that reached it: both refusals below are
|
|
133
|
+
// path-sensitive (already in `seen`; depth cap hit), so a cached `null` may be pessimistic
|
|
134
|
+
// for a shorter path. Safe in ONE direction only — path-dependence can make this refuse
|
|
135
|
+
// more, never fold a wrong address, which is the direction this module may be wrong in.
|
|
136
|
+
//
|
|
137
|
+
// The memo being PER TOP-LEVEL MACRO (the default parameter, fresh at each entry) is
|
|
138
|
+
// load-bearing rather than incidental: hoisting it across macros to "go faster" would let
|
|
139
|
+
// one deep macro poison a name for every macro after it, silently dropping recognized cells.
|
|
140
|
+
let inner: number | null;
|
|
141
|
+
if (memo.has(tok)) {
|
|
142
|
+
inner = memo.get(tok)!;
|
|
143
|
+
} else {
|
|
144
|
+
inner = evalAddressExpr(body, defines, new Set([...seen, tok]), memo);
|
|
145
|
+
memo.set(tok, inner);
|
|
146
|
+
}
|
|
147
|
+
if (inner === null) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
expanded.push(`(${inner})`);
|
|
151
|
+
} else if (/^0[xX]/.test(tok)) {
|
|
152
|
+
expanded.push(String(Number.parseInt(tok, 16)));
|
|
153
|
+
} else {
|
|
154
|
+
expanded.push(tok);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const folded = foldIntegerExpr(expanded.join(' '));
|
|
158
|
+
return folded !== null && Number.isSafeInteger(folded) && folded >= 0 ? folded : null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Fold a fully-expanded `+`/`-`/parenthesis integer expression. Written out rather than handed to
|
|
162
|
+
* an evaluator so nothing outside that grammar can ever be executed. */
|
|
163
|
+
function foldIntegerExpr(text: string): number | null {
|
|
164
|
+
const toks = text.match(/\d+|[()+-]/g);
|
|
165
|
+
if (!toks || toks.join('') !== text.replace(/\s+/g, '')) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
let at = 0;
|
|
169
|
+
const expr = (): number | null => {
|
|
170
|
+
let acc = term();
|
|
171
|
+
if (acc === null) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
while (toks[at] === '+' || toks[at] === '-') {
|
|
175
|
+
const op = toks[at++];
|
|
176
|
+
const rhs = term();
|
|
177
|
+
if (rhs === null) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
acc = op === '+' ? acc + rhs : acc - rhs;
|
|
181
|
+
}
|
|
182
|
+
return acc;
|
|
183
|
+
};
|
|
184
|
+
const term = (): number | null => {
|
|
185
|
+
if (toks[at] === '(') {
|
|
186
|
+
at++;
|
|
187
|
+
const inner = expr();
|
|
188
|
+
if (inner === null || toks[at] !== ')') {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
at++;
|
|
192
|
+
return inner;
|
|
193
|
+
}
|
|
194
|
+
if (toks[at] === '-') {
|
|
195
|
+
at++;
|
|
196
|
+
const v = term();
|
|
197
|
+
return v === null ? null : -v;
|
|
198
|
+
}
|
|
199
|
+
const tok = toks[at];
|
|
200
|
+
if (tok === undefined || !/^\d+$/.test(tok)) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
at++;
|
|
204
|
+
return Number(tok);
|
|
205
|
+
};
|
|
206
|
+
const value = expr();
|
|
207
|
+
return value !== null && at === toks.length ? value : null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** `#define NAME (*(TYPE *)ADDR)` — the ONE shape recognized, where ADDR is any constant
|
|
211
|
+
* expression {@link evalAddressExpr} can be sure of (a literal, or names that resolve to one).
|
|
212
|
+
* Anything else — a two-level indirection `(*(T **)…)`, a function-like macro, a bare integer
|
|
213
|
+
* constant — does not match and is therefore refused by construction. */
|
|
214
|
+
const ADDRESS_CAST = /^\s*#define\s+([A-Za-z_]\w*)\s+(\(\s*\*\s*\(\s*(\w+)\s*\*\s*\)\s*(.+?)\s*\))\s*$/;
|
|
215
|
+
|
|
216
|
+
/** A bare hex literal — the operand form whose macro body is already self-contained. */
|
|
217
|
+
const HEX_LITERAL = /^0[xX][0-9A-Fa-f]+$/;
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Recognize the address-cast macros in `cpp -dD` output, keyed by the address each names.
|
|
221
|
+
*
|
|
222
|
+
* REFUSALS, all of them because the alternative is a plausible-but-wrong spelling:
|
|
223
|
+
* - a cast type outside {@link SCALAR_TYPES} — a project typedef, an enum, a struct;
|
|
224
|
+
* - an address expression {@link evalAddressExpr} cannot fold to a definite number;
|
|
225
|
+
* - two macros naming the SAME address (`REG_VCOUNT`/`REG_VCOUNT_L`/`REG_VCOUNT_H` at 0x04000006
|
|
226
|
+
* differ in width, and picking wrong turns an `ldrh` into an `ldrb`) — both are dropped;
|
|
227
|
+
* - one name defined at two addresses, which no correct spelling can disambiguate.
|
|
228
|
+
*/
|
|
229
|
+
export function addressCastMacros(cppOutput: string): Map<number, AddressMacro> {
|
|
230
|
+
return addressCastMacrosFrom(cppOutput.split('\n'));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** The same recognizer over already-split `#define NAME body` lines — what a DWARF
|
|
234
|
+
* `.debug_macinfo` reader produces once each definition is re-spelled as a directive. */
|
|
235
|
+
export function addressCastMacrosFrom(defineLines: readonly string[]): Map<number, AddressMacro> {
|
|
236
|
+
// Pass 1: every object-like define, so an address expression can resolve the names it mentions.
|
|
237
|
+
// A macro's address is frequently spelled in terms of others (`REG_BASE + REG_OFFSET_X`), and
|
|
238
|
+
// those helpers are not themselves address casts — they exist only to be expanded.
|
|
239
|
+
//
|
|
240
|
+
// LAST DEFINITION WINS, and `#undef` is not modelled: the record is a flat list with no scope, so
|
|
241
|
+
// a name redefined differently across translation units resolves to whichever came last. Sound
|
|
242
|
+
// for a project whose headers agree (the Klonoa ELF redefines no name with a differing body);
|
|
243
|
+
// a project where they disagree would need per-CU scoping, which the record does not carry.
|
|
244
|
+
const defines = new Map<string, string>();
|
|
245
|
+
for (const line of defineLines) {
|
|
246
|
+
const d = OBJECT_DEFINE.exec(line);
|
|
247
|
+
if (d) {
|
|
248
|
+
defines.set(d[1], d[2]);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const byAddress = new Map<number, AddressMacro>();
|
|
252
|
+
const collided = new Set<number>();
|
|
253
|
+
const seenNames = new Map<string, number>();
|
|
254
|
+
for (const line of defineLines) {
|
|
255
|
+
const m = ADDRESS_CAST.exec(line);
|
|
256
|
+
if (!m) {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const [, name, rawBody, typeName, addrText] = m;
|
|
260
|
+
const type = SCALAR_TYPES[typeName];
|
|
261
|
+
if (!type) {
|
|
262
|
+
continue; // a spelling outside the closed table — refuse
|
|
263
|
+
}
|
|
264
|
+
const address = evalAddressExpr(addrText, defines, new Set([name]));
|
|
265
|
+
if (address === null) {
|
|
266
|
+
continue; // an address expression this module cannot be sure of — refuse
|
|
267
|
+
}
|
|
268
|
+
// The body must be SELF-CONTAINED and COMPILABLE, because it is republished verbatim as the
|
|
269
|
+
// definition a reproduction compiles against (macroDefinesUsedBy) — a body naming
|
|
270
|
+
// `REG_ADDR_VCOUNT` would need that macro, and its two helpers, carried along with it. An
|
|
271
|
+
// unqualified literal address keeps the project's own spelling; anything else is re-spelled at
|
|
272
|
+
// the address it evaluated to, which is the same cell and the same type.
|
|
273
|
+
// A VOLATILE body is re-spelled even when its address is already a literal: the alias it uses
|
|
274
|
+
// (`vu8`) is a PROJECT typedef, and the prelude a candidate compiles against declares only
|
|
275
|
+
// u8/u16/u32 + s8/s16/s32. Keeping such a body verbatim republishes a `#define` that does not
|
|
276
|
+
// compile — latent, because it only bites in the self-declared world.
|
|
277
|
+
const body =
|
|
278
|
+
HEX_LITERAL.test(addrText) && !type.volatile
|
|
279
|
+
? rawBody
|
|
280
|
+
: `(*(${type.volatile ? 'volatile ' : ''}${type.signed ? 's' : 'u'}${type.size * 8} *)0x${address.toString(16).toUpperCase()})`;
|
|
281
|
+
const priorAddr = seenNames.get(name);
|
|
282
|
+
if (priorAddr !== undefined && priorAddr !== address) {
|
|
283
|
+
collided.add(priorAddr);
|
|
284
|
+
collided.add(address);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
seenNames.set(name, address);
|
|
288
|
+
const prior = byAddress.get(address);
|
|
289
|
+
if (prior && prior.name !== name) {
|
|
290
|
+
collided.add(address);
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
byAddress.set(address, {
|
|
294
|
+
name,
|
|
295
|
+
address,
|
|
296
|
+
body,
|
|
297
|
+
size: type.size,
|
|
298
|
+
signed: type.signed,
|
|
299
|
+
...(type.volatile ? { volatile: true as const } : {}),
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
for (const addr of collided) {
|
|
303
|
+
byAddress.delete(addr);
|
|
304
|
+
}
|
|
305
|
+
return byAddress;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* The `#define` lines for every address-cast macro in `symbols` that `source` actually names.
|
|
310
|
+
*
|
|
311
|
+
* A published source that spells `gCollisionMapPtr` only compiles where that macro is defined —
|
|
312
|
+
* and a REPRODUCTION of it must therefore carry the definition, or the script the benchmark
|
|
313
|
+
* publishes cannot build the very source it publishes. Selected by the names the source uses
|
|
314
|
+
* rather than by the whole map, so a reproduction context stays the size of what it needs.
|
|
315
|
+
*
|
|
316
|
+
* Name-sorted and deduplicated: the materialized context must be byte-stable across machines.
|
|
317
|
+
*/
|
|
318
|
+
export function macroDefinesUsedBy(
|
|
319
|
+
symbols: Map<number, { name: string; macroBody?: string }[]>,
|
|
320
|
+
source: string,
|
|
321
|
+
): string {
|
|
322
|
+
const used = new Map<string, string>();
|
|
323
|
+
for (const infos of symbols.values()) {
|
|
324
|
+
for (const info of infos) {
|
|
325
|
+
if (info.macroBody === undefined || used.has(info.name)) {
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (new RegExp(`\\b${info.name}\\b`).test(source)) {
|
|
329
|
+
used.set(info.name, info.macroBody);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const names = [...used.keys()].sort();
|
|
334
|
+
return names.length ? names.map((n) => `#define ${n} ${used.get(n)}`).join('\n') + '\n' : '';
|
|
335
|
+
}
|
package/src/pattern/engine.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// Crucially, rewrites go through replaceAllUsesWith + DCE — never in-place opcode
|
|
7
7
|
// mutation of a live value.
|
|
8
8
|
import { Fn, Op, Value, defOpMap, mkOp, mkValue, replaceAllUsesWith } from '../ir/core';
|
|
9
|
-
import { type Opcode, isDceSafe } from '../ir/opcodes';
|
|
9
|
+
import { NEGATED_ICMP, type Opcode, isDceSafe } from '../ir/opcodes';
|
|
10
10
|
import type { IrType } from '../ir/types';
|
|
11
11
|
import { T } from '../ir/types';
|
|
12
12
|
|
|
@@ -219,18 +219,107 @@ const sextPat = (w: number, k: number): RewritePattern => ({
|
|
|
219
219
|
|
|
220
220
|
/** Byte/half zero- and sign-extension casts. Byte = shift by 24, half = shift by 16. The
|
|
221
221
|
* zero-extend forms fix a miscompile; the sign-extend forms already byte-matched as raw shifts and
|
|
222
|
-
* fold here for readability + `(s8)`/`(s16)` parity, staying byte-exact (`(s8)x` → `lsl;asr`).
|
|
222
|
+
* fold here for readability + `(s8)`/`(s16)` parity, staying byte-exact (`(s8)x` → `lsl;asr`).
|
|
223
|
+
*
|
|
224
|
+
* SHADOWING NOTE: these run at the idiom stage, BEFORE structuring — so a symbol-map BITFIELD of
|
|
225
|
+
* width exactly 8 or 16 whose bits start at bit 0 of its load is folded to `(u8)x`/`(u16)x` here
|
|
226
|
+
* and never reaches the bitfield member recognizer (structure.ts, which matches the raw
|
|
227
|
+
* `shr(shl(load))` shape only). Honest output, not a miscompile — the field just keeps the cast
|
|
228
|
+
* spelling at those widths. Teaching the recognizer a zext/sext arm is the coverage extension if
|
|
229
|
+
* a row ever needs it. */
|
|
223
230
|
export const CAST_PATTERNS: RewritePattern[] = [zextPat(8, 24), zextPat(16, 16), sextPat(8, 24), sextPat(16, 16)];
|
|
224
231
|
|
|
232
|
+
// ── boolean-negation idiom ───────────────────────────────────────────────────────────────────
|
|
233
|
+
// `cmp ^ 1` IS `!cmp`: an `icmp_*` result is 0 or 1 by construction (ir/opcodes.ts), so xoring the
|
|
234
|
+
// low bit flips exactly the boolean. A compiler with no set-on-greater-equal spells a MATERIALISED
|
|
235
|
+
// `a >= b` as its opposite plus that flip — MIPS `slt v0,a0,a1; xori v0,v0,1`, the shape IDO and
|
|
236
|
+
// both GCCs emit and the only one asmlift has measured (m2c `40cbae3` reports ARM `eor #1` too;
|
|
237
|
+
// no agbcc row in the corpus carries it, agbcc materializing the same boolean via branches).
|
|
238
|
+
// The naive lift prints that as the double-negative `a < b ^ 1`, and hides the comparison from
|
|
239
|
+
// every consumer that reasons about booleans: the short-circuit recognizer's `&&`/`||` fold matches
|
|
240
|
+
// an `icmp` feeder, not an `xor` of one. (It does not by itself unblock that fold — measured on
|
|
241
|
+
// this idiom's one benchmark inhabitant, the diamond still declines because raise/shortcircuit.ts
|
|
242
|
+
// additionally wants a 0/1 CONST arm and both arms here are comparisons. It removes one of the two
|
|
243
|
+
// blockers, and the spelling win stands on its own.)
|
|
244
|
+
//
|
|
245
|
+
// UNGATED, unlike the compiler-pinned folds above. Two independent reasons, and the second is the
|
|
246
|
+
// load-bearing one:
|
|
247
|
+
// • it is a semantic IDENTITY on asmlift's own IR, not a spelling trade — `xor(icmp, 1)` cannot
|
|
248
|
+
// mean anything but the negated compare on any target;
|
|
249
|
+
// • THE SHAPE IS ITS OWN GATE. The pattern can only fire where the compiler itself emitted the
|
|
250
|
+
// flip, and wherever it did, "the negated comparison" is precisely what it was spelling. A
|
|
251
|
+
// compiler with a set-on-greater-equal never produces the shape and so can never be harmed.
|
|
252
|
+
// Byte evidence is narrower than the reasoning: synthetic:inrange stays MATCH under gcc2.7.2kmc
|
|
253
|
+
// with the folded spelling, which proves the round-trip there; elsewhere it is unmeasured.
|
|
254
|
+
//
|
|
255
|
+
// The negation comes from THE shared table (ir/opcodes.ts NEGATED_ICMP), so this fold, the MIPS
|
|
256
|
+
// `slt …; beqz` branch fold and the short-circuit diamond negation cannot disagree about what the
|
|
257
|
+
// opposite of a compare is. One pattern per comparison — a data-driven fold needs a fixed
|
|
258
|
+
// replacement opcode, so the table is unrolled into ten patterns rather than expressed as a
|
|
259
|
+
// (nonexistent) computed-opcode replacement.
|
|
260
|
+
const notCmpPat = (cmp: string): RewritePattern => ({
|
|
261
|
+
id: `not-${cmp}`,
|
|
262
|
+
applies: {},
|
|
263
|
+
match: {
|
|
264
|
+
op: 'xor',
|
|
265
|
+
args: [
|
|
266
|
+
{ op: cmp, args: [{ bind: 'A' }, { bind: 'B' }] },
|
|
267
|
+
{ op: 'const', attrEquals: { value: 1 }, args: [] },
|
|
268
|
+
],
|
|
269
|
+
},
|
|
270
|
+
// Pinned u32, matching CNTLZW_EQ0 — the other pattern in this file that produces a comparison.
|
|
271
|
+
// It is what raise/recover.ts stamps on every icmp result unconditionally anyway, so inheriting
|
|
272
|
+
// the `xor`'s type would reach the same place; saying it here keeps the two icmp-producing
|
|
273
|
+
// patterns on one discipline instead of leaving a reader to infer which is canonical.
|
|
274
|
+
replaceWith: { op: NEGATED_ICMP[cmp], args: ['A', 'B'], resultType: T.u(32) },
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// The BRANCH-form siblings. Testing a boolean against zero is the same negation by another
|
|
278
|
+
// spelling, and it is what a compare-and-branch ISA actually emits: MIPS `slt v0,…; xori v0,v0,1;
|
|
279
|
+
// beqz v0,L` lifts to `icmp_eq(icmp_sge(…), 0)` once the `xori` has folded, because the branch is a
|
|
280
|
+
// genuine test of the materialised boolean (the frontend's own `slt …; beqz` fusion cannot fire —
|
|
281
|
+
// its pending compare was invalidated by the `xori` that redefined the register). Without these the
|
|
282
|
+
// `^ 1` fold just trades one double negative for another: `a0 >= a1 == 0`.
|
|
283
|
+
// icmp_eq(cmp, 0) → !cmp `(a >= b) == 0` is `a < b`
|
|
284
|
+
// icmp_ne(cmp, 0) → cmp `(a >= b) != 0` is `a >= b`
|
|
285
|
+
// Same soundness argument as the `^ 1` fold (an icmp result is 0/1, so `== 0` is exactly negation)
|
|
286
|
+
// and the same shape-is-its-own-gate reason to leave them ungated.
|
|
287
|
+
const cmpZeroPat = (cmp: string, test: 'icmp_eq' | 'icmp_ne'): RewritePattern => ({
|
|
288
|
+
id: `${test === 'icmp_eq' ? 'not' : 'is'}-zerotest-${cmp}`,
|
|
289
|
+
applies: {},
|
|
290
|
+
match: {
|
|
291
|
+
op: test,
|
|
292
|
+
args: [
|
|
293
|
+
{ op: cmp, args: [{ bind: 'A' }, { bind: 'B' }] },
|
|
294
|
+
{ op: 'const', attrEquals: { value: 0 }, args: [] },
|
|
295
|
+
],
|
|
296
|
+
},
|
|
297
|
+
replaceWith: { op: test === 'icmp_eq' ? NEGATED_ICMP[cmp] : cmp, args: ['A', 'B'], resultType: T.u(32) },
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
/** `cmp ^ 1` and the zero-test forms `cmp == 0` / `cmp != 0` → the (negated) comparison. Every
|
|
301
|
+
* entry is one comparison of `NEGATED_ICMP`; the bundle is what the boolean-reasoning consumers
|
|
302
|
+
* downstream (short-circuit recovery, the structurer's condition spelling) actually match on. */
|
|
303
|
+
export const NOT_CMP_PATTERNS: RewritePattern[] = [
|
|
304
|
+
...Object.keys(NEGATED_ICMP).map(notCmpPat),
|
|
305
|
+
...Object.keys(NEGATED_ICMP).flatMap((cmp) => [cmpZeroPat(cmp, 'icmp_eq'), cmpZeroPat(cmp, 'icmp_ne')]),
|
|
306
|
+
];
|
|
307
|
+
|
|
225
308
|
// The DEFAULT idiom bundle `decompile()` applies when the caller passes no `patterns`. It is
|
|
226
|
-
// EVERY idiom asmlift owns;
|
|
227
|
-
//
|
|
228
|
-
//
|
|
309
|
+
// EVERY idiom asmlift owns; the list self-selects per target through patternApplies — agbcc/gcc get
|
|
310
|
+
// sdiv-pow2, agbcc/ido/gcc get the mul-const folds, mwcc gets cntlzw-eq0 + rotl-mirror, and agbcc
|
|
311
|
+
// gets the casts. MOST patterns are `{compilers}`-gated because they trade one spelling for another
|
|
312
|
+
// and are only byte-safe where measured; the boolean-negation folds are deliberately UNGATED (see
|
|
313
|
+
// their comment — the shape is its own gate), so "gated per compiler" is the common case, not the
|
|
314
|
+
// invariant. Ordered like the sub-bundles: the
|
|
229
315
|
// division idiom, then the multiplies (base folds before the composite tail). Passing an explicit
|
|
230
316
|
// `patterns` (including `[]`) overrides this — `[]` runs the naive lift with no idiom folding.
|
|
231
317
|
export const DEFAULT_IDIOM_PATTERNS: RewritePattern[] = [
|
|
232
318
|
SDIV_POW2_2,
|
|
233
319
|
CNTLZW_EQ0,
|
|
320
|
+
// AFTER cntlzw-eq0, which is what turns mwcc's `clz(x) >> 5` into the `icmp_eq` this fold then
|
|
321
|
+
// negates — `!(x == 0)` composes only in that order (each pattern runs to fixpoint in turn).
|
|
322
|
+
...NOT_CMP_PATTERNS,
|
|
234
323
|
ROTL_MIRROR,
|
|
235
324
|
...MUL_CONST_PATTERNS,
|
|
236
325
|
...CAST_PATTERNS,
|
|
@@ -238,7 +327,11 @@ export const DEFAULT_IDIOM_PATTERNS: RewritePattern[] = [
|
|
|
238
327
|
|
|
239
328
|
// Ops whose operands a compiler may emit in either order — so an idiom's match must try both
|
|
240
329
|
// (agbcc emits `add(X, shr_u(X,31))`; KMC GCC emits `add(shr_u(X,31), X)` for the SAME `x/2`).
|
|
241
|
-
|
|
330
|
+
// `icmp_eq`/`icmp_ne` are here for the same reason, not as arithmetic: `x == 0` and `0 == x` are the
|
|
331
|
+
// same test, and which one a frontend builds is an accident of how the branch was decoded — the
|
|
332
|
+
// zero-test folds must match either. The ORDERED comparisons are deliberately absent: swapping the
|
|
333
|
+
// operands of `a < b` is `b > a`, a different opcode, which this mechanism cannot express.
|
|
334
|
+
const COMMUTATIVE = new Set(['add', 'mul', 'and', 'or', 'xor', 'icmp_eq', 'icmp_ne']);
|
|
242
335
|
|
|
243
336
|
interface Binds {
|
|
244
337
|
values: Map<string, Value>;
|
package/src/pipeline.ts
CHANGED
|
@@ -12,13 +12,15 @@ import { VerifyError, verify } from './ir/verify';
|
|
|
12
12
|
import { Expr, LanguageBackend, SFn, Stmt, exprChildren, stmtChildren, stmtExprs } from './l3/ast';
|
|
13
13
|
import { hoistReusedGlobalBases } from './l3/basecse';
|
|
14
14
|
import { eliminateDeadStores } from './l3/dce';
|
|
15
|
+
import { mergeCommonTails } from './l3/tailmerge';
|
|
15
16
|
import { DEFAULT_IDIOM_PATTERNS, RewritePattern, applyPattern, dce, patternApplies } from './pattern/engine';
|
|
16
|
-
import type
|
|
17
|
+
import { type Prototypes, prototypesFromSymbols } from './proto';
|
|
17
18
|
import { RaiseUnsupportedError } from './raise/errors';
|
|
18
19
|
import { type PreRecoveryPass, runPreRecovery } from './raise/pre-recovery';
|
|
19
20
|
import { recoverTypes } from './raise/recover';
|
|
20
21
|
import { sinkReturns } from './raise/retsink';
|
|
21
22
|
import { StructureError, structure } from './structure/structure';
|
|
23
|
+
import { type SymbolMap, symbolsByName } from './symbols';
|
|
22
24
|
import { type TargetDescription, structureOptionsFor } from './target';
|
|
23
25
|
|
|
24
26
|
/** How a gap (a construct asmlift cannot faithfully model) degrades:
|
|
@@ -53,6 +55,10 @@ export interface DecompileOptions {
|
|
|
53
55
|
* MIPS/PPC switch declines/loud-fails; present ⇒ the frontend recovers the `switch_br`.
|
|
54
56
|
* Produced by `extractAsmData(obj, target)` from the scoring object. */
|
|
55
57
|
asmData?: AsmData;
|
|
58
|
+
/** OPTIONAL address→symbol map (symbols.ts) — the project's own names (ELF symtab) and
|
|
59
|
+
* declaration shapes (DWARF types-sidecar). Drives the Thumb numeric-pool promotion and the
|
|
60
|
+
* byte-sensitive global spellings. Absent ⇒ byte-identical to today. */
|
|
61
|
+
symbols?: SymbolMap;
|
|
56
62
|
/** gap policy — see `OnGap`. Default "strict". */
|
|
57
63
|
onGap?: OnGap;
|
|
58
64
|
}
|
|
@@ -97,9 +103,11 @@ function runTower(
|
|
|
97
103
|
onGap: OnGap,
|
|
98
104
|
): DecompileResult {
|
|
99
105
|
const backend = opts.backend ?? cBackend;
|
|
100
|
-
|
|
106
|
+
// The project's own DWARF signatures fill in what the caller did not state — in practice the
|
|
107
|
+
// CALLEES (a function still in assembly has none), which is what makes this transferable.
|
|
108
|
+
const prototypes = prototypesFromSymbols(opts.symbols, opts.prototypes ?? {});
|
|
101
109
|
// (1) lift: ISA frontend (resolved by target) → L1 with block-argument SSA
|
|
102
|
-
const fn = frontendFor(target).lift(name, asm, target, prototypes, opts.asmData);
|
|
110
|
+
const fn = frontendFor(target).lift(name, asm, target, prototypes, opts.asmData, opts.symbols);
|
|
103
111
|
verify(fn);
|
|
104
112
|
const raw = print(fn);
|
|
105
113
|
|
|
@@ -115,7 +123,11 @@ function runTower(
|
|
|
115
123
|
|
|
116
124
|
// (4) structure: IR → neutral AST; boundary contract: no unresolved value leaked (strict), or
|
|
117
125
|
// every unresolved value spelled as a loud ASMLIFT_ERROR marker (annotate).
|
|
118
|
-
const sfn = structureChecked(fn, {
|
|
126
|
+
const sfn = structureChecked(fn, {
|
|
127
|
+
...structureOptionsFor(target, prototypes[name]?.returnsVoid ?? false),
|
|
128
|
+
onGap,
|
|
129
|
+
...(opts.symbols ? { symbols: symbolsByName(opts.symbols) } : {}),
|
|
130
|
+
});
|
|
119
131
|
|
|
120
132
|
// (5) lower + print: neutral AST → target language
|
|
121
133
|
const source = backend.emit(sfn);
|
|
@@ -187,10 +199,12 @@ export function structureChecked(fn: Fn, opts: Parameters<typeof structure>[1]):
|
|
|
187
199
|
// removes statements/flips branches over an already-validated tree.
|
|
188
200
|
assertResolved(raw);
|
|
189
201
|
assertDerefsTyped(raw);
|
|
190
|
-
// Then the readability/quality rewrites:
|
|
202
|
+
// Then the readability/quality rewrites: merge a statement common to every arm of an if,
|
|
203
|
+
// drop dead stores (whose empty-then peephole flips the arm the merge empties), then hoist a
|
|
204
|
+
// reused aggregate-global
|
|
191
205
|
// base into a typed local pointer. The hoist moves the deref cast from each `index` node onto the
|
|
192
206
|
// local's initializer, so re-validate deref typing on the rewritten tree.
|
|
193
|
-
const sfn = hoistReusedGlobalBases(eliminateDeadStores(raw));
|
|
207
|
+
const sfn = hoistReusedGlobalBases(eliminateDeadStores(mergeCommonTails(raw)));
|
|
194
208
|
assertDerefsTyped(sfn);
|
|
195
209
|
return sfn;
|
|
196
210
|
}
|
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
|
+
}
|