@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,990 @@
|
|
|
1
|
+
// asmlift ISA frontend — PowerPC (GameCube/Wii, Metrowerks CodeWarrior `mwcceppc`). Input is
|
|
2
|
+
// disassembled text (`powerpc-eabi-objdump -d --no-show-raw-insn`), parsed by the shared
|
|
3
|
+
// `parseDisasm` with the reloc + branch-hint options enabled.
|
|
4
|
+
//
|
|
5
|
+
// ISA facts that shape this frontend:
|
|
6
|
+
// • NO DELAY SLOTS — a block simply ends at its terminator.
|
|
7
|
+
// • CONDITION REGISTERS, FUSED — a compare (`cmpw`/`cmpwi`; unsigned `cmplw`/`cmplwi`) is tracked
|
|
8
|
+
// and the branch that reads it fuses into a single `cond_br icmp_*`. The branch MNEMONIC
|
|
9
|
+
// carries the sense (`bge` ⇒ `icmp_sge`), so no negation fold is needed; an unsigned compare
|
|
10
|
+
// picks the unsigned icmp row.
|
|
11
|
+
// • CONDITIONAL RETURN — `cmpwi r3,0; bgelr` is "if cr0≥0, return r3". The `bXXlr` forms become a
|
|
12
|
+
// cond_br to a SYNTHETIC return block, so the structurer sees an ordinary divergent-if.
|
|
13
|
+
// • EXTENDED MNEMONICS — objdump prints the simplified forms: `mr`, `li`, `subf` (reversed
|
|
14
|
+
// operands: `subf rD,rA,rB` = rB−rA), `not`, and the rotate-and-mask family.
|
|
15
|
+
// `slwi`/`srwi`/`clrlwi`/`clrrwi` and rotate-0 `rlwinm` are exact; a non-zero-rotate `rlwinm`
|
|
16
|
+
// lowers only as the right-shift bitfield extract `(x>>n)&mask` (ME=31, non-wrapping); a
|
|
17
|
+
// genuine rotate/insert stays an opaque.
|
|
18
|
+
// • CALLS (`bl`) — the callee symbol comes from the interleaved `R_PPC_REL24` relocation (an
|
|
19
|
+
// unresolved `bl` in a .o encodes a 0 placeholder); arguments come from r3.. per the callee
|
|
20
|
+
// prototype (falling back to argument-register liveness). The frame — `stwu r1`, `mflr`/`mtlr`,
|
|
21
|
+
// r1-relative spills — is transparent to dataflow, so a value in a callee-saved register
|
|
22
|
+
// survives the call.
|
|
23
|
+
// • RECORD FORM (`.` = the Rc bit, e.g. `andi.`/`add.`) also sets cr0 from a signed compare of
|
|
24
|
+
// the result against 0; that implicit compare is wired so a following `beq`/`bne` fuses.
|
|
25
|
+
//
|
|
26
|
+
// TRUSTWORTHINESS: an unmodelled instruction with a register destination emits an `opaque` value
|
|
27
|
+
// (dead ⇒ vanishes, live ⇒ fails LOUD downstream); an unmodelled CONTROL TRANSFER throws
|
|
28
|
+
// PpcUnsupportedError in `lift`. Never plausible-but-wrong C.
|
|
29
|
+
//
|
|
30
|
+
// Scope: straight-line + `if`/diamond integer functions (incl. the conditional-return idiom),
|
|
31
|
+
// non-recursive `bl` calls, recovered dense-switch jump tables, and CTR-counted `bdnz` loops
|
|
32
|
+
// (`mtctr` seeds a `ctr` pseudo-register). Returns through r3. Out of scope, failing loud: the
|
|
33
|
+
// conditional-CTR forms (`bdz`/`bdnzt`/…), an unrecovered `bctr`, and a `bdnz` with no reaching
|
|
34
|
+
// `mtctr`. NOTE mwcc at -O4 aggressively UNROLLS loops into a `bdnz` main loop + a remainder
|
|
35
|
+
// loop; an unrolled loop recovers as the unrolled form (sound, rarely a match).
|
|
36
|
+
import { Fn, Op, Successor, Value, mkOp, mkValue } from '../ir/core';
|
|
37
|
+
import type { Opcode } from '../ir/opcodes';
|
|
38
|
+
import { T } from '../ir/types';
|
|
39
|
+
import { type Prototypes, protoArity } from '../proto';
|
|
40
|
+
import type { TargetDescription } from '../target';
|
|
41
|
+
import { type AsmData, readJumpTable } from './asmdata';
|
|
42
|
+
import {
|
|
43
|
+
type DisasmInstr,
|
|
44
|
+
parseImm,
|
|
45
|
+
parseDisasm as parseSharedDisasm,
|
|
46
|
+
parseMem as parseSharedMem,
|
|
47
|
+
sliceSymbol,
|
|
48
|
+
} from './disasm';
|
|
49
|
+
import { mkEmitKit, pushSwitchBr } from './emit';
|
|
50
|
+
import { FrontendUnsupportedError } from './errors';
|
|
51
|
+
import { assertInputFormat } from './format';
|
|
52
|
+
import type { Frontend } from './frontend';
|
|
53
|
+
import { opaqueDest } from './opaque';
|
|
54
|
+
import { abiSortEntryParams } from './ssa';
|
|
55
|
+
import { makeSsaBuilder } from './ssa';
|
|
56
|
+
|
|
57
|
+
type Instr = DisasmInstr;
|
|
58
|
+
|
|
59
|
+
// Branch-condition mnemonic → the icmp for its TAKEN edge, split by compare signedness. PowerPC's
|
|
60
|
+
// branch already names the relation (no negation fold needed). `signed` picks the row.
|
|
61
|
+
const COND_SIGNED: Record<string, Opcode> = {
|
|
62
|
+
blt: 'icmp_slt',
|
|
63
|
+
ble: 'icmp_sle',
|
|
64
|
+
bgt: 'icmp_sgt',
|
|
65
|
+
bge: 'icmp_sge',
|
|
66
|
+
beq: 'icmp_eq',
|
|
67
|
+
bne: 'icmp_ne',
|
|
68
|
+
};
|
|
69
|
+
const COND_UNSIGNED: Record<string, Opcode> = {
|
|
70
|
+
blt: 'icmp_ult',
|
|
71
|
+
ble: 'icmp_ule',
|
|
72
|
+
bgt: 'icmp_ugt',
|
|
73
|
+
bge: 'icmp_uge',
|
|
74
|
+
beq: 'icmp_eq',
|
|
75
|
+
bne: 'icmp_ne',
|
|
76
|
+
};
|
|
77
|
+
const CONDS = new Set(Object.keys(COND_SIGNED));
|
|
78
|
+
|
|
79
|
+
// Out-of-scope control flow (conditional-CTR forms `bdz`/`bdnzt`/…, indirect `bctr`/`bctrl`, …).
|
|
80
|
+
// PowerPC branch mnemonics all start with `b`; anything not in `isModeledBranch` is a real branch
|
|
81
|
+
// this frontend cannot lower, and silently dropping a branch is a silent miscompile — so `lift`
|
|
82
|
+
// throws a catchable "out of scope" signal instead. Subclasses the shared frontend signal so
|
|
83
|
+
// consumers can `instanceof FrontendUnsupportedError`; `.name` kept for stable stub text.
|
|
84
|
+
export class PpcUnsupportedError extends FrontendUnsupportedError {
|
|
85
|
+
constructor(message: string) {
|
|
86
|
+
super(message);
|
|
87
|
+
this.name = 'PpcUnsupportedError';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const isReturn = (ins: Instr) => ins.mnemonic === 'blr';
|
|
92
|
+
const isUncond = (ins: Instr) => ins.mnemonic === 'b';
|
|
93
|
+
const isCond = (ins: Instr) => CONDS.has(ins.mnemonic) && ins.target !== undefined;
|
|
94
|
+
// Conditional return, e.g. `bgelr`/`bltlr`: a cond mnemonic with the `lr` suffix and no target.
|
|
95
|
+
const condReturnBase = (ins: Instr): string | null => {
|
|
96
|
+
if (!ins.mnemonic.endsWith('lr')) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
const base = 'b' + ins.mnemonic.slice(1, -2);
|
|
100
|
+
return CONDS.has(base) ? base : null;
|
|
101
|
+
};
|
|
102
|
+
const isCondReturn = (ins: Instr) => condReturnBase(ins) !== null;
|
|
103
|
+
// `bdnz L` — CTR ← CTR−1; branch to L if CTR ≠ 0. The ONLY CTR form modelled — `bdz`, the
|
|
104
|
+
// conditional-CTR forms (`bdnzt`/`bdzf`/…), and the indirect `bctr`/`bctrl` fail loud (below).
|
|
105
|
+
// A `bdnz` with no target is malformed.
|
|
106
|
+
const isCtrLoop = (ins: Instr) => ins.mnemonic === 'bdnz' && ins.target !== undefined;
|
|
107
|
+
const isXfer = (ins: Instr) => isReturn(ins) || isUncond(ins) || isCond(ins) || isCondReturn(ins) || isCtrLoop(ins);
|
|
108
|
+
// `bl` is a CALL (mid-block, control returns), not a block transfer, so it is modelled but not in
|
|
109
|
+
// isXfer. Every other `b*` mnemonic is a branch we can lower iff it is one of these forms.
|
|
110
|
+
const isModeledBranch = (ins: Instr) => isXfer(ins) || ins.mnemonic === 'bl';
|
|
111
|
+
|
|
112
|
+
const isReg = (s: string | undefined): s is string => /^r\d+$/.test(s ?? '');
|
|
113
|
+
|
|
114
|
+
// Shared objdump scaffolding (frontend/disasm.ts). parseMem narrowed to `r\d+` bases — a
|
|
115
|
+
// non-register base is an SDA/global placeholder assertOrdinaryMem declines.
|
|
116
|
+
const parseMem = (operand: string): { off: number; base: string } => parseSharedMem(operand, /r\d+/);
|
|
117
|
+
|
|
118
|
+
// 32-bit mask with bits [mb..me] set (PowerPC bit numbering: 0 = MSB). Wraps when mb > me.
|
|
119
|
+
const rlwinmMask = (mb: number, me: number): number => {
|
|
120
|
+
let m = 0;
|
|
121
|
+
for (let b = 0; b < 32; b++) {
|
|
122
|
+
const set = mb <= me ? b >= mb && b <= me : b >= mb || b <= me;
|
|
123
|
+
if (set) {
|
|
124
|
+
m |= 0x80000000 >>> b;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return m >>> 0;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Shared objdump scaffolding (frontend/disasm.ts), with the two PPC extras threaded as options:
|
|
131
|
+
// reloc lines (`-r` — the callee symbol for a `bl` whose encoded offset is a placeholder) and
|
|
132
|
+
// branch-prediction hint suffixes (`blt-`/`bge+` — a hint, not a different instruction; without
|
|
133
|
+
// stripping, the mnemonic misses the cond tables and the branch is silently dropped).
|
|
134
|
+
const parseDisasm = (disasm: string): Instr[] => parseSharedDisasm(disasm, { relocs: true, hintSuffixes: true });
|
|
135
|
+
|
|
136
|
+
interface PpcBlock {
|
|
137
|
+
startAddr: number;
|
|
138
|
+
body: Instr[];
|
|
139
|
+
branch: Instr | null; // terminating transfer (or null for a pure fall-through)
|
|
140
|
+
synthReturn?: boolean; // a synthetic block that just returns the return register
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// A recovered dense-switch jump table (Regime B), keyed
|
|
144
|
+
// by the BOUNDS branch (`bgt DEF`) whose block emits the `switch_br`. `caseAddrs[k]` is the `.text`
|
|
145
|
+
// address of case `k` (dense 0..N-1); `bctrAddr` is the elided dispatch's indirect jump. The mwcc
|
|
146
|
+
// idiom:
|
|
147
|
+
// cmplwi rS,N-1 ; bgt DEF (bounds)
|
|
148
|
+
// lis rT,0 [ADDR16_HA @tbl] ; slwi rIdx,rS,2 ; addi rB,rT,0 [ADDR16_LO @tbl]
|
|
149
|
+
// ; lwzx rV,rB,rIdx ; mtctr rV ; bctr (dispatch — table in .data)
|
|
150
|
+
interface PpcJT {
|
|
151
|
+
scrutReg: string;
|
|
152
|
+
caseAddrs: number[];
|
|
153
|
+
defaultAddr: number;
|
|
154
|
+
bctrAddr: number;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Recover mwcc jump tables from the dispatch idiom + the AsmData side-table. Fail-closed: any
|
|
158
|
+
// deviation from the exact idiom, or a table that doesn't resolve to N in-function targets, declines
|
|
159
|
+
// (→ the `bctr` loud-fail fires). Index IDENTITY guard: `slwi rIdx,rS,2` must be the bounds-checked
|
|
160
|
+
// scrutinee scaled only by <<2 — no xor/neg/extra op.
|
|
161
|
+
function recoverPpcJumpTables(instrs: Instr[], ad: AsmData): Map<number, PpcJT> {
|
|
162
|
+
const out = new Map<number, PpcJT>();
|
|
163
|
+
for (let i = 5; i < instrs.length; i++) {
|
|
164
|
+
if (instrs[i].mnemonic !== 'bctr') {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const [lis, slwi, addi, lwzx, mtctr] = [instrs[i - 5], instrs[i - 4], instrs[i - 3], instrs[i - 2], instrs[i - 1]];
|
|
168
|
+
if (
|
|
169
|
+
lis.mnemonic !== 'lis' ||
|
|
170
|
+
slwi.mnemonic !== 'slwi' ||
|
|
171
|
+
addi.mnemonic !== 'addi' ||
|
|
172
|
+
lwzx.mnemonic !== 'lwzx' ||
|
|
173
|
+
mtctr.mnemonic !== 'mtctr'
|
|
174
|
+
) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
const rV = mtctr.ops[0];
|
|
178
|
+
if (lwzx.ops[0] !== rV) {
|
|
179
|
+
continue;
|
|
180
|
+
} // rV = mem[rB + rIdx]
|
|
181
|
+
const [rB, rIdx] = [lwzx.ops[1], lwzx.ops[2]];
|
|
182
|
+
if (slwi.ops[0] !== rIdx || (slwi.ops[2] !== '2' && slwi.ops[2] !== '0x2')) {
|
|
183
|
+
continue;
|
|
184
|
+
} // identity: rIdx = rS<<2
|
|
185
|
+
const scrutReg = slwi.ops[1];
|
|
186
|
+
if (addi.ops[0] !== rB || parseImm(addi.ops[2]) !== 0) {
|
|
187
|
+
continue;
|
|
188
|
+
} // rB = &table (lo) + 0
|
|
189
|
+
const rT = addi.ops[1];
|
|
190
|
+
const tableSym = lis.sym; // ADDR16_HA/LO @tbl (from inline -r reloc)
|
|
191
|
+
if (lis.ops[0] !== rT || !tableSym || addi.sym !== tableSym) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
// Bounds: the nearest preceding `cmplwi scrutReg,N-1 ; bgt DEF` guard. The `bgt` is itself a
|
|
195
|
+
// transfer, so match the pair directly (the cmplwi sits one instruction behind it); a different
|
|
196
|
+
// transfer before the guard ⇒ decline.
|
|
197
|
+
let bounds: { addr: number; n: number; def: number } | null = null;
|
|
198
|
+
for (let j = i - 6; j >= 1; j--) {
|
|
199
|
+
const bgt = instrs[j];
|
|
200
|
+
if (bgt.mnemonic === 'bgt' && bgt.target !== undefined) {
|
|
201
|
+
const c = instrs[j - 1];
|
|
202
|
+
if (c && c.mnemonic === 'cmplwi' && c.ops[c.ops.length - 2] === scrutReg) {
|
|
203
|
+
bounds = { addr: bgt.addr, n: parseImm(c.ops[c.ops.length - 1]) + 1, def: bgt.target };
|
|
204
|
+
}
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
if (isXfer(bgt)) {
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (!bounds || bounds.n < 2) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const caseAddrs = readJumpTable(ad, tableSym, 0, bounds.n);
|
|
215
|
+
if (!caseAddrs) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
out.set(bounds.addr, { scrutReg, caseAddrs, defaultAddr: bounds.def, bctrAddr: instrs[i].addr });
|
|
219
|
+
}
|
|
220
|
+
return out;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Split into basic blocks and compute successor block INDICES (order [taken, fall] for a cond
|
|
224
|
+
// branch/return). Conditional-return branches get a synthetic return block as their taken edge.
|
|
225
|
+
// A recovered jump table (`jts`, keyed by bounds-branch addr) turns its bounds block into a
|
|
226
|
+
// `switch_br` dispatcher: the case + default addresses become leaders and its only successors.
|
|
227
|
+
function toBlocks(instrs: Instr[], name: string, jts: Map<number, PpcJT>): { blocks: PpcBlock[]; succIdx: number[][] } {
|
|
228
|
+
const leaders = new Set<number>(instrs.length ? [instrs[0].addr] : []);
|
|
229
|
+
instrs.forEach((ins, i) => {
|
|
230
|
+
if ((isCond(ins) || isUncond(ins) || isCtrLoop(ins)) && ins.target !== undefined) {
|
|
231
|
+
leaders.add(ins.target);
|
|
232
|
+
}
|
|
233
|
+
if ((isCond(ins) || isCondReturn(ins) || isCtrLoop(ins)) && instrs[i + 1]) {
|
|
234
|
+
leaders.add(instrs[i + 1].addr);
|
|
235
|
+
} // fall-through
|
|
236
|
+
});
|
|
237
|
+
for (const jt of jts.values()) {
|
|
238
|
+
for (const a of jt.caseAddrs) {
|
|
239
|
+
leaders.add(a);
|
|
240
|
+
}
|
|
241
|
+
leaders.add(jt.defaultAddr);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const blocks: PpcBlock[] = [];
|
|
245
|
+
let cur: PpcBlock | null = null;
|
|
246
|
+
for (const ins of instrs) {
|
|
247
|
+
if (cur === null || leaders.has(ins.addr)) {
|
|
248
|
+
cur = { startAddr: ins.addr, body: [], branch: null };
|
|
249
|
+
blocks.push(cur);
|
|
250
|
+
}
|
|
251
|
+
if (isXfer(ins)) {
|
|
252
|
+
cur.branch = ins;
|
|
253
|
+
cur = null;
|
|
254
|
+
} else {
|
|
255
|
+
cur.body.push(ins);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const idxOf = new Map(blocks.map((b, i) => [b.startAddr, i]));
|
|
260
|
+
const fallAddr = (b: PpcBlock) => (b.branch ?? b.body[b.body.length - 1]).addr + 4;
|
|
261
|
+
const succIdx: number[][] = blocks.map(() => []);
|
|
262
|
+
blocks.forEach((b, i) => {
|
|
263
|
+
const br = b.branch;
|
|
264
|
+
// Recovered switch: the bounds block dispatches to its case blocks + default (the `cmplwi`/`bgt`
|
|
265
|
+
// and the elided dispatch block are subsumed into a `switch_br`). Every case/default addr is a
|
|
266
|
+
// leader, so all resolve; a target that is not a block boundary makes it malformed → loud-fail.
|
|
267
|
+
const jt = br ? jts.get(br.addr) : undefined;
|
|
268
|
+
if (jt) {
|
|
269
|
+
const succ = [...jt.caseAddrs, jt.defaultAddr].map((a) => idxOf.get(a));
|
|
270
|
+
if (succ.some((x) => x === undefined)) {
|
|
271
|
+
throw new PpcUnsupportedError(`cannot lift '${name}': jump-table target is not a block boundary`);
|
|
272
|
+
}
|
|
273
|
+
succIdx[i] = succ as number[];
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (!br) {
|
|
277
|
+
const j = idxOf.get(fallAddr(b));
|
|
278
|
+
if (j !== undefined) {
|
|
279
|
+
succIdx[i] = [j];
|
|
280
|
+
}
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (isReturn(br)) {
|
|
284
|
+
succIdx[i] = [];
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (isUncond(br)) {
|
|
288
|
+
const j = idxOf.get(br.target!);
|
|
289
|
+
succIdx[i] = j !== undefined ? [j] : [];
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (isCondReturn(br)) {
|
|
293
|
+
const synth: PpcBlock = { startAddr: -1 - blocks.length, body: [], branch: null, synthReturn: true };
|
|
294
|
+
const si = blocks.length;
|
|
295
|
+
blocks.push(synth);
|
|
296
|
+
succIdx.push([]);
|
|
297
|
+
const fall = idxOf.get(br.addr + 4); // fall = instruction after the cond-return (no delay slot)
|
|
298
|
+
succIdx[i] = fall !== undefined ? [si, fall] : [si];
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
// conditional branch to a label: [taken, fall]. Both must land on block boundaries; otherwise the
|
|
302
|
+
// branch leaves the function (a tail branch) or targets unrecovered flow — fail LOUD and catchably
|
|
303
|
+
// rather than silently dropping an edge (which surfaces as an opaque `verify` successor-count error).
|
|
304
|
+
const taken = idxOf.get(br.target!);
|
|
305
|
+
const fall = idxOf.get(br.addr + 4);
|
|
306
|
+
if (taken === undefined || fall === undefined) {
|
|
307
|
+
throw new PpcUnsupportedError(
|
|
308
|
+
`cannot lift '${name}': conditional branch '${br.mnemonic}' at 0x${br.addr.toString(16)} ` +
|
|
309
|
+
`has a target/fall-through that is not a block boundary (tail branch or unrecovered control flow)`,
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
succIdx[i] = [taken, fall];
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Prune to blocks reachable from entry (drops trailing padding). Reindex succ accordingly.
|
|
316
|
+
const reachable = new Set<number>();
|
|
317
|
+
const queue = blocks.length ? [0] : [];
|
|
318
|
+
while (queue.length) {
|
|
319
|
+
const i = queue.pop()!;
|
|
320
|
+
if (reachable.has(i)) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
reachable.add(i);
|
|
324
|
+
for (const s of succIdx[i]) {
|
|
325
|
+
queue.push(s);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const keep = blocks.map((_, i) => i).filter((i) => reachable.has(i));
|
|
329
|
+
const remap = new Map(keep.map((old, neu) => [old, neu]));
|
|
330
|
+
return {
|
|
331
|
+
blocks: keep.map((i) => blocks[i]),
|
|
332
|
+
succIdx: keep.map((i) => succIdx[i].map((s) => remap.get(s)!)),
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Lift disassembled PowerPC text → an L1 Fn with block-argument SSA. `asmData` (optional) supplies
|
|
337
|
+
* the data-section jump table for dense-switch (Regime-B) recovery; absent ⇒ a `bctr` dispatch
|
|
338
|
+
* loud-fails. */
|
|
339
|
+
export function lift(
|
|
340
|
+
name: string,
|
|
341
|
+
asm: string,
|
|
342
|
+
target: TargetDescription,
|
|
343
|
+
prototypes: Prototypes = {},
|
|
344
|
+
asmData?: AsmData,
|
|
345
|
+
): Fn {
|
|
346
|
+
assertInputFormat('ppc', 'objdump', asm);
|
|
347
|
+
const instrs = parseDisasm(sliceSymbol(asm, name)); // ONE function only — an absent symbol declines loud
|
|
348
|
+
if (instrs.length === 0) {
|
|
349
|
+
throw new PpcUnsupportedError(`cannot lift '${name}': no instructions found in the input text`);
|
|
350
|
+
}
|
|
351
|
+
// Regime B: recover mwcc jump tables from the `bctr` dispatch idiom + the AsmData table. A
|
|
352
|
+
// recovered dispatch's `bctr` is subsumed into a `switch_br` (emitted from its bounds block), so
|
|
353
|
+
// it is exempted from the loud-fail below; an UNrecovered `bctr` still fails loud.
|
|
354
|
+
const jts = asmData ? recoverPpcJumpTables(instrs, asmData) : new Map<number, PpcJT>();
|
|
355
|
+
const recoveredBctr = new Set([...jts.values()].map((j) => j.bctrAddr));
|
|
356
|
+
// TRUSTWORTHINESS: fail loud on an unmodelled control transfer rather than dropping it (which
|
|
357
|
+
// would silently miscompile the control flow). CTR-counted loops and indirect branches land here.
|
|
358
|
+
for (const ins of instrs) {
|
|
359
|
+
if (ins.mnemonic.startsWith('b') && !isModeledBranch(ins)) {
|
|
360
|
+
if (ins.mnemonic === 'bctr' && recoveredBctr.has(ins.addr)) {
|
|
361
|
+
continue;
|
|
362
|
+
} // recovered switch dispatch
|
|
363
|
+
throw new PpcUnsupportedError(
|
|
364
|
+
`cannot lift '${name}': unmodelled control transfer '${ins.mnemonic}' at 0x${ins.addr.toString(16)} ` +
|
|
365
|
+
`(CTR-counted loop or indirect branch — mwcc -O4 loop unrolling is not yet supported)`,
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const { blocks, succIdx } = toBlocks(instrs, name, jts);
|
|
370
|
+
|
|
371
|
+
const preds: number[][] = blocks.map(() => []);
|
|
372
|
+
blocks.forEach((_, i) => {
|
|
373
|
+
for (const s of succIdx[i]) {
|
|
374
|
+
preds[s].push(i);
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const ssa = makeSsaBuilder(name, blocks.length, preds);
|
|
379
|
+
const { irBlocks, readVar, writeVar, paramReg } = ssa;
|
|
380
|
+
const RET = target.returnReg;
|
|
381
|
+
const ARG_REGS = target.argRegs;
|
|
382
|
+
|
|
383
|
+
// Best-effort call arity when a callee has no prototype: the count of contiguous argument
|
|
384
|
+
// registers (r3..) with a value reaching the call. A prototype's `params` is authoritative
|
|
385
|
+
// when supplied; this liveness heuristic covers the rest.
|
|
386
|
+
const fallbackArgc = (bi: number): number => {
|
|
387
|
+
let n = 0;
|
|
388
|
+
while (n < ARG_REGS.length && ssa.hasReachingDef(ARG_REGS[n], bi)) {
|
|
389
|
+
n++;
|
|
390
|
+
}
|
|
391
|
+
return n;
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
// Frame slots (r1-relative offsets) that hold a TRANSPARENT save — a callee-saved register's
|
|
395
|
+
// entry value or the saved link register. A reload from one of these is dropped (the value is
|
|
396
|
+
// unchanged, so the in-register SSA value already carries it). Function-scoped so a save in the
|
|
397
|
+
// prologue block matches a restore in a different epilogue block. Any OTHER r1 access is a genuine
|
|
398
|
+
// local spill / address-taken stack object this frontend cannot model — those fail LOUD (below),
|
|
399
|
+
// never silently drop, because a dropped local spill is a silent miscompile.
|
|
400
|
+
const savedSlots = new Set<number>();
|
|
401
|
+
|
|
402
|
+
const fillBlock = (b: PpcBlock, bi: number) => {
|
|
403
|
+
const ops = irBlocks[bi].ops;
|
|
404
|
+
const succ = (j: number): Successor => ({ block: irBlocks[j], args: [] });
|
|
405
|
+
|
|
406
|
+
// A synthetic conditional-return block: just return the current return register.
|
|
407
|
+
if (b.synthReturn) {
|
|
408
|
+
const retOps = ssa.hasReachingDef(RET, bi) ? [readVar(RET, bi)] : [];
|
|
409
|
+
ops.push(mkOp('ret', { operands: retOps }));
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// `lastDef` is the value most recently written to a register in this instruction — used to
|
|
414
|
+
// wire a record-form op's implicit cr0 side effect (see the `rc` handling in `decode`).
|
|
415
|
+
let lastDef: Value | null = null;
|
|
416
|
+
// Reading r1 (the stack pointer) as a DATA operand means frame-pointer arithmetic or an
|
|
417
|
+
// address-taken local (`addi r3,r1,8` = `&local`) — not modellable without a stack abstraction,
|
|
418
|
+
// and fabricating a value for r1 silently miscompiles. Fail LOUD. (Frame bookkeeping never
|
|
419
|
+
// reaches here: stwu / addi r1 / mflr / mtlr / r1-relative spills are handled before `read`.)
|
|
420
|
+
const read = (r: string): Value => {
|
|
421
|
+
if (r === 'r1') {
|
|
422
|
+
throw new PpcUnsupportedError(
|
|
423
|
+
`cannot lift '${name}': stack pointer r1 used as data (address-taken local / frame arithmetic) — not supported`,
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
return readVar(r, bi);
|
|
427
|
+
};
|
|
428
|
+
// Ordinary memory must be based on a real register that is not the frame pointer. A non-register
|
|
429
|
+
// base is an SDA/global-relative access (`stw r0,0(0)` — the base field is a 0 placeholder the
|
|
430
|
+
// relocation fills at link); a base of r1 that reaches here is a sub-word frame slot. Both are
|
|
431
|
+
// unmodelled — fail LOUD rather than fabricate a bogus pointer parameter or field.
|
|
432
|
+
const assertOrdinaryMem = (mem: string) => {
|
|
433
|
+
const { base } = parseMem(mem);
|
|
434
|
+
if (!isReg(base)) {
|
|
435
|
+
throw new PpcUnsupportedError(
|
|
436
|
+
`cannot lift '${name}': non-register memory base ('${mem}') — SDA/global-relative access not supported`,
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
if (base === 'r1') {
|
|
440
|
+
throw new PpcUnsupportedError(
|
|
441
|
+
`cannot lift '${name}': sub-word stack-frame access ('${mem}') — local stack frames not supported`,
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
// A word store/load based on r1. Returns true if it is TRANSPARENT frame bookkeeping (skip):
|
|
446
|
+
// a save of a register with no reaching def (callee-saved entry value / saved lr), or a reload
|
|
447
|
+
// from a recorded save slot. A store of a LIVE (reaching-def) value is a real local spill →
|
|
448
|
+
// fail LOUD. A reload from an unrecorded slot is a genuine stack local → fail LOUD.
|
|
449
|
+
const frameStore = (srcReg: string, mem: string): boolean => {
|
|
450
|
+
const { base, off } = parseMem(mem);
|
|
451
|
+
if (base !== 'r1') {
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
if (!ssa.hasReachingDef(srcReg, bi)) {
|
|
455
|
+
savedSlots.add(off);
|
|
456
|
+
return true;
|
|
457
|
+
}
|
|
458
|
+
throw new PpcUnsupportedError(
|
|
459
|
+
`cannot lift '${name}': spill of a live value to the stack ('${srcReg},${mem}') — local stack frames not supported`,
|
|
460
|
+
);
|
|
461
|
+
};
|
|
462
|
+
const frameLoad = (mem: string): boolean => {
|
|
463
|
+
const { base, off } = parseMem(mem);
|
|
464
|
+
if (base !== 'r1') {
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
if (savedSlots.has(off)) {
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
470
|
+
throw new PpcUnsupportedError(
|
|
471
|
+
`cannot lift '${name}': reload of a stack local ('${mem}') — local stack frames not supported`,
|
|
472
|
+
);
|
|
473
|
+
};
|
|
474
|
+
const write = (r: string, v: Value) => {
|
|
475
|
+
writeVar(r, bi, v);
|
|
476
|
+
lastDef = v;
|
|
477
|
+
};
|
|
478
|
+
// Shared emitter kit (frontend/emit.ts) — the ISA-specific readers/guards stay above.
|
|
479
|
+
const kit = mkEmitKit(ops, write);
|
|
480
|
+
const constVal = kit.cnst;
|
|
481
|
+
const emit = kit.emit;
|
|
482
|
+
// TRUSTWORTHINESS GUARD: an unmodelled instruction must not silently drop its destination —
|
|
483
|
+
// emit an honest `opaque` instead: dead ⇒ DCE'd; live ⇒ assertResolved fails LOUD (see
|
|
484
|
+
// frontend/opaque.ts for the policy).
|
|
485
|
+
const emitOpaqueDest = (ins: Instr) => {
|
|
486
|
+
// storeClass: every PPC store mnemonic is st* — an unmodelled one (`stwbrx`, `sthbrx`, …)
|
|
487
|
+
// must throw, never skip (its first token is the SOURCE register).
|
|
488
|
+
const od = opaqueDest(ins.mnemonic, ins.ops, {
|
|
489
|
+
isReg,
|
|
490
|
+
storeClass: /^st/,
|
|
491
|
+
skipSafe: /^nop$/,
|
|
492
|
+
context: `${name} @0x${ins.addr.toString(16)}`,
|
|
493
|
+
});
|
|
494
|
+
if (!od) {
|
|
495
|
+
return;
|
|
496
|
+
} // skip-safe only (opaqueDest throws on any other no-destination instruction)
|
|
497
|
+
// carry the mnemonic so annotate mode can name the gap (`ASMLIFT_ERROR("unmodelled 'xori'")`)
|
|
498
|
+
emit('opaque', od.dst, od.srcRegs.map(read), { mnemonic: ins.mnemonic });
|
|
499
|
+
};
|
|
500
|
+
const emitBin = kit.bin;
|
|
501
|
+
const emitUn = kit.un;
|
|
502
|
+
// Unwritten temporaries for the complemented-logic decodes: the value feeds a following op,
|
|
503
|
+
// it is not itself a register destination.
|
|
504
|
+
const binTmp = (opc: Opcode, x: Value, y: Value): Value => kit.tmp(opc, [x, y]);
|
|
505
|
+
const notOf = (r: string): Value => {
|
|
506
|
+
const v = mkValue(T.unk(32));
|
|
507
|
+
ops.push(mkOp('not', { operands: [read(r)], results: [v] }));
|
|
508
|
+
return v;
|
|
509
|
+
};
|
|
510
|
+
const emitShImm = kit.shImm;
|
|
511
|
+
const emitLoad = (d: string, mem: string, width: number, signed: boolean) => {
|
|
512
|
+
assertOrdinaryMem(mem);
|
|
513
|
+
const { off, base } = parseMem(mem);
|
|
514
|
+
emit('load', d, [read(base)], { off, width, signed });
|
|
515
|
+
};
|
|
516
|
+
const emitStore = (srcReg: string, mem: string, width: number) => {
|
|
517
|
+
assertOrdinaryMem(mem);
|
|
518
|
+
const { off, base } = parseMem(mem);
|
|
519
|
+
ops.push(mkOp('store', { operands: [read(base), read(srcReg)], attrs: { off, width } }));
|
|
520
|
+
};
|
|
521
|
+
// Register+register INDEXED addressing (`lwzx rD,rA,rB` = *(rA+rB), `stwx rS,rA,rB` = *(rA+rB)=rS).
|
|
522
|
+
// This is how mwcc emits EVERY variable-index array access (scalar and struct) — with rB the scaled
|
|
523
|
+
// index. Decode to `add(rA,rB)` + a zero-offset load/store, the exact shape recognizeArrays
|
|
524
|
+
// consumes (and raise/struct-arrays.ts targets), so `a[i]`
|
|
525
|
+
// recovers from here.
|
|
526
|
+
const addrX = (rA: string, rB: string): Value => {
|
|
527
|
+
const addr = mkValue(T.unk(32));
|
|
528
|
+
ops.push(mkOp('add', { operands: [read(rA), read(rB)], results: [addr] }));
|
|
529
|
+
return addr;
|
|
530
|
+
};
|
|
531
|
+
const emitLoadX = (d: string, rA: string, rB: string, width: number, signed: boolean) => {
|
|
532
|
+
emit('load', d, [addrX(rA, rB)], { off: 0, width, signed });
|
|
533
|
+
};
|
|
534
|
+
const emitStoreX = (srcReg: string, rA: string, rB: string, width: number) => {
|
|
535
|
+
ops.push(mkOp('store', { operands: [addrX(rA, rB), read(srcReg)], attrs: { off: 0, width } }));
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
// cr-field compare state, so a following branch fuses. Keyed by cr name ("cr0" default).
|
|
539
|
+
const cmpDef = new Map<string, { lhs: Value; rhs: Value; signed: boolean }>();
|
|
540
|
+
// One operand-grammar normalizer for the four compare decodes: `cmpX rA,…` (cr0 implicit)
|
|
541
|
+
// or `cmpX crN,rA,…` → the cr field, the lhs register token, and the rhs token (register or
|
|
542
|
+
// immediate — the case reads/parses it).
|
|
543
|
+
const parseCmpOps = (cmpOps: string[]): { cr: string; lhsTok: string; rhsTok: string } => {
|
|
544
|
+
const hasCr = cmpOps[0]?.startsWith('cr') ?? false;
|
|
545
|
+
const args = hasCr ? cmpOps.slice(1) : cmpOps;
|
|
546
|
+
return { cr: hasCr ? cmpOps[0] : 'cr0', lhsTok: args[0], rhsTok: args[args.length - 1] };
|
|
547
|
+
};
|
|
548
|
+
const recordCmp = (cr: string, lhs: Value, rhs: Value, signed: boolean) => {
|
|
549
|
+
cmpDef.set(cr, { lhs, rhs, signed });
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
const decode = (ins: Instr) => {
|
|
553
|
+
const [d, s, t] = ins.ops;
|
|
554
|
+
// Record form: a trailing `.` (the Rc bit) means the op ALSO sets cr0 from a signed compare
|
|
555
|
+
// of its result against 0 (e.g. `andi.`, `addic.`). Decode the base op, then record that
|
|
556
|
+
// implicit cr0 compare so a following `beq`/`bne` fuses. `mnem` is the base mnemonic the
|
|
557
|
+
// switch dispatches on.
|
|
558
|
+
const rc = ins.mnemonic.length > 1 && ins.mnemonic.endsWith('.');
|
|
559
|
+
const mnem = rc ? ins.mnemonic.slice(0, -1) : ins.mnemonic;
|
|
560
|
+
lastDef = null;
|
|
561
|
+
switch (mnem) {
|
|
562
|
+
case 'nop':
|
|
563
|
+
break;
|
|
564
|
+
// --- call + frame/link-register bookkeeping ---
|
|
565
|
+
// `bl <sym>`: read the argument registers (r3..), produce the return value in r3. The
|
|
566
|
+
// callee symbol comes from the relocation (ins.sym); caller-saved clobbering is implicit
|
|
567
|
+
// (anything live across the call has already been moved to a callee-saved register).
|
|
568
|
+
case 'bl': {
|
|
569
|
+
const sym = ins.sym ?? 'func';
|
|
570
|
+
const argc = protoArity(prototypes[sym]) ?? fallbackArgc(bi);
|
|
571
|
+
const args: Value[] = [];
|
|
572
|
+
for (let k = 0; k < argc; k++) {
|
|
573
|
+
args.push(read(ARG_REGS[k]));
|
|
574
|
+
}
|
|
575
|
+
emit('call', RET, args, { target: sym });
|
|
576
|
+
break;
|
|
577
|
+
}
|
|
578
|
+
// Stack-frame + link-register bookkeeping. `stwu r1,-N(r1)` / `addi r1,r1,N` adjust the frame
|
|
579
|
+
// pointer; `mflr`/`mtlr` save/restore the return address. Transparent. Register saves/restores
|
|
580
|
+
// (individual r1 spills, and `stmw`/`lmw` of a callee-saved range) are transparent ONLY when
|
|
581
|
+
// they move an unchanged entry value — `frameStore`/`frameLoad` enforce that (a spill of a
|
|
582
|
+
// LIVE value fails loud); `stmw`/`lmw` record/consume the slot directly.
|
|
583
|
+
// The GENERAL form `stwu rS,D(rA)` (base ≠ r1) is a real store-with-BASE-UPDATE
|
|
584
|
+
// (`*(rA+D)=rS; rA+=D`) — neither effect is modelled here, so loud-fail rather than drop both.
|
|
585
|
+
case 'stwu':
|
|
586
|
+
if (parseMem(s).base === 'r1') {
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
throw new PpcUnsupportedError(
|
|
590
|
+
`cannot lift '${name}': stwu with update on ${parseMem(s).base} (store-with-base-update) not modelled`,
|
|
591
|
+
);
|
|
592
|
+
case 'mflr':
|
|
593
|
+
case 'mtlr':
|
|
594
|
+
break;
|
|
595
|
+
// `mtctr rS` initialises the CTR loop counter — track it as the `ctr` pseudo-register so
|
|
596
|
+
// the `bdnz` back-branch reads/decrements it. (A recovered switch dispatch's mtctr block is
|
|
597
|
+
// elided; a leftover mtctr is inert if nothing reads `ctr`.) `mfctr` (CTR→GPR, rare) stays
|
|
598
|
+
// opaque via the default case.
|
|
599
|
+
case 'mtctr':
|
|
600
|
+
writeVar('ctr', bi, read(d));
|
|
601
|
+
break;
|
|
602
|
+
case 'stmw':
|
|
603
|
+
if (parseMem(s).base === 'r1') {
|
|
604
|
+
savedSlots.add(parseMem(s).off);
|
|
605
|
+
break;
|
|
606
|
+
}
|
|
607
|
+
assertOrdinaryMem(s);
|
|
608
|
+
emitOpaqueDest(ins);
|
|
609
|
+
break;
|
|
610
|
+
case 'lmw':
|
|
611
|
+
if (parseMem(s).base === 'r1') {
|
|
612
|
+
break;
|
|
613
|
+
}
|
|
614
|
+
assertOrdinaryMem(s);
|
|
615
|
+
emitOpaqueDest(ins);
|
|
616
|
+
break;
|
|
617
|
+
case 'mr':
|
|
618
|
+
write(d, read(s));
|
|
619
|
+
break; // move register (or rD,rS,rS)
|
|
620
|
+
case 'li':
|
|
621
|
+
write(d, constVal(parseImm(s)));
|
|
622
|
+
break; // load immediate (addi rD,0,imm)
|
|
623
|
+
case 'lis':
|
|
624
|
+
write(d, constVal((parseImm(s) << 16) >> 0));
|
|
625
|
+
break; // load immediate shifted
|
|
626
|
+
case 'add':
|
|
627
|
+
case 'addo':
|
|
628
|
+
emitBin('add', d, read(s), read(t));
|
|
629
|
+
break;
|
|
630
|
+
// `addi r1,r1,N` is frame teardown (skip); any other addi is a real add-immediate.
|
|
631
|
+
case 'addi':
|
|
632
|
+
case 'addic':
|
|
633
|
+
if (d === 'r1') {
|
|
634
|
+
break;
|
|
635
|
+
}
|
|
636
|
+
emitBin('add', d, read(s), constVal(parseImm(t)));
|
|
637
|
+
break;
|
|
638
|
+
case 'subf':
|
|
639
|
+
case 'subfc':
|
|
640
|
+
case 'subfo':
|
|
641
|
+
emitBin('sub', d, read(t), read(s));
|
|
642
|
+
break; // rD = rB - rA (reversed)
|
|
643
|
+
case 'subfic':
|
|
644
|
+
emitBin('sub', d, constVal(parseImm(t)), read(s));
|
|
645
|
+
break; // rD = imm - rA
|
|
646
|
+
case 'neg':
|
|
647
|
+
emitUn('neg', d, read(s));
|
|
648
|
+
break;
|
|
649
|
+
case 'mullw':
|
|
650
|
+
case 'mullwo':
|
|
651
|
+
emitBin('mul', d, read(s), read(t));
|
|
652
|
+
break;
|
|
653
|
+
case 'mulli':
|
|
654
|
+
emitBin('mul', d, read(s), constVal(parseImm(t)));
|
|
655
|
+
break;
|
|
656
|
+
// High word of the 32x32->64 product — the magic-number division idiom: mwcc lowers `x/C`
|
|
657
|
+
// to `mulhw(x,M)` plus shifts/corrections. Transient decode; raise/magicdiv.ts rewrites the
|
|
658
|
+
// DAG to `sdiv/udiv(x, const C)`. A `mulh`/`mulhu` that escapes recovery has no C spelling
|
|
659
|
+
// → loud-fail. Record forms `mulhw.`/`mulhwu.` arrive as the `.`-stripped `mnem`.
|
|
660
|
+
case 'mulhw':
|
|
661
|
+
emitBin('mulh', d, read(s), read(t));
|
|
662
|
+
break;
|
|
663
|
+
case 'mulhwu':
|
|
664
|
+
emitBin('mulhu', d, read(s), read(t));
|
|
665
|
+
break;
|
|
666
|
+
// Hardware divide: `divw rD,rA,rB` = rA/rB (signed), `divwu` = unsigned. Unlike MIPS there
|
|
667
|
+
// is NO hi/lo pair — the quotient lands directly in rD. The `o` (overflow-enable) suffix is
|
|
668
|
+
// a flag bit, semantically identical for the quotient. Classic PPC has no hardware remainder
|
|
669
|
+
// op (a `%` is `divw` + `mullw` + `subf`), so no smod/umod here.
|
|
670
|
+
case 'divw':
|
|
671
|
+
case 'divwo':
|
|
672
|
+
emitBin('sdiv', d, read(s), read(t));
|
|
673
|
+
break;
|
|
674
|
+
case 'divwu':
|
|
675
|
+
case 'divwuo':
|
|
676
|
+
emitBin('udiv', d, read(s), read(t));
|
|
677
|
+
break;
|
|
678
|
+
case 'and':
|
|
679
|
+
emitBin('and', d, read(s), read(t));
|
|
680
|
+
break;
|
|
681
|
+
case 'andi':
|
|
682
|
+
case 'andic':
|
|
683
|
+
emitBin('and', d, read(s), constVal(parseImm(t)));
|
|
684
|
+
break;
|
|
685
|
+
case 'or':
|
|
686
|
+
emitBin('or', d, read(s), read(t));
|
|
687
|
+
break;
|
|
688
|
+
case 'ori':
|
|
689
|
+
emitBin('or', d, read(s), constVal(parseImm(t)));
|
|
690
|
+
break;
|
|
691
|
+
case 'xor':
|
|
692
|
+
emitBin('xor', d, read(s), read(t));
|
|
693
|
+
break;
|
|
694
|
+
case 'xori':
|
|
695
|
+
emitBin('xor', d, read(s), constVal(parseImm(t)));
|
|
696
|
+
break;
|
|
697
|
+
case 'not':
|
|
698
|
+
emitUn('not', d, read(s));
|
|
699
|
+
break; // nor rD,rS,rS
|
|
700
|
+
case 'nor':
|
|
701
|
+
s === t ? emitUn('not', d, read(s)) : emitOpaqueDest(ins);
|
|
702
|
+
break; // true 2-reg nor: unmodelled
|
|
703
|
+
// Complemented-logic forms, decoded to the idiomatic C the compiler re-emits:
|
|
704
|
+
// andc rD,rA,rB = rA & ~rB orc rD,rA,rB = rA | ~rB
|
|
705
|
+
// eqv rD,rA,rB = ~(rA ^ rB) nand rD,rA,rB = ~(rA & rB)
|
|
706
|
+
// Appear in branchless clamps/masks (e.g. `x & ~(x>>31)` uses andc).
|
|
707
|
+
case 'andc':
|
|
708
|
+
emitBin('and', d, read(s), notOf(t));
|
|
709
|
+
break;
|
|
710
|
+
case 'orc':
|
|
711
|
+
emitBin('or', d, read(s), notOf(t));
|
|
712
|
+
break;
|
|
713
|
+
case 'eqv':
|
|
714
|
+
emitUn('not', d, binTmp('xor', read(s), read(t)));
|
|
715
|
+
break;
|
|
716
|
+
case 'nand':
|
|
717
|
+
emitUn('not', d, binTmp('and', read(s), read(t)));
|
|
718
|
+
break;
|
|
719
|
+
// Sign-extend byte/halfword to 32 bits: `extsb rD,rS` = (s32)(s8)rS, `extsh` = (s32)(s16)rS.
|
|
720
|
+
// `sext` carries the NARROW width; structure/backend spell it `(s8)e` / `(s16)e`.
|
|
721
|
+
case 'extsb':
|
|
722
|
+
emit('sext', d, [read(s)], { width: 8 });
|
|
723
|
+
break;
|
|
724
|
+
case 'extsh':
|
|
725
|
+
emit('sext', d, [read(s)], { width: 16 });
|
|
726
|
+
break;
|
|
727
|
+
case 'slw':
|
|
728
|
+
emitBin('shl', d, read(s), read(t));
|
|
729
|
+
break;
|
|
730
|
+
case 'srw':
|
|
731
|
+
emitBin('shr_u', d, read(s), read(t));
|
|
732
|
+
break;
|
|
733
|
+
case 'sraw':
|
|
734
|
+
emitBin('shr_s', d, read(s), read(t));
|
|
735
|
+
break;
|
|
736
|
+
case 'slwi':
|
|
737
|
+
emitShImm('shl', d, read(s), parseImm(t));
|
|
738
|
+
break;
|
|
739
|
+
case 'srwi':
|
|
740
|
+
emitShImm('shr_u', d, read(s), parseImm(t));
|
|
741
|
+
break;
|
|
742
|
+
case 'srawi':
|
|
743
|
+
emitShImm('shr_s', d, read(s), parseImm(t));
|
|
744
|
+
break;
|
|
745
|
+
case 'rotlw':
|
|
746
|
+
// rotate left by register — the C rotate idiom round-trips under mwcc (verified)
|
|
747
|
+
emitBin('rotl', d, read(s), read(t));
|
|
748
|
+
break;
|
|
749
|
+
case 'rotlwi':
|
|
750
|
+
emitShImm('rotl', d, read(s), parseImm(t));
|
|
751
|
+
break;
|
|
752
|
+
case 'cntlzw': {
|
|
753
|
+
// count leading zeros — transient (see ir/opcodes.ts): the CNTLZW_EQ0 pattern folds
|
|
754
|
+
// the ==0/`!` idiom; a bare survivor gaps loud at the structurer.
|
|
755
|
+
emit('clz', d, [read(s)]);
|
|
756
|
+
break;
|
|
757
|
+
}
|
|
758
|
+
// rotate-and-mask, rotate 0 only: `clrlwi rD,rS,n` = rS & (~0>>>n); `clrrwi` = rS & (~0<<n).
|
|
759
|
+
case 'clrlwi':
|
|
760
|
+
emitBin('and', d, read(s), constVal((0xffffffff >>> parseImm(t)) >>> 0));
|
|
761
|
+
break;
|
|
762
|
+
case 'clrrwi':
|
|
763
|
+
emitBin('and', d, read(s), constVal((0xffffffff << parseImm(t)) >>> 0));
|
|
764
|
+
break;
|
|
765
|
+
case 'rlwinm': {
|
|
766
|
+
const [sh, mb, me] = [parseImm(ins.ops[2]), parseImm(ins.ops[3]), parseImm(ins.ops[4])];
|
|
767
|
+
// `rlwinm rD,rS,SH,MB,ME` = rotl(rS,SH) & mask(MB,ME). Three cases we can lower exactly:
|
|
768
|
+
// • SH==0 — a pure masked AND.
|
|
769
|
+
// • right-shift EXTRACT `(x>>n)&m` — ME==31 and the field does not wrap (SH+MB>=32): the
|
|
770
|
+
// compiler's form for `(x>>n)&mask`, with n=32-SH and mask=mask(MB,31). Emit the shift
|
|
771
|
+
// then the AND — the faithful, idiomatic C that recompiles to this exact rlwinm.
|
|
772
|
+
// • anything else (a genuine rotate / bitfield insert) stays an opaque — loud, not dropped.
|
|
773
|
+
if (sh === 0) {
|
|
774
|
+
emitBin('and', d, read(s), constVal(rlwinmMask(mb, me)));
|
|
775
|
+
break;
|
|
776
|
+
}
|
|
777
|
+
if (me === 31 && sh + mb >= 32) {
|
|
778
|
+
const shifted = mkValue(T.unk(32));
|
|
779
|
+
ops.push(mkOp('shr_u', { operands: [read(s)], results: [shifted], attrs: { imm: (32 - sh) & 31 } }));
|
|
780
|
+
emitBin('and', d, shifted, constVal(rlwinmMask(mb, 31)));
|
|
781
|
+
break;
|
|
782
|
+
}
|
|
783
|
+
emitOpaqueDest(ins);
|
|
784
|
+
break;
|
|
785
|
+
}
|
|
786
|
+
// Word load/store: a transparent frame save/restore is skipped; a live-value spill or a
|
|
787
|
+
// stack local fails loud (frameStore/frameLoad); otherwise it is ordinary memory.
|
|
788
|
+
case 'lwz':
|
|
789
|
+
if (frameLoad(s)) {
|
|
790
|
+
break;
|
|
791
|
+
}
|
|
792
|
+
emitLoad(d, s, 4, true);
|
|
793
|
+
break;
|
|
794
|
+
case 'lha':
|
|
795
|
+
emitLoad(d, s, 2, true);
|
|
796
|
+
break;
|
|
797
|
+
case 'lhz':
|
|
798
|
+
emitLoad(d, s, 2, false);
|
|
799
|
+
break;
|
|
800
|
+
case 'lbz':
|
|
801
|
+
emitLoad(d, s, 1, false);
|
|
802
|
+
break;
|
|
803
|
+
case 'stw':
|
|
804
|
+
if (frameStore(d, s)) {
|
|
805
|
+
break;
|
|
806
|
+
}
|
|
807
|
+
emitStore(d, s, 4);
|
|
808
|
+
break;
|
|
809
|
+
case 'sth':
|
|
810
|
+
emitStore(d, s, 2);
|
|
811
|
+
break;
|
|
812
|
+
case 'stb':
|
|
813
|
+
emitStore(d, s, 1);
|
|
814
|
+
break;
|
|
815
|
+
// Register+register indexed forms (variable-index array access). Widths/signedness mirror the
|
|
816
|
+
// displacement loads/stores above; `lhax` is the sign-extending halfword (algebraic).
|
|
817
|
+
case 'lwzx':
|
|
818
|
+
emitLoadX(d, s, t, 4, true);
|
|
819
|
+
break;
|
|
820
|
+
case 'lhax':
|
|
821
|
+
emitLoadX(d, s, t, 2, true);
|
|
822
|
+
break;
|
|
823
|
+
case 'lhzx':
|
|
824
|
+
emitLoadX(d, s, t, 2, false);
|
|
825
|
+
break;
|
|
826
|
+
case 'lbzx':
|
|
827
|
+
emitLoadX(d, s, t, 1, false);
|
|
828
|
+
break;
|
|
829
|
+
case 'stwx':
|
|
830
|
+
emitStoreX(d, s, t, 4);
|
|
831
|
+
break;
|
|
832
|
+
case 'sthx':
|
|
833
|
+
emitStoreX(d, s, t, 2);
|
|
834
|
+
break;
|
|
835
|
+
case 'stbx':
|
|
836
|
+
emitStoreX(d, s, t, 1);
|
|
837
|
+
break;
|
|
838
|
+
// rhs is evaluated BEFORE lhs in each case: reads create Braun-SSA phis on demand, so read
|
|
839
|
+
// order affects block-param layout.
|
|
840
|
+
case 'cmpw': {
|
|
841
|
+
const c = parseCmpOps(ins.ops);
|
|
842
|
+
const rhs = read(c.rhsTok);
|
|
843
|
+
recordCmp(c.cr, read(c.lhsTok), rhs, true);
|
|
844
|
+
break;
|
|
845
|
+
}
|
|
846
|
+
case 'cmpwi': {
|
|
847
|
+
const c = parseCmpOps(ins.ops);
|
|
848
|
+
const rhs = constVal(parseImm(c.rhsTok));
|
|
849
|
+
recordCmp(c.cr, read(c.lhsTok), rhs, true);
|
|
850
|
+
break;
|
|
851
|
+
}
|
|
852
|
+
case 'cmplw': {
|
|
853
|
+
const c = parseCmpOps(ins.ops);
|
|
854
|
+
const rhs = read(c.rhsTok);
|
|
855
|
+
recordCmp(c.cr, read(c.lhsTok), rhs, false);
|
|
856
|
+
break;
|
|
857
|
+
}
|
|
858
|
+
case 'cmplwi': {
|
|
859
|
+
const c = parseCmpOps(ins.ops);
|
|
860
|
+
const rhs = constVal(parseImm(c.rhsTok));
|
|
861
|
+
recordCmp(c.cr, read(c.lhsTok), rhs, false);
|
|
862
|
+
break;
|
|
863
|
+
}
|
|
864
|
+
default:
|
|
865
|
+
emitOpaqueDest(ins);
|
|
866
|
+
break; // unmodelled: an honest opaque, never a silent drop
|
|
867
|
+
}
|
|
868
|
+
// A record-form op sets cr0 from a signed compare of its result against 0. Wire that so the
|
|
869
|
+
// next branch reading cr0 fuses (`andi. r0,r3,1; beq …` → `if ((a0 & 1) == 0)`).
|
|
870
|
+
if (rc && lastDef) {
|
|
871
|
+
cmpDef.set('cr0', { lhs: lastDef, rhs: constVal(0), signed: true });
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
for (const ins of b.body) {
|
|
876
|
+
decode(ins);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
const br = b.branch;
|
|
880
|
+
// Recovered dense switch: the bounds block dispatches a `switch_br` over the scrutinee — N case
|
|
881
|
+
// blocks (dense 0..N-1) then the default (last successor). The `cmplwi`/`bgt` and the elided
|
|
882
|
+
// dispatch (`lis…lwzx;mtctr;bctr`) are subsumed.
|
|
883
|
+
const jt = br ? jts.get(br.addr) : undefined;
|
|
884
|
+
if (jt) {
|
|
885
|
+
pushSwitchBr(
|
|
886
|
+
ops,
|
|
887
|
+
readVar(jt.scrutReg, bi),
|
|
888
|
+
succIdx[bi].map((j) => succ(j)),
|
|
889
|
+
);
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
// `bdnz L`: CTR ← CTR−1; branch to L while CTR ≠ 0. Modelled as an explicit decrement of the
|
|
893
|
+
// `ctr` pseudo-register plus a `cond_br` on `ctr ≠ 0`, which the structurer renders as a loop.
|
|
894
|
+
// succIdx is [taken=back-edge, fall=exit], so the taken predicate is `ctr ≠ 0`. Loud-fail
|
|
895
|
+
// without a reaching `mtctr`: no recoverable trip count means no sound loop.
|
|
896
|
+
if (br && br.mnemonic === 'bdnz') {
|
|
897
|
+
if (!ssa.hasReachingDef('ctr', bi)) {
|
|
898
|
+
throw new PpcUnsupportedError(
|
|
899
|
+
`cannot lift '${name}': 'bdnz' at 0x${br.addr.toString(16)} without a reaching 'mtctr' ` +
|
|
900
|
+
`(CTR loop count not recoverable)`,
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
// CTR is VOLATILE across calls (the ABI marks it caller-saved): a `bl` or re-seeding `mtctr`
|
|
904
|
+
// inside the loop body clobbers the hardware CTR, making the modelled down-count wrong —
|
|
905
|
+
// loud-fail rather than emit a confident-but-wrong count (hand-written asm can do this even
|
|
906
|
+
// though a conforming compiler won't). The loop body is the natural loop of the back-edge:
|
|
907
|
+
// header (bdnz's target) plus every block reaching the `bdnz` latch without passing back
|
|
908
|
+
// through the header. The preheader holding the count `mtctr` is NOT in the body — exempt.
|
|
909
|
+
const header = succIdx[bi][0];
|
|
910
|
+
const body = new Set<number>([header]);
|
|
911
|
+
for (const stack = [bi]; stack.length;) {
|
|
912
|
+
const n = stack.pop()!;
|
|
913
|
+
if (body.has(n) && n !== bi) {
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
body.add(n);
|
|
917
|
+
if (n !== header) {
|
|
918
|
+
for (const p of preds[n]) {
|
|
919
|
+
stack.push(p);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
for (const n of body) {
|
|
924
|
+
const bad = blocks[n].body.find((i) => i.mnemonic === 'bl' || i.mnemonic === 'bctrl' || i.mnemonic === 'mtctr');
|
|
925
|
+
if (bad) {
|
|
926
|
+
throw new PpcUnsupportedError(
|
|
927
|
+
`cannot lift '${name}': CTR loop body contains '${bad.mnemonic}' at 0x${bad.addr.toString(16)} ` +
|
|
928
|
+
`which clobbers CTR (loop trip count not recoverable)`,
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
const dec = mkValue(T.unk(32));
|
|
933
|
+
ops.push(mkOp('sub', { operands: [readVar('ctr', bi), constVal(1)], results: [dec] }));
|
|
934
|
+
writeVar('ctr', bi, dec);
|
|
935
|
+
const cond = mkCmp(ops, 'icmp_ne', dec, constVal(0));
|
|
936
|
+
ops.push(mkOp('cond_br', { operands: [cond], successors: succIdx[bi].map((j) => succ(j)) }));
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
if (br && (isCond(br) || isCondReturn(br))) {
|
|
940
|
+
const base = isCondReturn(br) ? condReturnBase(br)! : br.mnemonic;
|
|
941
|
+
// The branch names its cr field as the first operand when not cr0.
|
|
942
|
+
const crName = br.ops[0]?.startsWith('cr') ? br.ops[0] : 'cr0';
|
|
943
|
+
const cmp = cmpDef.get(crName);
|
|
944
|
+
// `cmpDef` is block-local; a compare split from its branch by a block boundary is a
|
|
945
|
+
// cross-block cr dependency this frontend does not model. Decline loud.
|
|
946
|
+
if (!cmp) {
|
|
947
|
+
throw new PpcUnsupportedError(
|
|
948
|
+
`cannot lift '${name}': conditional branch '${base}' has no reaching compare (${crName}) in its block`,
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
const table = !cmp.signed ? COND_UNSIGNED : COND_SIGNED;
|
|
952
|
+
const cond = mkCmp(ops, table[base], cmp.lhs, cmp.rhs);
|
|
953
|
+
ops.push(mkOp('cond_br', { operands: [cond], successors: succIdx[bi].map((j) => succ(j)) }));
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
if (!br || isReturn(br)) {
|
|
957
|
+
if (!br && succIdx[bi].length) {
|
|
958
|
+
ops.push(mkOp('br', { successors: [succ(succIdx[bi][0])] }));
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
const retOps = ssa.hasReachingDef(RET, bi) ? [readVar(RET, bi)] : [];
|
|
962
|
+
ops.push(mkOp('ret', { operands: retOps }));
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
// unconditional branch
|
|
966
|
+
ops.push(mkOp('br', { successors: [succ(succIdx[bi][0])] }));
|
|
967
|
+
};
|
|
968
|
+
|
|
969
|
+
blocks.forEach((b, bi) => {
|
|
970
|
+
fillBlock(b, bi);
|
|
971
|
+
ssa.markFilled(bi);
|
|
972
|
+
});
|
|
973
|
+
ssa.finish();
|
|
974
|
+
|
|
975
|
+
// ABI-ordered entry parameters (r3, r4, …) — a callee-saved copy can read a later argument
|
|
976
|
+
// register first, so sort the true entry's params by argument-register index.
|
|
977
|
+
const entry = irBlocks[0];
|
|
978
|
+
// non-ABI live-in ranks FIRST (indexOf's -1) — deliberate MIPS/PPC tie-break; Thumb's is 99/last
|
|
979
|
+
abiSortEntryParams(entry, preds[0].length > 0, (v) => ARG_REGS.indexOf(paramReg.get(v) ?? ''));
|
|
980
|
+
return ssa.fn;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function mkCmp(ops: Op[], opc: Opcode, l: Value, r: Value): Value {
|
|
984
|
+
const v = mkValue(T.unk(32));
|
|
985
|
+
ops.push(mkOp(opc, { operands: [l, r], results: [v] }));
|
|
986
|
+
return v;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/** The PowerPC / CodeWarrior frontend, registered for the `ppc` target. */
|
|
990
|
+
export const ppcFrontend: Frontend = { id: 'ppc', inputFormat: 'objdump', lift };
|