@asmlift/core 0.3.0 → 0.5.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 +130 -4
- package/src/backend/cpp.ts +3 -1
- package/src/backend/pascal.ts +11 -0
- package/src/contracts.ts +181 -4
- package/src/declare.ts +35 -9
- package/src/frontend/mips.ts +37 -29
- package/src/frontend/opaque.ts +70 -20
- package/src/frontend/ppc.ts +18 -7
- package/src/frontend/ssa.ts +279 -56
- package/src/frontend/thumb.ts +1372 -87
- package/src/ir/alias.ts +75 -0
- package/src/ir/opcodes.ts +57 -3
- package/src/ir/simplify.ts +72 -0
- package/src/l3/argbase.ts +221 -0
- package/src/l3/ast.ts +127 -5
- package/src/l3/basecse.ts +58 -62
- package/src/l3/coalesce.ts +215 -0
- package/src/l3/dce.ts +33 -41
- package/src/l3/gates.ts +67 -0
- package/src/l3/hoist.ts +65 -0
- package/src/l3/reindex.ts +7 -0
- package/src/l3/scopebase.ts +440 -0
- package/src/l3/tailmerge.ts +124 -0
- package/src/macros.ts +222 -13
- package/src/pattern/engine.ts +99 -6
- package/src/pipeline.ts +65 -6
- package/src/raise/divpow2.ts +227 -0
- package/src/raise/gvn.ts +151 -0
- package/src/raise/pre-recovery.ts +39 -3
- package/src/raise/recover.ts +24 -7
- package/src/raise/retsink.ts +37 -7
- package/src/raise/shortcircuit.ts +262 -22
- package/src/raise/struct-arrays.ts +2 -1
- package/src/raise/structs.ts +41 -3
- package/src/rank.ts +196 -20
- package/src/structure/analysis.ts +175 -89
- package/src/structure/structure.ts +588 -55
- package/src/structure/switch-recover.ts +117 -30
- package/src/symbols.ts +128 -13
- package/src/target.ts +4 -2
- package/src/trace.ts +9 -0
package/src/frontend/thumb.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// modelling is needed; they simply fall through the decode/fill switch. Because agbcc may
|
|
15
15
|
// copy a callee-saved argument (e.g. into r4) before touching r0, entry parameters are
|
|
16
16
|
// ordered by ABI register (r0, r1, …), not by the order they were first read.
|
|
17
|
-
import { Fn, Successor, Value, mkOp, mkValue } from '../ir/core';
|
|
17
|
+
import { Fn, Op, Successor, Value, mkOp, mkValue } from '../ir/core';
|
|
18
18
|
import type { Opcode } from '../ir/opcodes';
|
|
19
19
|
import { T } from '../ir/types';
|
|
20
20
|
import { type Prototypes, protoArity } from '../proto';
|
|
@@ -27,17 +27,85 @@ import { FrontendUnsupportedError } from './errors';
|
|
|
27
27
|
import { assertInputFormat } from './format';
|
|
28
28
|
import type { Frontend } from './frontend';
|
|
29
29
|
import { opaqueDest } from './opaque';
|
|
30
|
-
import { abiSortEntryParams, fallbackArgc, makeSsaBuilder } from './ssa';
|
|
30
|
+
import { abiSortEntryParams, fallbackArgc, makeSsaBuilder, stackSlotKey } from './ssa';
|
|
31
31
|
|
|
32
32
|
interface Instr {
|
|
33
|
+
/** the CANONICAL spelling — legacy names are normalised (see LEGACY_MNEMONICS) so that every
|
|
34
|
+
* consumer matches one name. */
|
|
33
35
|
mnemonic: string;
|
|
34
36
|
ops: string[];
|
|
37
|
+
/** the spelling the input file actually used, present only when normalisation changed it.
|
|
38
|
+
* Messages must use this: a decline naming `ldrsh` for a file containing `ldsh` sends the
|
|
39
|
+
* reader looking for an instruction that is not there. */
|
|
40
|
+
asWritten?: string;
|
|
35
41
|
}
|
|
36
42
|
interface AsmBlock {
|
|
37
43
|
label: string;
|
|
38
44
|
instrs: Instr[];
|
|
39
45
|
}
|
|
40
46
|
|
|
47
|
+
// Alternative mnemonic spellings, normalised at the single point where an instruction enters the
|
|
48
|
+
// IR. Each maps to a name the decode switch below already handles.
|
|
49
|
+
//
|
|
50
|
+
// These are not "similar" instructions — each pair is ONE instruction with two accepted spellings.
|
|
51
|
+
// The ARM7TDMI Technical Reference Manual (ARM DDI 0029G) gives a single encoding for each:
|
|
52
|
+
// Figure 1-6 "Thumb instruction set formats" lists Format 08 "Load and store sign-extended byte and
|
|
53
|
+
// halfword" (0101 H S 1 Ro Rb Rd) and Format 15 "Multiple load and store" (1100 L Rb Rlist), and
|
|
54
|
+
// Table 1-7 "Thumb instruction set summary" spells them `LDRSH Rd, [Rb, Ro]`, `LDRSB Rd, [Rb, Ro]`,
|
|
55
|
+
// `LDMIA Rb!, <reglist>` and `STMIA Rb!, <reglist>`. Older ARM7TDMI documentation used LDSH/LDSB,
|
|
56
|
+
// which is where the short spellings come from; the stack-suffix forms (FD, EA) are the same
|
|
57
|
+
// instructions named after the stack discipline they implement.
|
|
58
|
+
//
|
|
59
|
+
// Confirmed with this project's own toolchain — same encoding, and gba-kit executes them with the
|
|
60
|
+
// same architectural effect (sign extension, transfers, base writeback):
|
|
61
|
+
//
|
|
62
|
+
// ldsh / ldrsh 885e / 885e ldm / ldmia / ldmfd 01c9 / 01c9 / 01c9
|
|
63
|
+
// ldsb / ldrsb 8856 / 8856 stm / stmia / stmea 01c1 / 01c1 / 01c1
|
|
64
|
+
//
|
|
65
|
+
// Measured on the Klonoa: Empire of Dreams disassembly (luvdis, 469 .s files): `ldsh` 292 and
|
|
66
|
+
// `ldsb` 180 against `ldrsh` 0 and `ldrsb` 12 — the same tool emits both spellings for the signed
|
|
67
|
+
// byte load — and `ldm` 12 / `stm` 34 against `ldmia` 0 / `stmia` 0. The UAL names this frontend
|
|
68
|
+
// cased for the multiple forms never appear in that corpus at all.
|
|
69
|
+
//
|
|
70
|
+
// These are PURE SYNONYMS — identical operands — which is why they belong in a table here rather
|
|
71
|
+
// than in decode arms like MIPS's `move` or PPC's `slwi`. That distinction, and why there is no
|
|
72
|
+
// shared alias helper across the three frontends, is written up once in ./opaque.ts.
|
|
73
|
+
//
|
|
74
|
+
// This table is about COVERAGE, not soundness: a spelling missing from it declines loudly like any
|
|
75
|
+
// other unmodelled instruction, and listing one buys that the function LIFTS instead. Every load
|
|
76
|
+
// spelling ARMv4T Thumb accepts is listed, because declining a whole function over a synonym is a
|
|
77
|
+
// poor trade. Note what may NOT justify an omission: "`as` rejects it, so it cannot appear". This
|
|
78
|
+
// frontend parses TEXT — from luvdis/objdump/IDA/Ghidra and hand-written .s — so what some
|
|
79
|
+
// assembler accepts says nothing about what it will be handed.
|
|
80
|
+
//
|
|
81
|
+
// `stmfd` is deliberately absent, and the asymmetry is real rather than an oversight: `stmfd` IS
|
|
82
|
+
// `stmdb` (decrement-before), and ARMv4T Thumb has no decrement-before store — so there is nothing
|
|
83
|
+
// to normalise it TO. A fact about the instruction set, not about an assembler. It declines.
|
|
84
|
+
//
|
|
85
|
+
// Null-prototype so that an inherited key (`constructor`, `toString`) cannot be mistaken for an
|
|
86
|
+
// entry. Unreachable from real assembly, but the lookup should not depend on that.
|
|
87
|
+
const LEGACY_MNEMONICS: Readonly<Record<string, string>> = Object.assign(Object.create(null), {
|
|
88
|
+
ldsh: 'ldrsh',
|
|
89
|
+
ldsb: 'ldrsb',
|
|
90
|
+
ldm: 'ldmia',
|
|
91
|
+
ldmfd: 'ldmia',
|
|
92
|
+
stm: 'stmia',
|
|
93
|
+
stmea: 'stmia',
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// An offset far above the frame cannot be an argument — agbcc passes at most a handful on the
|
|
97
|
+
// stack, and an absurd index would mint a signature with hundreds of parameters from one bad
|
|
98
|
+
// offset. 16 is well past any real agbcc call and still refuses nonsense loudly.
|
|
99
|
+
//
|
|
100
|
+
// It bounds TOTAL arity, register arguments included (12 stack slots on a 4-register ABI), because
|
|
101
|
+
// that is the unit the index it is compared against is counted in. A refusal bound, not an ABI
|
|
102
|
+
// fact: nothing may read it as a statement about how many arguments the convention allows.
|
|
103
|
+
const MAX_RECOVERED_ARITY = 16;
|
|
104
|
+
|
|
105
|
+
function canonicalMnemonic(mn: string): string {
|
|
106
|
+
return LEGACY_MNEMONICS[mn] ?? mn;
|
|
107
|
+
}
|
|
108
|
+
|
|
41
109
|
// Map a Thumb conditional-branch mnemonic to the icmp opcode for "branch taken". The signed forms
|
|
42
110
|
// (`blt`/`ble`/`bgt`/`bge`) follow a signed `cmp`; the UNSIGNED forms carry the carry/borrow sense:
|
|
43
111
|
// `bhi` = unsigned > (higher), `bls` = unsigned <= (lower-or-same), `bcc`/`blo` = unsigned <
|
|
@@ -118,7 +186,53 @@ const imm = (s: string) => parseInt(s.replace(/^#/, ''), s.includes('0x') ? 16 :
|
|
|
118
186
|
// ambiguous and left UNEXPANDED — but its endpoints ARE surfaced as separate tokens so pc/lr
|
|
119
187
|
// detection sees them, and any consumer that needs the exact list rejects the leftover `-` token
|
|
120
188
|
// loudly rather than treating the fused range as one phantom register.
|
|
121
|
-
|
|
189
|
+
// Null-prototype for the same reason LEGACY_MNEMONICS has one: this table is consulted with `in`
|
|
190
|
+
// to decide whether a reglist token is a DEFINITE register, and an inherited key (`constructor`,
|
|
191
|
+
// `toString`) answering true would let a junk token pass that test instead of poisoning the frame
|
|
192
|
+
// depth. Unreachable from real assembly; the guarantee should not depend on that.
|
|
193
|
+
const REG_NUM: Record<string, number> = Object.assign(Object.create(null), { sp: 13, lr: 14, pc: 15 });
|
|
194
|
+
|
|
195
|
+
// Thumb-1 data-processing mnemonics that write the condition flags when their destination is a LOW
|
|
196
|
+
// register — which is all of them on this ISA, `s`-suffix or not (the assembler picks the encoding).
|
|
197
|
+
// Used to invalidate a pending compare: see the decode loop. `cmp`/`cmn`/`tst` are absent on purpose
|
|
198
|
+
// — they set flags but define no register, and `cmp` is the very instruction that seeds the pending
|
|
199
|
+
// compare. Loads, stores, push/pop, `bl` and the high-register forms leave the flags alone.
|
|
200
|
+
const FLAG_SETTING = new Set([
|
|
201
|
+
'mov',
|
|
202
|
+
'movs',
|
|
203
|
+
'add',
|
|
204
|
+
'adds',
|
|
205
|
+
'sub',
|
|
206
|
+
'subs',
|
|
207
|
+
'lsl',
|
|
208
|
+
'lsls',
|
|
209
|
+
'lsr',
|
|
210
|
+
'lsrs',
|
|
211
|
+
'asr',
|
|
212
|
+
'asrs',
|
|
213
|
+
'neg',
|
|
214
|
+
'negs',
|
|
215
|
+
'rsb',
|
|
216
|
+
'rsbs',
|
|
217
|
+
'mvn',
|
|
218
|
+
'mvns',
|
|
219
|
+
'bic',
|
|
220
|
+
'bics',
|
|
221
|
+
'ror',
|
|
222
|
+
'rors',
|
|
223
|
+
'mul',
|
|
224
|
+
'muls',
|
|
225
|
+
'and',
|
|
226
|
+
'ands',
|
|
227
|
+
'orr',
|
|
228
|
+
'orrs',
|
|
229
|
+
'eor',
|
|
230
|
+
'eors',
|
|
231
|
+
'adc',
|
|
232
|
+
'adcs',
|
|
233
|
+
'sbc',
|
|
234
|
+
'sbcs',
|
|
235
|
+
]);
|
|
122
236
|
const regNum = (r: string) => (r[0] === 'r' ? Number(r.slice(1)) : REG_NUM[r]);
|
|
123
237
|
function expandRegList(tokens: string[]): string[] {
|
|
124
238
|
const out: string[] = [];
|
|
@@ -145,6 +259,29 @@ function expandRegList(tokens: string[]): string[] {
|
|
|
145
259
|
return out;
|
|
146
260
|
}
|
|
147
261
|
|
|
262
|
+
// Expand a register list and vouch that every entry is a DEFINITE register, or return null.
|
|
263
|
+
//
|
|
264
|
+
// The two consumers tokenize differently (the ldm/stm arm has a base register to slice off, the
|
|
265
|
+
// frame walk does not), so each keeps its own tokenizing — but the VALIDATION has to be one
|
|
266
|
+
// function, because the two hand-rolled versions had drifted to unequal strength. The frame walk
|
|
267
|
+
// required every token to be a real register; the ldm/stm arm only rejected a leftover `-`, so
|
|
268
|
+
// `ldmia r1!, {foo}` lifted and emitted `s32 f(s32 a0, s32 a1) { return a0; }` — a parameter
|
|
269
|
+
// fabricated from a token that names no register, which is the phantom this frontend's guards
|
|
270
|
+
// exist to prevent.
|
|
271
|
+
//
|
|
272
|
+
// An unexpandable range leaves its raw `-` token (see expandRegList) and fails here; so does an
|
|
273
|
+
// unknown alias, which a `Number.isNaN(regNum(t))` test would MISS, since regNum returns undefined
|
|
274
|
+
// for one and `Number.isNaN(undefined)` is false. An empty list is a malformed list, not an empty
|
|
275
|
+
// transfer. Lowercase-only is deliberate and free: an uppercase mnemonic declines as unmodelled
|
|
276
|
+
// long before either consumer runs.
|
|
277
|
+
function definiteRegList(tokens: string[]): string[] | null {
|
|
278
|
+
const list = expandRegList(tokens);
|
|
279
|
+
if (list.length === 0) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
return list.every((s) => /^r\d+$/.test(s) || s in REG_NUM) ? list : null;
|
|
283
|
+
}
|
|
284
|
+
|
|
148
285
|
// Split an operand list on commas that are NOT inside brackets, so a memory operand like
|
|
149
286
|
// `[r0, #0x8]` (base + offset) stays a single token instead of being torn at its comma.
|
|
150
287
|
function splitOperands(s: string): string[] {
|
|
@@ -305,7 +442,14 @@ function decode(name: string, asm: string): { blocks: AsmBlock[]; dataWords: Map
|
|
|
305
442
|
continue;
|
|
306
443
|
}
|
|
307
444
|
dataLabel = null; // a real instruction ends a data run
|
|
308
|
-
|
|
445
|
+
const canon = canonicalMnemonic(m[1]);
|
|
446
|
+
flat.push({
|
|
447
|
+
instr: {
|
|
448
|
+
mnemonic: canon,
|
|
449
|
+
ops: m[2] ? splitOperands(m[2]) : [],
|
|
450
|
+
...(canon === m[1] ? {} : { asWritten: m[1] }),
|
|
451
|
+
},
|
|
452
|
+
});
|
|
309
453
|
}
|
|
310
454
|
if (
|
|
311
455
|
armLabels.has(name) ||
|
|
@@ -608,6 +752,41 @@ function decode(name: string, asm: string): { blocks: AsmBlock[]; dataWords: Map
|
|
|
608
752
|
`cannot lift '${name}': block '${mixed.label}' interleaves raw data (.${subwordData.get(mixed.label)}) with instructions`,
|
|
609
753
|
);
|
|
610
754
|
}
|
|
755
|
+
// Two labels on the same instruction (`.LCB80:` immediately followed by `.L7:`) make the first
|
|
756
|
+
// an ALIAS of the second, not a block of its own — agbcc emits exactly that when a long-jump
|
|
757
|
+
// helper label lands on an existing one. The empty block is dropped just below, so a branch
|
|
758
|
+
// naming the alias would afterwards resolve to nothing and decline as a dangling target. Point
|
|
759
|
+
// those branches at the block the label actually names, before anything reads the CFG.
|
|
760
|
+
// A label naming DATA is emphatically NOT an alias, and this is the guard the whole pass turns
|
|
761
|
+
// on. Decode pushes an empty block for a literal-pool / jump-table label too, so aliasing them
|
|
762
|
+
// blindly would silently retarget `beq .Lpool` at whatever code happens to follow the pool —
|
|
763
|
+
// marker-free, plausible, wrong C where the frontend used to decline. Every agbcc pool is a
|
|
764
|
+
// label on data, so that is the common case, not an exotic one. A data label therefore neither
|
|
765
|
+
// aliases nor is aliased THROUGH: scanning past one for a later code block would silently jump
|
|
766
|
+
// over the data.
|
|
767
|
+
const isDataLabel = (l: string) => dataWords.has(l) || subwordData.has(l);
|
|
768
|
+
const aliasOf = new Map<string, string>();
|
|
769
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
770
|
+
if (blocks[i].instrs.length > 0 || isDataLabel(blocks[i].label)) {
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
let j = i + 1;
|
|
774
|
+
while (j < blocks.length && blocks[j].instrs.length === 0 && !isDataLabel(blocks[j].label)) {
|
|
775
|
+
j++;
|
|
776
|
+
}
|
|
777
|
+
const next = blocks[j];
|
|
778
|
+
if (next && next.instrs.length > 0) {
|
|
779
|
+
aliasOf.set(blocks[i].label, next.label);
|
|
780
|
+
} // otherwise a trailing or data-fronted label: left dangling so a branch to it still declines
|
|
781
|
+
}
|
|
782
|
+
for (const b of aliasOf.size ? blocks : []) {
|
|
783
|
+
for (const ins of b.instrs) {
|
|
784
|
+
const k = ins.ops.length - 1;
|
|
785
|
+
if ((ins.mnemonic === 'b' || COND_OPCODE[ins.mnemonic]) && k >= 0) {
|
|
786
|
+
ins.ops[k] = aliasOf.get(ins.ops[k]) ?? ins.ops[k];
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
611
790
|
let live = blocks.filter((b) => b.instrs.length > 0);
|
|
612
791
|
// Alignment-pad NOPs a splitter emits around returns and literal pools: `lsls r0, r0, #0`
|
|
613
792
|
// is the 0x0000 halfword, `mov r8, r8` is 0x46C0, plus a literal `nop`. A block made ONLY
|
|
@@ -697,7 +876,10 @@ function decode(name: string, asm: string): { blocks: AsmBlock[]; dataWords: Map
|
|
|
697
876
|
// (they did — the drift fabricated phantom pointer params on symbol-pool loads).
|
|
698
877
|
const POOL_LABEL = /^([A-Za-z_.$][\w.$]*)(?:\s*\+\s*(0x[0-9a-fA-F]+|\d+))?$/;
|
|
699
878
|
|
|
700
|
-
type PoolRef =
|
|
879
|
+
type PoolRef =
|
|
880
|
+
| { kind: 'const'; value: number }
|
|
881
|
+
| { kind: 'gaddr'; sym: string; addend: number }
|
|
882
|
+
| { kind: 'unmodelled'; why: string };
|
|
701
883
|
|
|
702
884
|
/** Classify a word-load operand `LABEL[+N]` against the captured literal pools. Returns null when
|
|
703
885
|
* the operand does NOT name a pool (a real register/memory base → the normal load path). When it
|
|
@@ -723,12 +905,21 @@ function poolRef(operand: string, dataWords: Map<string, string[]>): PoolRef | n
|
|
|
723
905
|
const val = w.startsWith('-') ? -Number(w.slice(1)) : Number(w);
|
|
724
906
|
return Number.isFinite(val) ? { kind: 'const', value: val } : { kind: 'unmodelled', why: `unparsable word '${w}'` };
|
|
725
907
|
}
|
|
726
|
-
// A
|
|
727
|
-
//
|
|
728
|
-
|
|
729
|
-
|
|
908
|
+
// A C identifier that is NOT a `.L` code label → the address of a named global, optionally with
|
|
909
|
+
// a byte ADDEND folded into the pool word (`gBgTilemapBufs+0x14a` — agbcc pre-computes a fixed
|
|
910
|
+
// element's address into the pool rather than emitting an add). The addend stays in VALUE space:
|
|
911
|
+
// the consumer emits `gaddr` then an explicit `add`, the exact spelling the register-materialised
|
|
912
|
+
// `ldr rN,=gSym; add rN,#k` shape already lowers to — so it renders through the same audited
|
|
913
|
+
// cast-based path (`((u8 *)&gSym) + k`), never through a typed-pointer scale that a
|
|
914
|
+
// rendered-vs-value addend could silently multiply (the DEREF-TYPING class).
|
|
915
|
+
const sm = w.match(/^([A-Za-z_]\w*)\s*(?:([+-])\s*(0x[0-9a-fA-F]+|\d+))?$/);
|
|
916
|
+
if (sm && !sm[1].startsWith('.L')) {
|
|
917
|
+
const mag = sm[3] ? Number(sm[3]) : 0;
|
|
918
|
+
if (Number.isFinite(mag)) {
|
|
919
|
+
return { kind: 'gaddr', sym: sm[1], addend: sm[2] === '-' ? -mag : mag };
|
|
920
|
+
}
|
|
730
921
|
}
|
|
731
|
-
return { kind: 'unmodelled', why: `pool word '${w}' is a symbol offset or
|
|
922
|
+
return { kind: 'unmodelled', why: `pool word '${w}' is not a symbol, symbol±offset, or number` };
|
|
732
923
|
}
|
|
733
924
|
|
|
734
925
|
/** Does this function's literal pool name at least one EXTERNAL symbol?
|
|
@@ -751,10 +942,13 @@ function poolNamesASymbol(dataWords: Map<string, string[]>, blockLabels: Set<str
|
|
|
751
942
|
for (const [, words] of dataWords) {
|
|
752
943
|
for (const raw of words) {
|
|
753
944
|
const w = raw.trim();
|
|
754
|
-
|
|
945
|
+
// `gSym+0x14a` names a symbol as surely as `gSym` does — the witness must count both, or an
|
|
946
|
+
// asm whose pools carry only addend words would wrongly permit numeric promotion.
|
|
947
|
+
const sym = w.match(/^([A-Za-z_]\w*)\s*(?:[+-]\s*(?:0x[0-9a-fA-F]+|\d+))?$/)?.[1];
|
|
948
|
+
if (sym === undefined || sym.startsWith('.L')) {
|
|
755
949
|
continue;
|
|
756
950
|
}
|
|
757
|
-
if (!dataWords.has(
|
|
951
|
+
if (!dataWords.has(sym) && !blockLabels.has(sym)) {
|
|
758
952
|
return true;
|
|
759
953
|
}
|
|
760
954
|
}
|
|
@@ -779,26 +973,122 @@ interface JumpTable {
|
|
|
779
973
|
caseLabels: string[];
|
|
780
974
|
defaultLabel: string;
|
|
781
975
|
}
|
|
976
|
+
|
|
977
|
+
// Accept a data-processing mnemonic in either the pre-UAL (`lsl`, `add`) or UAL (`lsls`, `adds`)
|
|
978
|
+
// spelling, WITHIN THE DISPATCH BLOCK ONLY.
|
|
979
|
+
//
|
|
980
|
+
// In Thumb-1 the trailing `s` is a DIALECT MARKER, not a modifier: `.syntax divided` spells the
|
|
981
|
+
// flag-setting data-processing instructions without it, `.syntax unified` with it. For a LOW-register
|
|
982
|
+
// destination each pair is one halfword — measured with this project's own assembler, one instruction
|
|
983
|
+
// per file, both dialects, encoding read back with objdump:
|
|
984
|
+
//
|
|
985
|
+
// add/adds 1888 sub/subs 1a88 lsl/lsls 0088 lsr/lsrs 0888 asr/asrs 1088 neg/negs 4248
|
|
986
|
+
//
|
|
987
|
+
// This is the dominant dialect of the input format asmlift advertises: every pret-style split wraps
|
|
988
|
+
// its `INCLUDE_ASM` bodies in `.syntax unified`, where the non-suffixed spelling is a syntax ERROR,
|
|
989
|
+
// and the split `.s` files carry no `.syntax` directive of their own — so this frontend cannot tell
|
|
990
|
+
// from its input which dialect it is reading, and must accept both. Counted under
|
|
991
|
+
// `asm/nonmatchings` of the Klonoa: Empire of Dreams tree: `lsls` 5795 against `lsl` 0, and `adds`
|
|
992
|
+
// 8883 against `add` 600 — where **every one** of those 600 is an `sp`/high-register/pc form and not
|
|
993
|
+
// one is the three-operand low-register `add rD, rN, rM` this idiom uses. Comparing the text alone
|
|
994
|
+
// therefore declined every jump table in that corpus, on input that is not malformed in any way.
|
|
995
|
+
// (agbcc's own output is the other dialect — 2957 `lsl` in this project's `build-gdwarf/src/*.s` —
|
|
996
|
+
// which is why the benchmark, built from compiler `.s`, never exercised this.)
|
|
997
|
+
//
|
|
998
|
+
// Why LOCAL rather than an entry in LEGACY_MNEMONICS, which is where synonyms belong: normalising
|
|
999
|
+
// the suffix away is safe for every input an ASSEMBLER ACCEPTS — the operands that distinguish `add`
|
|
1000
|
+
// from `adds` (the SP adjust `add sp, sp, #4` = b001, the high-register `add r8, r0` = 4480) have no
|
|
1001
|
+
// S-form at all, so `adds sp` and `adds r8, r0` are rejected in both dialects and cannot appear in
|
|
1002
|
+
// any assemblable file. But this frontend REFUSES those spellings loudly today, and a flat
|
|
1003
|
+
// `adds: 'add'` entry silently turns `adds sp` into ordinary frame bookkeeping instead: the entry
|
|
1004
|
+
// was written, and `decline-guards.test.ts` failed on exactly that case. So the reason to keep it
|
|
1005
|
+
// local is input VALIDATION, not semantics — a name-keyed table cannot say "only when the
|
|
1006
|
+
// destination is a low register", and giving up the refusal buys nothing, since no assembler emits
|
|
1007
|
+
// what it refuses.
|
|
1008
|
+
//
|
|
1009
|
+
// Known false declines, all loud, none with a corpus instance: a parenthesised immediate (`#(2)`,
|
|
1010
|
+
// which gas assembles), a two-operand `adds rA, rP` (the same add, but `addSrcs` has one source),
|
|
1011
|
+
// and `movs pc, rV` (which IS `mov pc, rV` — 4687 — under divided syntax, and which `classifyXfer`
|
|
1012
|
+
// accepts, so the frontend is internally inconsistent about it).
|
|
1013
|
+
//
|
|
1014
|
+
// Inside the dispatch block the distinction is additionally UNOBSERVABLE: the block computes
|
|
1015
|
+
// `table_base + index*4`, loads the target and writes `pc`. Nothing between the `lsl` and the
|
|
1016
|
+
// `mov pc` reads NZCV, no path leaves the block by falling through, and on a successful recovery the
|
|
1017
|
+
// block is ELIDED from the CFG entirely — the bounds test that does feed a conditional branch lives
|
|
1018
|
+
// in `bounds`, whose `cmp`/`bhi`/`bls` this function matches by exact name. Doubly moot in fact,
|
|
1019
|
+
// since `lsl rD, rS, #imm` is low-register-only, so the add here is always the low-register form.
|
|
1020
|
+
const isDataOp = (mn: string, base: 'lsl' | 'add'): boolean => mn === base || mn === `${base}s`;
|
|
1021
|
+
|
|
1022
|
+
// A shift amount is a NUMBER, not a spelling. `#2`, `#0x2` and `#0x02` are the same shift; the
|
|
1023
|
+
// recogniser used to compare the operand text and so rejected the third.
|
|
1024
|
+
//
|
|
1025
|
+
// The operand must be a plain integer LITERAL, and that shape check is the whole point of this
|
|
1026
|
+
// helper rather than an incidental guard. `imm()` is `parseInt`, which stops at the first character
|
|
1027
|
+
// it cannot consume, so it reads `#2*2` as 2 — and `#2*2` is not malformed, it is an expression gas
|
|
1028
|
+
// accepts and assembles to `lsls r0, r1, #4`. Matching it as a shift by two would recover a switch
|
|
1029
|
+
// whose stride is wrong by a factor of four: the emitted C is entirely ordinary and dispatches to
|
|
1030
|
+
// the wrong BLOCK, with no marker. `#2+1`, `#2-1` and `#2<<1` are the same trap. An adversarial
|
|
1031
|
+
// probe found this after the first cut of this helper shipped with exactly that hole, and the test
|
|
1032
|
+
// that claimed to pin the property sampled only `#3`/`#0x3`/`#0x1`/`r2` and so passed anyway.
|
|
1033
|
+
//
|
|
1034
|
+
// Anything that is not a bare decimal or hex literal therefore DECLINES, which is the safe
|
|
1035
|
+
// direction: a real dispatch spells its shift as a literal. Known false declines, all loud and none
|
|
1036
|
+
// observed in any corpus: a binary literal (`#0b10`) and a signed one (`#+2`).
|
|
1037
|
+
//
|
|
1038
|
+
// One divergence is knowingly left in, and it is inert AT THE ONLY VALUE THIS IS USED WITH.
|
|
1039
|
+
// A leading zero means octal to gas and decimal to `Number`, so `#010` is 8 there and 10 here —
|
|
1040
|
+
// but both readings are compared against 2, both fail, and the dispatch declines either way; `#02`
|
|
1041
|
+
// is 2 under both. **If this helper is ever reused for a `want` other than 2, that has to be
|
|
1042
|
+
// revisited**, because for e.g. `want === 8` the two readings disagree about `#010`.
|
|
1043
|
+
const IMM_LITERAL = /^#\s*(?:0[xX][0-9a-fA-F]+|[0-9]+)$/;
|
|
1044
|
+
const immEq = (op: string | undefined, want: number): boolean =>
|
|
1045
|
+
op !== undefined && IMM_LITERAL.test(op) && Number(op.slice(1).trim()) === want;
|
|
782
1046
|
function recoverJumpTable(
|
|
783
1047
|
bounds: AsmBlock,
|
|
784
1048
|
disp: AsmBlock,
|
|
785
1049
|
dataWords: Map<string, string[]>,
|
|
786
1050
|
blockLabels: Set<string>,
|
|
1051
|
+
longDefault?: string,
|
|
787
1052
|
): JumpTable | null {
|
|
788
|
-
// bounds: last two instrs
|
|
1053
|
+
// bounds: last two instrs are `cmp rX,#M` then the out-of-range guard, in one of two spellings.
|
|
1054
|
+
//
|
|
1055
|
+
// direct cmp rX,#M ; bhi DEF → fall through to the dispatch
|
|
1056
|
+
// long jump cmp rX,#M ; bls DISP ; b DEF → branch TO the dispatch, long-branch the default
|
|
1057
|
+
//
|
|
1058
|
+
// The second is what agbcc emits whenever the default is out of a conditional branch's reach —
|
|
1059
|
+
// Thumb-1 `B<cond>` carries a signed 8-bit HALFWORD offset, so ±256 BYTES, about 128
|
|
1060
|
+
// instructions — which on a real switch it usually is: five of the six benchmark
|
|
1061
|
+
// functions with a table use it, and only the sixth uses the direct form. `longDefault` is the
|
|
1062
|
+
// target of that trailing `b`, read by the caller from the block after `bounds`.
|
|
789
1063
|
const bi = bounds.instrs;
|
|
790
|
-
const
|
|
1064
|
+
const guard = bi[bi.length - 1],
|
|
791
1065
|
cmp = bi[bi.length - 2];
|
|
792
|
-
if (!
|
|
1066
|
+
if (!guard || !cmp || cmp.mnemonic !== 'cmp') {
|
|
793
1067
|
return null;
|
|
794
1068
|
}
|
|
1069
|
+
let defaultLabel: string;
|
|
1070
|
+
if (longDefault === undefined) {
|
|
1071
|
+
if (guard.mnemonic !== 'bhi') {
|
|
1072
|
+
return null;
|
|
1073
|
+
}
|
|
1074
|
+
defaultLabel = guard.ops[0];
|
|
1075
|
+
} else {
|
|
1076
|
+
// The `bls` must name THIS dispatch block, or the guard belongs to some other branch and the
|
|
1077
|
+
// `b` we picked up is not its default.
|
|
1078
|
+
if (guard.mnemonic !== 'bls' || guard.ops[0] !== disp.label) {
|
|
1079
|
+
return null;
|
|
1080
|
+
}
|
|
1081
|
+
defaultLabel = longDefault;
|
|
1082
|
+
}
|
|
795
1083
|
const scrutReg = cmp.ops[0];
|
|
796
1084
|
const m = cmp.ops[1];
|
|
797
1085
|
if (!m?.startsWith('#')) {
|
|
798
1086
|
return null;
|
|
799
1087
|
}
|
|
800
1088
|
const n = imm(m) + 1; // cases 0..M → N = M+1
|
|
801
|
-
|
|
1089
|
+
if (n < 1) {
|
|
1090
|
+
return null; // a bound that admits no case at all is not a dispatch — fail closed
|
|
1091
|
+
}
|
|
802
1092
|
|
|
803
1093
|
// disp: exactly the 5-op idiom, threading a single index register from `lsl rY,rX,#2`.
|
|
804
1094
|
const d = disp.instrs;
|
|
@@ -806,7 +1096,7 @@ function recoverJumpTable(
|
|
|
806
1096
|
return null;
|
|
807
1097
|
}
|
|
808
1098
|
const [lsl, ldrP, add, ldrV, movpc] = d;
|
|
809
|
-
if (lsl.mnemonic
|
|
1099
|
+
if (!isDataOp(lsl.mnemonic, 'lsl') || lsl.ops[1] !== scrutReg || !immEq(lsl.ops[2], 2)) {
|
|
810
1100
|
return null;
|
|
811
1101
|
}
|
|
812
1102
|
const idxReg = lsl.ops[0]; // rY = rX << 2 (index*4, identity guard)
|
|
@@ -815,19 +1105,40 @@ function recoverJumpTable(
|
|
|
815
1105
|
}
|
|
816
1106
|
const ptrReg = ldrP.ops[0],
|
|
817
1107
|
ptrLabel = ldrP.ops[1]; // rP = *(PTR literal)
|
|
818
|
-
if (add.mnemonic
|
|
1108
|
+
if (!isDataOp(add.mnemonic, 'add') || add.ops[0] !== idxReg) {
|
|
819
1109
|
return null;
|
|
820
1110
|
}
|
|
821
1111
|
// add rY, rY, rP (either operand order) — the address = table_base + index*4, nothing else.
|
|
1112
|
+
//
|
|
1113
|
+
// The two sources must be DISTINCT registers. Membership alone is satisfied by one register
|
|
1114
|
+
// listed twice, and that is not a hypothetical shape: if the pointer load targets the index
|
|
1115
|
+
// register (`lsl r0,r1,#2 ; ldr r0,=PTR ; add r0,r0,r0`) the index is overwritten before it is
|
|
1116
|
+
// ever added, `idxReg === ptrReg`, and both `includes` tests pass on `r0`. The address formed is
|
|
1117
|
+
// `2 * table_base` and the scrutinee is dead — yet the recogniser would emit `switch (a0)` and
|
|
1118
|
+
// dispatch on a value the hardware never uses. Wrong block, no marker. Found by an adversarial
|
|
1119
|
+
// probe; it predates the spelling fix this guard sits next to, and is fixed here because it is
|
|
1120
|
+
// the same identity-or-decline rule.
|
|
822
1121
|
const addSrcs = [add.ops[1], add.ops[2]];
|
|
823
|
-
if (!(addSrcs.includes(idxReg) && addSrcs.includes(ptrReg))) {
|
|
1122
|
+
if (idxReg === ptrReg || !(addSrcs.includes(idxReg) && addSrcs.includes(ptrReg))) {
|
|
824
1123
|
return null;
|
|
825
1124
|
}
|
|
826
1125
|
if (ldrV.mnemonic !== 'ldr') {
|
|
827
1126
|
return null;
|
|
828
1127
|
}
|
|
829
|
-
|
|
830
|
-
|
|
1128
|
+
// rV = *(rY), and the address must be EXACTLY rY: no displacement, no register index.
|
|
1129
|
+
//
|
|
1130
|
+
// `parseAddr` surfaces `off` and `regOff` for precisely this reason — its own comment says
|
|
1131
|
+
// "surfaced to the caller so load/store DECLINE loud: silently reading `[rB]` dropped the index
|
|
1132
|
+
// — a silent miscompile" — and this caller used to destructure `base` alone and throw both away.
|
|
1133
|
+
// `ldr rV, [rA, #4]` loads table[i+1]: the recovered switch says `case 0` while the hardware
|
|
1134
|
+
// reaches case 1's block, and the last case reads a word past the table. `ldr rV, [rA, r2]` adds
|
|
1135
|
+
// an unrelated register. Both used to recover an ordinary-looking `switch` — a wrong BLOCK, with
|
|
1136
|
+
// no marker. The header of this function already claimed to refuse an "extra offset"; now it does.
|
|
1137
|
+
//
|
|
1138
|
+
// `#0` is the spelling the corpus actually uses (`ldr r0, [r0, #0x00]`), so the check is on the
|
|
1139
|
+
// VALUE, not on the operand's absence.
|
|
1140
|
+
const { base, off, regOff } = parseAddr(ldrV.ops[1]);
|
|
1141
|
+
if (base !== idxReg || off !== 0 || regOff !== undefined || ldrV.ops[0] !== movpc.ops[1]) {
|
|
831
1142
|
return null;
|
|
832
1143
|
}
|
|
833
1144
|
if (movpc.mnemonic !== 'mov' || movpc.ops[0] !== 'pc') {
|
|
@@ -835,11 +1146,28 @@ function recoverJumpTable(
|
|
|
835
1146
|
}
|
|
836
1147
|
|
|
837
1148
|
// Read the table: the ldr loads a POINTER word (PTR: .word TABLE); the table is TABLE: .word C0…
|
|
838
|
-
|
|
839
|
-
|
|
1149
|
+
// Note the case labels are matched against `blockLabels` as WRITTEN: the adjacent-label aliasing in
|
|
1150
|
+
// `decode` rewrites branch operands, not `.word` entries, so a table naming an aliased label would
|
|
1151
|
+
// decline here rather than dispatch anywhere. Loud, and no corpus instance — left as a known edge
|
|
1152
|
+
// rather than fixed speculatively.
|
|
1153
|
+
//
|
|
1154
|
+
// The pointer word is addressed the same way every other pool load in this frontend is —
|
|
1155
|
+
// `LABEL[+N]`, selecting word N/4 — because a literal pool is a POOL: agbcc packs the dispatch
|
|
1156
|
+
// pointer in beside whatever else the function needed, and which slot it lands in is an artifact
|
|
1157
|
+
// of emission order. Reading only a bare label whose pool held exactly ONE word declined six real
|
|
1158
|
+
// benchmark functions whose table pointer merely sat later in the pool. Same fix m2c made in
|
|
1159
|
+
// `a7c5c2d`, and the same shared POOL_LABEL grammar the const/gaddr resolvers use, so the three
|
|
1160
|
+
// cannot disagree about what `.L21+0x4` addresses.
|
|
1161
|
+
const pm = ptrLabel.match(POOL_LABEL);
|
|
1162
|
+
const ptrWords = pm ? dataWords.get(pm[1]) : undefined;
|
|
1163
|
+
if (!pm || !ptrWords) {
|
|
840
1164
|
return null;
|
|
841
1165
|
}
|
|
842
|
-
const
|
|
1166
|
+
const ptrOff = pm[2] ? Number(pm[2]) : 0;
|
|
1167
|
+
if (ptrOff % 4 !== 0 || ptrOff / 4 >= ptrWords.length) {
|
|
1168
|
+
return null; // misaligned or past the end of the pool — not a word this pool holds
|
|
1169
|
+
}
|
|
1170
|
+
const caseLabels = dataWords.get(ptrWords[ptrOff / 4].trim());
|
|
843
1171
|
if (!caseLabels || caseLabels.length !== n) {
|
|
844
1172
|
return null;
|
|
845
1173
|
} // table length must equal the bound
|
|
@@ -875,26 +1203,49 @@ export function lift(
|
|
|
875
1203
|
const poolNamesSymbols = poolNamesASymbol(dataWords, blockLabels);
|
|
876
1204
|
// Any label referenced as a branch target (so we can tell if an elided dispatch block has a SECOND
|
|
877
1205
|
// predecessor — a `b disp` from elsewhere — which would dangle after elision; decline if so).
|
|
878
|
-
|
|
1206
|
+
// How many branches name each label — not just whether any does, because the long-jump bounds
|
|
1207
|
+
// form legitimately branches to its own dispatch block exactly once.
|
|
1208
|
+
const branchRefs = new Map<string, number>();
|
|
879
1209
|
for (const b of rawBlocks) {
|
|
880
1210
|
for (const ins of b.instrs) {
|
|
881
1211
|
if ((ins.mnemonic === 'b' || COND_OPCODE[ins.mnemonic]) && ins.ops.length) {
|
|
882
|
-
|
|
1212
|
+
const t = ins.ops[ins.ops.length - 1];
|
|
1213
|
+
branchRefs.set(t, (branchRefs.get(t) ?? 0) + 1);
|
|
883
1214
|
}
|
|
884
1215
|
}
|
|
885
1216
|
}
|
|
886
1217
|
const tables = new Map<AsmBlock, JumpTable>(); // bounds block → recovered table
|
|
887
|
-
const elided = new Set<AsmBlock>(); // dispatch blocks removed from the CFG
|
|
1218
|
+
const elided = new Set<AsmBlock>(); // dispatch (and long-jump default) blocks removed from the CFG
|
|
888
1219
|
rawBlocks.forEach((d, i) => {
|
|
889
1220
|
const last = d.instrs[d.instrs.length - 1];
|
|
890
|
-
if (last
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
1221
|
+
if (!last || last.mnemonic !== 'mov' || last.ops[0] !== 'pc' || last.ops[1] === 'lr') {
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
const refs = branchRefs.get(d.label) ?? 0;
|
|
1225
|
+
const prev = rawBlocks[i - 1];
|
|
1226
|
+
// Direct form: the dispatch is reached ONLY by falling through from its bounds predecessor. A
|
|
1227
|
+
// `b disp` from anywhere else would leave a dangling edge after elision, so decline (→ loud-fail).
|
|
1228
|
+
if (prev && refs === 0) {
|
|
1229
|
+
const jt = recoverJumpTable(prev, d, dataWords, blockLabels);
|
|
895
1230
|
if (jt) {
|
|
896
|
-
tables.set(
|
|
1231
|
+
tables.set(prev, jt);
|
|
897
1232
|
elided.add(d);
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
// Long-jump form: `bounds` (cmp; bls DISP), then a lone `b DEF` block, then the dispatch. The
|
|
1237
|
+
// dispatch is entered by exactly that one `bls` and nothing else, and the `b DEF` block —
|
|
1238
|
+
// synthetically labelled, so unnameable and unreachable once the bounds block dispatches — is
|
|
1239
|
+
// elided WITH it. Leaving it would make it a parameterless predecessor of the default block,
|
|
1240
|
+
// and wiring a phi through it fabricates an entry parameter (the phantom-param miscompile).
|
|
1241
|
+
const boundsB = rawBlocks[i - 2];
|
|
1242
|
+
const prevNamed = prev ? (branchRefs.get(prev.label) ?? 0) > 0 : false;
|
|
1243
|
+
if (refs === 1 && prev && boundsB && !prevNamed && prev.instrs.length === 1 && prev.instrs[0].mnemonic === 'b') {
|
|
1244
|
+
const jt = recoverJumpTable(boundsB, d, dataWords, blockLabels, prev.instrs[0].ops[0]);
|
|
1245
|
+
if (jt) {
|
|
1246
|
+
tables.set(boundsB, jt);
|
|
1247
|
+
elided.add(d);
|
|
1248
|
+
elided.add(prev);
|
|
898
1249
|
}
|
|
899
1250
|
}
|
|
900
1251
|
});
|
|
@@ -988,16 +1339,73 @@ export function lift(
|
|
|
988
1339
|
};
|
|
989
1340
|
const reg = (s: string) => s.replace(/[[\]]/g, '');
|
|
990
1341
|
|
|
1342
|
+
// THE one test for "is this token the stack pointer". Case-insensitive because GNU as accepts
|
|
1343
|
+
// uppercase register names, and a case-sensitive test here is a silent-wrong-answer hole rather
|
|
1344
|
+
// than a cosmetic one: `add r0, SP, #4` is `&local`, and missing it fabricates a phantom
|
|
1345
|
+
// parameter and emits confident arithmetic on it.
|
|
1346
|
+
const isSpReg = (s: string | undefined): boolean => {
|
|
1347
|
+
const r = reg(s ?? '').toLowerCase();
|
|
1348
|
+
return r === 'sp' || r === 'r13';
|
|
1349
|
+
};
|
|
1350
|
+
|
|
1351
|
+
// Writing sp is transparent frame bookkeeping ONLY in the one shape that cannot change anything
|
|
1352
|
+
// observable: `sp = sp ± immediate`. Two producers feed this frontend and each emits exactly ONE
|
|
1353
|
+
// spelling, which is why both must work and why handling only one silently halved the input:
|
|
1354
|
+
//
|
|
1355
|
+
// producer `add sp, #N` `add sp, sp, #N`
|
|
1356
|
+
// agbcc's own .s (checkouts/*/build/src) 0 98
|
|
1357
|
+
// disassembly (klonoa asm/ · sa3 asm/) 203 · 1250 0
|
|
1358
|
+
//
|
|
1359
|
+
// So this is not "one tool with two spellings" — it is the compiler's convention against the
|
|
1360
|
+
// disassembler's, and asmlift reads both kinds of file. (An earlier commit message on this branch
|
|
1361
|
+
// claimed agbcc emitted both; it does not, and the counts above are the check that settles it.)
|
|
1362
|
+
//
|
|
1363
|
+
// Everything else that writes sp is a
|
|
1364
|
+
// frame change this frontend cannot model: a register-sized adjustment (`add sp, r4`, agbcc's
|
|
1365
|
+
// way of spelling a frame too large for the 7-bit immediate), a computed stack pointer
|
|
1366
|
+
// (`add sp, r0, #4`), or `mov sp, rN`. Those must DECLINE, not vanish — dropping them deletes a
|
|
1367
|
+
// frame change with no diagnostic, which is the exact
|
|
1368
|
+
// loud-becomes-silent trade this frontend's guards exist to prevent.
|
|
1369
|
+
//
|
|
1370
|
+
// Flag-setting spellings (`adds`/`subs`) are excluded deliberately: ARMv4T's SP-adjust encoding
|
|
1371
|
+
// does not set flags, so a flag-setting one can only come from hand-written asm, where dropping
|
|
1372
|
+
// it would leave a stale compare for a following conditional branch to fold. There are 0 in the
|
|
1373
|
+
// benchmark corpus and 0 across the klonoa and sa3 checkouts, so excluding them costs nothing.
|
|
1374
|
+
// The decline names WHY the slot model is off when it is (slotsOffReason, assigned below —
|
|
1375
|
+
// referenced through the closure, so this reads the final value at throw time). The gap
|
|
1376
|
+
// histogram is the improvement loop's work-list, and "local stack frames not supported" was a
|
|
1377
|
+
// false attribution for a function whose frame IS modelled but whose blocker is, say, an
|
|
1378
|
+
// address-taken local or an outgoing stack argument — it sent the loop to build the wrong thing.
|
|
1379
|
+
const spAsDataError = () =>
|
|
1380
|
+
new FrontendUnsupportedError(
|
|
1381
|
+
`cannot lift '${name}': stack pointer used as data — ` +
|
|
1382
|
+
(slotsOffReason ?? 'not a modelled slot (address-taken local / frame arithmetic / above the local area)'),
|
|
1383
|
+
);
|
|
1384
|
+
|
|
1385
|
+
const isFrameAdjust = (
|
|
1386
|
+
mnemonic: string,
|
|
1387
|
+
dest: string | undefined,
|
|
1388
|
+
base: string | undefined,
|
|
1389
|
+
off: string | undefined,
|
|
1390
|
+
): boolean =>
|
|
1391
|
+
(mnemonic === 'add' || mnemonic === 'sub') &&
|
|
1392
|
+
isSpReg(dest) &&
|
|
1393
|
+
(base === undefined || isSpReg(base)) &&
|
|
1394
|
+
(off?.startsWith('#') ?? false);
|
|
1395
|
+
|
|
991
1396
|
// Reading sp as a DATA operand means an address-taken local (`add rD, sp, #N` = `&local`),
|
|
992
1397
|
// an sp-relative spill slot (`ldr/str …, [sp, #N]`), or frame-pointer arithmetic — none
|
|
993
|
-
// modellable without a stack abstraction.
|
|
994
|
-
//
|
|
995
|
-
//
|
|
1398
|
+
// modellable without a stack abstraction. Without this guard Braun SSA would materialize sp as a
|
|
1399
|
+
// fabricated PHANTOM parameter that scrambles the signature. Fail LOUD instead, mirroring MIPS
|
|
1400
|
+
// (`isStackPtr`) and PPC (`r1`).
|
|
1401
|
+
//
|
|
1402
|
+
// sp is never WRITTEN either — but by `writeData` declining, NOT because sp-dest ops are inert.
|
|
1403
|
+
// That was this file's premise until the frame-adjust whitelist landed, and it was wrong: five
|
|
1404
|
+
// decode arms wrote sp and dropped it silently. The single transparent shape is `sp = sp ± imm`,
|
|
1405
|
+
// whitelisted in the add/sub arms. Read and write are now guarded symmetrically.
|
|
996
1406
|
const readData = (r: string, b: number): Value => {
|
|
997
|
-
if (r
|
|
998
|
-
throw
|
|
999
|
-
`cannot lift '${name}': stack pointer used as data (address-taken local / sp-relative slot / frame arithmetic) — local stack frames not supported`,
|
|
1000
|
-
);
|
|
1407
|
+
if (isSpReg(r)) {
|
|
1408
|
+
throw spAsDataError();
|
|
1001
1409
|
}
|
|
1002
1410
|
if (r === 'pc' || r === 'r15') {
|
|
1003
1411
|
// A pc-relative literal load is rewritten to a pool label before reaching here (decode's
|
|
@@ -1014,6 +1422,539 @@ export function lift(
|
|
|
1014
1422
|
return readVar(r, b);
|
|
1015
1423
|
};
|
|
1016
1424
|
|
|
1425
|
+
// A virtual register key per incoming stack argument. Reading it goes through the ordinary Braun
|
|
1426
|
+
// live-in path (frontend/ssa.ts), which turns a read with no reaching def into a function
|
|
1427
|
+
// parameter — so this needs NO new representation, opcode or pass. The `@` cannot appear in a
|
|
1428
|
+
// real register token, so the key cannot collide with one.
|
|
1429
|
+
const stackArgKey = (index: number) => `@sarg${index}`;
|
|
1430
|
+
// Defined beside its mint site on purpose: the format string and its parser drifted 650 lines
|
|
1431
|
+
// apart in the first version, with the convention explained only at one end.
|
|
1432
|
+
const stackArgIndex = (key: string): number | null => {
|
|
1433
|
+
const m = /^@sarg(\d+)$/.exec(key);
|
|
1434
|
+
return m ? +m[1] : null;
|
|
1435
|
+
};
|
|
1436
|
+
|
|
1437
|
+
// INCOMING STACK ARGUMENTS (AAPCS). Args 1-4 arrive in r0-r3; args 5+ are pushed by the CALLER,
|
|
1438
|
+
// so the callee reads them at `[sp, #N]` where N is at or above its own frame. Those are
|
|
1439
|
+
// PARAMETERS, not locals — declining them as "sp used as data" refuses a calling convention.
|
|
1440
|
+
//
|
|
1441
|
+
// The frame depth is tracked by a linear walk of the ENTRY BLOCK only, which is
|
|
1442
|
+
// why the gate is what it is: within one straight-line block the depth at each instruction is
|
|
1443
|
+
// exact and needs no CFG reasoning. Every case that would need more declines.
|
|
1444
|
+
//
|
|
1445
|
+
// `push {a,b,c}` deepens by 4 per register; `sub sp, #N` and `add sp, sp, #-N` deepen by N.
|
|
1446
|
+
//
|
|
1447
|
+
// The walk, its two invariants and the predicate that reads them are ONE object on purpose. They
|
|
1448
|
+
// were three loose pieces — a delta function, a pair of `let`s updated in the instruction loop,
|
|
1449
|
+
// and a seven-parameter predicate taking the pair by value — and both bugs the second review pass
|
|
1450
|
+
// found lived in the seams: an invariant the predicate's proof needed but only the loop could
|
|
1451
|
+
// enforce, and a `0` returned for an unrecognised shape that only a guard in a third place made
|
|
1452
|
+
// safe. Anything that changes how sp moves now has one place to change, and the proof it has to
|
|
1453
|
+
// preserve is written next to the state it is about.
|
|
1454
|
+
const makeFrameWalk = () => {
|
|
1455
|
+
// Bytes the frame has grown since function entry, along this block's linear order only.
|
|
1456
|
+
let depth = 0;
|
|
1457
|
+
let depthKnown = true;
|
|
1458
|
+
|
|
1459
|
+
// Returns null whenever the depth cannot be computed exactly — including for any write to sp
|
|
1460
|
+
// this does not model. The capability rests on the depth being EXACT, so an approximation is
|
|
1461
|
+
// never acceptable: understate the frame and a local sits above the computed top and gets
|
|
1462
|
+
// minted as a parameter reading uninitialised stack. A null poisons the depth for the rest of
|
|
1463
|
+
// the block, which disables argument recovery and leaves every `[sp,#N]` to decline as before.
|
|
1464
|
+
// Nothing here may return a NUMBER for a shape it merely failed to recognise.
|
|
1465
|
+
const delta = (ins: { mnemonic: string; ops: string[] }): number | null => {
|
|
1466
|
+
const m = ins.mnemonic;
|
|
1467
|
+
if (m === 'push' || m === 'pop') {
|
|
1468
|
+
// expandRegList, NOT a comma count: `push {r4-r7, lr}` is FIVE registers, and counting it as
|
|
1469
|
+
// two makes the frame 12 bytes too shallow — which turns a genuine LOCAL into a fabricated
|
|
1470
|
+
// parameter reading uninitialised stack. Caught by a probe, not by the corpus: agbcc emits no
|
|
1471
|
+
// range pushes and 0 of the 743 benchmark rows contain one, but GNU as accepts them and the
|
|
1472
|
+
// disassembly path can produce them.
|
|
1473
|
+
// Counting comma tokens instead would undercount `{r4-lr}` as two registers, and an
|
|
1474
|
+
// unexpandable range must poison the depth rather than be guessed at — definiteRegList owns
|
|
1475
|
+
// both rules, and owns them for the ldm/stm arm too.
|
|
1476
|
+
const list = definiteRegList(
|
|
1477
|
+
ins.ops
|
|
1478
|
+
.join(',')
|
|
1479
|
+
.replace(/[{}]/g, '')
|
|
1480
|
+
.split(',')
|
|
1481
|
+
.map((r) => r.trim())
|
|
1482
|
+
.filter(Boolean),
|
|
1483
|
+
);
|
|
1484
|
+
if (list === null) {
|
|
1485
|
+
return null;
|
|
1486
|
+
}
|
|
1487
|
+
return (m === 'push' ? 1 : -1) * 4 * list.length;
|
|
1488
|
+
}
|
|
1489
|
+
if ((m === 'add' || m === 'sub') && isSpReg(ins.ops[0])) {
|
|
1490
|
+
const off = ins.ops[2] ?? ins.ops[1];
|
|
1491
|
+
if (off?.startsWith('#')) {
|
|
1492
|
+
const v = imm(off);
|
|
1493
|
+
return m === 'sub' ? v : -v;
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
// Any OTHER write to sp poisons the depth. This used to fall through to 0 — "no change" — for
|
|
1497
|
+
// shapes it does not model (`add sp, r4`, `mov sp, rN`, `add sp, r0, #4`), which was safe only
|
|
1498
|
+
// because writeData declines every one of them elsewhere. That is the same
|
|
1499
|
+
// enumeration-of-arms mistake writeData itself exists to end, exported one function away: a
|
|
1500
|
+
// number meaning "no change" is the wrong answer to "I do not understand this". Now the walk is
|
|
1501
|
+
// self-sufficient — unknown ⇒ depth poisoned ⇒ decline — and writeData's refusal is an
|
|
1502
|
+
// independent second guarantee instead of a load-bearing one.
|
|
1503
|
+
if (isSpReg(ins.ops[0])) {
|
|
1504
|
+
return null;
|
|
1505
|
+
}
|
|
1506
|
+
return 0;
|
|
1507
|
+
};
|
|
1508
|
+
|
|
1509
|
+
return {
|
|
1510
|
+
// Advance the walk over one instruction. Call for EVERY instruction, in order.
|
|
1511
|
+
step(ins: { mnemonic: string; ops: string[] }): void {
|
|
1512
|
+
const d = delta(ins);
|
|
1513
|
+
if (d === null) {
|
|
1514
|
+
depthKnown = false;
|
|
1515
|
+
} else {
|
|
1516
|
+
depth += d;
|
|
1517
|
+
}
|
|
1518
|
+
// sp ABOVE the incoming sp poisons the walk for the rest of the block, permanently — the
|
|
1519
|
+
// premise argIndex's proof rests on. See the proof there for what it costs to omit.
|
|
1520
|
+
if (depth < 0) {
|
|
1521
|
+
depthKnown = false;
|
|
1522
|
+
}
|
|
1523
|
+
},
|
|
1524
|
+
|
|
1525
|
+
// Is `[sp, #off]` an incoming stack argument, and which one? `null` means NO — and every null is
|
|
1526
|
+
// a DECLINE, because the caller falls through to readData's sp guard.
|
|
1527
|
+
//
|
|
1528
|
+
// Why a slot at or above the frame top cannot have been written by this function, which is the
|
|
1529
|
+
// whole soundness argument: every sp-relative STORE declines (readData, via the str arm), and a
|
|
1530
|
+
// `push` only ever writes strictly BELOW the current top. An argument slot is at or above the
|
|
1531
|
+
// incoming sp, so no instruction here can have defined it — the value can only be the caller's.
|
|
1532
|
+
//
|
|
1533
|
+
// That second step needs sp to have stayed at or below where it came in — `depth >= 0` at every
|
|
1534
|
+
// point of the walk — or a `push` reaches back up over the argument area and the conclusion is
|
|
1535
|
+
// false:
|
|
1536
|
+
//
|
|
1537
|
+
// add sp, sp, #8 ; sp = S+8
|
|
1538
|
+
// push {r4, r5, r6} ; sp = S-4, and this WROTE r5 to S+0
|
|
1539
|
+
// ldr r0, [sp, #4] ; = S+0 — r5's slot, not the caller's argument
|
|
1540
|
+
//
|
|
1541
|
+
// The depth is back to a plausible +4 by the load, so nothing downstream can tell: it emitted
|
|
1542
|
+
// `s32 f(s32 a0, …, s32 a4) { return a4; }`, the function's own incoming r5 handed back as
|
|
1543
|
+
// argument 5. `pop {r4}; push {r4,r5}` gets there without an `add sp` at all, and a sliced
|
|
1544
|
+
// fragment whose prologue was cut off is exactly this shape — which is the corpus this frontend
|
|
1545
|
+
// reads. `step` enforces it, and never un-poisons: once sp has been above the line, a push during
|
|
1546
|
+
// the excursion may have written the later slots too. This is a PREMISE, not a detail — leaving
|
|
1547
|
+
// it unstated is what let the first version hand back a callee-saved register as an argument.
|
|
1548
|
+
argIndex(addr: { base: string; off: number; regOff?: string }, width: number, bi: number): number | null {
|
|
1549
|
+
const { base, off, regOff } = addr;
|
|
1550
|
+
if (!isSpReg(base) || regOff !== undefined) {
|
|
1551
|
+
return null; // not sp, or `[sp, rX]` — not a fixed argument slot
|
|
1552
|
+
}
|
|
1553
|
+
if (bi !== 0 || preds[0].length > 0) {
|
|
1554
|
+
return null; // depth is exact only along the ENTRY block's linear order, and only when its
|
|
1555
|
+
// params are parameters rather than phis
|
|
1556
|
+
}
|
|
1557
|
+
if (!depthKnown || depth <= 0) {
|
|
1558
|
+
return null; // an unmeasurable frame, or none established: a headerless FRAGMENT whose
|
|
1559
|
+
// prologue was sliced off looks identical to a frameless function, and there the slots are
|
|
1560
|
+
// locals — minting one would be the silent-wrong trade this frontend refuses
|
|
1561
|
+
}
|
|
1562
|
+
if (width !== 4 || off < depth || (off - depth) % 4 !== 0) {
|
|
1563
|
+
return null; // the argument area is word-granular; BELOW the top is a local, which is the
|
|
1564
|
+
// separate slot-promotion capability
|
|
1565
|
+
}
|
|
1566
|
+
const index = target.argRegs.length + (off - depth) / 4;
|
|
1567
|
+
return index < MAX_RECOVERED_ARITY ? index : null; // a wild offset must not mint a
|
|
1568
|
+
// 400-parameter signature
|
|
1569
|
+
},
|
|
1570
|
+
};
|
|
1571
|
+
};
|
|
1572
|
+
|
|
1573
|
+
// LOCAL STACK SLOTS. A spill or a local that never has its address taken is transparent to
|
|
1574
|
+
// dataflow: `str rX,[sp,#k]` … `ldr rY,[sp,#k]` moves a value, it does not touch memory anyone
|
|
1575
|
+
// else can see. Modelling it as an SSA variable keyed by the offset (the same `sp@<off>` spelling
|
|
1576
|
+
// the MIPS frontend uses) makes it exactly that, and Braun's phi construction handles a slot that
|
|
1577
|
+
// is read-modify-written across a loop with no extra machinery.
|
|
1578
|
+
//
|
|
1579
|
+
// What Thumb does NOT inherit from MIPS: keying by the RAW sp offset is only sound while sp holds
|
|
1580
|
+
// the same value at every access, and MIPS gets that for free (IDO establishes sp with one
|
|
1581
|
+
// `addiu` and never moves it). Thumb's `push` moves sp, so constancy has to be PROVEN here.
|
|
1582
|
+
const slotKey = stackSlotKey; // shared spelling: frontend/ssa.ts
|
|
1583
|
+
// Deliberately OVER-inclusive: a `cmp sp, rN` only reads sp but counts here too. Every false
|
|
1584
|
+
// positive costs a decline, every false negative costs a wrong slot — so it errs loudly.
|
|
1585
|
+
const modifiesSp = (ins: Instr): boolean =>
|
|
1586
|
+
ins.mnemonic === 'push' || ins.mnemonic === 'pop' || isSpReg((ins.ops[0] ?? '').replace(/!$/, ''));
|
|
1587
|
+
// A `mov rD, sp` CAPTURES the frame address; the captured value means "the frame base" only if
|
|
1588
|
+
// sp still holds that base wherever the value is used — so a capture participates in the
|
|
1589
|
+
// constancy proof exactly like a literal [sp,#k] access, and ends the prologue for localArea.
|
|
1590
|
+
const capturesSp = (ins: Instr): boolean =>
|
|
1591
|
+
/^movs?$/.test(ins.mnemonic) && !isSpReg(ins.ops[0] ?? '') && isSpReg(ins.ops[1] ?? '');
|
|
1592
|
+
const touchesFrame = (ins: Instr): boolean => spMemAccess(ins) !== null || capturesSp(ins);
|
|
1593
|
+
const spMemAccess = (ins: Instr): { off: number; width: number; regOff: boolean } | null => {
|
|
1594
|
+
if (!/^(ldr|ldrb|ldrh|ldrsb|ldrsh|str|strb|strh)$/.test(ins.mnemonic)) {
|
|
1595
|
+
return null;
|
|
1596
|
+
}
|
|
1597
|
+
const mem = ins.ops[1];
|
|
1598
|
+
if (mem === undefined) {
|
|
1599
|
+
return null;
|
|
1600
|
+
}
|
|
1601
|
+
const { base, off, regOff } = parseAddr(mem);
|
|
1602
|
+
return isSpReg(base)
|
|
1603
|
+
? { off, width: /b$/.test(ins.mnemonic) ? 1 : /h$/.test(ins.mnemonic) ? 2 : 4, regOff: regOff !== undefined }
|
|
1604
|
+
: null;
|
|
1605
|
+
};
|
|
1606
|
+
// Is the word-slot model safe for THIS function? Every disqualifier below leaves every `[sp,#k]`
|
|
1607
|
+
// access on the old path, which declines — so the answer to "not sure" is the loud one.
|
|
1608
|
+
// Returns null when the word-slot model is SAFE for this function, else the reason it is off —
|
|
1609
|
+
// which the sp declines append, so a refused function names the capability actually missing
|
|
1610
|
+
// instead of the generic "local stack frames". The gap histogram is the improvement loop's
|
|
1611
|
+
// work-list; a misattributed refusal sends that loop to build the wrong thing.
|
|
1612
|
+
const slotModelBlocker = (): string | null => {
|
|
1613
|
+
for (const ab of asmBlocks) {
|
|
1614
|
+
for (const ins of ab.instrs) {
|
|
1615
|
+
const acc = spMemAccess(ins);
|
|
1616
|
+
// A SUB-WORD access aliasing a word slot is the hazard MIPS paid for the hard way: routing
|
|
1617
|
+
// the word store to an SSA slot while the byte reload stays on the memory path drops the
|
|
1618
|
+
// store and reads uninitialised memory. One anywhere disables the model for the whole
|
|
1619
|
+
// function. A register offset can alias any slot, so it disqualifies the same way.
|
|
1620
|
+
if (acc && (acc.width !== 4 || acc.regOff)) {
|
|
1621
|
+
return acc.regOff
|
|
1622
|
+
? 'a register-offset sp access can alias any slot'
|
|
1623
|
+
: 'a sub-word sp access aliases the word-slot model';
|
|
1624
|
+
}
|
|
1625
|
+
// sp escaping into a register: a computed form (`add rD, sp, #k`) is still a refusal, but a
|
|
1626
|
+
// plain COPY (`mov rD, sp`) is now the address-taken-local capability — the mov arm emits a
|
|
1627
|
+
// `laddr` for it and the post-lift frame-object audit proves every use, so the model's
|
|
1628
|
+
// remaining precondition is that the frame has a reserved local area for the object to
|
|
1629
|
+
// live in. A frameless function taking sp's address has nothing to model and refuses.
|
|
1630
|
+
if (ins.mnemonic === 'add' && !isSpReg(ins.ops[0] ?? '') && ins.ops.slice(1).some((o) => isSpReg(o))) {
|
|
1631
|
+
return `the address of a stack local is computed (\`${ins.mnemonic} ${ins.ops.join(', ')}\`) — only a plain \`mov rD, sp\` capture is modelled`;
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
// sp must be CONSTANT wherever a slot is keyed, because the key IS the raw offset. Two shapes
|
|
1636
|
+
// are legitimate and everything else refuses: a PROLOGUE (sp moves before the block touches the
|
|
1637
|
+
// frame) and an EPILOGUE (sp moves after it has finished touching it, and the block returns, so
|
|
1638
|
+
// nothing downstream can key a slot against the changed sp). A modification BETWEEN two
|
|
1639
|
+
// accesses moves the frame under a slot already keyed, and only the entry block may deepen —
|
|
1640
|
+
// elsewhere a pre-access modification would put the block at a different depth from the one the
|
|
1641
|
+
// prologue established.
|
|
1642
|
+
for (let bi = 0; bi < asmBlocks.length; bi++) {
|
|
1643
|
+
const ins = asmBlocks[bi].instrs;
|
|
1644
|
+
const mems = ins.map((x, i) => (touchesFrame(x) ? i : -1)).filter((i) => i >= 0);
|
|
1645
|
+
const mods = ins.map((x, i) => (modifiesSp(x) ? i : -1)).filter((i) => i >= 0);
|
|
1646
|
+
if (mods.length === 0) {
|
|
1647
|
+
continue;
|
|
1648
|
+
}
|
|
1649
|
+
const last = ins[ins.length - 1];
|
|
1650
|
+
const returns = last !== undefined && classifyXfer(last) === 'return';
|
|
1651
|
+
if (mems.length === 0) {
|
|
1652
|
+
// moves sp and never keys a slot itself: safe only if nothing downstream can key one
|
|
1653
|
+
if (!returns && bi !== 0) {
|
|
1654
|
+
return 'sp moves in a block that neither returns nor is the entry';
|
|
1655
|
+
}
|
|
1656
|
+
continue;
|
|
1657
|
+
}
|
|
1658
|
+
for (const m of mods) {
|
|
1659
|
+
if (m < mems[0]) {
|
|
1660
|
+
if (bi !== 0) {
|
|
1661
|
+
return 'a non-entry block establishes its own frame depth'; // not the prologue's
|
|
1662
|
+
}
|
|
1663
|
+
} else if (m > mems[mems.length - 1]) {
|
|
1664
|
+
if (!returns) {
|
|
1665
|
+
return 'sp unwinds mid-function and execution continues';
|
|
1666
|
+
}
|
|
1667
|
+
} else {
|
|
1668
|
+
return 'the frame moves between two accesses that keyed slots against it';
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
// OUTGOING ARGUMENTS. agbcc reserves the BOTTOM of the frame for arguments 5+ of the calls this
|
|
1673
|
+
// function makes: `add sp,sp,#-8` … `str r2,[sp]` / `str r3,[sp,#4]` … `bl callee`. Those
|
|
1674
|
+
// offsets are inside the frame and nothing this function does ever reloads them, so modelling
|
|
1675
|
+
// them as locals makes them dead defs and DCE deletes them — the arguments vanish from the call
|
|
1676
|
+
// with no diagnostic. Ground truth: sa3's CreateEntity_Platform_0_0 (platform.c:734) forwards
|
|
1677
|
+
// SIX arguments and came out as `CreateEntity_Platform(0, 0, a0, (u16)a1)`. "Inside my frame"
|
|
1678
|
+
// does not mean "private": the outgoing area belongs to the callee, which may even assign to a
|
|
1679
|
+
// stack parameter.
|
|
1680
|
+
//
|
|
1681
|
+
// How big is that area? A declared arity bounds it from BELOW — `4 * max(0, arity - 4)` — and
|
|
1682
|
+
// that is all the facts available here can support. It is used in exactly one direction: to
|
|
1683
|
+
// REFUSE. A callee declared with five parameters proves this frame has an outgoing area, and
|
|
1684
|
+
// consuming those stores as call operands is the dual capability, unbuilt, so the model
|
|
1685
|
+
// declines. A callee declared with four proves NOTHING, because a declaration is a lower bound
|
|
1686
|
+
// on the words a call actually pushes:
|
|
1687
|
+
//
|
|
1688
|
+
// * a parameter may occupy more than one word (`double`, `long long`, a struct by value),
|
|
1689
|
+
// * a variadic callee's list is a prefix — `sprintf` truthfully declares two and is handed six,
|
|
1690
|
+
// * a large struct return adds a hidden pointer argument that appears in no parameter list.
|
|
1691
|
+
//
|
|
1692
|
+
// None of those is recorded by `FnProto` or `SymbolSignature`, so no arity here can license an
|
|
1693
|
+
// ACCEPTANCE. An earlier cut treated `arity <= 4` as proof of an empty area and had all three
|
|
1694
|
+
// holes: supplying a TRUE fact (`{ sprintf: { params: 2 } }`) turned a correct decline into
|
|
1695
|
+
// `return sprintf(a0, a1)` with both stack arguments deleted, where supplying nothing declined.
|
|
1696
|
+
// A fact must only ever move a function toward refusal — never toward an acceptance the facts
|
|
1697
|
+
// do not entail. Refusing on a lower bound is monotone in exactly that way: a true arity larger
|
|
1698
|
+
// than declared can only make the area bigger, and the answer is already "decline".
|
|
1699
|
+
//
|
|
1700
|
+
// Measured, this costs nothing it was buying: forcing the old acceptance path off changed 0
|
|
1701
|
+
// lift/decline verdicts across 2686 corpus functions (sa3's vendored map carries no signatures
|
|
1702
|
+
// at all), so the path that carried those holes was never load-bearing.
|
|
1703
|
+
for (const ab of asmBlocks) {
|
|
1704
|
+
for (const ins of ab.instrs) {
|
|
1705
|
+
if (ins.mnemonic !== 'bl' && ins.mnemonic !== 'blx') {
|
|
1706
|
+
continue;
|
|
1707
|
+
}
|
|
1708
|
+
const c = ins.ops[0] ?? '';
|
|
1709
|
+
const arity = protoArity(prototypes[c]) ?? protoArity(RUNTIME_HELPERS[c]);
|
|
1710
|
+
if (arity !== undefined && arity > target.argRegs.length) {
|
|
1711
|
+
return `callee \`${c}\` is declared with ${arity} arguments, so this frame has an outgoing stack-argument area — consuming stack call arguments is not implemented`;
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
// Nothing above could prove the area empty, so fall back to reading the CODE. Two conditions,
|
|
1716
|
+
// covering different escapes:
|
|
1717
|
+
// (a) every slot store must be reloaded somewhere reachable. An outgoing argument is read by
|
|
1718
|
+
// the CALLEE, never by the caller, so a store never read back is the signature of one.
|
|
1719
|
+
// (b) no slot store may reach a `bl` unread ALONG A PATH.
|
|
1720
|
+
//
|
|
1721
|
+
// Neither is sound alone and the pair is not either, so keep two things straight. (a)'s real
|
|
1722
|
+
// theorem is not "the callee reads it, the caller does not" — it is that agbcc's
|
|
1723
|
+
// ACCUMULATE_OUTGOING_ARGS puts the outgoing area at the BOTTOM of localArea, disjoint from the
|
|
1724
|
+
// locals, so no local load can land on an argument offset. That disjointness is what a
|
|
1725
|
+
// tail-merged call site breaks, and agbcc DOES tail-merge: `Task_BonusFlower_Spawn` (sa3
|
|
1726
|
+
// bonus_game_enemies) stores argument 5 in both predecessors with the `bl` in the join.
|
|
1727
|
+
//
|
|
1728
|
+
// (b) is a forward may-analysis over the CFG, and it has been wrong twice in the other
|
|
1729
|
+
// direction. Scanning per block let a LABEL decide accept versus refuse; scanning the flat
|
|
1730
|
+
// listing let BLOCK ORDER decide, because a load in one arm of a branch cleared a store that
|
|
1731
|
+
// reaches the call through the other arm — swap the arms in the listing, same CFG and same
|
|
1732
|
+
// semantics, and the verdict flipped. Only the path-sensitive form is stable under layout.
|
|
1733
|
+
//
|
|
1734
|
+
// Only for a function that CALLS. With no call there is no outgoing area to mistake a local
|
|
1735
|
+
// for, and a never-reloaded store there is an ordinary dead local — which PR #30 modelled and
|
|
1736
|
+
// which must keep working.
|
|
1737
|
+
if (asmBlocks.some((ab) => ab.instrs.some((i) => i.mnemonic === 'bl' || i.mnemonic === 'blx'))) {
|
|
1738
|
+
const slotAcc = (ins: Instr) => {
|
|
1739
|
+
const a = spMemAccess(ins);
|
|
1740
|
+
return a && !a.regOff && a.width === 4 && a.off % 4 === 0 && a.off >= 0 && a.off < localArea ? a.off : null;
|
|
1741
|
+
};
|
|
1742
|
+
const isStore = (ins: Instr) => /^str/.test(ins.mnemonic);
|
|
1743
|
+
// Entry-REACHABLE blocks only: a reload in dead code is not evidence that live code reads the
|
|
1744
|
+
// slot back, and counting it lets an argument store satisfy (a) on the strength of an
|
|
1745
|
+
// instruction that never executes.
|
|
1746
|
+
const live = new Set<number>([0]);
|
|
1747
|
+
for (let changed = true; changed;) {
|
|
1748
|
+
changed = false;
|
|
1749
|
+
for (let b = 0; b < asmBlocks.length; b++) {
|
|
1750
|
+
if (live.has(b)) {
|
|
1751
|
+
continue;
|
|
1752
|
+
}
|
|
1753
|
+
if (preds[b].some((q) => live.has(q))) {
|
|
1754
|
+
live.add(b);
|
|
1755
|
+
changed = true;
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
const reloaded = new Set<number>();
|
|
1760
|
+
for (const b of live) {
|
|
1761
|
+
for (const ins of asmBlocks[b].instrs) {
|
|
1762
|
+
const off = slotAcc(ins);
|
|
1763
|
+
if (off !== null && !isStore(ins)) {
|
|
1764
|
+
reloaded.add(off);
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
// CONTIGUITY. AAPCS lays the outgoing stack arguments at [sp,#0] upward, one word each, so an
|
|
1769
|
+
// argument block is CONTIGUOUS FROM ZERO: a store at [sp,#4] can be argument 6 of a call only
|
|
1770
|
+
// if argument 5 at [sp,#0] is also supplied on a path to that same call. A pending store
|
|
1771
|
+
// whose lower slots are nowhere supplied is therefore provably not an argument block, and
|
|
1772
|
+
// refusing it is a false alarm — the exact false alarm that blocked the commonest real shape,
|
|
1773
|
+
// a value spilled at [sp,#4] and kept live across calls (kleod's ProcessInputAndUpdateEntities
|
|
1774
|
+
// stores its `sp4` local and calls m4aSongNumStart 80 lines later, with offset 0 never stored
|
|
1775
|
+
// in the whole function).
|
|
1776
|
+
//
|
|
1777
|
+
// The calibration in this: a conforming caller stores EVERY argument slot of a call it makes,
|
|
1778
|
+
// so "slot 0 unsupplied" rules out "slot 4 is an argument". Hand-written asm could skip
|
|
1779
|
+
// storing an argument the callee never reads; agbcc cannot (no interprocedural dead-argument
|
|
1780
|
+
// elimination). That is the same producer assumption the reload conditions above already
|
|
1781
|
+
// make, stated once here.
|
|
1782
|
+
const prefixStored = (k: number, st: Set<number>): boolean => {
|
|
1783
|
+
for (let j = 0; j < k; j += 4) {
|
|
1784
|
+
if (!st.has(j)) {
|
|
1785
|
+
return false;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
return true;
|
|
1789
|
+
};
|
|
1790
|
+
// (a), contiguity-filtered: a store never reloaded ANYWHERE is an argument's signature only
|
|
1791
|
+
// if its lower slots are supplied somewhere too; otherwise it is an ordinary dead local.
|
|
1792
|
+
const storedAnywhere = new Set<number>();
|
|
1793
|
+
for (const b of live) {
|
|
1794
|
+
for (const ins of asmBlocks[b].instrs) {
|
|
1795
|
+
const off = slotAcc(ins);
|
|
1796
|
+
if (off !== null && isStore(ins)) {
|
|
1797
|
+
storedAnywhere.add(off);
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
for (const off of storedAnywhere) {
|
|
1802
|
+
if (!reloaded.has(off) && prefixStored(off, storedAnywhere)) {
|
|
1803
|
+
return `the store to [sp,#${off}] is never reloaded and its lower slots are supplied — it may be an outgoing stack argument of one of this function's calls`; // (a)
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
// (b): `pendingOut[b]` = offsets stored and not yet reloaded on SOME path through b;
|
|
1807
|
+
// `storedOut[b]` = offsets stored on SOME path through b (a reload does not remove the value
|
|
1808
|
+
// from memory, so it does not remove the offset from this set — the callee would still read
|
|
1809
|
+
// what the store put there).
|
|
1810
|
+
const pendingOut: Array<Set<number>> = asmBlocks.map(() => new Set<number>());
|
|
1811
|
+
const storedOut: Array<Set<number>> = asmBlocks.map(() => new Set<number>());
|
|
1812
|
+
for (let changed = true; changed;) {
|
|
1813
|
+
changed = false;
|
|
1814
|
+
for (let b = 0; b < asmBlocks.length; b++) {
|
|
1815
|
+
if (!live.has(b)) {
|
|
1816
|
+
continue;
|
|
1817
|
+
}
|
|
1818
|
+
const pend = new Set<number>();
|
|
1819
|
+
const st = new Set<number>();
|
|
1820
|
+
for (const q of preds[b]) {
|
|
1821
|
+
for (const off of pendingOut[q]) {
|
|
1822
|
+
pend.add(off);
|
|
1823
|
+
}
|
|
1824
|
+
for (const off of storedOut[q]) {
|
|
1825
|
+
st.add(off);
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
for (const ins of asmBlocks[b].instrs) {
|
|
1829
|
+
const off = slotAcc(ins);
|
|
1830
|
+
if (off !== null) {
|
|
1831
|
+
if (isStore(ins)) {
|
|
1832
|
+
pend.add(off);
|
|
1833
|
+
st.add(off);
|
|
1834
|
+
} else {
|
|
1835
|
+
pend.delete(off);
|
|
1836
|
+
}
|
|
1837
|
+
} else if (ins.mnemonic === 'bl' || ins.mnemonic === 'blx') {
|
|
1838
|
+
for (const k of pend) {
|
|
1839
|
+
if (prefixStored(k, st)) {
|
|
1840
|
+
// (b) — a plausible argument block reaches this call unread
|
|
1841
|
+
return `the store to [sp,#${k}] reaches \`bl ${ins.ops[0] ?? '?'}\` unread with its lower slots supplied — it may be that call's outgoing stack argument`;
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
const grow = (out: Array<Set<number>>, cur: Set<number>): void => {
|
|
1847
|
+
if (cur.size !== out[b].size || [...cur].some((o) => !out[b].has(o))) {
|
|
1848
|
+
out[b] = cur;
|
|
1849
|
+
changed = true;
|
|
1850
|
+
}
|
|
1851
|
+
};
|
|
1852
|
+
grow(pendingOut, pend);
|
|
1853
|
+
grow(storedOut, st);
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
// A `pop`/`ldm` off sp READS frame memory, and push/pop are transparent to dataflow, so a pop
|
|
1858
|
+
// taken while the local area is still reserved reads a slot this model has retargeted into SSA
|
|
1859
|
+
// — the load simply disagrees with the store. A real epilogue releases the locals first
|
|
1860
|
+
// (`add sp,#N; pop {…}`), which is what this requires; `pop {r1}` mid-frame does not.
|
|
1861
|
+
for (const ab of asmBlocks) {
|
|
1862
|
+
let released = 0;
|
|
1863
|
+
for (const ins of ab.instrs) {
|
|
1864
|
+
if ((ins.mnemonic === 'pop' || ins.mnemonic === 'ldmia') && released < localArea) {
|
|
1865
|
+
const base = ins.mnemonic === 'pop' ? 'sp' : (ins.ops[0] ?? '').replace(/!$/, '');
|
|
1866
|
+
if (isSpReg(base)) {
|
|
1867
|
+
return 'a pop reads the frame while the local area is still reserved';
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
if ((ins.mnemonic === 'add' || ins.mnemonic === 'sub') && isSpReg(ins.ops[0])) {
|
|
1871
|
+
const d = spAdjust(ins);
|
|
1872
|
+
if (d !== null && d > 0) {
|
|
1873
|
+
released += d;
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
return null;
|
|
1879
|
+
};
|
|
1880
|
+
// The frame the body sees: the entry block's PROLOGUE, i.e. everything up to the first
|
|
1881
|
+
// instruction that touches the frame (or the whole block if it never does). Stepped through the
|
|
1882
|
+
// same walk `argIndex` uses, so "what does this do to sp" has one implementation. A frame the
|
|
1883
|
+
// walk cannot measure yields 0, which disables every slot (`off < localArea` is then false).
|
|
1884
|
+
// How much sp moves for `add/sub sp, #imm`, positive = sp RISES (frame shrinks). null if not that
|
|
1885
|
+
// shape. Only used to recognise a release; the authoritative depth arithmetic is makeFrameWalk's.
|
|
1886
|
+
const spAdjust = (ins: Instr): number | null => {
|
|
1887
|
+
if (!/^(add|sub)$/.test(ins.mnemonic) || !isSpReg(ins.ops[0])) {
|
|
1888
|
+
return null;
|
|
1889
|
+
}
|
|
1890
|
+
const o = ins.ops[2] ?? ins.ops[1];
|
|
1891
|
+
if (o === undefined || !o.startsWith('#')) {
|
|
1892
|
+
return null;
|
|
1893
|
+
}
|
|
1894
|
+
const v = imm(o);
|
|
1895
|
+
return ins.mnemonic === 'sub' ? -v : v;
|
|
1896
|
+
};
|
|
1897
|
+
// The EXPLICITLY reserved local area — the prologue's `add sp,sp,#-N`. A slot must live strictly
|
|
1898
|
+
// inside this, not merely inside the whole frame: the rest of the frame is the callee-saved block
|
|
1899
|
+
// the entry `push` wrote, which belongs to the epilogue's `pop`, so a `str` there is retargeted
|
|
1900
|
+
// away from the memory the pop will read.
|
|
1901
|
+
const localArea = ((): number => {
|
|
1902
|
+
// The PROLOGUE only — everything before the entry block first touches the frame. Summing the
|
|
1903
|
+
// whole block instead let a store made BEFORE the reservation fall inside `off < localArea`, so
|
|
1904
|
+
// `str r0,[sp]; add sp,sp,#-4; …` claimed a write to the CALLER's frame as a private local and
|
|
1905
|
+
// deleted it.
|
|
1906
|
+
//
|
|
1907
|
+
// NET, not the sum of the negatives. Counting only reservations and discarding releases leaves
|
|
1908
|
+
// localArea larger than the region that is actually below the callee-saved block, and `off <
|
|
1909
|
+
// localArea` then claims the SAVED REGISTERS as private locals — which the epilogue's `pop`
|
|
1910
|
+
// reads back. `push {lr}; add sp,#-8; add sp,#8; str r0,[sp]; pop {r1}; bx r1` deleted the
|
|
1911
|
+
// store and rendered a computed `bx` as an ordinary return, and it fooled the pop gate too
|
|
1912
|
+
// (`released` is compared against this number). Not corpus-reachable — 0 of 2805 Thumb
|
|
1913
|
+
// functions adjust sp upward before their first frame access — which is exactly why only a
|
|
1914
|
+
// probe finds it.
|
|
1915
|
+
//
|
|
1916
|
+
// An sp write this cannot read poisons the whole thing to 0, disabling every slot, rather than
|
|
1917
|
+
// being skipped as if it were no movement: the same rule spDelta follows.
|
|
1918
|
+
const ins = asmBlocks[0].instrs;
|
|
1919
|
+
const firstMem = ins.findIndex((x) => touchesFrame(x));
|
|
1920
|
+
let reserved = 0;
|
|
1921
|
+
for (const x of ins.slice(0, firstMem === -1 ? ins.length : firstMem)) {
|
|
1922
|
+
const d = spAdjust(x);
|
|
1923
|
+
if (d !== null) {
|
|
1924
|
+
reserved -= d; // d > 0 = sp rises = the frame shrinks
|
|
1925
|
+
} else if (x.mnemonic !== 'push' && x.mnemonic !== 'pop' && isSpReg((x.ops[0] ?? '').replace(/!$/, ''))) {
|
|
1926
|
+
return 0; // an sp write of a shape this does not model
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
return Math.max(0, reserved);
|
|
1930
|
+
})();
|
|
1931
|
+
|
|
1932
|
+
const slotsOffReason = slotModelBlocker();
|
|
1933
|
+
const slotsOk = slotsOffReason === null;
|
|
1934
|
+
// Every offset the body actually keys as an SSA slot — the frame-object audit checks the
|
|
1935
|
+
// address-taken object cannot overlap one (two models for one byte is a silent disagreement).
|
|
1936
|
+
const usedSlotOffsets = new Set<number>();
|
|
1937
|
+
|
|
1938
|
+
// The WRITE dual of readData, and the reason it exists is a lesson rather than a symmetry: the
|
|
1939
|
+
// first version of this guard checked sp in three decode arms (mov/add/sub) and its commit message
|
|
1940
|
+
// claimed "every write to sp declines". It did not — `lsl sp, r4, #2`, `neg sp, r4`, `mvn sp, r4`,
|
|
1941
|
+
// `ldr sp, [r0,#4]` and `ldmia r0!, {sp}` all still lifted, dropping the sp write silently, because
|
|
1942
|
+
// an enumeration of arms can only cover the arms someone thought of. Guarding the write ITSELF
|
|
1943
|
+
// cannot be incomplete.
|
|
1944
|
+
//
|
|
1945
|
+
// sp is writable in exactly one shape — the frame adjust the add/sub arms `break` on before
|
|
1946
|
+
// reaching here (see isFrameAdjust). Anything else that writes sp is a frame change this frontend
|
|
1947
|
+
// cannot model, and dropping it silently deletes that change while the function keeps compiling.
|
|
1948
|
+
//
|
|
1949
|
+
// Known residual, zero inhabitants: `pop {sp}` never reaches here (push/pop are skipSafe in the
|
|
1950
|
+
// opaque policy), so it stays silently transparent. ARMv4T Thumb cannot encode sp in a pop reglist.
|
|
1951
|
+
const writeData = (r: string, b: number, v: Value): void => {
|
|
1952
|
+
if (isSpReg(r)) {
|
|
1953
|
+
throw spAsDataError();
|
|
1954
|
+
}
|
|
1955
|
+
writeVar(r, b, v);
|
|
1956
|
+
};
|
|
1957
|
+
|
|
1017
1958
|
// Best-effort call arity via the shared helper (frontend/ssa.ts).
|
|
1018
1959
|
const fallbackArgcHere = (b: number): number => fallbackArgc(ssa, target.argRegs, b);
|
|
1019
1960
|
|
|
@@ -1021,14 +1962,17 @@ export function lift(
|
|
|
1021
1962
|
const fillBlock = (ab: AsmBlock, bi: number) => {
|
|
1022
1963
|
const irb = irBlocks[bi];
|
|
1023
1964
|
let pendingCmp: { lhs: Value; rhs: Value } | null = null;
|
|
1965
|
+
// Tracks the frame through this block's linear instruction order. Meaningful for the entry
|
|
1966
|
+
// block; elsewhere a `[sp,#N]` access declines.
|
|
1967
|
+
const frame = makeFrameWalk();
|
|
1024
1968
|
|
|
1025
1969
|
// TRUSTWORTHINESS GUARD (mirrors the MIPS/PPC frontends): an unmodelled instruction must not
|
|
1026
|
-
// silently drop its destination register — emit an honest `opaque
|
|
1027
|
-
// assertResolved
|
|
1970
|
+
// silently drop its destination register — emit an honest `opaque`, which fails LOUD at
|
|
1971
|
+
// assertResolved whether or not anything reads that register (see frontend/opaque.ts). Push/pop and sp
|
|
1028
1972
|
// adjustments have no low-register data destination, so they fall through harmlessly;
|
|
1029
1973
|
// terminators are handled in the terminator section below.
|
|
1030
1974
|
const isThumbReg = (s: string | undefined): s is string => /^r\d+$/.test(s ?? '');
|
|
1031
|
-
const emitOpaqueDest = (ins: { mnemonic: string; ops: string[] }) => {
|
|
1975
|
+
const emitOpaqueDest = (ins: { mnemonic: string; ops: string[]; asWritten?: string }) => {
|
|
1032
1976
|
// storeClass: unmodelled Thumb stores are str*/stm* — `stmia rN!, {…}`'s dest token `r0!`
|
|
1033
1977
|
// fails isReg, so without this it would be skipped as "no reg dest", silently deleting the
|
|
1034
1978
|
// memory writes AND the base writeback. push/pop stay transparent frame ops (they don't match).
|
|
@@ -1037,9 +1981,10 @@ export function lift(
|
|
|
1037
1981
|
const od = opaqueDest(ins.mnemonic, ins.ops, {
|
|
1038
1982
|
isReg: isThumbReg,
|
|
1039
1983
|
normalize: reg,
|
|
1040
|
-
storeClass: /^(str|stm)
|
|
1041
|
-
skipSafe: /^(push|pop|nop)
|
|
1984
|
+
storeClass: /^(str|stm)/i,
|
|
1985
|
+
skipSafe: /^(push|pop|nop)$/i,
|
|
1042
1986
|
context: name,
|
|
1987
|
+
display: ins.asWritten,
|
|
1043
1988
|
});
|
|
1044
1989
|
if (!od) {
|
|
1045
1990
|
return;
|
|
@@ -1047,8 +1992,8 @@ export function lift(
|
|
|
1047
1992
|
const operands = od.srcRegs.map((r) => readVar(r, bi));
|
|
1048
1993
|
const res = mkValue(T.unk(32));
|
|
1049
1994
|
// carry the mnemonic so annotate mode can name the gap (`ASMLIFT_ERROR("unmodelled 'rsb'")`)
|
|
1050
|
-
irb.ops.push(mkOp('opaque', { operands, results: [res], attrs: { mnemonic: ins.mnemonic } }));
|
|
1051
|
-
|
|
1995
|
+
irb.ops.push(mkOp('opaque', { operands, results: [res], attrs: { mnemonic: ins.asWritten ?? ins.mnemonic } }));
|
|
1996
|
+
writeData(od.dst, bi, res);
|
|
1052
1997
|
};
|
|
1053
1998
|
// 2-operand ALU form `op rD, op2` (rD = rD ⟨op⟩ op2). `op2` is an immediate (`#N`) or a
|
|
1054
1999
|
// register. A destination that is NOT a low data register (`add sp, #8` / `sub sp, #N` frame
|
|
@@ -1056,9 +2001,25 @@ export function lift(
|
|
|
1056
2001
|
// harmlessly, matching the documented sp handling. A malformed operand (missing / non-register
|
|
1057
2002
|
// non-immediate) degrades to a loud opaque rather than a crash or a silent data-dest drop.
|
|
1058
2003
|
const emit2op = (opc: Opcode, dReg: string, op2: string | undefined, bi: number) => {
|
|
2004
|
+
// The one sp guard writeData CANNOT supply: this path returns without ever producing a value
|
|
2005
|
+
// to write, so a bad sp destination would never reach the write. It is only reachable from the
|
|
2006
|
+
// add/sub arms, which have already let the whitelisted frame adjust `break` out — so an sp
|
|
2007
|
+
// destination here is by construction NOT that shape (`add sp, r4`: a register-sized frame
|
|
2008
|
+
// adjustment, how agbcc spells a frame too large for the 7-bit immediate).
|
|
2009
|
+
//
|
|
2010
|
+
// Honesty about what this is worth: the 4 real `add sp, rN` sites in the sa3 checkout all sit
|
|
2011
|
+
// in functions that ALSO do `mov rN, sp` 70+ times, so they declined before this guard and
|
|
2012
|
+
// decline after it. No wrong C was ever emitted by this shape. The guard is defence in depth
|
|
2013
|
+
// for the day a stack capability makes those functions liftable — not a miscompile fixed.
|
|
2014
|
+
//
|
|
2015
|
+
// This looked dead during review and is not: it becomes reachable the moment the arm-local
|
|
2016
|
+
// guards are removed, which is exactly what the test at 'add sp, r4' pins.
|
|
2017
|
+
if (isSpReg(dReg)) {
|
|
2018
|
+
throw spAsDataError();
|
|
2019
|
+
}
|
|
1059
2020
|
if (!isThumbReg(reg(dReg))) {
|
|
1060
2021
|
return;
|
|
1061
|
-
} //
|
|
2022
|
+
} // pc: claimed by classifyXfer first
|
|
1062
2023
|
if (op2 === undefined) {
|
|
1063
2024
|
emitOpaqueDest({ mnemonic: opc, ops: [dReg] });
|
|
1064
2025
|
return;
|
|
@@ -1066,7 +2027,7 @@ export function lift(
|
|
|
1066
2027
|
const rhs = op2.startsWith('#') ? constVal(imm(op2), bi) : readData(reg(op2), bi);
|
|
1067
2028
|
const res = mkValue(T.unk(32));
|
|
1068
2029
|
irb.ops.push(mkOp(opc, { operands: [readData(reg(dReg), bi), rhs], results: [res] }));
|
|
1069
|
-
|
|
2030
|
+
writeData(reg(dReg), bi, res);
|
|
1070
2031
|
};
|
|
1071
2032
|
|
|
1072
2033
|
for (const ins of ab.instrs) {
|
|
@@ -1076,23 +2037,61 @@ export function lift(
|
|
|
1076
2037
|
if (classifyXfer(ins)) {
|
|
1077
2038
|
continue;
|
|
1078
2039
|
}
|
|
2040
|
+
// A Thumb-1 data-processing instruction on LOW registers writes the condition flags whether or
|
|
2041
|
+
// not the mnemonic carries the `s` (agbcc spells `adds r0,r0,r3` as `add r0,r0,r3`, and the
|
|
2042
|
+
// assembler picks the flag-setting encoding) — so an instruction between a `cmp` and its branch
|
|
2043
|
+
// REPLACES the flags the branch will test. Folding the earlier `cmp` in anyway would emit a
|
|
2044
|
+
// condition on the wrong operands: silently wrong C with no marker. Drop the pending compare
|
|
2045
|
+
// and let the terminator's existing "no reaching compare in its block" decline fire — the loud
|
|
2046
|
+
// answer, since modelling arithmetic flags is a capability asmlift does not have.
|
|
2047
|
+
//
|
|
2048
|
+
// The HIGH-register forms (`mov rD,rH`, `add rD,rH`) do NOT set flags and stay transparent,
|
|
2049
|
+
// which is what keeps agbcc's callee-saved shuffling from tripping this. Measured free: across
|
|
2050
|
+
// every agbcc row in the benchmark, no conditional-branch block has ANY instruction between its
|
|
2051
|
+
// compare and the branch — compilers keep the pair adjacent. The inhabitant this guards is
|
|
2052
|
+
// hand-written asm in the playground, where there is no oracle to catch a lie.
|
|
2053
|
+
if (FLAG_SETTING.has(ins.mnemonic) && /^r[0-7]$/.test(reg(ins.ops[0] ?? ''))) {
|
|
2054
|
+
pendingCmp = null;
|
|
2055
|
+
}
|
|
2056
|
+
frame.step(ins);
|
|
1079
2057
|
const [a, b, c] = ins.ops;
|
|
1080
2058
|
switch (ins.mnemonic) {
|
|
1081
2059
|
case 'mov':
|
|
1082
2060
|
case 'movs': {
|
|
2061
|
+
// `mov rD, sp` captures the address of the frame's local area — the DMA-fill idiom
|
|
2062
|
+
// (`DmaFill16` expands to `vu16 tmp; DmaSet(…, &tmp, …)`) and any `&local` argument.
|
|
2063
|
+
// Emitted as `laddr`, gaddr's local twin; every use is proven by the frame-object audit
|
|
2064
|
+
// after the blocks are filled, and any use it cannot vouch for declines the function
|
|
2065
|
+
// loudly there. Gated on the slot model (the frame must be private and immovable) and on
|
|
2066
|
+
// a reserved local area for the object to live in.
|
|
2067
|
+
if (!b?.startsWith('#') && isSpReg(b ?? '') && !isSpReg(a ?? '')) {
|
|
2068
|
+
if (slotsOk && localArea > 0) {
|
|
2069
|
+
const res = mkValue(T.unk(32));
|
|
2070
|
+
irb.ops.push(mkOp('laddr', { results: [res], attrs: { off: 0 } }));
|
|
2071
|
+
writeData(reg(a), bi, res);
|
|
2072
|
+
break;
|
|
2073
|
+
}
|
|
2074
|
+
throw spAsDataError();
|
|
2075
|
+
}
|
|
1083
2076
|
const v = b?.startsWith('#') ? constVal(imm(b), bi) : readData(reg(b), bi);
|
|
1084
|
-
|
|
2077
|
+
writeData(reg(a), bi, v);
|
|
1085
2078
|
break;
|
|
1086
2079
|
}
|
|
1087
2080
|
case 'add':
|
|
1088
2081
|
case 'adds': {
|
|
2082
|
+
// Frame bookkeeping first: it must outrank the `#0` copy idiom below, or `add sp, sp, #0`
|
|
2083
|
+
// takes the copy path and declines while `add sp, #0` is transparent — the same
|
|
2084
|
+
// two-spellings inconsistency one N lower down.
|
|
2085
|
+
if (isFrameAdjust(ins.mnemonic, a, c === undefined ? undefined : b, c ?? b)) {
|
|
2086
|
+
break;
|
|
2087
|
+
}
|
|
1089
2088
|
// `add rD, rS, #0` is agbcc's low-register copy idiom (Thumb `mov rD, rS` between
|
|
1090
2089
|
// low regs isn't always available). Model it as a pure copy — same SSA value — not
|
|
1091
2090
|
// an `x + 0` add. This keeps output clean and, crucially, makes a value copied to a
|
|
1092
2091
|
// callee-saved register before a call read as still-live *after* the call, which is
|
|
1093
2092
|
// how call-argument liveness tells a passed argument from a preserved one.
|
|
1094
2093
|
if (c === '#0') {
|
|
1095
|
-
|
|
2094
|
+
writeData(reg(a), bi, readData(reg(b), bi));
|
|
1096
2095
|
break;
|
|
1097
2096
|
}
|
|
1098
2097
|
// 2-operand form `add rD, op2` (rD = rD + op2): op2 in `b`, no third operand.
|
|
@@ -1101,22 +2100,27 @@ export function lift(
|
|
|
1101
2100
|
emit2op('add', a, b, bi);
|
|
1102
2101
|
break;
|
|
1103
2102
|
}
|
|
2103
|
+
|
|
1104
2104
|
const rhs = c?.startsWith('#') ? constVal(imm(c), bi) : readData(reg(c), bi);
|
|
1105
2105
|
const res = mkValue(T.unk(32));
|
|
1106
2106
|
irb.ops.push(mkOp('add', { operands: [readData(reg(b), bi), rhs], results: [res] }));
|
|
1107
|
-
|
|
2107
|
+
writeData(reg(a), bi, res);
|
|
1108
2108
|
break;
|
|
1109
2109
|
}
|
|
1110
2110
|
case 'sub':
|
|
1111
2111
|
case 'subs': {
|
|
2112
|
+
if (isFrameAdjust(ins.mnemonic, a, c === undefined ? undefined : b, c ?? b)) {
|
|
2113
|
+
break;
|
|
2114
|
+
}
|
|
1112
2115
|
if (c === undefined) {
|
|
1113
2116
|
emit2op('sub', a, b, bi);
|
|
1114
2117
|
break;
|
|
1115
2118
|
} // `sub rD, op2` → rD = rD - op2
|
|
2119
|
+
|
|
1116
2120
|
const rhs = c?.startsWith('#') ? constVal(imm(c), bi) : readData(reg(c), bi);
|
|
1117
2121
|
const res = mkValue(T.unk(32));
|
|
1118
2122
|
irb.ops.push(mkOp('sub', { operands: [readData(reg(b), bi), rhs], results: [res] }));
|
|
1119
|
-
|
|
2123
|
+
writeData(reg(a), bi, res);
|
|
1120
2124
|
break;
|
|
1121
2125
|
}
|
|
1122
2126
|
case 'lsr':
|
|
@@ -1143,7 +2147,7 @@ export function lift(
|
|
|
1143
2147
|
// register form `lsl rD, rS, rN` → rD = rS << rN
|
|
1144
2148
|
irb.ops.push(mkOp(opc, { operands: [readData(reg(b), bi), readData(reg(c), bi)], results: [res] }));
|
|
1145
2149
|
}
|
|
1146
|
-
|
|
2150
|
+
writeData(reg(a), bi, res);
|
|
1147
2151
|
break;
|
|
1148
2152
|
}
|
|
1149
2153
|
case 'neg':
|
|
@@ -1151,7 +2155,7 @@ export function lift(
|
|
|
1151
2155
|
// `neg rD, rS` (and `rsb rD, rS, #0`) = arithmetic negation → -x
|
|
1152
2156
|
const res = mkValue(T.unk(32));
|
|
1153
2157
|
irb.ops.push(mkOp('neg', { operands: [readData(reg(b), bi)], results: [res] }));
|
|
1154
|
-
|
|
2158
|
+
writeData(reg(a), bi, res);
|
|
1155
2159
|
break;
|
|
1156
2160
|
}
|
|
1157
2161
|
case 'rsb':
|
|
@@ -1162,7 +2166,7 @@ export function lift(
|
|
|
1162
2166
|
if (c === '#0') {
|
|
1163
2167
|
const res = mkValue(T.unk(32));
|
|
1164
2168
|
irb.ops.push(mkOp('neg', { operands: [readData(reg(b), bi)], results: [res] }));
|
|
1165
|
-
|
|
2169
|
+
writeData(reg(a), bi, res);
|
|
1166
2170
|
} else {
|
|
1167
2171
|
emitOpaqueDest(ins);
|
|
1168
2172
|
}
|
|
@@ -1173,7 +2177,7 @@ export function lift(
|
|
|
1173
2177
|
// `mvn rD, rS` = bitwise NOT → ~x
|
|
1174
2178
|
const res = mkValue(T.unk(32));
|
|
1175
2179
|
irb.ops.push(mkOp('not', { operands: [readData(reg(b), bi)], results: [res] }));
|
|
1176
|
-
|
|
2180
|
+
writeData(reg(a), bi, res);
|
|
1177
2181
|
break;
|
|
1178
2182
|
}
|
|
1179
2183
|
case 'bic':
|
|
@@ -1190,7 +2194,7 @@ export function lift(
|
|
|
1190
2194
|
irb.ops.push(mkOp('not', { operands: [readData(mr, bi)], results: [inv] }));
|
|
1191
2195
|
const res = mkValue(T.unk(32));
|
|
1192
2196
|
irb.ops.push(mkOp('and', { operands: [readData(xr, bi), inv], results: [res] }));
|
|
1193
|
-
|
|
2197
|
+
writeData(reg(a), bi, res);
|
|
1194
2198
|
break;
|
|
1195
2199
|
}
|
|
1196
2200
|
case 'ror':
|
|
@@ -1204,7 +2208,7 @@ export function lift(
|
|
|
1204
2208
|
const [xr, nr] = c !== undefined ? [reg(b), reg(c)] : [reg(a), reg(b)];
|
|
1205
2209
|
const res = mkValue(T.unk(32));
|
|
1206
2210
|
irb.ops.push(mkOp('rotr', { operands: [readData(xr, bi), readData(nr, bi)], results: [res] }));
|
|
1207
|
-
|
|
2211
|
+
writeData(reg(a), bi, res);
|
|
1208
2212
|
break;
|
|
1209
2213
|
}
|
|
1210
2214
|
case 'ldmia':
|
|
@@ -1215,9 +2219,18 @@ export function lift(
|
|
|
1215
2219
|
// rejoin below also tolerates a split list defensively. Thumb-1 LDMIA skips the
|
|
1216
2220
|
// writeback when rN is itself in the list (the loaded value wins) — modelled; any
|
|
1217
2221
|
// malformed shape degrades to the loud opaque.
|
|
1218
|
-
//
|
|
1219
|
-
//
|
|
1220
|
-
//
|
|
2222
|
+
// There is NO no-writeback form in Thumb-1, so the `!` is decoration and must not drive
|
|
2223
|
+
// the model. Four sources agree:
|
|
2224
|
+
// * ARM DDI 0029G Table 1-7 gives the canonical syntax as `LDMIA Rb!, <reglist>` and
|
|
2225
|
+
// `STMIA Rb!, <reglist>` — the `!` is part of the mnemonic, not an option, and
|
|
2226
|
+
// Figure 1-6 Format 15 has no bit that could encode its absence;
|
|
2227
|
+
// * GNU as assembles `ldm r1,{r0}` and `ldm r1!,{r0}` to the same halfword, 0xc901,
|
|
2228
|
+
// and warns "this instruction will write back the base register";
|
|
2229
|
+
// * gba-kit executes both with the base advanced by 4;
|
|
2230
|
+
// * GBATEK, THUMB.15: "Both STM and LDM are incrementing the Base Register".
|
|
2231
|
+
// An earlier version of this comment called the `!`-less spelling "the valid
|
|
2232
|
+
// no-writeback form — same transfers, base unchanged", which is false, and the code
|
|
2233
|
+
// below acted on it. A missing register list is malformed → loud opaque.
|
|
1221
2234
|
const baseTok = a;
|
|
1222
2235
|
const writeback = !!baseTok?.endsWith('!');
|
|
1223
2236
|
if (baseTok === undefined || b === undefined || !b.startsWith('{')) {
|
|
@@ -1225,7 +2238,11 @@ export function lift(
|
|
|
1225
2238
|
break;
|
|
1226
2239
|
}
|
|
1227
2240
|
const baseReg = reg(writeback ? baseTok.slice(0, -1) : baseTok);
|
|
1228
|
-
|
|
2241
|
+
// Anything but a list of definite registers — an unexpandable range (alias endpoint,
|
|
2242
|
+
// e.g. `r4-lr`), a token naming no register, an empty list — leaves the transfer set
|
|
2243
|
+
// ambiguous, so degrade to the loud opaque rather than guess. Checking only for the
|
|
2244
|
+
// leftover `-` let `{foo}` through and fabricated a parameter out of it.
|
|
2245
|
+
const list = definiteRegList(
|
|
1229
2246
|
ins.ops
|
|
1230
2247
|
.slice(1)
|
|
1231
2248
|
.join(',')
|
|
@@ -1234,15 +2251,33 @@ export function lift(
|
|
|
1234
2251
|
.map((r) => r.trim())
|
|
1235
2252
|
.filter(Boolean),
|
|
1236
2253
|
);
|
|
1237
|
-
|
|
1238
|
-
// exact transfer set is ambiguous, so degrade to the loud opaque rather than guess.
|
|
1239
|
-
if (list.some((r) => r.includes('-'))) {
|
|
2254
|
+
if (list === null) {
|
|
1240
2255
|
emitOpaqueDest(ins);
|
|
1241
2256
|
break;
|
|
1242
2257
|
}
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
2258
|
+
// An STM whose base is in its own list, but is not the LOWEST entry, stores a value this
|
|
2259
|
+
// frontend must not guess — because the available references DISAGREE about what it is.
|
|
2260
|
+
//
|
|
2261
|
+
// ARM: UNPREDICTABLE, "the stored value cannot be relied upon".
|
|
2262
|
+
// GNU as: warns "value stored for rN is UNKNOWN".
|
|
2263
|
+
// GBATEK: version-specific — "Store OLD base if Rb is FIRST entry in Rlist,
|
|
2264
|
+
// otherwise store NEW base (STM/ARMv4), always store OLD base (STM/ARMv5)".
|
|
2265
|
+
// mGBA: stores the OLD base unconditionally, on an ARMv4T core — its STM_LOOP
|
|
2266
|
+
// reads gprs[i] during the loop and the writeback runs after it.
|
|
2267
|
+
//
|
|
2268
|
+
// So GBATEK's ARMv4 rule and the reference emulator's behaviour do not agree, and no
|
|
2269
|
+
// hardware test result was found either way. This frontend used to emit the old base,
|
|
2270
|
+
// i.e. it silently picked one side of that disagreement. Declining is the contract:
|
|
2271
|
+
// where the architecture declines to define a value, so do we.
|
|
2272
|
+
//
|
|
2273
|
+
// (One site in the Klonoa corpus, in unreachable code after a `pop`/`bx`, and it already
|
|
2274
|
+
// declines for an unrelated pc-relative-pool reason — so this costs nothing today.)
|
|
2275
|
+
if (ins.mnemonic === 'stmia' && list.some((r) => reg(r) === baseReg) && reg(list[0]) !== baseReg) {
|
|
2276
|
+
throw new FrontendUnsupportedError(
|
|
2277
|
+
`cannot lift '${name}': stm with the base register in its own list, not as the lowest ` +
|
|
2278
|
+
`entry — the value stored for that register is UNPREDICTABLE and differs between ` +
|
|
2279
|
+
`ARMv4 (new base) and ARMv5 (old base)`,
|
|
2280
|
+
);
|
|
1246
2281
|
}
|
|
1247
2282
|
// SNAPSHOT the base ONCE: hardware performs every transfer from the ORIGINAL base, but
|
|
1248
2283
|
// a base-in-list ldmia overwrites that register mid-list — re-reading it per iteration
|
|
@@ -1255,18 +2290,20 @@ export function lift(
|
|
|
1255
2290
|
irb.ops.push(
|
|
1256
2291
|
mkOp('load', { operands: [base0], results: [res], attrs: { off: 4 * i, signed: true, width: 4 } }),
|
|
1257
2292
|
);
|
|
1258
|
-
|
|
2293
|
+
writeData(reg(r), bi, res);
|
|
1259
2294
|
} else {
|
|
1260
2295
|
irb.ops.push(mkOp('store', { operands: [base0, readData(reg(r), bi)], attrs: { off: 4 * i, width: 4 } }));
|
|
1261
2296
|
}
|
|
1262
2297
|
});
|
|
1263
|
-
// Writeback advances the base by 4×count
|
|
1264
|
-
//
|
|
2298
|
+
// Writeback advances the base by 4×count. It is suppressed ONLY for an ldmia whose base
|
|
2299
|
+
// is in its own list — the loaded value wins. GBATEK, THUMB.15: "no writeback
|
|
2300
|
+
// (LDM/ARMv4/ARMv5; at this point, THUMB opcodes work different than ARM opcodes)".
|
|
2301
|
+
// The `!` is NOT what decides it: see above, there is no encoding without writeback.
|
|
1265
2302
|
const wroteBase = ins.mnemonic === 'ldmia' && list.some((r) => reg(r) === baseReg);
|
|
1266
|
-
if (
|
|
2303
|
+
if (!wroteBase) {
|
|
1267
2304
|
const adv = mkValue(T.unk(32));
|
|
1268
2305
|
irb.ops.push(mkOp('add', { operands: [base0, constVal(4 * list.length, bi)], results: [adv] }));
|
|
1269
|
-
|
|
2306
|
+
writeData(baseReg, bi, adv);
|
|
1270
2307
|
}
|
|
1271
2308
|
break;
|
|
1272
2309
|
}
|
|
@@ -1302,7 +2339,7 @@ export function lift(
|
|
|
1302
2339
|
: [readData(reg(a), bi), readData(reg(b), bi)];
|
|
1303
2340
|
const res = mkValue(T.unk(32));
|
|
1304
2341
|
irb.ops.push(mkOp(opc, { operands: [x, y], results: [res] }));
|
|
1305
|
-
|
|
2342
|
+
writeData(reg(a), bi, res);
|
|
1306
2343
|
break;
|
|
1307
2344
|
}
|
|
1308
2345
|
case 'cmp': {
|
|
@@ -1354,7 +2391,7 @@ export function lift(
|
|
|
1354
2391
|
attrs: { sym: si.name, ...(si.kind === 'code' ? { code: true } : {}) },
|
|
1355
2392
|
}),
|
|
1356
2393
|
);
|
|
1357
|
-
|
|
2394
|
+
writeData(reg(a), bi, res);
|
|
1358
2395
|
break;
|
|
1359
2396
|
}
|
|
1360
2397
|
// INTERIOR attribution: a value strictly inside a sized data symbol becomes
|
|
@@ -1371,18 +2408,28 @@ export function lift(
|
|
|
1371
2408
|
irb.ops.push(mkOp('gaddr', { results: [g], attrs: { sym: interior.info.name } }));
|
|
1372
2409
|
irb.ops.push(mkOp('const', { results: [k], attrs: { value: interior.offset } }));
|
|
1373
2410
|
irb.ops.push(mkOp('add', { operands: [g, k], results: [res] }));
|
|
1374
|
-
|
|
2411
|
+
writeData(reg(a), bi, res);
|
|
1375
2412
|
break;
|
|
1376
2413
|
}
|
|
1377
2414
|
const res = mkValue(T.unk(32));
|
|
1378
2415
|
irb.ops.push(mkOp('const', { results: [res], attrs: { value: pr.value } }));
|
|
1379
|
-
|
|
2416
|
+
writeData(reg(a), bi, res);
|
|
1380
2417
|
break;
|
|
1381
2418
|
}
|
|
1382
2419
|
if (pr?.kind === 'gaddr') {
|
|
1383
2420
|
const res = mkValue(T.unk(32));
|
|
1384
2421
|
irb.ops.push(mkOp('gaddr', { results: [res], attrs: { sym: pr.sym } }));
|
|
1385
|
-
|
|
2422
|
+
if (pr.addend !== 0) {
|
|
2423
|
+
// `.word gSym+N` = the machine loads gSym's address plus N. Emitted as an explicit
|
|
2424
|
+
// add so the addend is a VALUE, not an attribute a renderer could re-scale.
|
|
2425
|
+
const k = mkValue(T.unk(32));
|
|
2426
|
+
irb.ops.push(mkOp('const', { results: [k], attrs: { value: pr.addend } }));
|
|
2427
|
+
const sum = mkValue(T.unk(32));
|
|
2428
|
+
irb.ops.push(mkOp('add', { operands: [res, k], results: [sum] }));
|
|
2429
|
+
writeData(reg(a), bi, sum);
|
|
2430
|
+
break;
|
|
2431
|
+
}
|
|
2432
|
+
writeData(reg(a), bi, res);
|
|
1386
2433
|
break;
|
|
1387
2434
|
}
|
|
1388
2435
|
if (pr?.kind === 'unmodelled') {
|
|
@@ -1404,6 +2451,67 @@ export function lift(
|
|
|
1404
2451
|
// the same address arithmetic the encoding performs. (parseAddr used to silently
|
|
1405
2452
|
// read `[rB]`, dropping the index — a silent miscompile; ldrsh exists ONLY in this
|
|
1406
2453
|
// form in Thumb-1, so every ldrsh went through here.)
|
|
2454
|
+
// An incoming stack argument, read before its base becomes an sp decline. Every
|
|
2455
|
+
// condition below is a refusal that keeps a LOCAL from being mistaken for a parameter:
|
|
2456
|
+
// • entry block only — the depth is exact only along this block's linear order
|
|
2457
|
+
// • entry has no preds — otherwise its params are phis, not parameters
|
|
2458
|
+
// • no register offset — `[sp, rX]` is not a fixed argument slot
|
|
2459
|
+
// • word width, word-aligned — the argument area is word-granular
|
|
2460
|
+
// • off >= the frame depth — BELOW the frame top is a local/spill: still declines,
|
|
2461
|
+
// that is the separate slot-promotion capability
|
|
2462
|
+
// • a sane arity bound — a wild offset must not mint a 400-parameter signature
|
|
2463
|
+
{
|
|
2464
|
+
const index = frame.argIndex({ base, off, regOff }, width, bi);
|
|
2465
|
+
if (index !== null) {
|
|
2466
|
+
// Mint EVERY argument below this one — the register half included. Downstream naming
|
|
2467
|
+
// is POSITIONAL (structure.ts), so any hole binds every later parameter to the wrong
|
|
2468
|
+
// ABI slot, silently: `push {r4,r5,lr}; add r4,r3,#0; ldr r0,[sp,#0xc]` emitted a
|
|
2469
|
+
// 2-parameter signature where the ABI proves 5, with both of them bound wrong.
|
|
2470
|
+
//
|
|
2471
|
+
// Reading slot k proves the caller passed arguments 0..k: the register arguments are
|
|
2472
|
+
// filled before any stack argument exists, and the stack area is contiguous with slot
|
|
2473
|
+
// 4 at the lowest offset. So this is entailed by the calling convention, not guessed —
|
|
2474
|
+
// which is what separates it from inventing parameters a function might not have.
|
|
2475
|
+
// (It assumes one word per argument, which is what this frontend assumes everywhere —
|
|
2476
|
+
// it types every parameter s32. An 8-byte argument, which AAPCS may align into r1 or
|
|
2477
|
+
// straddle across r3 and the stack, would break the index↔slot correspondence; no
|
|
2478
|
+
// agbcc row in the corpus has one, and recovering them is its own capability.)
|
|
2479
|
+
//
|
|
2480
|
+
// ensureParam, NOT readVar: a register the entry block DEFINES before this point
|
|
2481
|
+
// (`bl g` then a read of the frame, the commonest shape there is) answers readVar with
|
|
2482
|
+
// that local definition and no parameter appears — reopening the very hole this loop
|
|
2483
|
+
// closes. It emitted `s32 f(s32 a0, s32 a1, s32 a2, s32 a3) { return g() + a3; }`:
|
|
2484
|
+
// arity 4 where the ABI proves 5, with the stack argument bound to r3's slot.
|
|
2485
|
+
for (let j = 0; j < index; j++) {
|
|
2486
|
+
ssa.ensureParam(j < target.argRegs.length ? target.argRegs[j] : stackArgKey(j), bi);
|
|
2487
|
+
}
|
|
2488
|
+
writeData(reg(a), bi, readVar(stackArgKey(index), bi));
|
|
2489
|
+
break;
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
// A word reload from this function's own frame — the dual of the spill in the str arm.
|
|
2493
|
+
//
|
|
2494
|
+
// The reaching-def test is the whole soundness of it, and it mirrors the MIPS guard
|
|
2495
|
+
// exactly: a slot that was never STORED holds nothing this function put there, so
|
|
2496
|
+
// `readVar` would mint a phantom entry parameter for it and hand back a value the machine
|
|
2497
|
+
// never had. Above the frame that reading is right and is the incoming-argument path
|
|
2498
|
+
// above; INSIDE the frame it is an uninitialised local (or one whose address escaped
|
|
2499
|
+
// through a path the model missed), and the honest answer is the decline this falls
|
|
2500
|
+
// through to.
|
|
2501
|
+
if (
|
|
2502
|
+
slotsOk &&
|
|
2503
|
+
isSpReg(base) &&
|
|
2504
|
+
regOff === undefined &&
|
|
2505
|
+
width === 4 &&
|
|
2506
|
+
off % 4 === 0 &&
|
|
2507
|
+
off >= 0 &&
|
|
2508
|
+
off < localArea &&
|
|
2509
|
+
ssa.hasReachingDef(slotKey(off), bi)
|
|
2510
|
+
) {
|
|
2511
|
+
usedSlotOffsets.add(off);
|
|
2512
|
+
writeData(reg(a), bi, readVar(slotKey(off), bi));
|
|
2513
|
+
break;
|
|
2514
|
+
}
|
|
1407
2515
|
let baseVal = readData(base, bi);
|
|
1408
2516
|
if (regOff !== undefined) {
|
|
1409
2517
|
const sum = mkValue(T.unk(32));
|
|
@@ -1412,7 +2520,7 @@ export function lift(
|
|
|
1412
2520
|
}
|
|
1413
2521
|
const res = mkValue(T.unk(32));
|
|
1414
2522
|
irb.ops.push(mkOp('load', { operands: [baseVal], results: [res], attrs: { off, width, signed } }));
|
|
1415
|
-
|
|
2523
|
+
writeData(reg(a), bi, res);
|
|
1416
2524
|
break;
|
|
1417
2525
|
}
|
|
1418
2526
|
case 'str':
|
|
@@ -1425,6 +2533,27 @@ export function lift(
|
|
|
1425
2533
|
}
|
|
1426
2534
|
const width = /b/.test(ins.mnemonic) ? 1 : /h/.test(ins.mnemonic) ? 2 : 4;
|
|
1427
2535
|
const { base, off, regOff } = parseAddr(b);
|
|
2536
|
+
// A word spill into this function's own frame: record the slot's value in SSA rather than
|
|
2537
|
+
// emitting a store through sp. `slotsOk` has already proven the frame is private and does
|
|
2538
|
+
// not move; `off < localArea` keeps this strictly inside the EXPLICITLY reserved local
|
|
2539
|
+
// area — not merely inside the frame, whose upper part is the callee-saved block the
|
|
2540
|
+
// epilogue pops — so it can never
|
|
2541
|
+
// collide with the incoming-argument area above it (which the load path recovers as
|
|
2542
|
+
// parameters, and where a STORE is still a decline — writing a caller's slot is a
|
|
2543
|
+
// different capability). A spill that is never reloaded becomes a dead def and drops.
|
|
2544
|
+
if (
|
|
2545
|
+
slotsOk &&
|
|
2546
|
+
isSpReg(base) &&
|
|
2547
|
+
regOff === undefined &&
|
|
2548
|
+
width === 4 &&
|
|
2549
|
+
off % 4 === 0 &&
|
|
2550
|
+
off >= 0 &&
|
|
2551
|
+
off < localArea
|
|
2552
|
+
) {
|
|
2553
|
+
usedSlotOffsets.add(off);
|
|
2554
|
+
writeVar(slotKey(off), bi, readData(reg(a), bi));
|
|
2555
|
+
break;
|
|
2556
|
+
}
|
|
1428
2557
|
let storeBase = readData(base, bi);
|
|
1429
2558
|
if (regOff !== undefined) {
|
|
1430
2559
|
// register-offset store: same exact `rB + rX` lowering as the load path above
|
|
@@ -1443,15 +2572,23 @@ export function lift(
|
|
|
1443
2572
|
const targetSym = a;
|
|
1444
2573
|
// Caller-supplied prototype wins; otherwise a known runtime helper (`__divsi3` &c.)
|
|
1445
2574
|
// supplies its arity so its arguments are recovered; only then fall back to guessing.
|
|
1446
|
-
const
|
|
1447
|
-
|
|
2575
|
+
const declared = protoArity(prototypes[targetSym]) ?? protoArity(RUNTIME_HELPERS[targetSym]);
|
|
2576
|
+
const argc = declared ?? fallbackArgcHere(bi);
|
|
1448
2577
|
const args: Value[] = [];
|
|
1449
2578
|
for (let k = 0; k < argc; k++) {
|
|
1450
2579
|
args.push(readVar(`r${k}`, bi));
|
|
1451
2580
|
}
|
|
1452
2581
|
const res = mkValue(T.unk(32));
|
|
1453
|
-
|
|
1454
|
-
|
|
2582
|
+
const callOp = mkOp('call', { operands: args, results: [res], attrs: { target: targetSym } });
|
|
2583
|
+
irb.ops.push(callOp);
|
|
2584
|
+
// A GUESSED arity is revisited in `finish()`: only once the whole function is lifted is it
|
|
2585
|
+
// known whether every path to here passes through another call, which would have clobbered
|
|
2586
|
+
// the argument registers this guess just read.
|
|
2587
|
+
if (declared === undefined) {
|
|
2588
|
+
ssa.recordGuessedCall(callOp, bi, target.argRegs);
|
|
2589
|
+
}
|
|
2590
|
+
ssa.noteCall(bi); // the callee clobbers r0..r3 …
|
|
2591
|
+
writeData('r0', bi, res); // … and then defines r0, which IS fresh for the next call
|
|
1455
2592
|
break;
|
|
1456
2593
|
}
|
|
1457
2594
|
default:
|
|
@@ -1477,7 +2614,20 @@ export function lift(
|
|
|
1477
2614
|
irb.ops.push(mkOp('br', { successors: [succ(fallLabel(bi))] }));
|
|
1478
2615
|
} else if (kind === 'return') {
|
|
1479
2616
|
// bx lr / pop {…,pc} / mov pc,lr
|
|
1480
|
-
|
|
2617
|
+
//
|
|
2618
|
+
// A `bx rN` BRANCHES THROUGH rN, so at that instruction rN holds the RETURN ADDRESS. When rN
|
|
2619
|
+
// is the return-VALUE register the two uses collide, and the address wins by definition —
|
|
2620
|
+
// whatever value was in r0 is gone, so the function cannot be returning one. agbcc spells an
|
|
2621
|
+
// interworking return that way (`push {lr}` … `pop {r0}; bx r0`), and reading r0 as a value
|
|
2622
|
+
// there invents a return the machine provably cannot make: a phantom `return`, a non-`void`
|
|
2623
|
+
// signature that would contradict the project's own prototype, and a live range that keeps
|
|
2624
|
+
// otherwise-dead computation alive.
|
|
2625
|
+
//
|
|
2626
|
+
// The other return forms are untouched, because none of them writes the return register:
|
|
2627
|
+
// `bx lr` and `bx r1`/`bx r2` branch through a different one, and `pop {…,pc}` / `mov pc,lr`
|
|
2628
|
+
// load PC directly. Only the register actually branched through is disqualified.
|
|
2629
|
+
const viaReturnReg = last.mnemonic === 'bx' && last.ops[0] === target.returnReg;
|
|
2630
|
+
irb.ops.push(mkOp('ret', { operands: viaReturnReg ? [] : [readVar(target.returnReg, bi)] }));
|
|
1481
2631
|
} else if (kind === 'uncond') {
|
|
1482
2632
|
irb.ops.push(mkOp('br', { successors: [succ(last.ops[0])] }));
|
|
1483
2633
|
} else if (kind === 'cond') {
|
|
@@ -1515,15 +2665,150 @@ export function lift(
|
|
|
1515
2665
|
|
|
1516
2666
|
ssa.finish();
|
|
1517
2667
|
|
|
2668
|
+
// FRAME-OBJECT AUDIT. Every `laddr` the mov arm emitted is only a CLAIM that the captured
|
|
2669
|
+
// address is used as "the address of one scalar local"; this proves it, over the finished
|
|
2670
|
+
// function, the same boundary-total style as the slot-escape assert in finish(). The address may
|
|
2671
|
+
// flow anywhere as a VALUE — into an MMIO register (the DMA-fill idiom), a call, a phi — but
|
|
2672
|
+
// every MEMORY access through it must be at offset 0 with one agreed width, and any use the
|
|
2673
|
+
// audit cannot vouch for declines the whole function loudly. Nothing here guesses: the object's
|
|
2674
|
+
// declared type is exactly the access type the machine used.
|
|
2675
|
+
{
|
|
2676
|
+
const laddrs: Op[] = [];
|
|
2677
|
+
for (const blk of irBlocks) {
|
|
2678
|
+
for (const op of blk.ops) {
|
|
2679
|
+
if (op.opcode === 'laddr') {
|
|
2680
|
+
laddrs.push(op);
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
if (laddrs.length > 0) {
|
|
2685
|
+
const fail = (why: string): never => {
|
|
2686
|
+
throw new FrontendUnsupportedError(`cannot lift '${name}': address-taken stack local — ${why}`);
|
|
2687
|
+
};
|
|
2688
|
+
// Taint = values that may hold the object's address, closed over phis: a tainted edge arg
|
|
2689
|
+
// taints the receiving block param. (A phi mixing the address with a non-address would taint
|
|
2690
|
+
// the param and then fail the use judgement below — mixing is not vouched for.)
|
|
2691
|
+
for (const op of laddrs) {
|
|
2692
|
+
if ((op.attrs.off as number) !== 0) {
|
|
2693
|
+
// the audit's offset arithmetic, overlap window and naming all assume the frame base;
|
|
2694
|
+
// if a computed capture ever mints off!==0, this line is what keeps it loud
|
|
2695
|
+
fail(`a capture at frame offset ${op.attrs.off} — only the frame base is modelled`);
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
const taint = new Set<Value>(laddrs.flatMap((o) => o.results));
|
|
2699
|
+
for (let changed = true; changed;) {
|
|
2700
|
+
changed = false;
|
|
2701
|
+
for (const blk of irBlocks) {
|
|
2702
|
+
for (const op of blk.ops) {
|
|
2703
|
+
for (const s of op.successors ?? []) {
|
|
2704
|
+
s.args.forEach((arg, i) => {
|
|
2705
|
+
const param = s.block.params[i];
|
|
2706
|
+
if (taint.has(arg) && param !== undefined && !taint.has(param)) {
|
|
2707
|
+
taint.add(param);
|
|
2708
|
+
changed = true;
|
|
2709
|
+
}
|
|
2710
|
+
});
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
// Judge every use of a tainted value.
|
|
2716
|
+
const accesses: { width: number; signed: boolean }[] = [];
|
|
2717
|
+
let escapes = false;
|
|
2718
|
+
for (const blk of irBlocks) {
|
|
2719
|
+
for (const op of blk.ops) {
|
|
2720
|
+
op.operands.forEach((v, idx) => {
|
|
2721
|
+
if (!taint.has(v)) {
|
|
2722
|
+
return;
|
|
2723
|
+
}
|
|
2724
|
+
if (op.opcode === 'load' && idx === 0) {
|
|
2725
|
+
if ((op.attrs.off as number) !== 0) {
|
|
2726
|
+
fail(
|
|
2727
|
+
`a load at [+${op.attrs.off}] through the captured address — only the scalar at offset 0 is modelled`,
|
|
2728
|
+
);
|
|
2729
|
+
}
|
|
2730
|
+
accesses.push({ width: op.attrs.width as number, signed: (op.attrs.signed as boolean) ?? false });
|
|
2731
|
+
return;
|
|
2732
|
+
}
|
|
2733
|
+
if (op.opcode === 'store' && idx === 0) {
|
|
2734
|
+
if ((op.attrs.off as number) !== 0) {
|
|
2735
|
+
fail(
|
|
2736
|
+
`a store at [+${op.attrs.off}] through the captured address — only the scalar at offset 0 is modelled`,
|
|
2737
|
+
);
|
|
2738
|
+
}
|
|
2739
|
+
accesses.push({ width: op.attrs.width as number, signed: false });
|
|
2740
|
+
return;
|
|
2741
|
+
}
|
|
2742
|
+
if ((op.opcode === 'store' && idx === 1) || op.opcode === 'call') {
|
|
2743
|
+
escapes = true; // the address ESCAPES as a value — the point of the capability
|
|
2744
|
+
return;
|
|
2745
|
+
}
|
|
2746
|
+
fail(`the captured address flows into \`${op.opcode}\` — not an access, an escape, or a phi`);
|
|
2747
|
+
});
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
if (accesses.length === 0) {
|
|
2751
|
+
// nothing in-function pins the object's type, and a guessed declaration is the
|
|
2752
|
+
// plausible-but-wrong class — decline until an inhabitant needs this
|
|
2753
|
+
fail('the captured address is never dereferenced in this function, so nothing pins the local object type');
|
|
2754
|
+
}
|
|
2755
|
+
const widths = new Set(accesses.map((a) => a.width));
|
|
2756
|
+
if (widths.size > 1) {
|
|
2757
|
+
fail(`the accesses through the captured address disagree on width (${[...widths].join(' vs ')})`);
|
|
2758
|
+
}
|
|
2759
|
+
const width = accesses[0].width;
|
|
2760
|
+
const signed = accesses.some((a) => a.signed);
|
|
2761
|
+
if (width > localArea) {
|
|
2762
|
+
fail('the object extends past the reserved local area');
|
|
2763
|
+
}
|
|
2764
|
+
for (const off of usedSlotOffsets) {
|
|
2765
|
+
if (off < width) {
|
|
2766
|
+
fail(`the object at [0,${width}) overlaps the SSA slot at [sp,#${off}] — one byte, two models`);
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
// Proven. Stamp the MACHINE FACTS the audit established — width and signedness are what the
|
|
2770
|
+
// accesses used, so the declaration downstream is a fact, not a guess. The C-level NAME is
|
|
2771
|
+
// deliberately NOT chosen here: identifiers live in the structurer's namespace (params,
|
|
2772
|
+
// locals, globals, the symbol map), which the frontend cannot see — a frontend-chosen `sp0`
|
|
2773
|
+
// silently shadowed a project global of the same name.
|
|
2774
|
+
// `volatile` iff the address escapes: gcc-2.9 DELETES a store to a non-volatile local nothing
|
|
2775
|
+
// in-function reads — measured, the recompiled loop loaded the value and never stored it —
|
|
2776
|
+
// and the reference idiom's own spelling is `vu16 tmp` for exactly that reason. An object
|
|
2777
|
+
// whose address never leaves the function needs no volatile and must not pay its codegen.
|
|
2778
|
+
for (const op of laddrs) {
|
|
2779
|
+
op.attrs = { ...op.attrs, width, signed, ...(escapes ? { volatile: true } : {}) };
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
|
|
1518
2784
|
// Order the entry block's parameters by ABI register (r0, r1, r2, …) so downstream
|
|
1519
2785
|
// naming (`a0`, `a1`, …) matches the calling convention, not the read order. Safe only
|
|
1520
2786
|
// for the true entry (no predecessors) — a loop header's params are phis whose position
|
|
1521
2787
|
// is index-aligned with predecessor terminator args and must not be reordered.
|
|
1522
2788
|
const entry = irBlocks[0];
|
|
1523
2789
|
// non-ABI live-in ranks LAST (99) — deliberate Thumb tie-break; MIPS/PPC's is -1/first
|
|
2790
|
+
//
|
|
2791
|
+
// "Non-ABI" means NOT AN ARGUMENT REGISTER, and it has to be tested that way rather than by the
|
|
2792
|
+
// shape of the name. A `/^r(\d+)$/` test ranked `r8` at 8 and `r4` at 4 while sending only `sl`
|
|
2793
|
+
// and `sb` to 99 — harmless while nothing else occupied ranks >= 4, and a positional miscompile
|
|
2794
|
+
// the moment incoming stack arguments started ranking there. `sub_80B6B3C` in sa3's
|
|
2795
|
+
// `asm/code_x.s` — still undecompiled, so not a benchmark row — takes 10 arguments (its caller
|
|
2796
|
+
// stores six words at [sp,#0]..[sp,#0x14] plus r0-r3) and saves r8 in its prologue;
|
|
2797
|
+
// the `r8` live-in and `@sarg8` tied at 8, the sort is stable, the prologue reads r8 first — so
|
|
2798
|
+
// ABI argument 8 was emitted as `a9` and every parameter after it was off by one.
|
|
2799
|
+
//
|
|
2800
|
+
// A callee-saved register read before it is written is not an argument in any case: it is a
|
|
2801
|
+
// fragment artifact or an unmodelled effect, and the honest place for it is after everything the
|
|
2802
|
+
// convention actually describes.
|
|
1524
2803
|
abiSortEntryParams(entry, preds[0].length > 0, (v) => {
|
|
1525
|
-
const
|
|
1526
|
-
|
|
2804
|
+
const key = paramReg.get(v) ?? '';
|
|
2805
|
+
// an incoming STACK argument ranks by its ABI index, after every register argument
|
|
2806
|
+
const s = stackArgIndex(key);
|
|
2807
|
+
if (s !== null) {
|
|
2808
|
+
return s;
|
|
2809
|
+
}
|
|
2810
|
+
const i = target.argRegs.indexOf(key);
|
|
2811
|
+
return i >= 0 ? i : 99;
|
|
1527
2812
|
});
|
|
1528
2813
|
return fn;
|
|
1529
2814
|
}
|