@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/symbols.ts
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
// asmlift — the address→symbol map seam (research/symbol-map-plan-2026-07-22.md).
|
|
2
|
+
//
|
|
3
|
+
// A `SymbolMap` tells the pipeline what the project knows about its absolute addresses: the
|
|
4
|
+
// name (from the ELF `.symtab`), and optionally the byte-sensitive declaration shape (from the
|
|
5
|
+
// project's DWARF types-sidecar). Core only consumes the VALUE — providers that read files live
|
|
6
|
+
// in @asmlift/cli; tests and the webapp hand-build maps. Absent map ⇒ behavior byte-identical
|
|
7
|
+
// (the `prototypes`/`asmData` optionality contract).
|
|
8
|
+
//
|
|
9
|
+
// An address legitimately carries SEVERAL symbols in real projects (ldscript aliases, rename
|
|
10
|
+
// leftovers, deliberate typed views of one RAM region), hence `SymbolInfo[]` per address with
|
|
11
|
+
// the provider's canonical pick at index 0.
|
|
12
|
+
import { type IrType, T } from './ir/types';
|
|
13
|
+
|
|
14
|
+
/** One field of a struct-shaped global, from the sidecar DWARF layout. */
|
|
15
|
+
export interface SymbolStructField {
|
|
16
|
+
name: string;
|
|
17
|
+
/** byte offset from the struct start */
|
|
18
|
+
offset: number;
|
|
19
|
+
/** bytes read at `offset` (null for flexible/unknown members) */
|
|
20
|
+
size: number | null;
|
|
21
|
+
/** the field type's base-type signedness (absent = not a base type / unknown) — drives the
|
|
22
|
+
* u8-vs-s8 spelling of a SYNTHESIZED field decl (an s8 read is ldrb+lsl+asr, u8 is ldrb) */
|
|
23
|
+
signed?: boolean;
|
|
24
|
+
/** the field's resolved type is a pointer — synthesis must spell it as one, or relational
|
|
25
|
+
* compares of the loaded value flip signedness (s32 `blt` vs the pointer truth's `bcc`) */
|
|
26
|
+
pointer?: boolean;
|
|
27
|
+
/** POINTER field only: the byte width of what it points AT, when that is a base type
|
|
28
|
+
* (`u16 *p` → 2). The cell is 4 bytes whatever it addresses; this is the OTHER end, and it is
|
|
29
|
+
* byte-load-bearing because POINTER ARITHMETIC SCALES BY IT — `p - 4` through a `u16 *` and
|
|
30
|
+
* through a `void *` address different memory. Absent ⇒ the target is not a base type
|
|
31
|
+
* (`void *`, `struct S *`) and `void *` remains the honest spelling. */
|
|
32
|
+
pointeeSize?: number;
|
|
33
|
+
/** POINTER field only: signedness of the pointed-at base type, on the same terms as
|
|
34
|
+
* {@link pointeeSize} — it types the LOAD through the pointer (`s8` is ldrsb, `u8` ldrb). */
|
|
35
|
+
pointeeSigned?: boolean;
|
|
36
|
+
/** the field's type chain is volatile-qualified (the `vu16 field;` MMIO idiom) — a decl that
|
|
37
|
+
* drops it lets the compiler fold repeated reads (wrong bytes AND wrong semantics) */
|
|
38
|
+
volatile?: boolean;
|
|
39
|
+
/** the field's type chain is const-qualified — a STORE through the member's name is a hard
|
|
40
|
+
* error where the cast spelling it replaces only warned, so a named store declines on it */
|
|
41
|
+
const?: boolean;
|
|
42
|
+
/** ARRAY field only: the byte size of ONE element — `size` above is the WHOLE member (`u8
|
|
43
|
+
* x[16]` → 16), so this is the stride an indexed `field[i]` spelling needs. Its PRESENCE is
|
|
44
|
+
* what marks a field an array, which the exact-match field rules must exclude: a one-element
|
|
45
|
+
* array (`u8 x[1]`, size 1) would otherwise match a byte access and spell `->x`, which is not
|
|
46
|
+
* an lvalue of that width. */
|
|
47
|
+
elemSize?: number;
|
|
48
|
+
/** ARRAY field only: the ELEMENT's base-type signedness (absent = not a base type) — the same
|
|
49
|
+
* u8-vs-s8 fact `signed` carries for a scalar field, and the guard an indexed spelling needs
|
|
50
|
+
* (an s8 element read is ldrb+lsl+asr where u8 is ldrb alone) */
|
|
51
|
+
elemSigned?: boolean;
|
|
52
|
+
/** ARRAY field only: the element count (absent for a flexible array member, which declares a
|
|
53
|
+
* stride but no bound) — types the synthesized `T name[n];` field decl */
|
|
54
|
+
length?: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** What a `shape:'pointer'` global POINTS AT, when the sidecar says its target is a struct/union.
|
|
58
|
+
* The pointer cell itself is 4 bytes whatever it addresses; this is about the OTHER end — it is
|
|
59
|
+
* what lets an access through the LOADED pointer spell `gPtr->field` instead of byte arithmetic
|
|
60
|
+
* on the cell's value. Absent when the target is not a struct (a scalar/pointer/function target). */
|
|
61
|
+
export interface SymbolPointee {
|
|
62
|
+
/** the name the pointee type is declared under — a struct tag, or the typedef alias for the
|
|
63
|
+
* `typedef struct {…} T;` idiom (absent when the DWARF gives the target no name at all) */
|
|
64
|
+
structName?: string;
|
|
65
|
+
/** total byte size of the pointee type */
|
|
66
|
+
size?: number;
|
|
67
|
+
/** the pointee's fields — absent when the sidecar carries no layout for the named type, which
|
|
68
|
+
* is exactly when no field spelling may be attempted */
|
|
69
|
+
layout?: SymbolStructField[];
|
|
70
|
+
/** the POINTEE type is volatile-qualified (`volatile struct S *g`) — a fact about the OTHER end
|
|
71
|
+
* of the pointer, independent of the cell's own qualifiers (`struct S *volatile g`, which is
|
|
72
|
+
* `SymbolInfo.volatile`). Synthesis must reproduce it, and it forbids the named spelling
|
|
73
|
+
* outright: `gPtr->m` would be a volatile access where the cast form it replaces was plain */
|
|
74
|
+
volatile?: boolean;
|
|
75
|
+
/** the POINTEE type is const-qualified (`const struct S *g`) — same independence from the
|
|
76
|
+
* cell's own `const`. A STORE through a member's name is then a hard error */
|
|
77
|
+
const?: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** One declared type in a signature — width, signedness, pointer-ness. Deliberately the same
|
|
81
|
+
* vocabulary a struct member uses, so a parameter and a field of the same C type describe
|
|
82
|
+
* identically. `size: null` = the DWARF did not size it. */
|
|
83
|
+
export interface SymbolTypeFacts {
|
|
84
|
+
size: number | null;
|
|
85
|
+
signed: boolean | null;
|
|
86
|
+
pointer?: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A CODE symbol's declared signature, read from the project's own DWARF.
|
|
90
|
+
*
|
|
91
|
+
* LEAKAGE WARNING, and it is the whole reason `asIfUndecompiled` exists: a compiler emits this
|
|
92
|
+
* only for a function it COMPILED. Every benchmark row is already decompiled, so the row's own
|
|
93
|
+
* signature is present there and absent for the user, who is decompiling the one function whose
|
|
94
|
+
* definition their project does not have. Only CALLEE signatures transfer. */
|
|
95
|
+
export interface SymbolSignature {
|
|
96
|
+
/** the return type, or null for `void` */
|
|
97
|
+
returns: SymbolTypeFacts | null;
|
|
98
|
+
/** the definition's own parameter list — authoritative (a definition records what it takes) */
|
|
99
|
+
params: SymbolTypeFacts[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface SymbolInfo {
|
|
103
|
+
name: string;
|
|
104
|
+
kind: 'code' | 'data';
|
|
105
|
+
/** `kind: 'code'` only — the declared signature from the project's DWARF. DEFINITION-DERIVED:
|
|
106
|
+
* see {@link SymbolSignature} and {@link asIfUndecompiled}. */
|
|
107
|
+
signature?: SymbolSignature;
|
|
108
|
+
/** a DWARF DIE exists for this name ⇒ the project headers declare it (safe to emit) */
|
|
109
|
+
declared?: boolean;
|
|
110
|
+
/** total byte size — complete-typed globals only; an unsized extern array has none */
|
|
111
|
+
size?: number;
|
|
112
|
+
/** the byte-sensitive declaration shape (drives P2 rendering; absent ⇒ name-only) */
|
|
113
|
+
shape?: 'scalar' | 'array' | 'struct' | 'pointer';
|
|
114
|
+
/** scalar signedness for `shape:'scalar'` (absent = not a base type, e.g. an enum) — types
|
|
115
|
+
* the synthesized `extern T name;` declaration */
|
|
116
|
+
signed?: boolean;
|
|
117
|
+
/** element byte width for `shape:'array'` — enables the bare `gSym[i]` spelling */
|
|
118
|
+
elemSize?: number;
|
|
119
|
+
/** element signedness for `shape:'array'` (default unsigned) — types the env entry */
|
|
120
|
+
elemSigned?: boolean;
|
|
121
|
+
/** the real struct tag for `shape:'struct'` — names the synthesized struct declaration
|
|
122
|
+
* (absent ⇒ synthesis mints a placeholder tag; the tag is codegen-arbitrary) */
|
|
123
|
+
structName?: string;
|
|
124
|
+
/** the declaration is volatile-qualified — load-bearing for synthesis: a non-volatile decl
|
|
125
|
+
* of an MMIO global lets the compiler fold/reorder accesses (wrong bytes AND semantics) */
|
|
126
|
+
volatile?: boolean;
|
|
127
|
+
/** the declaration is const-qualified (ROM tables) — spelling fidelity */
|
|
128
|
+
const?: boolean;
|
|
129
|
+
/** field names/offsets for `shape:'struct'` — enables `gSym.field` interior spelling */
|
|
130
|
+
layout?: SymbolStructField[];
|
|
131
|
+
/** the pointee facts for `shape:'pointer'` — enables the `gPtr->field` interior spelling
|
|
132
|
+
* (absent ⇒ the target is not a struct, or the sidecar named no layout for it) */
|
|
133
|
+
pointee?: SymbolPointee;
|
|
134
|
+
/** This name is an ADDRESS-CAST MACRO, and this is its body verbatim from the project header
|
|
135
|
+
* (`(*(u32 *)0x03005290)`). Some projects name a fixed RAM cell that way instead of declaring
|
|
136
|
+
* an `extern` — and the two are not interchangeable in the bytes: an `extern` makes the
|
|
137
|
+
* compiler emit a RELOCATED pool word (`.word gSym`), while the macro expands to a literal
|
|
138
|
+
* address and emits a NUMERIC one (`.word 0x3005290`). Matching a target that shows the
|
|
139
|
+
* numeric word therefore requires the macro spelling, not merely a name.
|
|
140
|
+
*
|
|
141
|
+
* Everything else about it is already the global machinery: the macro expands to an lvalue, so
|
|
142
|
+
* `gName`, `gName = v` and `&gName` all mean what they mean for an `extern`. Only the
|
|
143
|
+
* DECLARATION differs — `#define name body` instead of `extern T name;` — which is why the
|
|
144
|
+
* body is carried rather than reconstructed. */
|
|
145
|
+
macroBody?: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** address → symbols at that address; `[0]` is the provider's canonical pick. */
|
|
149
|
+
export type SymbolMap = Map<number, SymbolInfo[]>;
|
|
150
|
+
|
|
151
|
+
/** THE one test for "is this field an array". The PRESENCE of `elemSize` is what marks one (see
|
|
152
|
+
* the field doc) — `length` is a separate fact that a flexible array member legitimately lacks,
|
|
153
|
+
* so testing it instead silently reclassifies such a member as a scalar cell. */
|
|
154
|
+
export function isArrayField(f: SymbolStructField): boolean {
|
|
155
|
+
return f.elemSize !== undefined;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** A layout member that {@link declaredFields} passed: sizable, and seated at an offset no
|
|
159
|
+
* earlier member already covers. */
|
|
160
|
+
export type DeclaredField = SymbolStructField & { size: number };
|
|
161
|
+
|
|
162
|
+
/** Is `f` shaped like a layout member at all? `SymbolMap` is public API — a caller-supplied map
|
|
163
|
+
* (the webapp accepts one) must be DECLINED, never crash the pipeline. */
|
|
164
|
+
function wellFormedField(f: unknown): f is SymbolStructField {
|
|
165
|
+
if (typeof f !== 'object' || f === null) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
const m = f as Partial<SymbolStructField>;
|
|
169
|
+
return (
|
|
170
|
+
typeof m.name === 'string' &&
|
|
171
|
+
typeof m.offset === 'number' &&
|
|
172
|
+
Number.isFinite(m.offset) &&
|
|
173
|
+
(m.size === null || (typeof m.size === 'number' && Number.isFinite(m.size) && m.size >= 0))
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* THE one definition of "which members of this layout exist", for every consumer: the declaration
|
|
179
|
+
* SYNTHESIS that PRINTS them (declare.ts) and the access rules that NAME them (structure.ts).
|
|
180
|
+
* Returns the members in offset order, or null when the layout cannot be reproduced faithfully at
|
|
181
|
+
* all — and the two answers must be the same answer, because core naming a member that synthesis
|
|
182
|
+
* does not declare is non-compiling C.
|
|
183
|
+
*
|
|
184
|
+
* Declines the WHOLE layout on an unsizable member (its successors' offsets are then unknowable,
|
|
185
|
+
* so no member of it can be seated), on a malformed one, and on an array member whose
|
|
186
|
+
* `elemSize * length` does not account for its `size` (the three facts contradict each other, so
|
|
187
|
+
* none of them can be trusted). SELECTS by dropping a member an earlier one already covers — the
|
|
188
|
+
* union-alias rule: `struct { u32 word; u16 half; }` at one offset declares the first view only,
|
|
189
|
+
* so `half` is a name no declaration carries and no access may spell.
|
|
190
|
+
*/
|
|
191
|
+
export function declaredFields(layout: SymbolStructField[] | undefined): DeclaredField[] | null {
|
|
192
|
+
if (!Array.isArray(layout)) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
// Validate BEFORE sorting: the comparator reads `.offset`, so a malformed entry would throw
|
|
196
|
+
// there rather than decline here — the crash this function exists to prevent.
|
|
197
|
+
for (const m of layout) {
|
|
198
|
+
if (!wellFormedField(m) || m.size === null) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
if (isArrayField(m) && m.length !== undefined && m.elemSize! * m.length !== m.size) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const members = (layout as DeclaredField[]).slice().sort((a, b) => a.offset - b.offset);
|
|
206
|
+
const out: DeclaredField[] = [];
|
|
207
|
+
let cursor = 0;
|
|
208
|
+
for (const m of members) {
|
|
209
|
+
if (m.offset < cursor) {
|
|
210
|
+
continue; // an overlapping (union) member: the first view is declared, the alias is not
|
|
211
|
+
}
|
|
212
|
+
out.push(m);
|
|
213
|
+
cursor = m.offset + m.size;
|
|
214
|
+
}
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* THE one copy of "what C type does a map field declare", consumed by the declaration SYNTHESIS
|
|
220
|
+
* (declare.ts, which prints it). A per-consumer copy would let the emitted declaration and the
|
|
221
|
+
* type a consumer reasoned against disagree about what a member is.
|
|
222
|
+
*
|
|
223
|
+
* An ARRAY field declares its own element type and length — spelling `u16 x[8]` as `u8 x[16]`
|
|
224
|
+
* keeps the layout but makes `x[i]` index BYTES, a wrong address. That spelling is used ONLY when
|
|
225
|
+
* the element is a 1/2/4-byte BASE type, `elemSigned` being the witness that it is one: an array
|
|
226
|
+
* of 2-byte STRUCTS declared `u16 x[n]` acquires an alignment the real member does not have, and
|
|
227
|
+
* at an odd offset the compiler then inserts padding that shifts every later member. Such a
|
|
228
|
+
* member declares the byte array of its own size instead, which has no alignment to acquire.
|
|
229
|
+
*
|
|
230
|
+
* A POINTER field types `void *` (an integer guess flips relational compares of the loaded value).
|
|
231
|
+
* Everything else is the 1/2/4 scalar cell at its declared signedness — with the 4-byte
|
|
232
|
+
* no-base-type case (an enum member) spelled s32 on the C89 enum=int rule — or a `u8 name[size]`
|
|
233
|
+
* byte array when it is no scalar cell at all (a nested struct, an 8-byte member).
|
|
234
|
+
*/
|
|
235
|
+
export function symbolFieldType(f: DeclaredField): IrType {
|
|
236
|
+
if (isArrayField(f)) {
|
|
237
|
+
const scalarElem = f.elemSigned !== undefined && (f.elemSize === 1 || f.elemSize === 2 || f.elemSize === 4);
|
|
238
|
+
return scalarElem && f.length !== undefined && f.elemSize! * f.length === f.size
|
|
239
|
+
? T.array(T.int(f.elemSize! * 8, f.elemSigned!), f.length)
|
|
240
|
+
: T.array(T.u(8), f.size);
|
|
241
|
+
}
|
|
242
|
+
if (f.pointer && f.size === 4) {
|
|
243
|
+
// The pointee width is byte-load-bearing: arithmetic on the loaded pointer scales by it, so
|
|
244
|
+
// `p - 4` through the header's `u16 *` and through a guessed `void *` reach different bytes.
|
|
245
|
+
// Only a base-type target is spelled; anything else keeps `void *`, which is address-identical
|
|
246
|
+
// for any object pointer and never derefs.
|
|
247
|
+
const scalarPointee = f.pointeeSize === 1 || f.pointeeSize === 2 || f.pointeeSize === 4;
|
|
248
|
+
return T.ptr(scalarPointee ? T.int(f.pointeeSize! * 8, f.pointeeSigned ?? false) : T.void());
|
|
249
|
+
}
|
|
250
|
+
if (f.size === 1 || f.size === 2 || f.size === 4) {
|
|
251
|
+
return T.int(f.size * 8, f.signed ?? (f.size === 4 ? ENUM_IS_SIGNED : false));
|
|
252
|
+
}
|
|
253
|
+
return T.array(T.u(8), f.size);
|
|
254
|
+
}
|
|
255
|
+
/** A 4-byte member/scalar with NO base-type signedness is the enum idiom — C89 says int. */
|
|
256
|
+
export const ENUM_IS_SIGNED = true;
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* THE gate on every spelling through a POINTER global's value: the members a `gPtr->member`
|
|
260
|
+
* spelling may name, or null when nothing may be named through this pointee at all. Null unless
|
|
261
|
+
* the pointee is named (synthesis has no tag to declare it under otherwise), sized (the struct
|
|
262
|
+
* type is incomplete otherwise), and its layout is declarable ({@link declaredFields}) — the same
|
|
263
|
+
* three conditions declare.ts needs to emit `struct Tag *gPtr;` rather than falling back to
|
|
264
|
+
* `extern void *gPtr;`. Both must decline together: core naming a member of an undeclared pointee
|
|
265
|
+
* is non-compiling C.
|
|
266
|
+
*/
|
|
267
|
+
export function pointeeFields(pointee: SymbolPointee | undefined): DeclaredField[] | null {
|
|
268
|
+
if (pointee?.structName === undefined || pointee.size === undefined) {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
return declaredFields(pointee.layout);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Kind-aware two-probe lookup for a pool-loaded 32-bit value. Exact match first (any kind);
|
|
275
|
+
* on miss, `value & ~1` — accepted ONLY when the hit is code, because ELF function addresses
|
|
276
|
+
* are stored with the Thumb bit cleared while a Thumb code pointer in a pool is odd. An exact
|
|
277
|
+
* odd-DATA hit therefore wins over a masked code hit (odd data addresses are real). */
|
|
278
|
+
export function lookupSymbol(map: SymbolMap, value: number): SymbolInfo | null {
|
|
279
|
+
const exact = map.get(value)?.[0];
|
|
280
|
+
if (exact) {
|
|
281
|
+
return exact;
|
|
282
|
+
}
|
|
283
|
+
if ((value & 1) === 1) {
|
|
284
|
+
const masked = map.get(value & ~1)?.[0];
|
|
285
|
+
if (masked?.kind === 'code') {
|
|
286
|
+
return masked;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Interior attribution: the data symbol whose `[address, address+size)` range contains
|
|
293
|
+
* `value` strictly inside (offset > 0 — exact bases go through `lookupSymbol`). Only
|
|
294
|
+
* complete-typed globals carry a size, so unsized arrays never attribute. */
|
|
295
|
+
export function lookupInterior(map: SymbolMap, value: number): { info: SymbolInfo; offset: number } | null {
|
|
296
|
+
for (const [addr, infos] of map) {
|
|
297
|
+
const info = infos[0];
|
|
298
|
+
if (info.kind !== 'data' || info.size === undefined) {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (value > addr && value < addr + info.size) {
|
|
302
|
+
return { info, offset: value - addr };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** The `SymbolInfo` keys a project's DWARF can only carry because the symbol's DEFINITION was
|
|
309
|
+
* compiled from C. Everything else in the map survives a function that is still `INCLUDE_ASM`:
|
|
310
|
+
* its `.symtab` entry exists (the asm defines the label), and its globals are typed by the OTHER
|
|
311
|
+
* translation units that declare them. Listed here, once, so {@link asIfUndecompiled} and any
|
|
312
|
+
* later definition-derived fact (a signature, a local's type, a register location) stay in sync. */
|
|
313
|
+
const DEFINITION_DERIVED_KEYS = ['declared', 'signature'] as const satisfies readonly (keyof SymbolInfo)[];
|
|
314
|
+
|
|
315
|
+
/** The map a user actually has while decompiling `fn` — i.e. with `fn` still an `INCLUDE_ASM`
|
|
316
|
+
* stub in their project.
|
|
317
|
+
*
|
|
318
|
+
* Every benchmark row is a function someone ALREADY decompiled, so the project ELF carries
|
|
319
|
+
* facts about it that exist only *because* the work is done. Scoring against those facts
|
|
320
|
+
* measures the harness, not the tool: it flatters any feature that reads them and transfers
|
|
321
|
+
* nothing to the user, who is decompiling the one function whose definition is absent. This
|
|
322
|
+
* rebuilds the map as that user's ELF would give it.
|
|
323
|
+
*
|
|
324
|
+
* What it strips is the row's own DEFINITION-derived facts ({@link DEFINITION_DERIVED_KEYS}),
|
|
325
|
+
* NOT its name: an `INCLUDE_ASM` function still has a `.symtab` entry, so dropping the symbol
|
|
326
|
+
* outright would understate what a user has and make the map look worse than it is. Callee
|
|
327
|
+
* signatures, globals and struct layouts all stay — those are the transferable facts, and they
|
|
328
|
+
* are the point.
|
|
329
|
+
*
|
|
330
|
+
* Address identity is preserved (aliases keep their order, `[0]` stays canonical) so a filtered
|
|
331
|
+
* map is a drop-in for the unfiltered one. */
|
|
332
|
+
export function asIfUndecompiled(map: SymbolMap, fn: string): SymbolMap {
|
|
333
|
+
const leaks = (info: SymbolInfo): boolean =>
|
|
334
|
+
info.kind === 'code' && info.name === fn && DEFINITION_DERIVED_KEYS.some((k) => info[k] !== undefined);
|
|
335
|
+
// Return the SAME map when the row's own symbol carries no definition-derived fact — the common
|
|
336
|
+
// case today, and it keeps the filter free to apply unconditionally on every row of a run.
|
|
337
|
+
let any = false;
|
|
338
|
+
for (const infos of map.values()) {
|
|
339
|
+
if (infos.some(leaks)) {
|
|
340
|
+
any = true;
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (!any) {
|
|
345
|
+
return map;
|
|
346
|
+
}
|
|
347
|
+
const out: SymbolMap = new Map();
|
|
348
|
+
for (const [addr, infos] of map) {
|
|
349
|
+
out.set(
|
|
350
|
+
addr,
|
|
351
|
+
infos.map((info) => {
|
|
352
|
+
if (!leaks(info)) {
|
|
353
|
+
return info;
|
|
354
|
+
}
|
|
355
|
+
const stripped = { ...info };
|
|
356
|
+
for (const k of DEFINITION_DERIVED_KEYS) {
|
|
357
|
+
delete stripped[k];
|
|
358
|
+
}
|
|
359
|
+
return stripped;
|
|
360
|
+
}),
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
return out;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Every fact but the name, canonically ordered — the equality a name collision is judged by. */
|
|
367
|
+
function factsOf(info: SymbolInfo): string {
|
|
368
|
+
return JSON.stringify(
|
|
369
|
+
Object.keys(info)
|
|
370
|
+
.filter((k) => k !== 'name')
|
|
371
|
+
.sort()
|
|
372
|
+
.map((k) => [k, info[k as keyof SymbolInfo]]),
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** NAME-keyed view over every symbol in the map — what the structurer consumes (it sees gaddr
|
|
377
|
+
* symbol names, not addresses). Aliases at one address each appear under their own name.
|
|
378
|
+
*
|
|
379
|
+
* One name can sit at SEVERAL addresses in a real project (file-static `sMenu` in two
|
|
380
|
+
* translation units, a `.symtab` full of same-named locals). Where those entries agree on their
|
|
381
|
+
* facts the collision is harmless — `InitSprite` at 16 sa3 addresses is 16 identical name-only
|
|
382
|
+
* entries. Where they DISAGREE, silently keeping whichever the map iterated last would apply one
|
|
383
|
+
* address's declaration shape to another address's global: the same layout, the wrong struct.
|
|
384
|
+
*
|
|
385
|
+
* So a disagreeing name degrades to NAME-ONLY rather than picking. The name survives (dropping it
|
|
386
|
+
* outright would leave the reference undeclarable in the self-declared scoring world, turning a
|
|
387
|
+
* spelling question into a compile failure); only the shape facts, which are what could be wrong,
|
|
388
|
+
* are withheld — the honest cast spellings take over. `kind` is kept: it never disagrees in the
|
|
389
|
+
* vendored maps, and it is settled address-side by `lookupSymbol` before a name is ever used. */
|
|
390
|
+
export function symbolsByName(map: SymbolMap): Map<string, SymbolInfo> {
|
|
391
|
+
const byName = new Map<string, SymbolInfo>();
|
|
392
|
+
const conflicted = new Set<string>();
|
|
393
|
+
for (const infos of map.values()) {
|
|
394
|
+
for (const info of infos) {
|
|
395
|
+
const prev = byName.get(info.name);
|
|
396
|
+
if (prev === undefined) {
|
|
397
|
+
byName.set(info.name, info);
|
|
398
|
+
} else if (factsOf(prev) !== factsOf(info)) {
|
|
399
|
+
conflicted.add(info.name);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
for (const name of conflicted) {
|
|
404
|
+
byName.set(name, { name, kind: byName.get(name)!.kind });
|
|
405
|
+
}
|
|
406
|
+
return byName;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** Serialize a SymbolMap to a byte-stable JSON object (hex keys, sorted; array order kept —
|
|
410
|
+
* `[0]` is the canonical pick). The benchmark vendors this; the ELF itself never leaves the
|
|
411
|
+
* project checkout. */
|
|
412
|
+
export function symbolMapToJson(map: SymbolMap): Record<string, SymbolInfo[]> {
|
|
413
|
+
const out: Record<string, SymbolInfo[]> = {};
|
|
414
|
+
for (const addr of [...map.keys()].sort((a, b) => a - b)) {
|
|
415
|
+
out[`0x${addr.toString(16).padStart(8, '0')}`] = map.get(addr)!;
|
|
416
|
+
}
|
|
417
|
+
return out;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export function symbolMapFromJson(obj: Record<string, SymbolInfo[]>): SymbolMap {
|
|
421
|
+
const map: SymbolMap = new Map();
|
|
422
|
+
for (const [k, infos] of Object.entries(obj)) {
|
|
423
|
+
map.set(Number.parseInt(k, 16), infos);
|
|
424
|
+
}
|
|
425
|
+
return map;
|
|
426
|
+
}
|
package/src/trace.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type { LanguageBackend } from './l3/ast';
|
|
|
14
14
|
import { DEFAULT_IDIOM_PATTERNS, RewritePattern, applyPattern, dce, patternApplies } from './pattern/engine';
|
|
15
15
|
import { type OnGap, raiseRecovered, structureChecked, stubResult } from './pipeline';
|
|
16
16
|
import type { Prototypes } from './proto';
|
|
17
|
+
import { type SymbolMap, symbolsByName } from './symbols';
|
|
17
18
|
import { type TargetDescription, structureOptionsFor } from './target';
|
|
18
19
|
|
|
19
20
|
export interface StageTrace {
|
|
@@ -60,6 +61,7 @@ export interface TraceOptions {
|
|
|
60
61
|
backend?: LanguageBackend;
|
|
61
62
|
prototypes?: Prototypes; // header facts (callee arities + void-ness), keyed by symbol
|
|
62
63
|
asmData?: AsmData; // data-section side table (Regime-B jump tables), as in decompile()
|
|
64
|
+
symbols?: SymbolMap; // address→symbol map (symbols.ts), as in decompile(); absent ⇒ inert
|
|
63
65
|
onGap?: OnGap; // "strict" (default) | "annotate", as in decompile()
|
|
64
66
|
/** Score probe at pattern boundaries (cli report's objdiff hook). One call per boundary:
|
|
65
67
|
* pattern N's after-score is pattern N+1's before-score. Absent ⇒ score fields stay unset. */
|
|
@@ -130,7 +132,7 @@ function traceTower(
|
|
|
130
132
|
const patternEvents: PatternEvent[] = [];
|
|
131
133
|
|
|
132
134
|
// (1) lift → typed-SSA IR
|
|
133
|
-
const fn = frontendFor(target).lift(name, asm, target, prototypes, opts.asmData);
|
|
135
|
+
const fn = frontendFor(target).lift(name, asm, target, prototypes, opts.asmData, opts.symbols);
|
|
134
136
|
verify(fn);
|
|
135
137
|
trace.push({ id: 'stage:lift', title: 'Lift (ISA frontend → typed-SSA IR)', irDump: print(fn), verified: true });
|
|
136
138
|
|
|
@@ -200,7 +202,11 @@ function traceTower(
|
|
|
200
202
|
|
|
201
203
|
// (4) structure → neutral AST; boundary contract: no unresolved value leaked (strict) or
|
|
202
204
|
// spelled as a loud ASMLIFT_ERROR marker (annotate) — same onGap lever as decompile()
|
|
203
|
-
const sfn = structureChecked(fn, {
|
|
205
|
+
const sfn = structureChecked(fn, {
|
|
206
|
+
...structureOptionsFor(target, returnsVoid),
|
|
207
|
+
onGap: opts.onGap ?? 'strict',
|
|
208
|
+
...(opts.symbols ? { symbols: symbolsByName(opts.symbols) } : {}),
|
|
209
|
+
});
|
|
204
210
|
trace.push({
|
|
205
211
|
id: 'stage:structure',
|
|
206
212
|
title: 'Structuring (IR → neutral AST)',
|