@asmlift/core 0.1.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/LICENSE +21 -0
- package/README.md +148 -0
- package/package.json +14 -0
- package/src/backend/c.ts +20 -0
- package/src/backend/cfamily.ts +352 -0
- package/src/backend/cpp.ts +145 -0
- package/src/backend/pascal.ts +279 -0
- package/src/contracts.ts +131 -0
- package/src/detect.ts +12 -0
- package/src/frontend/asmdata.ts +170 -0
- package/src/frontend/disasm.ts +102 -0
- package/src/frontend/emit.ts +57 -0
- package/src/frontend/errors.ts +14 -0
- package/src/frontend/format.ts +47 -0
- package/src/frontend/frontend.ts +22 -0
- package/src/frontend/mips.ts +875 -0
- package/src/frontend/opaque.ts +82 -0
- package/src/frontend/ppc.ts +990 -0
- package/src/frontend/registry.ts +34 -0
- package/src/frontend/ssa.ts +214 -0
- package/src/frontend/thumb.ts +1419 -0
- package/src/ir/core.ts +104 -0
- package/src/ir/opcodes.ts +143 -0
- package/src/ir/parse.ts +221 -0
- package/src/ir/print.ts +77 -0
- package/src/ir/types.ts +106 -0
- package/src/ir/verify.ts +221 -0
- package/src/l3/ast.ts +301 -0
- package/src/l3/basecse.ts +218 -0
- package/src/l3/dce.ts +256 -0
- package/src/l3/regspell.ts +331 -0
- package/src/l3/reindex.ts +447 -0
- package/src/l3/typing.ts +145 -0
- package/src/mangle.ts +135 -0
- package/src/pattern/engine.ts +392 -0
- package/src/pipeline.ts +272 -0
- package/src/proto.ts +42 -0
- package/src/raise/arrays.ts +84 -0
- package/src/raise/const.ts +52 -0
- package/src/raise/errors.ts +10 -0
- package/src/raise/magicdiv.ts +386 -0
- package/src/raise/pre-recovery.ts +71 -0
- package/src/raise/recover.ts +215 -0
- package/src/raise/retsink.ts +72 -0
- package/src/raise/shortcircuit.ts +207 -0
- package/src/raise/softdiv.ts +62 -0
- package/src/raise/struct-arrays.ts +257 -0
- package/src/raise/structs.ts +223 -0
- package/src/rank.ts +208 -0
- package/src/structure/analysis.ts +410 -0
- package/src/structure/hazards.ts +142 -0
- package/src/structure/loops.ts +169 -0
- package/src/structure/structure.ts +1726 -0
- package/src/structure/switch-recover.ts +410 -0
- package/src/target.ts +140 -0
- package/src/trace.ts +233 -0
|
@@ -0,0 +1,1419 @@
|
|
|
1
|
+
// asmlift ISA frontend — ARMv4T / Thumb (agbcc). Decode GNU-as text → CFG of basic
|
|
2
|
+
// blocks → L1 with multi-block SSA via Braun et al. 2013 ("Simple and Efficient
|
|
3
|
+
// Construction of SSA Form"), emitting block-arguments at joins.
|
|
4
|
+
//
|
|
5
|
+
// `cmp`+`b<cond>` become `cond_br` over a real join. Loops: a back-edge target is read
|
|
6
|
+
// before its back-edge predecessor is filled, so Braun's incomplete-phi + sealBlock
|
|
7
|
+
// schedule handles it — a block's phis are wired only once all its predecessors are
|
|
8
|
+
// filled. Trivial phis (one real operand) are removed afterwards so a loop-invariant
|
|
9
|
+
// register does not leak a spurious block parameter.
|
|
10
|
+
//
|
|
11
|
+
// Callee-saved stack frames: `push`/`pop` (and the `pop {rN}; bx rN` return idiom) are
|
|
12
|
+
// transparent to dataflow — the pushed registers are restored to the same values, and a
|
|
13
|
+
// callee-saved register is always written in the body before it is read — so no explicit
|
|
14
|
+
// modelling is needed; they simply fall through the decode/fill switch. Because agbcc may
|
|
15
|
+
// copy a callee-saved argument (e.g. into r4) before touching r0, entry parameters are
|
|
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';
|
|
18
|
+
import type { Opcode } from '../ir/opcodes';
|
|
19
|
+
import { T } from '../ir/types';
|
|
20
|
+
import { type Prototypes, protoArity } from '../proto';
|
|
21
|
+
import { RUNTIME_HELPERS } from '../raise/softdiv';
|
|
22
|
+
import type { TargetDescription } from '../target';
|
|
23
|
+
import { pushSwitchBr } from './emit';
|
|
24
|
+
import { FrontendUnsupportedError } from './errors';
|
|
25
|
+
import { assertInputFormat } from './format';
|
|
26
|
+
import type { Frontend } from './frontend';
|
|
27
|
+
import { opaqueDest } from './opaque';
|
|
28
|
+
import { abiSortEntryParams, fallbackArgc, makeSsaBuilder } from './ssa';
|
|
29
|
+
|
|
30
|
+
interface Instr {
|
|
31
|
+
mnemonic: string;
|
|
32
|
+
ops: string[];
|
|
33
|
+
}
|
|
34
|
+
interface AsmBlock {
|
|
35
|
+
label: string;
|
|
36
|
+
instrs: Instr[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Map a Thumb conditional-branch mnemonic to the icmp opcode for "branch taken". The signed forms
|
|
40
|
+
// (`blt`/`ble`/`bgt`/`bge`) follow a signed `cmp`; the UNSIGNED forms carry the carry/borrow sense:
|
|
41
|
+
// `bhi` = unsigned > (higher), `bls` = unsigned <= (lower-or-same), `bcc`/`blo` = unsigned <
|
|
42
|
+
// (carry-clear / lower), `bcs`/`bhs` = unsigned >= (carry-set / higher-or-same).
|
|
43
|
+
const COND_OPCODE: Record<string, Opcode> = {
|
|
44
|
+
beq: 'icmp_eq',
|
|
45
|
+
bne: 'icmp_ne',
|
|
46
|
+
blt: 'icmp_slt',
|
|
47
|
+
ble: 'icmp_sle',
|
|
48
|
+
bgt: 'icmp_sgt',
|
|
49
|
+
bge: 'icmp_sge',
|
|
50
|
+
bhi: 'icmp_ugt',
|
|
51
|
+
bls: 'icmp_ule',
|
|
52
|
+
bcc: 'icmp_ult',
|
|
53
|
+
blo: 'icmp_ult',
|
|
54
|
+
bcs: 'icmp_uge',
|
|
55
|
+
bhs: 'icmp_uge',
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Classify a block-terminating control transfer, or `null` for a non-transfer instruction (the block
|
|
59
|
+
// falls through). The SINGLE source of truth for "what ends a Thumb block and how", used by decode
|
|
60
|
+
// (block splitting), succLabels (CFG edges), the fill loop (skip transfers), and the terminator
|
|
61
|
+
// emitter — so a transfer form can't be modelled in one place and missed in another. A return via a
|
|
62
|
+
// restored link register is distinguished from a COMPUTED/loaded PC write (jump table / computed
|
|
63
|
+
// goto / register tail call), which this frontend does not model and must LOUD-FAIL rather than
|
|
64
|
+
// silently drop — mirroring the MIPS `jr` and PPC `bctr` guards. (agbcc dispatches a dense switch
|
|
65
|
+
// via `mov pc, rN`.)
|
|
66
|
+
type XferKind = 'return' | 'uncond' | 'cond' | 'indirect';
|
|
67
|
+
function classifyXfer(ins: Instr): XferKind | null {
|
|
68
|
+
const mn = ins.mnemonic;
|
|
69
|
+
if (mn === 'b') {
|
|
70
|
+
return 'uncond';
|
|
71
|
+
}
|
|
72
|
+
if (COND_OPCODE[mn]) {
|
|
73
|
+
return 'cond';
|
|
74
|
+
}
|
|
75
|
+
// `bx rN`: agbcc's return is `bx lr` or `pop {rN}; bx rN` (rN holds the restored LR) — a return.
|
|
76
|
+
// (agbcc emits jump tables via `mov pc`, NOT `bx`; a computed tail-call `bx rN` is out of scope and
|
|
77
|
+
// would need call modelling — accepted limitation, not a jump-table dispatch form.)
|
|
78
|
+
if (mn === 'bx') {
|
|
79
|
+
return 'return';
|
|
80
|
+
}
|
|
81
|
+
// A write to PC is a control transfer. `mov pc, lr` restores the link register → return; any other
|
|
82
|
+
// computed/loaded PC write (`mov pc, rN` rN≠lr, `ldr pc, …`, `add/sub pc, …`) is an indirect jump.
|
|
83
|
+
const dest = ins.ops[0]?.replace(/[[\]]/g, '');
|
|
84
|
+
if (dest === 'pc') {
|
|
85
|
+
if ((mn === 'mov' || mn === 'movs') && ins.ops[1] === 'lr') {
|
|
86
|
+
return 'return';
|
|
87
|
+
}
|
|
88
|
+
return 'indirect';
|
|
89
|
+
}
|
|
90
|
+
// `pop {…, pc}` restores the saved LR into PC → return. `ldmia rN!, {…, pc}` is a return iff the base
|
|
91
|
+
// is sp (a stack unwind); any other base is a computed multi-load jump → indirect. The register
|
|
92
|
+
// list is EXPANDED first, so `pc` inside a fused range (`{r4-pc}`) is seen — an unexpanded
|
|
93
|
+
// detection silently deleted the return.
|
|
94
|
+
const popsPc =
|
|
95
|
+
(mn === 'pop' || mn === 'ldmia' || mn === 'ldmfd') &&
|
|
96
|
+
expandRegList(
|
|
97
|
+
ins.ops
|
|
98
|
+
.join(' ')
|
|
99
|
+
.replace(/[{}!]/g, '')
|
|
100
|
+
.split(/[,\s]+/)
|
|
101
|
+
.filter(Boolean),
|
|
102
|
+
).includes('pc');
|
|
103
|
+
if (popsPc) {
|
|
104
|
+
if (mn === 'pop') {
|
|
105
|
+
return 'return';
|
|
106
|
+
}
|
|
107
|
+
return ins.ops[0]?.replace(/!$/, '') === 'sp' ? 'return' : 'indirect';
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const imm = (s: string) => parseInt(s.replace(/^#/, ''), s.includes('0x') ? 16 : 10);
|
|
113
|
+
|
|
114
|
+
// Expand fused register-range tokens (`r4-r7` → r4,r5,r6,r7) in a register list. Ranges are
|
|
115
|
+
// numeric-endpoint only (`rN-rM`); a range whose endpoint is an ALIAS (`r4-pc`/`-lr`/`-sp`) is
|
|
116
|
+
// ambiguous and left UNEXPANDED — but its endpoints ARE surfaced as separate tokens so pc/lr
|
|
117
|
+
// detection sees them, and any consumer that needs the exact list rejects the leftover `-` token
|
|
118
|
+
// loudly rather than treating the fused range as one phantom register.
|
|
119
|
+
const REG_NUM: Record<string, number> = { sp: 13, lr: 14, pc: 15 };
|
|
120
|
+
const regNum = (r: string) => (r[0] === 'r' ? Number(r.slice(1)) : REG_NUM[r]);
|
|
121
|
+
function expandRegList(tokens: string[]): string[] {
|
|
122
|
+
const out: string[] = [];
|
|
123
|
+
for (const t of tokens) {
|
|
124
|
+
const dash = t.indexOf('-');
|
|
125
|
+
if (dash === -1) {
|
|
126
|
+
out.push(t);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const lo = t.slice(0, dash);
|
|
130
|
+
const hi = t.slice(dash + 1);
|
|
131
|
+
const a = regNum(lo);
|
|
132
|
+
const b = regNum(hi);
|
|
133
|
+
if (/^r\d+$/.test(lo) && /^r\d+$/.test(hi) && Number.isFinite(a) && Number.isFinite(b) && a <= b) {
|
|
134
|
+
for (let i = a; i <= b; i++) {
|
|
135
|
+
out.push(`r${i}`);
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
// alias-endpoint or malformed range: surface both endpoints (so pc/lr is visible) AND keep
|
|
139
|
+
// the raw token (so a list consumer sees the unexpanded `-` and declines).
|
|
140
|
+
out.push(lo, hi, t);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Split an operand list on commas that are NOT inside brackets, so a memory operand like
|
|
147
|
+
// `[r0, #0x8]` (base + offset) stays a single token instead of being torn at its comma.
|
|
148
|
+
function splitOperands(s: string): string[] {
|
|
149
|
+
const out: string[] = [];
|
|
150
|
+
let depth = 0,
|
|
151
|
+
cur = '';
|
|
152
|
+
for (const ch of s) {
|
|
153
|
+
if (ch === '[' || ch === '{') {
|
|
154
|
+
depth++;
|
|
155
|
+
} else if (ch === ']' || ch === '}') {
|
|
156
|
+
depth--;
|
|
157
|
+
}
|
|
158
|
+
if (ch === ',' && depth === 0) {
|
|
159
|
+
out.push(cur.trim());
|
|
160
|
+
cur = '';
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
cur += ch;
|
|
164
|
+
}
|
|
165
|
+
if (cur.trim()) {
|
|
166
|
+
out.push(cur.trim());
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Parse a Thumb memory addressing operand `[base]` or `[base, #off]` into base register +
|
|
172
|
+
// constant byte offset. (Register-scaled indices like `[base, r1, lsl #2]` are not handled
|
|
173
|
+
// yet — agbcc materialises those as explicit add/lsl before the load in the cases we target.)
|
|
174
|
+
function parseAddr(operand: string): { base: string; off: number } {
|
|
175
|
+
const inner = operand.replace(/[[\]]/g, '').trim();
|
|
176
|
+
const parts = inner.split(',').map((s) => s.trim());
|
|
177
|
+
const base = parts[0];
|
|
178
|
+
const off = parts[1]?.startsWith('#') ? imm(parts[1]) : 0;
|
|
179
|
+
return { base, off };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Parse one function's GNU-as text into labelled basic blocks + the CFG, plus the inline `.word`
|
|
183
|
+
* data tables (label → the list of label operands under it) — the jump-table target arrays agbcc
|
|
184
|
+
* emits in `.text` (Regime B). Non-`.word` directives are skipped, EXCEPT sub-word data
|
|
185
|
+
* directives, which fail loud: disassembler-extracted asm (pret projects' `.s` splits) spells
|
|
186
|
+
* raw undecoded instructions as `.2byte 0xD101` — skipping one would silently delete a branch.
|
|
187
|
+
*
|
|
188
|
+
* Two input dialects share this parser: agbcc compiler output (`.thumb_func` + `.L` labels) and
|
|
189
|
+
* pret-project splits (luvdis-extracted: `thumb_func_start NAME` macros, `_08xxxxxx` labels,
|
|
190
|
+
* `LABEL: .4byte VALUE` literal pools on one line). The pret function macros are bookkeeping
|
|
191
|
+
* (they expand to `.align`/`.global`/`.thumb_func`/`.type`) except for the mode they declare:
|
|
192
|
+
* `arm_func_start` marks an ARM-mode body this Thumb frontend must refuse to lift. */
|
|
193
|
+
interface FlatItem {
|
|
194
|
+
label?: string;
|
|
195
|
+
instr?: Instr;
|
|
196
|
+
/** a data directive's payload, kept in-stream so byte layout is computable */
|
|
197
|
+
data?: { halfwords: boolean; values: string[]; inCode: boolean };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function decode(name: string, asm: string): { blocks: AsmBlock[]; dataWords: Map<string, string[]> } {
|
|
201
|
+
// Flatten to (label | instr | data) items, then split into blocks at labels / after branches.
|
|
202
|
+
// `.word LABEL` directives are captured into dataWords keyed by the most recent label (the
|
|
203
|
+
// jump table); ALL word/halfword data also stays in-stream as items, so the raw-halfword and
|
|
204
|
+
// pc-relative resolution below can compute byte-accurate layout.
|
|
205
|
+
let flat: FlatItem[] = [];
|
|
206
|
+
const dataWords = new Map<string, string[]>();
|
|
207
|
+
const funcLabels: string[] = []; // labels marked as function starts (.thumb_func / pret macros)
|
|
208
|
+
const armLabels = new Set<string>(); // function starts declared ARM-mode (arm_func_start)
|
|
209
|
+
const subwordData = new Map<string, string>(); // label → sub-word data directive under it
|
|
210
|
+
// Directives whose byte size we cannot know, recorded by allFlat POSITION so the layout check
|
|
211
|
+
// is scoped to the SELECTED function's slice — a `.align` between two functions must not
|
|
212
|
+
// poison a sibling that needs byte-accurate layout.
|
|
213
|
+
const hazards: { at: number; what: string }[] = [];
|
|
214
|
+
let dataLabel: string | null = null;
|
|
215
|
+
let pendingFn = false;
|
|
216
|
+
let pendingArm = false;
|
|
217
|
+
for (const rawLine of asm.split('\n')) {
|
|
218
|
+
let rest = rawLine.split('@')[0].trim();
|
|
219
|
+
if (!rest) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
// A label may share the line with what follows it (pret pools: `_08x: .4byte 0x…`) — peel it.
|
|
223
|
+
const lm = rest.match(/^([A-Za-z_.$][\w.$]*):\s*(.*)$/);
|
|
224
|
+
if (lm) {
|
|
225
|
+
const lab = lm[1];
|
|
226
|
+
if (pendingFn || pendingArm) {
|
|
227
|
+
funcLabels.push(lab);
|
|
228
|
+
if (pendingArm) {
|
|
229
|
+
armLabels.add(lab);
|
|
230
|
+
}
|
|
231
|
+
pendingFn = pendingArm = false;
|
|
232
|
+
}
|
|
233
|
+
dataLabel = lab;
|
|
234
|
+
flat.push({ label: lab });
|
|
235
|
+
rest = lm[2];
|
|
236
|
+
if (!rest) {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (rest.startsWith('.')) {
|
|
241
|
+
if (rest === '.thumb_func') {
|
|
242
|
+
pendingFn = true;
|
|
243
|
+
}
|
|
244
|
+
const wm = rest.match(/^\.(word|4byte|long)\s+(.+)$/);
|
|
245
|
+
if (wm) {
|
|
246
|
+
const values = wm[2].split(',').map((w) => w.trim()); // one-per-line and comma lists
|
|
247
|
+
if (dataLabel) {
|
|
248
|
+
const arr = dataWords.get(dataLabel) ?? dataWords.set(dataLabel, []).get(dataLabel)!;
|
|
249
|
+
arr.push(...values);
|
|
250
|
+
}
|
|
251
|
+
flat.push({ data: { halfwords: false, values, inCode: dataLabel === null } });
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
const hw = rest.match(/^\.(2byte|hword|short)\s+(.+)$/);
|
|
255
|
+
if (hw) {
|
|
256
|
+
// In the instruction stream these are raw undecoded instructions (luvdis emits branches
|
|
257
|
+
// this way) — kept as items and DECODED (or declined) below. Under a label: a sub-word
|
|
258
|
+
// data table — declines below iff the selected function references it.
|
|
259
|
+
if (dataLabel !== null) {
|
|
260
|
+
subwordData.set(dataLabel, hw[1]);
|
|
261
|
+
}
|
|
262
|
+
flat.push({
|
|
263
|
+
data: { halfwords: true, values: hw[2].split(',').map((w) => w.trim()), inCode: dataLabel === null },
|
|
264
|
+
});
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
const raw = rest.match(
|
|
268
|
+
/^\.(byte|ascii|asciz|string|space|skip|quad|8byte|octa|double|float|single|incbin|fill|zero)\b/,
|
|
269
|
+
);
|
|
270
|
+
if (raw) {
|
|
271
|
+
if (dataLabel === null) {
|
|
272
|
+
throw new FrontendUnsupportedError(
|
|
273
|
+
`cannot lift '${name}': raw data directive '.${raw[1]}' in the code stream — ` +
|
|
274
|
+
`it may encode an instruction the disassembler left undecoded (skipping it would silently delete its effect)`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
subwordData.set(dataLabel, raw[1]);
|
|
278
|
+
hazards.push({ at: flat.length - 1, what: `.${raw[1]}` }); // byte size unknown / non-word
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (/^\.align\b/.test(rest)) {
|
|
282
|
+
hazards.push({ at: flat.length - 1, what: '.align' });
|
|
283
|
+
}
|
|
284
|
+
continue; // other directives skipped
|
|
285
|
+
}
|
|
286
|
+
// pret function macros (asm/macros.inc): pure bookkeeping except the declared mode.
|
|
287
|
+
const macro = rest.match(/^(non_word_aligned_thumb_func_start|thumb_func_start|arm_func_start)\s+\S+$/);
|
|
288
|
+
if (macro) {
|
|
289
|
+
pendingFn = macro[1] !== 'arm_func_start';
|
|
290
|
+
pendingArm = macro[1] === 'arm_func_start';
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
if (/^(thumb_func_end|arm_func_end)\b/.test(rest)) {
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
const m = rest.match(/^(\w+)\s*(.*)$/);
|
|
297
|
+
if (!m) {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
dataLabel = null; // a real instruction ends a data run
|
|
301
|
+
flat.push({ instr: { mnemonic: m[1], ops: m[2] ? splitOperands(m[2]) : [] } });
|
|
302
|
+
}
|
|
303
|
+
if (
|
|
304
|
+
armLabels.has(name) ||
|
|
305
|
+
(funcLabels.length === 1 && armLabels.has(funcLabels[0]) && !flat.some((f) => f.label === name))
|
|
306
|
+
) {
|
|
307
|
+
throw new FrontendUnsupportedError(
|
|
308
|
+
`cannot lift '${name}': ARM-mode function (arm_func_start) — this frontend lifts Thumb only`,
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// FUNCTION SELECTION. `.thumb_func`-marked labels are function starts; when any exist, the
|
|
313
|
+
// requested `name` must resolve to exactly one of them and the text is sliced to it — emitting
|
|
314
|
+
// some OTHER symbol's body under `name` is precisely the silent miscompile the cardinal rule
|
|
315
|
+
// forbids. A fragment with no `.thumb_func` markers is lifted whole, as a single body.
|
|
316
|
+
//
|
|
317
|
+
// A slice may END without a terminator because the function genuinely FALLS THROUGH into the
|
|
318
|
+
// next `.thumb_func` entry (a shared tail — splitters mark the tail as its own function). The
|
|
319
|
+
// build below then retries with the slice extended through that next function: the machine
|
|
320
|
+
// code executed IS the continuation, so including it is the faithful lift. Falling into an
|
|
321
|
+
// ARM-mode function declines.
|
|
322
|
+
const allFlat = flat;
|
|
323
|
+
let sliceStart = 0;
|
|
324
|
+
let boundaries: number[] = [allFlat.length];
|
|
325
|
+
if (funcLabels.length > 0) {
|
|
326
|
+
const fi = funcLabels.indexOf(name);
|
|
327
|
+
if (fi !== -1) {
|
|
328
|
+
sliceStart = allFlat.findIndex((f) => f.label === name);
|
|
329
|
+
const starts = funcLabels
|
|
330
|
+
.map((l) => allFlat.findIndex((f) => f.label === l))
|
|
331
|
+
.filter((s) => s > sliceStart)
|
|
332
|
+
.sort((a, b) => a - b);
|
|
333
|
+
boundaries = [...starts, allFlat.length];
|
|
334
|
+
} else if (funcLabels.length >= 2) {
|
|
335
|
+
throw new FrontendUnsupportedError(
|
|
336
|
+
`cannot lift '${name}': not a function label in this asm (functions present: ${funcLabels.join(', ')})`,
|
|
337
|
+
);
|
|
338
|
+
} else if (allFlat.some((f) => f.label === name)) {
|
|
339
|
+
throw new FrontendUnsupportedError(
|
|
340
|
+
`cannot lift '${name}': '${name}' is a label here but not a function (the function is '${funcLabels[0]}')`,
|
|
341
|
+
);
|
|
342
|
+
} else {
|
|
343
|
+
// `name` absent entirely + exactly one function: an intentional rename of that function —
|
|
344
|
+
// slice from its start so preceding data labels never masquerade as its code.
|
|
345
|
+
sliceStart = allFlat.findIndex((f) => f.label === funcLabels[0]);
|
|
346
|
+
boundaries = [allFlat.length];
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
let boundaryIdx = 0;
|
|
350
|
+
// eslint-disable-next-line no-constant-condition
|
|
351
|
+
while (true) {
|
|
352
|
+
flat = allFlat.slice(sliceStart, boundaries[boundaryIdx]);
|
|
353
|
+
|
|
354
|
+
// Sub-word data tables are unmodelled: lifting a load through one fabricates values (the old
|
|
355
|
+
// silent-skip emitted wrong-but-compiling code). Decline iff the SELECTED code reaches such a
|
|
356
|
+
// table — via a direct label operand, or via a literal-pool word naming the table's symbol.
|
|
357
|
+
if (subwordData.size > 0) {
|
|
358
|
+
const reachable = new Set<string>();
|
|
359
|
+
const labelShape = /^([A-Za-z_.$][\w.$]*)/;
|
|
360
|
+
for (const f of flat) {
|
|
361
|
+
if (f.label && dataWords.has(f.label)) {
|
|
362
|
+
for (const w of dataWords.get(f.label)!) {
|
|
363
|
+
const wm = w.match(labelShape);
|
|
364
|
+
if (wm) {
|
|
365
|
+
reachable.add(wm[1]);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
for (const op of f.instr?.ops ?? []) {
|
|
370
|
+
const om = op.match(labelShape);
|
|
371
|
+
if (om) {
|
|
372
|
+
reachable.add(om[1]);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
for (const [lab, directive] of subwordData) {
|
|
377
|
+
if (reachable.has(lab)) {
|
|
378
|
+
throw new FrontendUnsupportedError(
|
|
379
|
+
`cannot lift '${name}': reads the sub-word data table '${lab}' (.${directive}) — sub-word table data is not modelled`,
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ── luvdis raw-encoding mode ─────────────────────────────────────────────────────────────
|
|
386
|
+
// Disassembler-extracted splits carry two things only byte-accurate LAYOUT can resolve:
|
|
387
|
+
// raw branch halfwords (`.2byte 0xD10E` — the target exists only as an encoded offset) and
|
|
388
|
+
// pc-relative literal loads (`ldr rD, [pc, #off]` into an unlabelled pool). Both are decoded
|
|
389
|
+
// here against computed byte offsets and rewritten into the labelled forms the rest of the
|
|
390
|
+
// frontend already models; anything the decoder cannot prove declines loud.
|
|
391
|
+
const isPcRelLdr = (ins?: Instr) =>
|
|
392
|
+
ins?.mnemonic === 'ldr' && /^\[pc,\s*#(0x[0-9a-fA-F]+|\d+)\]$/.test(ins.ops[1] ?? '');
|
|
393
|
+
const needsLayout = flat.some((f) => (f.data?.inCode ?? false) || isPcRelLdr(f.instr));
|
|
394
|
+
if (needsLayout) {
|
|
395
|
+
// Only a hazard WITHIN this function's slice makes its layout unknowable.
|
|
396
|
+
const sliceHazard = hazards.find((h) => h.at >= sliceStart && h.at < boundaries[boundaryIdx]);
|
|
397
|
+
if (sliceHazard) {
|
|
398
|
+
throw new FrontendUnsupportedError(
|
|
399
|
+
`cannot lift '${name}': raw-encoded input needs byte-accurate layout, but '${sliceHazard.what}' makes item sizes unknowable`,
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
// Byte offset of every item (Thumb-1: 2 bytes per instruction, `bl` is the 4-byte pair).
|
|
403
|
+
const itemOff: number[] = [];
|
|
404
|
+
const labelOff = new Map<string, number>();
|
|
405
|
+
const codeStart = new Set<number>(); // offsets that begin an instruction or carry a label
|
|
406
|
+
let off = 0;
|
|
407
|
+
flat.forEach((f, i) => {
|
|
408
|
+
itemOff[i] = off;
|
|
409
|
+
if (f.label && !labelOff.has(f.label)) {
|
|
410
|
+
labelOff.set(f.label, off);
|
|
411
|
+
codeStart.add(off);
|
|
412
|
+
}
|
|
413
|
+
if (f.instr) {
|
|
414
|
+
codeStart.add(off);
|
|
415
|
+
off += f.instr.mnemonic === 'bl' ? 4 : 2;
|
|
416
|
+
}
|
|
417
|
+
if (f.data) {
|
|
418
|
+
off += f.data.values.length * (f.data.halfwords ? 2 : 4);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
const labelAt = new Map<number, string>();
|
|
422
|
+
for (const [lab, lo] of labelOff) {
|
|
423
|
+
if (!labelAt.has(lo)) {
|
|
424
|
+
labelAt.set(lo, lab);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
// Thumb-1 branch encodings this frontend models (cond codes 4–7 = mi/pl/vs/vc have no
|
|
428
|
+
// lifted comparison semantics here; 14 is undefined, 15 is swi — all decline).
|
|
429
|
+
const COND_MN = ['beq', 'bne', 'bcs', 'bcc', '', '', '', '', 'bhi', 'bls', 'bge', 'blt', 'bgt', 'ble'];
|
|
430
|
+
const decodeHalfword = (v: number, at: number): { mnemonic: string; target: number } | null => {
|
|
431
|
+
if (v >= 0xd000 && v <= 0xddff) {
|
|
432
|
+
const mn = COND_MN[(v >> 8) & 0xf];
|
|
433
|
+
if (!mn) {
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
const d = (v & 0xff) - (v & 0x80 ? 0x100 : 0);
|
|
437
|
+
return { mnemonic: mn, target: at + 4 + d * 2 };
|
|
438
|
+
}
|
|
439
|
+
if (v >= 0xe000 && v <= 0xe7ff) {
|
|
440
|
+
const d = (v & 0x7ff) - (v & 0x400 ? 0x800 : 0);
|
|
441
|
+
return { mnemonic: 'b', target: at + 4 + d * 2 };
|
|
442
|
+
}
|
|
443
|
+
return null;
|
|
444
|
+
};
|
|
445
|
+
// Pass 1: decode every in-code halfword; collect synthesized labels for branch targets.
|
|
446
|
+
const synthLabels = new Map<number, string>(); // target offset → label to ensure there
|
|
447
|
+
const decoded = new Map<number, Instr>(); // flat index → replacement branch instr
|
|
448
|
+
flat.forEach((f, i) => {
|
|
449
|
+
if (!f.data?.inCode) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
if (!f.data.halfwords) {
|
|
453
|
+
return; // unlabelled word pool — layout bytes only (reached via [pc, #off] below)
|
|
454
|
+
}
|
|
455
|
+
f.data.values.forEach((raw, k) => {
|
|
456
|
+
const at = itemOff[i] + k * 2;
|
|
457
|
+
const v = parseInt(raw, 16);
|
|
458
|
+
const br = Number.isFinite(v) ? decodeHalfword(v, at) : null;
|
|
459
|
+
if (!br) {
|
|
460
|
+
throw new FrontendUnsupportedError(
|
|
461
|
+
`cannot lift '${name}': raw halfword '${raw}' in the code stream is not a decodable branch — ` +
|
|
462
|
+
`skipping it would silently delete its effect`,
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
if (!codeStart.has(br.target)) {
|
|
466
|
+
throw new FrontendUnsupportedError(
|
|
467
|
+
`cannot lift '${name}': raw branch '${raw}' targets byte offset 0x${br.target.toString(16)}, which is not an instruction boundary`,
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
if (f.data!.values.length > 1) {
|
|
471
|
+
throw new FrontendUnsupportedError(
|
|
472
|
+
`cannot lift '${name}': multi-value raw halfword directive mixing branches is not supported`,
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
const lab = labelAt.get(br.target) ?? synthLabels.get(br.target) ?? `.Lraw_${br.target.toString(16)}`;
|
|
476
|
+
synthLabels.set(br.target, lab);
|
|
477
|
+
decoded.set(i, { mnemonic: br.mnemonic, ops: [lab] });
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
// Pass 2: pc-relative literal loads → rewrite to a synthesized pool label so the existing
|
|
481
|
+
// resolvePoolConst/resolvePoolSymbol machinery applies. `(pc & ~3) + off` depends on the
|
|
482
|
+
// function's absolute alignment (mod 4) — derived STRUCTURALLY: pool words are 4-aligned in
|
|
483
|
+
// the ROM, so the file-relative offset of any `.4byte` word fixes the base parity (the
|
|
484
|
+
// luvdis `@ address` comments are not trusted).
|
|
485
|
+
let basePar: number | undefined;
|
|
486
|
+
flat.forEach((g, j) => {
|
|
487
|
+
if (g.data && !g.data.halfwords) {
|
|
488
|
+
const p = (4 - (itemOff[j] % 4)) % 4;
|
|
489
|
+
if (basePar === undefined) {
|
|
490
|
+
basePar = p;
|
|
491
|
+
} else if (basePar !== p) {
|
|
492
|
+
throw new FrontendUnsupportedError(
|
|
493
|
+
`cannot lift '${name}': literal pools at inconsistent alignments — cannot determine the function's base alignment`,
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
flat.forEach((f, i) => {
|
|
499
|
+
if (!isPcRelLdr(f.instr)) {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
if (basePar === undefined) {
|
|
503
|
+
throw new FrontendUnsupportedError(
|
|
504
|
+
`cannot lift '${name}': pc-relative literal load with no literal pool in the function to resolve into`,
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
const imm = parseInt(
|
|
508
|
+
f.instr!.ops[1].match(/#(0x[0-9a-fA-F]+|\d+)/)![1],
|
|
509
|
+
f.instr!.ops[1].includes('0x') ? 16 : 10,
|
|
510
|
+
);
|
|
511
|
+
const wordOff = ((basePar + itemOff[i] + 4) & ~3) - basePar + imm;
|
|
512
|
+
// locate the word: a 4-byte data item covering [wordOff, wordOff+4)
|
|
513
|
+
let value: string | undefined;
|
|
514
|
+
flat.forEach((g, j) => {
|
|
515
|
+
if (!g.data || g.data.halfwords) {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
const rel = wordOff - itemOff[j];
|
|
519
|
+
if (rel >= 0 && rel < g.data.values.length * 4 && rel % 4 === 0) {
|
|
520
|
+
value = g.data.values[rel / 4];
|
|
521
|
+
}
|
|
522
|
+
});
|
|
523
|
+
if (value === undefined) {
|
|
524
|
+
throw new FrontendUnsupportedError(
|
|
525
|
+
`cannot lift '${name}': pc-relative load at offset 0x${itemOff[i].toString(16)} resolves to byte offset ` +
|
|
526
|
+
`0x${wordOff.toString(16)}, which is not a word in a literal pool`,
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
const poolLab = `.Lpcpool_${wordOff.toString(16)}`;
|
|
530
|
+
dataWords.set(poolLab, [value]);
|
|
531
|
+
f.instr = { mnemonic: 'ldr', ops: [f.instr!.ops[0], poolLab] };
|
|
532
|
+
});
|
|
533
|
+
// Pass 3: rebuild flat — insert synthesized target labels, replace decoded halfwords.
|
|
534
|
+
const next: FlatItem[] = [];
|
|
535
|
+
flat.forEach((f, i) => {
|
|
536
|
+
const lab = synthLabels.get(itemOff[i]);
|
|
537
|
+
if (lab && f.label !== lab && !labelAt.has(itemOff[i])) {
|
|
538
|
+
next.push({ label: lab });
|
|
539
|
+
}
|
|
540
|
+
const br = decoded.get(i);
|
|
541
|
+
if (br) {
|
|
542
|
+
next.push({ instr: br });
|
|
543
|
+
} else {
|
|
544
|
+
next.push(f);
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
flat = next;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const blocks: AsmBlock[] = [];
|
|
551
|
+
const fallsIntoData = new Set<string>(); // blocks whose straight-line next bytes are data
|
|
552
|
+
let cur: AsmBlock | null = null;
|
|
553
|
+
let anon = 0,
|
|
554
|
+
first = true;
|
|
555
|
+
for (const f of flat) {
|
|
556
|
+
if (f.label) {
|
|
557
|
+
cur = { label: f.label, instrs: [] };
|
|
558
|
+
blocks.push(cur);
|
|
559
|
+
first = false;
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
if (f.data) {
|
|
563
|
+
// Data in the stream: never part of a block. Find the nearest preceding block WITH
|
|
564
|
+
// instructions (a bare `LABEL:` on the data — a labelled pool/table — pushes an empty
|
|
565
|
+
// block that must NOT hide the real code block behind it; skipping that was the silent
|
|
566
|
+
// deletion of a branch that fell into labelled data). If that block's straight-line path
|
|
567
|
+
// continues (open, or a conditional branch), it falls into these bytes — record it;
|
|
568
|
+
// reachable ⇒ decline below, unreachable ⇒ luvdis pool-alignment padding, pruned.
|
|
569
|
+
let prev: AsmBlock | null = cur && cur.instrs.length > 0 ? cur : null;
|
|
570
|
+
for (let j = blocks.length - 1; prev === null && j >= 0; j--) {
|
|
571
|
+
if (blocks[j].instrs.length > 0) {
|
|
572
|
+
prev = blocks[j];
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if (prev) {
|
|
576
|
+
const k = classifyXfer(prev.instrs[prev.instrs.length - 1]);
|
|
577
|
+
if (k === null || k === 'cond') {
|
|
578
|
+
fallsIntoData.add(prev.label);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
cur = null;
|
|
582
|
+
continue;
|
|
583
|
+
}
|
|
584
|
+
if (!cur) {
|
|
585
|
+
cur = { label: first ? name : `.L_anon${anon++}`, instrs: [] };
|
|
586
|
+
blocks.push(cur);
|
|
587
|
+
first = false;
|
|
588
|
+
}
|
|
589
|
+
cur.instrs.push(f.instr!);
|
|
590
|
+
// Any control transfer ends a block (see classifyXfer — the single source of truth: `b`, a
|
|
591
|
+
// conditional branch, a `bx`/PC-write return, and a computed/loaded PC write).
|
|
592
|
+
if (classifyXfer(f.instr!)) {
|
|
593
|
+
cur = null;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
// Raw data INTERLEAVED with instructions under one label: the lifted block would silently
|
|
597
|
+
// omit whatever the data encodes — decline instead.
|
|
598
|
+
const mixed = blocks.find((b) => b.instrs.length > 0 && subwordData.has(b.label));
|
|
599
|
+
if (mixed) {
|
|
600
|
+
throw new FrontendUnsupportedError(
|
|
601
|
+
`cannot lift '${name}': block '${mixed.label}' interleaves raw data (.${subwordData.get(mixed.label)}) with instructions`,
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
let live = blocks.filter((b) => b.instrs.length > 0);
|
|
605
|
+
// Alignment-pad NOPs a splitter emits around returns and literal pools: `lsls r0, r0, #0`
|
|
606
|
+
// is the 0x0000 halfword, `mov r8, r8` is 0x46C0, plus a literal `nop`. A block made ONLY
|
|
607
|
+
// of these is pool/section padding when unreachable — pruned below. A REACHABLE pad block
|
|
608
|
+
// is a real (degenerate) instruction and is kept.
|
|
609
|
+
const isPadInstr = (i: Instr) =>
|
|
610
|
+
i.mnemonic === 'nop' ||
|
|
611
|
+
((i.mnemonic === 'lsl' || i.mnemonic === 'lsls') &&
|
|
612
|
+
i.ops[0] === 'r0' &&
|
|
613
|
+
i.ops[1] === 'r0' &&
|
|
614
|
+
/^#0x?0*$/.test(i.ops[2] ?? '')) ||
|
|
615
|
+
((i.mnemonic === 'mov' || i.mnemonic === 'movs') && i.ops[0] === 'r8' && i.ops[1] === 'r8');
|
|
616
|
+
const padBlocks = new Set(live.filter((b) => b.instrs.every(isPadInstr)).map((b) => b.label));
|
|
617
|
+
if (fallsIntoData.size > 0 || padBlocks.size > 0) {
|
|
618
|
+
// Targeted reachability: a block that falls into data is either luvdis's unreachable
|
|
619
|
+
// pool-alignment padding (pruned) or genuinely reachable (decline — its fall-through
|
|
620
|
+
// successor would silently skip over the data bytes); an all-pad block after the final
|
|
621
|
+
// return (before a labelled pool or EOF) is pruned when unreachable. Other unreachable
|
|
622
|
+
// blocks are LEFT ALONE — this pass judges only those two sets, so genuine truncation
|
|
623
|
+
// still declines "falls off the end".
|
|
624
|
+
const idx = new Map(live.map((b, i) => [b.label, i] as const));
|
|
625
|
+
const reach = new Set<number>([0]);
|
|
626
|
+
const work = [0];
|
|
627
|
+
while (work.length > 0) {
|
|
628
|
+
const i = work.pop()!;
|
|
629
|
+
const b = live[i];
|
|
630
|
+
const last = b.instrs[b.instrs.length - 1];
|
|
631
|
+
const kind = last ? classifyXfer(last) : null;
|
|
632
|
+
const targets: string[] = [];
|
|
633
|
+
if (kind === 'cond' || kind === 'uncond') {
|
|
634
|
+
targets.push(last.ops[0]);
|
|
635
|
+
}
|
|
636
|
+
if (kind === null || kind === 'cond') {
|
|
637
|
+
const fall = live[i + 1]?.label;
|
|
638
|
+
if (fall !== undefined && !fallsIntoData.has(b.label)) {
|
|
639
|
+
targets.push(fall);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
for (const t of targets) {
|
|
643
|
+
const ti = idx.get(t);
|
|
644
|
+
if (ti !== undefined && !reach.has(ti)) {
|
|
645
|
+
reach.add(ti);
|
|
646
|
+
work.push(ti);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
for (const b of live) {
|
|
651
|
+
if (fallsIntoData.has(b.label) && reach.has(idx.get(b.label)!)) {
|
|
652
|
+
const last = b.instrs[b.instrs.length - 1];
|
|
653
|
+
if (!last || classifyXfer(last) === null || classifyXfer(last) === 'cond') {
|
|
654
|
+
throw new FrontendUnsupportedError(
|
|
655
|
+
`cannot lift '${name}': reachable code in block '${b.label}' falls through into data bytes`,
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
live = live.filter(
|
|
661
|
+
(b) => (!fallsIntoData.has(b.label) && !padBlocks.has(b.label)) || reach.has(idx.get(b.label)!),
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
// Fall-through into the NEXT function (shared tail): the slice's last block has no
|
|
665
|
+
// terminator, and a further function region exists — retry with the slice extended.
|
|
666
|
+
const lastLive = live[live.length - 1];
|
|
667
|
+
const lastInstr = lastLive?.instrs[lastLive.instrs.length - 1];
|
|
668
|
+
if (lastInstr && classifyXfer(lastInstr) === null && boundaryIdx + 1 < boundaries.length) {
|
|
669
|
+
const nextLab = allFlat[boundaries[boundaryIdx]]?.label;
|
|
670
|
+
if (nextLab !== undefined && armLabels.has(nextLab)) {
|
|
671
|
+
throw new FrontendUnsupportedError(
|
|
672
|
+
`cannot lift '${name}': control falls through into the ARM-mode function '${nextLab}'`,
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
boundaryIdx++;
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
return { blocks: live, dataWords };
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Resolve an agbcc/Thumb literal-pool reference (`ldr rD, .Lpool` / `.Lpool+byteOff`) to the NUMERIC
|
|
683
|
+
// 32-bit word it loads — the `ldr rD, =const` idiom. Returns null when the operand is NOT a numeric
|
|
684
|
+
// pool constant: a register/`[base]` memory operand, an unknown label, a misaligned offset, or a
|
|
685
|
+
// word that is a SYMBOL (an address / jump-table pointer — left for recoverJumpTable or the normal
|
|
686
|
+
// load path). The byte offset selects the word (index = off/4). This keeps a real literal constant
|
|
687
|
+
// (`.word 0x8408`) from being lifted as a phantom pointer parameter and dereferenced (`*a2`).
|
|
688
|
+
// The label-operand shape shared by BOTH pool paths: agbcc `.Lpool`, pret `_08012358`, with an
|
|
689
|
+
// optional `+N` byte offset. Kept in one place so the const and symbol resolvers cannot drift
|
|
690
|
+
// (they did — the drift fabricated phantom pointer params on symbol-pool loads).
|
|
691
|
+
const POOL_LABEL = /^([A-Za-z_.$][\w.$]*)(?:\s*\+\s*(0x[0-9a-fA-F]+|\d+))?$/;
|
|
692
|
+
|
|
693
|
+
type PoolRef = { kind: 'const'; value: number } | { kind: 'gaddr'; sym: string } | { kind: 'unmodelled'; why: string };
|
|
694
|
+
|
|
695
|
+
/** Classify a word-load operand `LABEL[+N]` against the captured literal pools. Returns null when
|
|
696
|
+
* the operand does NOT name a pool (a real register/memory base → the normal load path). When it
|
|
697
|
+
* DOES name a pool the outcome is const | gaddr | unmodelled — NEVER a fall-through to the load
|
|
698
|
+
* path, which would materialise the pool label as a phantom pointer parameter (a silent
|
|
699
|
+
* miscompile). `unmodelled` (a `sym+N` offset, a misaligned/out-of-range index, a `.L` code
|
|
700
|
+
* label) is the caller's cue to decline loud. */
|
|
701
|
+
function poolRef(operand: string, dataWords: Map<string, string[]>): PoolRef | null {
|
|
702
|
+
const m = operand.match(POOL_LABEL);
|
|
703
|
+
if (!m) {
|
|
704
|
+
return null;
|
|
705
|
+
}
|
|
706
|
+
const words = dataWords.get(m[1]);
|
|
707
|
+
if (!words) {
|
|
708
|
+
return null; // not a pool — an ordinary register/memory operand
|
|
709
|
+
}
|
|
710
|
+
const byteOff = m[2] ? Number(m[2]) : 0;
|
|
711
|
+
if (byteOff % 4 !== 0 || byteOff / 4 >= words.length) {
|
|
712
|
+
return { kind: 'unmodelled', why: `offset ${byteOff} is not a whole word in pool '${m[1]}'` };
|
|
713
|
+
}
|
|
714
|
+
const w = words[byteOff / 4].trim();
|
|
715
|
+
if (/^-?(0x[0-9a-fA-F]+|\d+)$/.test(w)) {
|
|
716
|
+
const val = w.startsWith('-') ? -Number(w.slice(1)) : Number(w);
|
|
717
|
+
return Number.isFinite(val) ? { kind: 'const', value: val } : { kind: 'unmodelled', why: `unparsable word '${w}'` };
|
|
718
|
+
}
|
|
719
|
+
// A bare C identifier that is NOT a `.L` code label → the address of a named global. A `sym+N`
|
|
720
|
+
// offset, or a `.L` label (jump table / code address surviving to here), is unmodelled.
|
|
721
|
+
if (/^[A-Za-z_]\w*$/.test(w) && !w.startsWith('.L')) {
|
|
722
|
+
return { kind: 'gaddr', sym: w };
|
|
723
|
+
}
|
|
724
|
+
return { kind: 'unmodelled', why: `pool word '${w}' is a symbol offset or code label` };
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Recover an agbcc Thumb jump-table dispatch. Given a dispatch block `disp` ending in `mov pc, rV`
|
|
728
|
+
// and its unique bounds predecessor `bounds` ending in `cmp rX,#(N-1); bhi DEF`, verify the exact
|
|
729
|
+
// idiom and read the inline table — else return null (→ the indirect-jump loud-fail fires). The
|
|
730
|
+
// recovered switch REPLACES both blocks: `bounds` emits a `switch_br` (scrutinee rX; successors =
|
|
731
|
+
// case blocks + DEF).
|
|
732
|
+
//
|
|
733
|
+
// bounds: cmp rX,#(N-1); bhi DEF disp: lsl rY,rX,#2 ; ldr rP,=PTR ; add rA,rY,rP
|
|
734
|
+
// ; ldr rV,[rA] ; mov pc,rV
|
|
735
|
+
// PTR: .word TABLE TABLE: .word C0 … C_{N-1}
|
|
736
|
+
//
|
|
737
|
+
// Index IDENTITY-OR-DECLINE guard: the value feeding the table load must be EXACTLY the
|
|
738
|
+
// bounds-checked scrutinee scaled only by `<<2` — any other op (xor/neg/extra offset) → decline.
|
|
739
|
+
interface JumpTable {
|
|
740
|
+
scrutReg: string;
|
|
741
|
+
caseLabels: string[];
|
|
742
|
+
defaultLabel: string;
|
|
743
|
+
}
|
|
744
|
+
function recoverJumpTable(
|
|
745
|
+
bounds: AsmBlock,
|
|
746
|
+
disp: AsmBlock,
|
|
747
|
+
dataWords: Map<string, string[]>,
|
|
748
|
+
blockLabels: Set<string>,
|
|
749
|
+
): JumpTable | null {
|
|
750
|
+
// bounds: last two instrs must be `cmp rX,#M` then `bhi DEF` (unsigned upper-bound guard).
|
|
751
|
+
const bi = bounds.instrs;
|
|
752
|
+
const bhi = bi[bi.length - 1],
|
|
753
|
+
cmp = bi[bi.length - 2];
|
|
754
|
+
if (!bhi || !cmp || bhi.mnemonic !== 'bhi' || cmp.mnemonic !== 'cmp') {
|
|
755
|
+
return null;
|
|
756
|
+
}
|
|
757
|
+
const scrutReg = cmp.ops[0];
|
|
758
|
+
const m = cmp.ops[1];
|
|
759
|
+
if (!m?.startsWith('#')) {
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
const n = imm(m) + 1; // cases 0..M → N = M+1
|
|
763
|
+
const defaultLabel = bhi.ops[0];
|
|
764
|
+
|
|
765
|
+
// disp: exactly the 5-op idiom, threading a single index register from `lsl rY,rX,#2`.
|
|
766
|
+
const d = disp.instrs;
|
|
767
|
+
if (d.length !== 5) {
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
const [lsl, ldrP, add, ldrV, movpc] = d;
|
|
771
|
+
if (lsl.mnemonic !== 'lsl' || lsl.ops[1] !== scrutReg || (lsl.ops[2] !== '#0x2' && lsl.ops[2] !== '#2')) {
|
|
772
|
+
return null;
|
|
773
|
+
}
|
|
774
|
+
const idxReg = lsl.ops[0]; // rY = rX << 2 (index*4, identity guard)
|
|
775
|
+
if (ldrP.mnemonic !== 'ldr') {
|
|
776
|
+
return null;
|
|
777
|
+
}
|
|
778
|
+
const ptrReg = ldrP.ops[0],
|
|
779
|
+
ptrLabel = ldrP.ops[1]; // rP = *(PTR literal)
|
|
780
|
+
if (add.mnemonic !== 'add' || add.ops[0] !== idxReg) {
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
// add rY, rY, rP (either operand order) — the address = table_base + index*4, nothing else.
|
|
784
|
+
const addSrcs = [add.ops[1], add.ops[2]];
|
|
785
|
+
if (!(addSrcs.includes(idxReg) && addSrcs.includes(ptrReg))) {
|
|
786
|
+
return null;
|
|
787
|
+
}
|
|
788
|
+
if (ldrV.mnemonic !== 'ldr') {
|
|
789
|
+
return null;
|
|
790
|
+
}
|
|
791
|
+
const { base } = parseAddr(ldrV.ops[1]); // rV = *(rY)
|
|
792
|
+
if (base !== idxReg || ldrV.ops[0] !== movpc.ops[1]) {
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
if (movpc.mnemonic !== 'mov' || movpc.ops[0] !== 'pc') {
|
|
796
|
+
return null;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Read the table: the ldr loads a POINTER word (PTR: .word TABLE); the table is TABLE: .word C0…
|
|
800
|
+
const ptrWords = dataWords.get(ptrLabel);
|
|
801
|
+
if (!ptrWords || ptrWords.length !== 1) {
|
|
802
|
+
return null;
|
|
803
|
+
}
|
|
804
|
+
const caseLabels = dataWords.get(ptrWords[0]);
|
|
805
|
+
if (!caseLabels || caseLabels.length !== n) {
|
|
806
|
+
return null;
|
|
807
|
+
} // table length must equal the bound
|
|
808
|
+
// Every case target and the default must resolve to a real decoded block; a label that is an
|
|
809
|
+
// expression (`.L4+4`) or points outside the function would otherwise crash later — decline cleanly.
|
|
810
|
+
if (!blockLabels.has(defaultLabel) || caseLabels.some((l) => !blockLabels.has(l))) {
|
|
811
|
+
return null;
|
|
812
|
+
}
|
|
813
|
+
return { scrutReg, caseLabels, defaultLabel };
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/** Lift decoded asm → an L1 Fn with block-argument SSA. `prototypes` supplies each callee's
|
|
817
|
+
* declared parameter count (from the project's headers); it is authoritative for recovering
|
|
818
|
+
* how many argument registers a `bl` passes (falling back to a heuristic when absent). */
|
|
819
|
+
export function lift(name: string, asm: string, target: TargetDescription, prototypes: Prototypes = {}): Fn {
|
|
820
|
+
assertInputFormat('thumb', 'gnu-as', asm);
|
|
821
|
+
const { blocks: rawBlocks, dataWords } = decode(name, asm);
|
|
822
|
+
|
|
823
|
+
// Regime B: recover agbcc jump tables. A dispatch block (`mov pc, rN`) plus its bounds
|
|
824
|
+
// predecessor (`cmp; bhi DEF`) collapse into a `switch_br` emitted from the BOUNDS block; the
|
|
825
|
+
// dispatch block is ELIDED from the CFG. A `mov pc` that is NOT a recognised table falls through
|
|
826
|
+
// to the loud-fail below.
|
|
827
|
+
const blockLabels = new Set(rawBlocks.map((b) => b.label));
|
|
828
|
+
// Any label referenced as a branch target (so we can tell if an elided dispatch block has a SECOND
|
|
829
|
+
// predecessor — a `b disp` from elsewhere — which would dangle after elision; decline if so).
|
|
830
|
+
const branchTargets = new Set<string>();
|
|
831
|
+
for (const b of rawBlocks) {
|
|
832
|
+
for (const ins of b.instrs) {
|
|
833
|
+
if ((ins.mnemonic === 'b' || COND_OPCODE[ins.mnemonic]) && ins.ops.length) {
|
|
834
|
+
branchTargets.add(ins.ops[ins.ops.length - 1]);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
const tables = new Map<AsmBlock, JumpTable>(); // bounds block → recovered table
|
|
839
|
+
const elided = new Set<AsmBlock>(); // dispatch blocks removed from the CFG
|
|
840
|
+
rawBlocks.forEach((d, i) => {
|
|
841
|
+
const last = d.instrs[d.instrs.length - 1];
|
|
842
|
+
if (last && last.mnemonic === 'mov' && last.ops[0] === 'pc' && last.ops[1] !== 'lr') {
|
|
843
|
+
const bounds = rawBlocks[i - 1];
|
|
844
|
+
// The dispatch block must be reached ONLY by falling through from its bounds predecessor — a
|
|
845
|
+
// `b disp` target elsewhere would leave a dangling edge after elision, so decline (→ loud-fail).
|
|
846
|
+
const jt = bounds && !branchTargets.has(d.label) ? recoverJumpTable(bounds, d, dataWords, blockLabels) : null;
|
|
847
|
+
if (jt) {
|
|
848
|
+
tables.set(bounds, jt);
|
|
849
|
+
elided.add(d);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
const asmBlocks = rawBlocks.filter((b) => !elided.has(b));
|
|
854
|
+
|
|
855
|
+
// TRUSTWORTHINESS: loud-fail on a control transfer this frontend cannot model, rather than
|
|
856
|
+
// silently dropping it. A computed/loaded PC write (`mov pc, rN`, `ldr pc, …`, `add/sub pc`,
|
|
857
|
+
// `ldmia rN!,{…,pc}` with rN≠sp) is a jump table / computed goto / register tail call — decode
|
|
858
|
+
// ends the block at it, but it has no static successor, so it must be a catchable "out of scope"
|
|
859
|
+
// signal, not a vanished branch. Mirrors MIPS `jr`/PPC `bctr`. (A RECOGNISED jump table's
|
|
860
|
+
// dispatch block is already elided above, so it is not scanned here.)
|
|
861
|
+
for (const ab of asmBlocks) {
|
|
862
|
+
for (const ins of ab.instrs) {
|
|
863
|
+
if (classifyXfer(ins) === 'indirect') {
|
|
864
|
+
throw new FrontendUnsupportedError(
|
|
865
|
+
`cannot lift '${name}': indirect/computed jump '${ins.mnemonic} ${ins.ops.join(', ')}' ` +
|
|
866
|
+
`— jump tables / computed gotos / register tail calls not supported`,
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// --- CFG (successors per block) as label lists; fallthrough + branch targets (via classifyXfer) ---
|
|
873
|
+
const buildCfg = (blocks: AsmBlock[]) => {
|
|
874
|
+
const labelIndex = new Map<string, number>();
|
|
875
|
+
blocks.forEach((b, i) => labelIndex.set(b.label, i));
|
|
876
|
+
const succLabels: string[][] = blocks.map((b, i) => {
|
|
877
|
+
// A recovered jump-table BOUNDS block dispatches to its case blocks + default (the elided
|
|
878
|
+
// dispatch block's targets); its `bhi`/fall-through successors are replaced entirely.
|
|
879
|
+
const jt = tables.get(b);
|
|
880
|
+
if (jt) {
|
|
881
|
+
return [...jt.caseLabels, jt.defaultLabel];
|
|
882
|
+
}
|
|
883
|
+
const last = b.instrs[b.instrs.length - 1];
|
|
884
|
+
const fall = i + 1 < blocks.length ? blocks[i + 1].label : null;
|
|
885
|
+
const kind = last ? classifyXfer(last) : null;
|
|
886
|
+
if (kind === 'return') {
|
|
887
|
+
return [];
|
|
888
|
+
} // bx lr / pop {…,pc} / mov pc,lr
|
|
889
|
+
if (kind === 'uncond') {
|
|
890
|
+
return [last!.ops[0]];
|
|
891
|
+
} // unconditional
|
|
892
|
+
if (kind === 'cond') {
|
|
893
|
+
return [last!.ops[0], fall!];
|
|
894
|
+
} // taken, fallthrough
|
|
895
|
+
return fall ? [fall] : []; // fallthrough (or non-transfer last op / EMPTY synthetic block)
|
|
896
|
+
});
|
|
897
|
+
const preds: number[][] = blocks.map(() => []);
|
|
898
|
+
// A branch to a label that is not a code block (a data label, or a target outside the sliced
|
|
899
|
+
// function) cannot be modelled — fail loud, mirroring the MIPS/PPC non-block-boundary guards.
|
|
900
|
+
succLabels.forEach((ss, i) =>
|
|
901
|
+
ss.forEach((s) => {
|
|
902
|
+
const ti = labelIndex.get(s);
|
|
903
|
+
if (ti === undefined) {
|
|
904
|
+
throw new FrontendUnsupportedError(
|
|
905
|
+
`cannot lift '${name}': branch target '${s}' is not a code block in this function (a data label, or outside the sliced function)`,
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
preds[ti].push(i);
|
|
909
|
+
}),
|
|
910
|
+
);
|
|
911
|
+
return { labelIndex, succLabels, preds };
|
|
912
|
+
};
|
|
913
|
+
|
|
914
|
+
// A function whose ENTRY block is itself a loop header (some block branches back to it — the tight
|
|
915
|
+
// `strcpy`/`strlen`/`memset` shape where block 0 IS the loop) has no preheader to carry the
|
|
916
|
+
// incoming argument registers into the header's phis. Braun SSA would then build each loop-carried
|
|
917
|
+
// register's phi from the back-edge ALONE, dropping the entry value → a use-before-def on the
|
|
918
|
+
// header's first op. Insert a synthetic EMPTY preheader that falls through to the old entry: it
|
|
919
|
+
// becomes the true entry (its arg-register reads create the params), and the old header now has a
|
|
920
|
+
// forward predecessor supplying the entry operand of each phi. Guarded on `preds[0]` so ordinary
|
|
921
|
+
// functions (entry not a branch target) are untouched.
|
|
922
|
+
let { labelIndex, preds } = buildCfg(asmBlocks);
|
|
923
|
+
if (preds[0].length > 0) {
|
|
924
|
+
let ph = '.Lasmlift_preheader';
|
|
925
|
+
while (labelIndex.has(ph)) {
|
|
926
|
+
ph += '_';
|
|
927
|
+
}
|
|
928
|
+
asmBlocks.unshift({ label: ph, instrs: [] });
|
|
929
|
+
({ labelIndex, preds } = buildCfg(asmBlocks));
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// --- ISA-neutral SSA construction (shared Braun builder) ---
|
|
933
|
+
const ssa = makeSsaBuilder(name, asmBlocks.length, preds);
|
|
934
|
+
const { fn, irBlocks, readVar, writeVar, paramReg } = ssa;
|
|
935
|
+
|
|
936
|
+
const constVal = (n: number, b: number): Value => {
|
|
937
|
+
const v = mkValue(T.unk(32));
|
|
938
|
+
irBlocks[b].ops.push(mkOp('const', { results: [v], attrs: { value: n } }));
|
|
939
|
+
return v;
|
|
940
|
+
};
|
|
941
|
+
const reg = (s: string) => s.replace(/[[\]]/g, '');
|
|
942
|
+
|
|
943
|
+
// Reading sp as a DATA operand means an address-taken local (`add rD, sp, #N` = `&local`),
|
|
944
|
+
// an sp-relative spill slot (`ldr/str …, [sp, #N]`), or frame-pointer arithmetic — none
|
|
945
|
+
// modellable without a stack abstraction. sp is never WRITTEN (sp-dest ops are transparent
|
|
946
|
+
// frame bookkeeping), so Braun SSA would materialize it as a fabricated PHANTOM parameter that
|
|
947
|
+
// scrambles the signature. Fail LOUD instead, mirroring MIPS (`isStackPtr`) and PPC (`r1`).
|
|
948
|
+
const readData = (r: string, b: number): Value => {
|
|
949
|
+
if (r === 'sp' || r === 'r13') {
|
|
950
|
+
throw new FrontendUnsupportedError(
|
|
951
|
+
`cannot lift '${name}': stack pointer used as data (address-taken local / sp-relative slot / frame arithmetic) — local stack frames not supported`,
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
if (r === 'pc' || r === 'r15') {
|
|
955
|
+
// A pc-relative literal load is rewritten to a pool label before reaching here (decode's
|
|
956
|
+
// isPcRelLdr pass); a `pc`/`r15` base that survives to a data read is an unmodelled shape
|
|
957
|
+
// (`ldr [pc]` with no `#imm`, computed-pc arithmetic) — decline, never fabricate a param.
|
|
958
|
+
throw new FrontendUnsupportedError(`cannot lift '${name}': program counter used as a data base — not modelled`);
|
|
959
|
+
}
|
|
960
|
+
if (dataWords.has(r)) {
|
|
961
|
+
// The operand is a literal-pool / data LABEL, not a register — reading it as dataflow would
|
|
962
|
+
// fabricate a phantom parameter. Word-pool loads are resolved by poolRef upstream; anything
|
|
963
|
+
// else reaching here (a sub-word load off a pool label, a label used in arithmetic) declines.
|
|
964
|
+
throw new FrontendUnsupportedError(`cannot lift '${name}': data label '${r}' used as a register — not modelled`);
|
|
965
|
+
}
|
|
966
|
+
return readVar(r, b);
|
|
967
|
+
};
|
|
968
|
+
|
|
969
|
+
// Best-effort call arity via the shared helper (frontend/ssa.ts).
|
|
970
|
+
const fallbackArgcHere = (b: number): number => fallbackArgc(ssa, target.argRegs, b);
|
|
971
|
+
|
|
972
|
+
// --- fill each block in order, sealing blocks as their predecessors complete ---
|
|
973
|
+
const fillBlock = (ab: AsmBlock, bi: number) => {
|
|
974
|
+
const irb = irBlocks[bi];
|
|
975
|
+
let pendingCmp: { lhs: Value; rhs: Value } | null = null;
|
|
976
|
+
|
|
977
|
+
// TRUSTWORTHINESS GUARD (mirrors the MIPS/PPC frontends): an unmodelled instruction must not
|
|
978
|
+
// silently drop its destination register — emit an honest `opaque`: dead ⇒ it vanishes; live ⇒
|
|
979
|
+
// assertResolved fails LOUD (see frontend/opaque.ts for the policy). Push/pop and sp
|
|
980
|
+
// adjustments have no low-register data destination, so they fall through harmlessly;
|
|
981
|
+
// terminators are handled in the terminator section below.
|
|
982
|
+
const isThumbReg = (s: string | undefined): s is string => /^r\d+$/.test(s ?? '');
|
|
983
|
+
const emitOpaqueDest = (ins: { mnemonic: string; ops: string[] }) => {
|
|
984
|
+
// storeClass: unmodelled Thumb stores are str*/stm* — `stmia rN!, {…}`'s dest token `r0!`
|
|
985
|
+
// fails isReg, so without this it would be skipped as "no reg dest", silently deleting the
|
|
986
|
+
// memory writes AND the base writeback. push/pop stay transparent frame ops (they don't match).
|
|
987
|
+
// skipSafe: push/pop stay transparent frame ops (the deliberate policy);
|
|
988
|
+
// everything else with no register destination (swi, …) throws in opaqueDest.
|
|
989
|
+
const od = opaqueDest(ins.mnemonic, ins.ops, {
|
|
990
|
+
isReg: isThumbReg,
|
|
991
|
+
normalize: reg,
|
|
992
|
+
storeClass: /^(str|stm)/,
|
|
993
|
+
skipSafe: /^(push|pop|nop)$/,
|
|
994
|
+
context: name,
|
|
995
|
+
});
|
|
996
|
+
if (!od) {
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
const operands = od.srcRegs.map((r) => readVar(r, bi));
|
|
1000
|
+
const res = mkValue(T.unk(32));
|
|
1001
|
+
// carry the mnemonic so annotate mode can name the gap (`ASMLIFT_ERROR("unmodelled 'rsb'")`)
|
|
1002
|
+
irb.ops.push(mkOp('opaque', { operands, results: [res], attrs: { mnemonic: ins.mnemonic } }));
|
|
1003
|
+
writeVar(od.dst, bi, res);
|
|
1004
|
+
};
|
|
1005
|
+
// 2-operand ALU form `op rD, op2` (rD = rD ⟨op⟩ op2). `op2` is an immediate (`#N`) or a
|
|
1006
|
+
// register. A destination that is NOT a low data register (`add sp, #8` / `sub sp, #N` frame
|
|
1007
|
+
// adjustments) is transparent to dataflow — the frame is push/pop-based — so it falls through
|
|
1008
|
+
// harmlessly, matching the documented sp handling. A malformed operand (missing / non-register
|
|
1009
|
+
// non-immediate) degrades to a loud opaque rather than a crash or a silent data-dest drop.
|
|
1010
|
+
const emit2op = (opc: Opcode, dReg: string, op2: string | undefined, bi: number) => {
|
|
1011
|
+
if (!isThumbReg(reg(dReg))) {
|
|
1012
|
+
return;
|
|
1013
|
+
} // sp/pc frame adjustment: transparent
|
|
1014
|
+
if (op2 === undefined) {
|
|
1015
|
+
emitOpaqueDest({ mnemonic: opc, ops: [dReg] });
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
const rhs = op2.startsWith('#') ? constVal(imm(op2), bi) : readData(reg(op2), bi);
|
|
1019
|
+
const res = mkValue(T.unk(32));
|
|
1020
|
+
irb.ops.push(mkOp(opc, { operands: [readData(reg(dReg), bi), rhs], results: [res] }));
|
|
1021
|
+
writeVar(reg(dReg), bi, res);
|
|
1022
|
+
};
|
|
1023
|
+
|
|
1024
|
+
for (const ins of ab.instrs) {
|
|
1025
|
+
// Control transfers (branches, returns) are emitted in the terminator section below — skip them
|
|
1026
|
+
// here so a return-form PC write (`mov pc, lr`, `pop {…,pc}`) is not decoded as a data write to a
|
|
1027
|
+
// phantom `pc` register (a silent drop of the return). `cmp` is not a transfer, so it still runs.
|
|
1028
|
+
if (classifyXfer(ins)) {
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
const [a, b, c] = ins.ops;
|
|
1032
|
+
switch (ins.mnemonic) {
|
|
1033
|
+
case 'mov':
|
|
1034
|
+
case 'movs': {
|
|
1035
|
+
const v = b?.startsWith('#') ? constVal(imm(b), bi) : readData(reg(b), bi);
|
|
1036
|
+
writeVar(reg(a), bi, v);
|
|
1037
|
+
break;
|
|
1038
|
+
}
|
|
1039
|
+
case 'add':
|
|
1040
|
+
case 'adds': {
|
|
1041
|
+
// `add rD, rS, #0` is agbcc's low-register copy idiom (Thumb `mov rD, rS` between
|
|
1042
|
+
// low regs isn't always available). Model it as a pure copy — same SSA value — not
|
|
1043
|
+
// an `x + 0` add. This keeps output clean and, crucially, makes a value copied to a
|
|
1044
|
+
// callee-saved register before a call read as still-live *after* the call, which is
|
|
1045
|
+
// how call-argument liveness tells a passed argument from a preserved one.
|
|
1046
|
+
if (c === '#0') {
|
|
1047
|
+
writeVar(reg(a), bi, readData(reg(b), bi));
|
|
1048
|
+
break;
|
|
1049
|
+
}
|
|
1050
|
+
// 2-operand form `add rD, op2` (rD = rD + op2): op2 in `b`, no third operand.
|
|
1051
|
+
// A malformed 1-operand `add` degrades to a loud opaque.
|
|
1052
|
+
if (c === undefined) {
|
|
1053
|
+
emit2op('add', a, b, bi);
|
|
1054
|
+
break;
|
|
1055
|
+
}
|
|
1056
|
+
const rhs = c?.startsWith('#') ? constVal(imm(c), bi) : readData(reg(c), bi);
|
|
1057
|
+
const res = mkValue(T.unk(32));
|
|
1058
|
+
irb.ops.push(mkOp('add', { operands: [readData(reg(b), bi), rhs], results: [res] }));
|
|
1059
|
+
writeVar(reg(a), bi, res);
|
|
1060
|
+
break;
|
|
1061
|
+
}
|
|
1062
|
+
case 'sub':
|
|
1063
|
+
case 'subs': {
|
|
1064
|
+
if (c === undefined) {
|
|
1065
|
+
emit2op('sub', a, b, bi);
|
|
1066
|
+
break;
|
|
1067
|
+
} // `sub rD, op2` → rD = rD - op2
|
|
1068
|
+
const rhs = c?.startsWith('#') ? constVal(imm(c), bi) : readData(reg(c), bi);
|
|
1069
|
+
const res = mkValue(T.unk(32));
|
|
1070
|
+
irb.ops.push(mkOp('sub', { operands: [readData(reg(b), bi), rhs], results: [res] }));
|
|
1071
|
+
writeVar(reg(a), bi, res);
|
|
1072
|
+
break;
|
|
1073
|
+
}
|
|
1074
|
+
case 'lsr':
|
|
1075
|
+
case 'lsl':
|
|
1076
|
+
case 'asr':
|
|
1077
|
+
case 'lsrs':
|
|
1078
|
+
case 'lsls':
|
|
1079
|
+
case 'asrs': {
|
|
1080
|
+
const shiftMn = ins.mnemonic.replace(/s$/, ''); // pret spells the flag-setting forms lsls/lsrs/asrs
|
|
1081
|
+
const opc = shiftMn === 'lsr' ? 'shr_u' : shiftMn === 'asr' ? 'shr_s' : 'shl';
|
|
1082
|
+
// A missing SECOND operand is malformed — degrade to a loud opaque like emit2op does.
|
|
1083
|
+
if (b === undefined) {
|
|
1084
|
+
emitOpaqueDest(ins);
|
|
1085
|
+
break;
|
|
1086
|
+
}
|
|
1087
|
+
const res = mkValue(T.unk(32));
|
|
1088
|
+
if (c === undefined) {
|
|
1089
|
+
// 2-operand register form `lsl rD, rS` → rD = rD << rS
|
|
1090
|
+
irb.ops.push(mkOp(opc, { operands: [readData(reg(a), bi), readData(reg(b), bi)], results: [res] }));
|
|
1091
|
+
} else if (c.startsWith('#')) {
|
|
1092
|
+
// immediate form `lsl rD, rS, #n`
|
|
1093
|
+
irb.ops.push(mkOp(opc, { operands: [readData(reg(b), bi)], results: [res], attrs: { imm: imm(c) } }));
|
|
1094
|
+
} else {
|
|
1095
|
+
// register form `lsl rD, rS, rN` → rD = rS << rN
|
|
1096
|
+
irb.ops.push(mkOp(opc, { operands: [readData(reg(b), bi), readData(reg(c), bi)], results: [res] }));
|
|
1097
|
+
}
|
|
1098
|
+
writeVar(reg(a), bi, res);
|
|
1099
|
+
break;
|
|
1100
|
+
}
|
|
1101
|
+
case 'neg':
|
|
1102
|
+
case 'negs': {
|
|
1103
|
+
// `neg rD, rS` (and `rsb rD, rS, #0`) = arithmetic negation → -x
|
|
1104
|
+
const res = mkValue(T.unk(32));
|
|
1105
|
+
irb.ops.push(mkOp('neg', { operands: [readData(reg(b), bi)], results: [res] }));
|
|
1106
|
+
writeVar(reg(a), bi, res);
|
|
1107
|
+
break;
|
|
1108
|
+
}
|
|
1109
|
+
case 'rsb':
|
|
1110
|
+
case 'rsbs': {
|
|
1111
|
+
// Reverse subtract. `rsb rD, rS, #0` is the negate idiom (0 - rS) → -x. Any other form
|
|
1112
|
+
// (`rsb rD, rS, #N`, N≠0 — not a Thumb-1 encoding, but be safe) is NOT modelled: degrade
|
|
1113
|
+
// to a loud `opaque` rather than silently leaving rD unwritten (a silent miscompile).
|
|
1114
|
+
if (c === '#0') {
|
|
1115
|
+
const res = mkValue(T.unk(32));
|
|
1116
|
+
irb.ops.push(mkOp('neg', { operands: [readData(reg(b), bi)], results: [res] }));
|
|
1117
|
+
writeVar(reg(a), bi, res);
|
|
1118
|
+
} else {
|
|
1119
|
+
emitOpaqueDest(ins);
|
|
1120
|
+
}
|
|
1121
|
+
break;
|
|
1122
|
+
}
|
|
1123
|
+
case 'mvn':
|
|
1124
|
+
case 'mvns': {
|
|
1125
|
+
// `mvn rD, rS` = bitwise NOT → ~x
|
|
1126
|
+
const res = mkValue(T.unk(32));
|
|
1127
|
+
irb.ops.push(mkOp('not', { operands: [readData(reg(b), bi)], results: [res] }));
|
|
1128
|
+
writeVar(reg(a), bi, res);
|
|
1129
|
+
break;
|
|
1130
|
+
}
|
|
1131
|
+
case 'bic':
|
|
1132
|
+
case 'bics': {
|
|
1133
|
+
// `bic rD, rM` (2-op) / `bic rD, rD, rM` (agbcc's redundant 3-op spelling) = rD & ~rM —
|
|
1134
|
+
// emitted verbatim by agbcc for the C idiom `x & ~y` (kleod's ReadKeyInput
|
|
1135
|
+
// key-transition mask), so the not+and pair recompiles to bic.
|
|
1136
|
+
if (b === undefined) {
|
|
1137
|
+
emitOpaqueDest(ins);
|
|
1138
|
+
break;
|
|
1139
|
+
}
|
|
1140
|
+
const [xr, mr] = c !== undefined ? [reg(b), reg(c)] : [reg(a), reg(b)];
|
|
1141
|
+
const inv = mkValue(T.unk(32));
|
|
1142
|
+
irb.ops.push(mkOp('not', { operands: [readData(mr, bi)], results: [inv] }));
|
|
1143
|
+
const res = mkValue(T.unk(32));
|
|
1144
|
+
irb.ops.push(mkOp('and', { operands: [readData(xr, bi), inv], results: [res] }));
|
|
1145
|
+
writeVar(reg(a), bi, res);
|
|
1146
|
+
break;
|
|
1147
|
+
}
|
|
1148
|
+
case 'ror':
|
|
1149
|
+
case 'rors': {
|
|
1150
|
+
// `ror rD, rS` (2-op) / `ror rD, rD, rS` (redundant 3-op) = rotate right → the rotr
|
|
1151
|
+
// op; the structurer spells the C rotate idiom, which agbcc compiles back to this ror.
|
|
1152
|
+
if (b === undefined) {
|
|
1153
|
+
emitOpaqueDest(ins);
|
|
1154
|
+
break;
|
|
1155
|
+
}
|
|
1156
|
+
const [xr, nr] = c !== undefined ? [reg(b), reg(c)] : [reg(a), reg(b)];
|
|
1157
|
+
const res = mkValue(T.unk(32));
|
|
1158
|
+
irb.ops.push(mkOp('rotr', { operands: [readData(xr, bi), readData(nr, bi)], results: [res] }));
|
|
1159
|
+
writeVar(reg(a), bi, res);
|
|
1160
|
+
break;
|
|
1161
|
+
}
|
|
1162
|
+
case 'ldmia':
|
|
1163
|
+
case 'stmia': {
|
|
1164
|
+
// Load/store-multiple with writeback: `ldmia rN!, {rA, rB…}` = one word access per
|
|
1165
|
+
// listed register at ascending offsets, then rN += 4×count. splitOperands is
|
|
1166
|
+
// brace-depth-aware, so the register list arrives as ONE token ('{rA, rB}'); the
|
|
1167
|
+
// rejoin below also tolerates a split list defensively. Thumb-1 LDMIA skips the
|
|
1168
|
+
// writeback when rN is itself in the list (the loaded value wins) — modelled; any
|
|
1169
|
+
// malformed shape degrades to the loud opaque.
|
|
1170
|
+
// `!` = writeback (`ldmia rN!, {…}`); its absence is the valid no-writeback form
|
|
1171
|
+
// (`ldmia rN, {…}` — same transfers, base unchanged). A missing register list is
|
|
1172
|
+
// malformed → loud opaque.
|
|
1173
|
+
const baseTok = a;
|
|
1174
|
+
const writeback = !!baseTok?.endsWith('!');
|
|
1175
|
+
if (baseTok === undefined || b === undefined || !b.startsWith('{')) {
|
|
1176
|
+
emitOpaqueDest(ins);
|
|
1177
|
+
break;
|
|
1178
|
+
}
|
|
1179
|
+
const baseReg = reg(writeback ? baseTok.slice(0, -1) : baseTok);
|
|
1180
|
+
const list = expandRegList(
|
|
1181
|
+
ins.ops
|
|
1182
|
+
.slice(1)
|
|
1183
|
+
.join(',')
|
|
1184
|
+
.replace(/[{}]/g, '')
|
|
1185
|
+
.split(',')
|
|
1186
|
+
.map((r) => r.trim())
|
|
1187
|
+
.filter(Boolean),
|
|
1188
|
+
);
|
|
1189
|
+
// An unexpandable range (alias endpoint, e.g. `r4-lr`) leaves a raw `-` token — the
|
|
1190
|
+
// exact transfer set is ambiguous, so degrade to the loud opaque rather than guess.
|
|
1191
|
+
if (list.some((r) => r.includes('-'))) {
|
|
1192
|
+
emitOpaqueDest(ins);
|
|
1193
|
+
break;
|
|
1194
|
+
}
|
|
1195
|
+
if (list.length === 0) {
|
|
1196
|
+
emitOpaqueDest(ins);
|
|
1197
|
+
break;
|
|
1198
|
+
}
|
|
1199
|
+
// SNAPSHOT the base ONCE: hardware performs every transfer from the ORIGINAL base, but
|
|
1200
|
+
// a base-in-list ldmia overwrites that register mid-list — re-reading it per iteration
|
|
1201
|
+
// loaded the siblings from the freshly-loaded value instead (silent wrong addresses,
|
|
1202
|
+
// adversarially reproduced). All accesses and the writeback read this snapshot.
|
|
1203
|
+
const base0 = readData(baseReg, bi);
|
|
1204
|
+
list.forEach((r, i) => {
|
|
1205
|
+
if (ins.mnemonic === 'ldmia') {
|
|
1206
|
+
const res = mkValue(T.unk(32));
|
|
1207
|
+
irb.ops.push(
|
|
1208
|
+
mkOp('load', { operands: [base0], results: [res], attrs: { off: 4 * i, signed: true, width: 4 } }),
|
|
1209
|
+
);
|
|
1210
|
+
writeVar(reg(r), bi, res);
|
|
1211
|
+
} else {
|
|
1212
|
+
irb.ops.push(mkOp('store', { operands: [base0, readData(reg(r), bi)], attrs: { off: 4 * i, width: 4 } }));
|
|
1213
|
+
}
|
|
1214
|
+
});
|
|
1215
|
+
// Writeback advances the base by 4×count — SUPPRESSED when there is no `!`, or (ldmia)
|
|
1216
|
+
// when the base is itself in the list (the loaded value wins, ARMv4T).
|
|
1217
|
+
const wroteBase = ins.mnemonic === 'ldmia' && list.some((r) => reg(r) === baseReg);
|
|
1218
|
+
if (writeback && !wroteBase) {
|
|
1219
|
+
const adv = mkValue(T.unk(32));
|
|
1220
|
+
irb.ops.push(mkOp('add', { operands: [base0, constVal(4 * list.length, bi)], results: [adv] }));
|
|
1221
|
+
writeVar(baseReg, bi, adv);
|
|
1222
|
+
}
|
|
1223
|
+
break;
|
|
1224
|
+
}
|
|
1225
|
+
case 'mul':
|
|
1226
|
+
case 'muls':
|
|
1227
|
+
case 'and':
|
|
1228
|
+
case 'ands':
|
|
1229
|
+
case 'orr':
|
|
1230
|
+
case 'orrs':
|
|
1231
|
+
case 'eor':
|
|
1232
|
+
case 'eors': {
|
|
1233
|
+
const opc = (
|
|
1234
|
+
{
|
|
1235
|
+
mul: 'mul',
|
|
1236
|
+
muls: 'mul',
|
|
1237
|
+
and: 'and',
|
|
1238
|
+
ands: 'and',
|
|
1239
|
+
orr: 'or',
|
|
1240
|
+
orrs: 'or',
|
|
1241
|
+
eor: 'xor',
|
|
1242
|
+
eors: 'xor',
|
|
1243
|
+
} as Record<string, Opcode>
|
|
1244
|
+
)[ins.mnemonic]!;
|
|
1245
|
+
// 3-operand (rD, rS, rM) or 2-operand (rD, rM) flag-setting form. A 1-operand form is
|
|
1246
|
+
// malformed — loud opaque, not a crash.
|
|
1247
|
+
if (b === undefined) {
|
|
1248
|
+
emitOpaqueDest(ins);
|
|
1249
|
+
break;
|
|
1250
|
+
}
|
|
1251
|
+
const [x, y] =
|
|
1252
|
+
c !== undefined
|
|
1253
|
+
? [readData(reg(b), bi), readData(reg(c), bi)]
|
|
1254
|
+
: [readData(reg(a), bi), readData(reg(b), bi)];
|
|
1255
|
+
const res = mkValue(T.unk(32));
|
|
1256
|
+
irb.ops.push(mkOp(opc, { operands: [x, y], results: [res] }));
|
|
1257
|
+
writeVar(reg(a), bi, res);
|
|
1258
|
+
break;
|
|
1259
|
+
}
|
|
1260
|
+
case 'cmp': {
|
|
1261
|
+
if (a === undefined || b === undefined) {
|
|
1262
|
+
emitOpaqueDest(ins);
|
|
1263
|
+
break;
|
|
1264
|
+
}
|
|
1265
|
+
const rhs = b.startsWith('#') ? constVal(imm(b), bi) : readData(reg(b), bi);
|
|
1266
|
+
pendingCmp = { lhs: readData(reg(a), bi), rhs };
|
|
1267
|
+
break;
|
|
1268
|
+
}
|
|
1269
|
+
case 'ldr':
|
|
1270
|
+
case 'ldrb':
|
|
1271
|
+
case 'ldrh':
|
|
1272
|
+
case 'ldrsb':
|
|
1273
|
+
case 'ldrsh': {
|
|
1274
|
+
// A word load whose operand NAMES a literal pool is a pool reference, not a memory base:
|
|
1275
|
+
// a numeric word → `const`, a bare global → `gaddr` (structure.ts lowers a load/store
|
|
1276
|
+
// through it to `gSym`), anything else → loud decline. It must NEVER fall to the load
|
|
1277
|
+
// path below, which would materialise the pool label as a phantom pointer parameter.
|
|
1278
|
+
if (ins.mnemonic === 'ldr' && b !== undefined) {
|
|
1279
|
+
const pr = poolRef(b, dataWords);
|
|
1280
|
+
if (pr?.kind === 'const') {
|
|
1281
|
+
const res = mkValue(T.unk(32));
|
|
1282
|
+
irb.ops.push(mkOp('const', { results: [res], attrs: { value: pr.value } }));
|
|
1283
|
+
writeVar(reg(a), bi, res);
|
|
1284
|
+
break;
|
|
1285
|
+
}
|
|
1286
|
+
if (pr?.kind === 'gaddr') {
|
|
1287
|
+
const res = mkValue(T.unk(32));
|
|
1288
|
+
irb.ops.push(mkOp('gaddr', { results: [res], attrs: { sym: pr.sym } }));
|
|
1289
|
+
writeVar(reg(a), bi, res);
|
|
1290
|
+
break;
|
|
1291
|
+
}
|
|
1292
|
+
if (pr?.kind === 'unmodelled') {
|
|
1293
|
+
throw new FrontendUnsupportedError(
|
|
1294
|
+
`cannot lift '${name}': literal-pool load of ${pr.why} — not modelled`,
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
// rD, [base, #off] — a typed load. Width/signedness come from the mnemonic; the
|
|
1299
|
+
// base becomes a pointer to that element type during type recovery.
|
|
1300
|
+
if (a === undefined || b === undefined) {
|
|
1301
|
+
emitOpaqueDest(ins);
|
|
1302
|
+
break;
|
|
1303
|
+
}
|
|
1304
|
+
const width = /b/.test(ins.mnemonic) ? 1 : /h/.test(ins.mnemonic) ? 2 : 4;
|
|
1305
|
+
const signed = ins.mnemonic === 'ldr' || /s/.test(ins.mnemonic.slice(3));
|
|
1306
|
+
const { base, off } = parseAddr(b);
|
|
1307
|
+
const res = mkValue(T.unk(32));
|
|
1308
|
+
irb.ops.push(mkOp('load', { operands: [readData(base, bi)], results: [res], attrs: { off, width, signed } }));
|
|
1309
|
+
writeVar(reg(a), bi, res);
|
|
1310
|
+
break;
|
|
1311
|
+
}
|
|
1312
|
+
case 'str':
|
|
1313
|
+
case 'strb':
|
|
1314
|
+
case 'strh': {
|
|
1315
|
+
// rS, [base, #off] — a typed store (a side-effecting statement, no result).
|
|
1316
|
+
if (a === undefined || b === undefined) {
|
|
1317
|
+
emitOpaqueDest(ins);
|
|
1318
|
+
break;
|
|
1319
|
+
}
|
|
1320
|
+
const width = /b/.test(ins.mnemonic) ? 1 : /h/.test(ins.mnemonic) ? 2 : 4;
|
|
1321
|
+
const { base, off } = parseAddr(b);
|
|
1322
|
+
irb.ops.push(mkOp('store', { operands: [readData(base, bi), readData(reg(a), bi)], attrs: { off, width } }));
|
|
1323
|
+
break;
|
|
1324
|
+
}
|
|
1325
|
+
case 'bl':
|
|
1326
|
+
case 'blx': {
|
|
1327
|
+
// A call: read the argument registers (r0..), produce the return value in r0. The
|
|
1328
|
+
// callee's caller-saved clobber (r1..r3, lr) needs no modelling — agbcc has already
|
|
1329
|
+
// moved anything live across the call into a callee-saved register (a copy we alias).
|
|
1330
|
+
const targetSym = a;
|
|
1331
|
+
// Caller-supplied prototype wins; otherwise a known runtime helper (`__divsi3` &c.)
|
|
1332
|
+
// supplies its arity so its arguments are recovered; only then fall back to guessing.
|
|
1333
|
+
const argc =
|
|
1334
|
+
protoArity(prototypes[targetSym]) ?? protoArity(RUNTIME_HELPERS[targetSym]) ?? fallbackArgcHere(bi);
|
|
1335
|
+
const args: Value[] = [];
|
|
1336
|
+
for (let k = 0; k < argc; k++) {
|
|
1337
|
+
args.push(readVar(`r${k}`, bi));
|
|
1338
|
+
}
|
|
1339
|
+
const res = mkValue(T.unk(32));
|
|
1340
|
+
irb.ops.push(mkOp('call', { operands: args, results: [res], attrs: { target: targetSym } }));
|
|
1341
|
+
writeVar('r0', bi, res);
|
|
1342
|
+
break;
|
|
1343
|
+
}
|
|
1344
|
+
default:
|
|
1345
|
+
// Control transfers are already skipped above; any other unmodelled op fails loud (opaque)
|
|
1346
|
+
// instead of silently dropping its destination.
|
|
1347
|
+
emitOpaqueDest(ins);
|
|
1348
|
+
break;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
// terminator (via classifyXfer — the single source of truth shared with decode/succLabels)
|
|
1353
|
+
const last = ab.instrs[ab.instrs.length - 1];
|
|
1354
|
+
const kind = last ? classifyXfer(last) : null;
|
|
1355
|
+
const succ = (label: string): Successor => ({ block: irBlocks[labelIndex.get(label)!], args: [] });
|
|
1356
|
+
const jt = tables.get(ab);
|
|
1357
|
+
if (jt) {
|
|
1358
|
+
// Regime B: the bounds block dispatches a `switch_br` over the scrutinee — N case blocks (values
|
|
1359
|
+
// 0..N-1, dense) followed by the default block (last successor). The `cmp`/`bhi` are subsumed.
|
|
1360
|
+
pushSwitchBr(irb.ops, readVar(reg(jt.scrutReg), bi), [...jt.caseLabels.map(succ), succ(jt.defaultLabel)]);
|
|
1361
|
+
} else if (!last) {
|
|
1362
|
+
// an EMPTY block is only ever the synthetic entry preheader (decoded blocks are non-empty):
|
|
1363
|
+
// fall through to the real entry, whose loop-header phis take their entry operand from here.
|
|
1364
|
+
irb.ops.push(mkOp('br', { successors: [succ(fallLabel(bi))] }));
|
|
1365
|
+
} else if (kind === 'return') {
|
|
1366
|
+
// bx lr / pop {…,pc} / mov pc,lr
|
|
1367
|
+
irb.ops.push(mkOp('ret', { operands: [readVar(target.returnReg, bi)] }));
|
|
1368
|
+
} else if (kind === 'uncond') {
|
|
1369
|
+
irb.ops.push(mkOp('br', { successors: [succ(last.ops[0])] }));
|
|
1370
|
+
} else if (kind === 'cond') {
|
|
1371
|
+
// `pendingCmp` is block-local; a `cmp` split from its branch by a label means the flags
|
|
1372
|
+
// cross a block boundary — not modelled. Decline loud.
|
|
1373
|
+
if (!pendingCmp) {
|
|
1374
|
+
throw new FrontendUnsupportedError(
|
|
1375
|
+
`cannot lift '${name}': conditional branch '${last.mnemonic}' has no reaching compare in its block`,
|
|
1376
|
+
);
|
|
1377
|
+
}
|
|
1378
|
+
const cond = mkValue(T.unk(32));
|
|
1379
|
+
irb.ops.push(mkOp(COND_OPCODE[last.mnemonic], { operands: [pendingCmp.lhs, pendingCmp.rhs], results: [cond] }));
|
|
1380
|
+
irb.ops.push(mkOp('cond_br', { operands: [cond], successors: [succ(last.ops[0]), succ(fallLabel(bi))] }));
|
|
1381
|
+
} else {
|
|
1382
|
+
// fallthrough (last instruction is a call / data op, no control transfer)
|
|
1383
|
+
irb.ops.push(mkOp('br', { successors: [succ(fallLabel(bi))] }));
|
|
1384
|
+
}
|
|
1385
|
+
};
|
|
1386
|
+
|
|
1387
|
+
// The fall-through label after block `bi` — a LAST block needing one means control runs off
|
|
1388
|
+
// the end of the function (truncated/misparsed input): decline loud, never a TypeError.
|
|
1389
|
+
const fallLabel = (bi: number): string => {
|
|
1390
|
+
const nb = asmBlocks[bi + 1];
|
|
1391
|
+
if (!nb) {
|
|
1392
|
+
throw new FrontendUnsupportedError(
|
|
1393
|
+
`cannot lift '${name}': control falls off the end (block '${asmBlocks[bi].label}' has no terminator and no successor)`,
|
|
1394
|
+
);
|
|
1395
|
+
}
|
|
1396
|
+
return nb.label;
|
|
1397
|
+
};
|
|
1398
|
+
asmBlocks.forEach((ab, bi) => {
|
|
1399
|
+
fillBlock(ab, bi);
|
|
1400
|
+
ssa.markFilled(bi);
|
|
1401
|
+
});
|
|
1402
|
+
|
|
1403
|
+
ssa.finish();
|
|
1404
|
+
|
|
1405
|
+
// Order the entry block's parameters by ABI register (r0, r1, r2, …) so downstream
|
|
1406
|
+
// naming (`a0`, `a1`, …) matches the calling convention, not the read order. Safe only
|
|
1407
|
+
// for the true entry (no predecessors) — a loop header's params are phis whose position
|
|
1408
|
+
// is index-aligned with predecessor terminator args and must not be reordered.
|
|
1409
|
+
const entry = irBlocks[0];
|
|
1410
|
+
// non-ABI live-in ranks LAST (99) — deliberate Thumb tie-break; MIPS/PPC's is -1/first
|
|
1411
|
+
abiSortEntryParams(entry, preds[0].length > 0, (v) => {
|
|
1412
|
+
const m = /^r(\d+)$/.exec(paramReg.get(v) ?? '');
|
|
1413
|
+
return m ? +m[1] : 99;
|
|
1414
|
+
});
|
|
1415
|
+
return fn;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
/** The ARMv4T / Thumb (agbcc) frontend, registered for the `armv4t` target. */
|
|
1419
|
+
export const thumbFrontend: Frontend = { id: 'thumb', inputFormat: 'gnu-as', lift };
|