@asmlift/core 0.7.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -24
- package/package.json +1 -1
- package/src/backend/pascal.ts +2 -2
- package/src/codegen-flags.ts +640 -0
- package/src/frontend/disasm.ts +141 -11
- package/src/frontend/high-half.ts +149 -0
- package/src/frontend/mips.ts +458 -209
- package/src/frontend/ppc.ts +332 -67
- package/src/frontend/reloc-symbol.ts +109 -0
- package/src/frontend/splat.ts +56 -18
- package/src/frontend/ssa.ts +126 -29
- package/src/frontend/stackargs.ts +420 -0
- package/src/frontend/thumb.ts +207 -230
- package/src/ir/core.ts +62 -3
- package/src/ir/opcodes.ts +9 -0
- package/src/ir/parse.ts +7 -1
- package/src/l3/advance.ts +2 -2
- package/src/l3/argbase.ts +2 -2
- package/src/l3/argcopy.ts +269 -0
- package/src/l3/ast.ts +45 -1
- package/src/l3/basecse.ts +2 -2
- package/src/l3/coalesce.ts +109 -52
- package/src/l3/scopebase.ts +4 -4
- package/src/l3/tailret.ts +70 -0
- package/src/l3/unmerge.ts +2 -2
- package/src/l3/unreduce.ts +2 -1
- package/src/mangle.ts +49 -0
- package/src/pattern/engine.ts +128 -13
- package/src/pipeline.ts +22 -11
- package/src/raise/extscale.ts +5 -2
- package/src/raise/paramwidth.ts +111 -3
- package/src/raise/pre-recovery.ts +11 -1
- package/src/raise/retsink.ts +8 -4
- package/src/raise/tailsink.ts +17 -2
- package/src/rank-declare.ts +17 -9
- package/src/rank.ts +54 -20
- package/src/structure/retspell.ts +95 -0
- package/src/structure/structure.ts +12 -3
- package/src/structure/switch-recover.ts +1 -1
- package/src/target.ts +224 -14
- package/src/trace.ts +27 -18
- package/src/variation-definitions.ts +52 -2
- package/src/variation-gates.ts +3 -0
- package/src/variation-tokens.ts +1 -0
package/src/frontend/mips.ts
CHANGED
|
@@ -24,11 +24,19 @@ import { T } from '../ir/types';
|
|
|
24
24
|
import type { Prototypes } from '../proto';
|
|
25
25
|
import type { TargetDescription } from '../target';
|
|
26
26
|
import { type AsmData, readJumpTable, textRelocAt } from './asmdata';
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
type DisasmInstr,
|
|
29
|
+
parseImm,
|
|
30
|
+
parseMem,
|
|
31
|
+
parseDisasm as parseSharedDisasm,
|
|
32
|
+
sliceSymbol,
|
|
33
|
+
symbolStart,
|
|
34
|
+
} from './disasm';
|
|
28
35
|
import { mkEmitKit, pushSwitchBr } from './emit';
|
|
29
36
|
import { FrontendUnsupportedError } from './errors';
|
|
30
37
|
import { assertInputFormat } from './format';
|
|
31
38
|
import type { Frontend } from './frontend';
|
|
39
|
+
import { makeHighHalves } from './high-half';
|
|
32
40
|
import { opaqueDest } from './opaque';
|
|
33
41
|
import { isSplatMips, parseSplatMips } from './splat';
|
|
34
42
|
import { abiSortEntryParams, stackSlotKey } from './ssa';
|
|
@@ -47,6 +55,23 @@ const COND_Z: Record<string, Opcode> = {
|
|
|
47
55
|
bgez: 'icmp_sge',
|
|
48
56
|
};
|
|
49
57
|
const COND_RR: Record<string, Opcode> = { beq: 'icmp_eq', bne: 'icmp_ne' };
|
|
58
|
+
// BRANCH-LIKELY → the ordinary branch it tests the same way. The difference is the delay slot:
|
|
59
|
+
// a likely branch NULLIFIES its slot when the branch is not taken, so the slot is conditional code
|
|
60
|
+
// (see `toBlocks`, which gives it its own block on the taken edge). `beqzl`/`bnezl` are objdump's
|
|
61
|
+
// printing of `beql`/`bnel` against `$0`; both spellings appear in the corpus.
|
|
62
|
+
const LIKELY_BASE: Record<string, string> = {
|
|
63
|
+
beql: 'beq',
|
|
64
|
+
bnel: 'bne',
|
|
65
|
+
beqzl: 'beqz',
|
|
66
|
+
bnezl: 'bnez',
|
|
67
|
+
blezl: 'blez',
|
|
68
|
+
bgtzl: 'bgtz',
|
|
69
|
+
bltzl: 'bltz',
|
|
70
|
+
bgezl: 'bgez',
|
|
71
|
+
};
|
|
72
|
+
// A coprocessor-1 branch tests an FP condition code (`fcc`) that `c.cond.s/d` sets. Neither the
|
|
73
|
+
// code nor the compare is modelled, so these refuse whether or not they are also likely.
|
|
74
|
+
const isFpCondBranch = (m: string) => m.startsWith('bc1');
|
|
50
75
|
|
|
51
76
|
const isZero = (r: string) => r === 'zero' || r === '$0';
|
|
52
77
|
// The stack pointer (`$29`). A `sw/lw` through it is not a store/load through a data pointer — it
|
|
@@ -69,110 +94,105 @@ const isReturn = (ins: Instr) => ins.mnemonic === 'jr';
|
|
|
69
94
|
const isUncond = (ins: Instr) => ins.mnemonic === 'b' || ins.mnemonic === 'j';
|
|
70
95
|
const isCond = (ins: Instr) => ins.mnemonic in COND_Z || ins.mnemonic in COND_RR;
|
|
71
96
|
const isXfer = (ins: Instr) => isReturn(ins) || isUncond(ins) || isCond(ins);
|
|
97
|
+
// ANY instruction that redirects control, modelled or not: on MIPS that is the whole `b*` space
|
|
98
|
+
// (minus the `break` trap) plus `j*`. `isXfer` is the subset this frontend models; this is the
|
|
99
|
+
// superset the refusals and the delay-slot guards have to reason about.
|
|
100
|
+
const isControlTransfer = (ins: Instr) =>
|
|
101
|
+
(ins.mnemonic[0] === 'b' && ins.mnemonic !== 'break') || ins.mnemonic[0] === 'j';
|
|
72
102
|
|
|
73
103
|
// Shared objdump scaffolding (frontend/disasm.ts): parseImm/parseMem/parseDisasm. MIPS needs no
|
|
74
104
|
// reloc or hint-suffix handling; register-scaled indices are materialised by IDO as explicit
|
|
75
105
|
// `sll`+`addu` before the access, so no `base+index` addressing form appears in parseMem input.
|
|
76
|
-
|
|
106
|
+
// A zero word IS an instruction here — `sll zero,zero,0`, which objdump prints as `nop` — so the
|
|
107
|
+
// runs of them objdump elides as `...` (GCC's `mflo` hazard pads) come back as the nops they are.
|
|
108
|
+
const parseDisasm = (disasm: string): Instr[] => parseSharedDisasm(disasm, { zeroWord: 'nop' });
|
|
77
109
|
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
|
|
110
|
+
// Bridge an object file's `.text` relocation records onto the instructions they fill, so the
|
|
111
|
+
// high/low fold reads ONE carrier (`ins.reloc`) whichever dialect the input arrived in — the same
|
|
112
|
+
// carrier frontend/ppc.ts reads, folded by the same shared invariant (frontend/high-half.ts). The
|
|
113
|
+
// Splat dialect spells `%hi`/`%lo` in the operand text and writes its own records
|
|
114
|
+
// (frontend/splat.ts); objdump hides the symbol in the relocation table, which is what this reads.
|
|
115
|
+
//
|
|
116
|
+
// EVERY HI16/LO16 IS CARRIED, WHATEVER IT NAMES, and that includes a SECTION symbol (`.data`,
|
|
117
|
+
// `.rodata` — a file-static array, a string literal, any anonymous datum). A record left off the
|
|
118
|
+
// carrier leaves its pair raw, and raw is the `lui`'s link-time placeholder — the literal 0 in a
|
|
119
|
+
// relocatable object — standing in for the address, which `*(u8 *)(0 + i)` renders and the compiler
|
|
120
|
+
// accepts. Only a carrier that holds every record can put the fold's refusals in front of them.
|
|
121
|
+
//
|
|
122
|
+
// WHETHER THE NAME CAN BE WRITTEN DOWN is a different question, and this frontend does NOT adopt
|
|
123
|
+
// frontend/ppc.ts's naming-policy refusal for it. A section name is never a C identifier, so a
|
|
124
|
+
// recovered `.data` access is already LOUD twice over downstream: rank-declare.ts refuses the
|
|
125
|
+
// declaration and REPORTS the name to the caller, and the candidate's own source does not compile
|
|
126
|
+
// — strictly more than a frontend refusal would say, and what the mapless-decls tests are built
|
|
127
|
+
// around. The rest of the policy has nothing to refuse here, measured rather than assumed: of the
|
|
128
|
+
// 20,262 distinct symbols a `.text` HI16/LO16 names across the three N64 checkouts,
|
|
129
|
+
// `classifyRelocSymbol` answers `plain` for 20,245, `section-local` for 4 and `cpp-mangled` for 13
|
|
130
|
+
// — and all 13 are ORDINARY C NAMES (`game_GameFrame__1F`, `ovl__0078CB80_VRAM`) that the policy's
|
|
131
|
+
// `__<digit>` class-scope marker misreads. Adopting it would refuse 13 names C spells perfectly
|
|
132
|
+
// well and buy nothing: the kind it exists for, a mwcc vtable that would otherwise COMPILE, has no
|
|
133
|
+
// inhabitant on this ISA.
|
|
134
|
+
//
|
|
135
|
+
// A recovered JUMP TABLE's dispatch is untouched: Regime B reads the table through `asmdata.ts` and
|
|
136
|
+
// prunes the dispatch block, so its `lui %hi(.rodata)` never reaches `decode`.
|
|
137
|
+
//
|
|
138
|
+
// `R_MIPS_GOT16`/`R_MIPS_CALL16`/`R_MIPS_GPREL16` are PIC/small-data access, already refused by the
|
|
139
|
+
// `gp`-as-data guard; widening this carrier to them would trade those messages for worse ones
|
|
140
|
+
// without recovering an address.
|
|
141
|
+
//
|
|
142
|
+
// THE ADDEND IS NOT ON THE RECORD. MIPS objects are REL: the relocation has no addend field and the
|
|
143
|
+
// address's low bits live in the two instruction immediates, so a record that DOES carry an addend
|
|
144
|
+
// is not the format assumed here, and refuses.
|
|
145
|
+
function attachMipsRelocs(name: string, instrs: Instr[], ad: AsmData): void {
|
|
105
146
|
const byAddr = new Map(instrs.map((ins) => [ins.addr, ins]));
|
|
106
|
-
const his: { addr: number; sym: string }[] = [];
|
|
107
|
-
const los = new Map<number, string>(); // LO16 instruction addr → symbol
|
|
108
147
|
for (const r of ad.relocs) {
|
|
109
|
-
if (r.section !== '.text' || r.
|
|
110
|
-
continue; // section-symbol relocs are jump tables / anonymous data — not named globals
|
|
111
|
-
}
|
|
112
|
-
if (r.type === 'R_MIPS_HI16') {
|
|
113
|
-
his.push({ addr: r.offset, sym: r.sym });
|
|
114
|
-
} else if (r.type === 'R_MIPS_LO16') {
|
|
115
|
-
los.set(r.offset, r.sym);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
if (his.length === 0) {
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
his.sort((a, b) => a.addr - b.addr);
|
|
122
|
-
const loAddrs = [...los.keys()].sort((a, b) => a - b);
|
|
123
|
-
const consumed = new Set<number>();
|
|
124
|
-
for (const hi of his) {
|
|
125
|
-
const lui = byAddr.get(hi.addr);
|
|
126
|
-
if (!lui || lui.mnemonic !== 'lui') {
|
|
148
|
+
if (r.section !== '.text' || (r.type !== 'R_MIPS_HI16' && r.type !== 'R_MIPS_LO16')) {
|
|
127
149
|
continue;
|
|
128
150
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
151
|
+
const ins = byAddr.get(r.offset);
|
|
152
|
+
if (!ins) {
|
|
153
|
+
// A `.text` record describes the whole section, so most belong to the object's OTHER
|
|
154
|
+
// functions and are not this slice's business. One that lands INSIDE the slice and still
|
|
155
|
+
// matches no instruction means the relocation table and this disassembly disagree about
|
|
156
|
+
// addressing, and the half it describes would stay raw — so refuse rather than skip it.
|
|
157
|
+
if (r.offset >= instrs[0].addr && r.offset <= instrs[instrs.length - 1].addr) {
|
|
158
|
+
throw new FrontendUnsupportedError(
|
|
159
|
+
`cannot lift '${name}': relocation '${r.type} ${r.sym}' at 0x${r.offset.toString(16)} falls inside the ` +
|
|
160
|
+
`function (0x${instrs[0].addr.toString(16)}..0x${instrs[instrs.length - 1].addr.toString(16)}) but on no ` +
|
|
161
|
+
`instruction — the relocation table and this disassembly disagree about addressing`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
134
164
|
continue;
|
|
135
165
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
lui.ops[1] = rw.n === 0 ? `%hi(${hi.sym})` : `%hi(${hi.sym} + 0x${rw.n.toString(16)})`;
|
|
143
|
-
consumed.add(loAddr!);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Rewrite a LO16 consumer's operand to `%lo(SYM[+N])`, returning the addend N, or null when the
|
|
148
|
-
// instruction is not a modelled global consumer (leave it raw). `hiBase` is the HI16 imm << 16.
|
|
149
|
-
function rewriteLoReloc(lo: Instr, sym: string, hiBase: number): { n: number } | null {
|
|
150
|
-
const macro = (n: number) => (n === 0 ? `%lo(${sym})` : `%lo(${sym} + 0x${n.toString(16)})`);
|
|
151
|
-
if (lo.mnemonic === 'addiu' || lo.mnemonic === 'addi') {
|
|
152
|
-
const n = hiBase + parseImm(lo.ops[2] ?? '0');
|
|
153
|
-
if (n < 0) {
|
|
154
|
-
return null; // a negative interior offset — unusual; leave raw
|
|
166
|
+
if (r.addend !== 0) {
|
|
167
|
+
throw new FrontendUnsupportedError(
|
|
168
|
+
`cannot lift '${name}': relocation '${r.type} ${r.sym}' at 0x${r.offset.toString(16)} carries an addend ` +
|
|
169
|
+
`(0x${r.addend.toString(16)}), but MIPS relocations are REL and hold the addend in the instruction ` +
|
|
170
|
+
`fields — this object is not the assumed format`,
|
|
171
|
+
);
|
|
155
172
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
173
|
+
// At most one relocation per instruction — the same invariant disasm.ts and frontend/splat.ts
|
|
174
|
+
// enforce on their own inputs: the carrier is one field, so keeping the last would leave one
|
|
175
|
+
// symbol standing for the other's operand.
|
|
176
|
+
if (ins.reloc) {
|
|
177
|
+
throw new FrontendUnsupportedError(
|
|
178
|
+
`cannot lift '${name}': two relocations on one instruction ('${ins.mnemonic}' at ` +
|
|
179
|
+
`0x${ins.addr.toString(16)}): '${ins.reloc.type} ${ins.reloc.sym}' and '${r.type} ${r.sym}'`,
|
|
180
|
+
);
|
|
164
181
|
}
|
|
165
|
-
|
|
166
|
-
return { n };
|
|
182
|
+
ins.reloc = { type: r.type, sym: r.sym, addend: 0 };
|
|
167
183
|
}
|
|
168
|
-
return null;
|
|
169
184
|
}
|
|
170
185
|
|
|
171
186
|
interface MipsBlock {
|
|
172
187
|
startAddr: number;
|
|
173
188
|
body: Instr[]; // computation instructions (excludes the branch and its delay slot)
|
|
174
189
|
branch: Instr | null; // terminating control transfer, or null for a pure fall-through
|
|
175
|
-
|
|
190
|
+
// The word at `branch + 4`, which this block runs after its branch. NULL for a branch-LIKELY:
|
|
191
|
+
// its slot is NULLIFIED when the branch is not taken, so it does not run in this block at all —
|
|
192
|
+
// it is its own block at `branch + 4`, reached only by the taken edge, which then jumps on to
|
|
193
|
+
// the branch's target.
|
|
194
|
+
delay: Instr | null;
|
|
195
|
+
delayAnnulled: boolean;
|
|
176
196
|
}
|
|
177
197
|
|
|
178
198
|
// Mnemonics whose destination is `ops[0]` (a plain register write) — the subset the jump-table
|
|
@@ -348,29 +368,135 @@ function recoverMipsJumpTables(instrs: Instr[], ad: AsmData): Map<number, MipsJT
|
|
|
348
368
|
return out;
|
|
349
369
|
}
|
|
350
370
|
|
|
371
|
+
// BRANCH-LIKELY → the ordinary branch that tests the same way, in place, returning the addresses
|
|
372
|
+
// rewritten. The mnemonic is all that differs in the COMPARISON; what differs in the FLOW is the
|
|
373
|
+
// delay slot, which `toBlocks` then places on the taken edge alone.
|
|
374
|
+
//
|
|
375
|
+
// Refuses, by name, every shape that placement cannot model. A nullified slot read as an ordinary
|
|
376
|
+
// always-executed one is not a cosmetic error: `absi` would return `-x` for every `x >= 0`, and a
|
|
377
|
+
// store in such a slot would be performed on a path that never performs it. That is C which
|
|
378
|
+
// compiles and is wrong — strictly worse than the decline it would replace.
|
|
379
|
+
function normaliseBranchLikely(name: string, instrs: Instr[], startAddr: number): Set<number> {
|
|
380
|
+
const likely = instrs.filter((ins) => ins.mnemonic in LIKELY_BASE);
|
|
381
|
+
if (likely.length === 0) {
|
|
382
|
+
return new Set();
|
|
383
|
+
}
|
|
384
|
+
const refusal = (ins: Instr, why: string) =>
|
|
385
|
+
new FrontendUnsupportedError(
|
|
386
|
+
`cannot lift '${name}': branch-likely '${ins.mnemonic}' at 0x${ins.addr.toString(16)} — ${why}`,
|
|
387
|
+
);
|
|
388
|
+
// Every address this function branches to, the likely branches' own targets included: one that
|
|
389
|
+
// jumps to its OWN slot is the same hazard as some other branch landing there. A recovered jump
|
|
390
|
+
// table's arms are not in here — they do not exist until `recoverMipsJumpTables` has run, so
|
|
391
|
+
// `lift` checks them against the slot addresses this returns.
|
|
392
|
+
const targets = new Set(instrs.map((ins) => ins.target).filter((t): t is number => t !== undefined));
|
|
393
|
+
// BY ADDRESS, never by array position: reading a branch's array NEIGHBOUR as its delay slot puts
|
|
394
|
+
// an arm the function always runs onto the taken edge of a branch that never guarded it, which is
|
|
395
|
+
// C that compiles and is wrong. The two readings agree only while the list has a word per
|
|
396
|
+
// address, which is what `parseDisasm` guarantees by refusing a listing it cannot account for.
|
|
397
|
+
const at = new Map(instrs.map((ins) => [ins.addr, ins]));
|
|
398
|
+
const hex = (a: number) => `0x${a.toString(16)}`;
|
|
399
|
+
const rewritten = new Set<number>();
|
|
400
|
+
for (const br of likely) {
|
|
401
|
+
const slot = at.get(br.addr + 4);
|
|
402
|
+
const fallThrough = at.get(br.addr + 8);
|
|
403
|
+
const prev = at.get(br.addr - 4);
|
|
404
|
+
if (br.target === undefined) {
|
|
405
|
+
throw refusal(br, 'the branch target is not a resolved address');
|
|
406
|
+
}
|
|
407
|
+
if (slot === undefined) {
|
|
408
|
+
throw refusal(br, `the disassembly has no instruction at ${hex(br.addr + 4)} to be its delay slot`);
|
|
409
|
+
}
|
|
410
|
+
if (fallThrough === undefined) {
|
|
411
|
+
throw refusal(br, `the disassembly has no instruction at ${hex(br.addr + 8)} for the not-taken edge to land on`);
|
|
412
|
+
}
|
|
413
|
+
if (prev === undefined && br.addr !== startAddr) {
|
|
414
|
+
// WHAT PRECEDES IT DECIDES WHETHER IT MAY BE PLACED AT ALL, so an unreadable predecessor is
|
|
415
|
+
// not a detail to shrug at: a likely branch sitting in some transfer's delay slot has no
|
|
416
|
+
// block of its own to put a slot in. The function's own first word is the one address with
|
|
417
|
+
// nothing before it, and that is read off the objdump HEADER — not off the first line
|
|
418
|
+
// parsed, which is the same thing only when the listing spells that word.
|
|
419
|
+
throw refusal(br, `the disassembly has no instruction at ${hex(br.addr - 4)}, so what precedes it is unknown`);
|
|
420
|
+
}
|
|
421
|
+
if (prev !== undefined && isControlTransfer(prev)) {
|
|
422
|
+
throw refusal(br, `it sits in the delay slot of '${prev.mnemonic}', so its own slot has no block to live in`);
|
|
423
|
+
}
|
|
424
|
+
if (isControlTransfer(slot)) {
|
|
425
|
+
throw refusal(
|
|
426
|
+
br,
|
|
427
|
+
`the delay slot is itself a control transfer ('${slot.mnemonic}'), which the ISA leaves undefined`,
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
// The slot's block is entered ONLY by this branch's taken edge. Anything else arriving there
|
|
431
|
+
// would run the slot with no branch conditioning it, and then take the branch's target.
|
|
432
|
+
if (targets.has(slot.addr)) {
|
|
433
|
+
throw refusal(br, 'another branch targets the delay slot, which would run it unconditioned');
|
|
434
|
+
}
|
|
435
|
+
br.mnemonic = LIKELY_BASE[br.mnemonic];
|
|
436
|
+
rewritten.add(br.addr);
|
|
437
|
+
}
|
|
438
|
+
return rewritten;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// EVERY control transfer this frontend models has a delay slot — the word at `branch + 4`, which
|
|
442
|
+
// runs after it — and every conditional branch's not-taken edge lands on the word at `branch + 8`.
|
|
443
|
+
// `toBlocks` places both, so a listing that does not spell them refuses here rather than there:
|
|
444
|
+
// with the word missing, the slot would be taken from whatever came next and the not-taken edge
|
|
445
|
+
// would silently lose its successor. A branch-LIKELY asks the same two questions in its own words,
|
|
446
|
+
// before the rewrite, because for it they decide conditional execution rather than placement.
|
|
447
|
+
function checkDelaySlots(name: string, instrs: Instr[]): void {
|
|
448
|
+
const at = new Set(instrs.map((ins) => ins.addr));
|
|
449
|
+
const hex = (a: number) => `0x${a.toString(16)}`;
|
|
450
|
+
for (const ins of instrs) {
|
|
451
|
+
if (!isXfer(ins)) {
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
if (!at.has(ins.addr + 4)) {
|
|
455
|
+
throw new FrontendUnsupportedError(
|
|
456
|
+
`cannot lift '${name}': '${ins.mnemonic}' at ${hex(ins.addr)} — the disassembly has no ` +
|
|
457
|
+
`instruction at ${hex(ins.addr + 4)} to be its delay slot`,
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
if (isCond(ins) && !at.has(ins.addr + 8)) {
|
|
461
|
+
throw new FrontendUnsupportedError(
|
|
462
|
+
`cannot lift '${name}': '${ins.mnemonic}' at ${hex(ins.addr)} — the disassembly has no ` +
|
|
463
|
+
`instruction at ${hex(ins.addr + 8)} for the not-taken edge to land on`,
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
351
469
|
// Split the instruction stream into basic blocks. Delay slots are consumed into their
|
|
352
470
|
// branching block; leaders are the entry, every branch target, and each conditional branch's
|
|
353
|
-
// fall-through.
|
|
471
|
+
// fall-through. A branch-LIKELY's slot is not consumed — it becomes its own block (see
|
|
472
|
+
// `normaliseBranchLikely`). Unreachable trailing blocks (padding `nop`s) are dropped.
|
|
354
473
|
function toBlocks(
|
|
355
474
|
instrs: Instr[],
|
|
356
475
|
jts: Map<number, MipsJT>,
|
|
476
|
+
likelyAddrs: Set<number>,
|
|
357
477
|
): { blocks: MipsBlock[]; succAddrs: Map<MipsBlock, number[]> } {
|
|
478
|
+
// BY ADDRESS, never by array position — the placement rule is the same one `normaliseBranchLikely`
|
|
479
|
+
// states, and `checkDelaySlots` has already refused every transfer whose words are not there.
|
|
480
|
+
const at = new Map(instrs.map((ins) => [ins.addr, ins]));
|
|
358
481
|
const consumed = new Set<number>();
|
|
359
|
-
|
|
360
|
-
if (isXfer(ins)) {
|
|
361
|
-
consumed.add(
|
|
482
|
+
for (const ins of instrs) {
|
|
483
|
+
if (isXfer(ins) && !likelyAddrs.has(ins.addr)) {
|
|
484
|
+
consumed.add(ins.addr + 4);
|
|
362
485
|
}
|
|
363
|
-
}
|
|
486
|
+
}
|
|
364
487
|
|
|
365
488
|
const leaders = new Set<number>(instrs.length ? [instrs[0].addr] : []);
|
|
366
|
-
|
|
489
|
+
for (const ins of instrs) {
|
|
367
490
|
if ((isCond(ins) || isUncond(ins)) && ins.target !== undefined) {
|
|
368
491
|
leaders.add(ins.target);
|
|
369
492
|
}
|
|
370
|
-
if (isCond(ins)
|
|
371
|
-
leaders.add(
|
|
372
|
-
} // fall-through
|
|
373
|
-
|
|
493
|
+
if (isCond(ins)) {
|
|
494
|
+
leaders.add(ins.addr + 8);
|
|
495
|
+
} // fall-through: past the branch and its slot
|
|
496
|
+
if (likelyAddrs.has(ins.addr)) {
|
|
497
|
+
leaders.add(ins.addr + 4);
|
|
498
|
+
} // the nullified slot's own block
|
|
499
|
+
}
|
|
374
500
|
// A recovered jump table makes its case + default targets leaders (the bounds block's `switch_br`
|
|
375
501
|
// successors); the dispatch block then becomes unreachable and is pruned below.
|
|
376
502
|
for (const jt of jts.values()) {
|
|
@@ -381,19 +507,25 @@ function toBlocks(
|
|
|
381
507
|
}
|
|
382
508
|
|
|
383
509
|
const blocks: MipsBlock[] = [];
|
|
510
|
+
// Where a nullified slot's block goes once the slot has run: its branch's target.
|
|
511
|
+
const slotGoto = new Map<number, number>();
|
|
384
512
|
let cur: MipsBlock | null = null;
|
|
385
|
-
for (
|
|
386
|
-
if (consumed.has(
|
|
513
|
+
for (const ins of instrs) {
|
|
514
|
+
if (consumed.has(ins.addr)) {
|
|
387
515
|
continue;
|
|
388
516
|
} // delay slot: handled with its branch
|
|
389
|
-
const ins = instrs[i];
|
|
390
517
|
if (cur === null || leaders.has(ins.addr)) {
|
|
391
|
-
cur = { startAddr: ins.addr, body: [], branch: null, delay: null };
|
|
518
|
+
cur = { startAddr: ins.addr, body: [], branch: null, delay: null, delayAnnulled: false };
|
|
392
519
|
blocks.push(cur);
|
|
393
520
|
}
|
|
394
521
|
if (isXfer(ins)) {
|
|
395
522
|
cur.branch = ins;
|
|
396
|
-
|
|
523
|
+
if (likelyAddrs.has(ins.addr)) {
|
|
524
|
+
cur.delayAnnulled = true;
|
|
525
|
+
slotGoto.set(ins.addr + 4, ins.target!);
|
|
526
|
+
} else {
|
|
527
|
+
cur.delay = at.get(ins.addr + 4) ?? null;
|
|
528
|
+
}
|
|
397
529
|
cur = null;
|
|
398
530
|
} else {
|
|
399
531
|
cur.body.push(ins);
|
|
@@ -403,6 +535,11 @@ function toBlocks(
|
|
|
403
535
|
// Successor addresses per block (before reachability pruning).
|
|
404
536
|
const succAddrs = new Map<MipsBlock, number[]>();
|
|
405
537
|
for (const b of blocks) {
|
|
538
|
+
const goto = slotGoto.get(b.startAddr);
|
|
539
|
+
if (goto !== undefined) {
|
|
540
|
+
succAddrs.set(b, [goto]);
|
|
541
|
+
continue;
|
|
542
|
+
} // a nullified slot's block: run the slot, then take the branch
|
|
406
543
|
const br = b.branch;
|
|
407
544
|
const jt = br ? jts.get(br.addr) : undefined;
|
|
408
545
|
if (jt) {
|
|
@@ -416,8 +553,11 @@ function toBlocks(
|
|
|
416
553
|
succAddrs.set(b, []);
|
|
417
554
|
} else if (isUncond(br)) {
|
|
418
555
|
succAddrs.set(b, br.target !== undefined ? [br.target] : []);
|
|
556
|
+
} else if (b.delayAnnulled) {
|
|
557
|
+
// branch-likely: TAKEN runs the nullified slot's block, NOT-TAKEN skips straight past it
|
|
558
|
+
succAddrs.set(b, [br.addr + 4, br.addr + 8]);
|
|
419
559
|
} else {
|
|
420
|
-
const fall =
|
|
560
|
+
const fall = br.addr + 8; // past the branch and its delay slot
|
|
421
561
|
succAddrs.set(b, br.target !== undefined ? [br.target, fall] : [fall]);
|
|
422
562
|
}
|
|
423
563
|
}
|
|
@@ -459,20 +599,60 @@ export function lift(
|
|
|
459
599
|
assertInputFormat('mips', 'objdump', asm);
|
|
460
600
|
}
|
|
461
601
|
// ONE function only — an absent symbol declines loud (either dialect's slicer enforces this).
|
|
462
|
-
const
|
|
602
|
+
const sliced = splat ? asm : sliceSymbol(asm, name);
|
|
603
|
+
const instrs = splat ? parseSplatMips(asm, name) : parseDisasm(sliced);
|
|
463
604
|
if (instrs.length === 0) {
|
|
464
605
|
throw new FrontendUnsupportedError(`cannot lift '${name}': no instructions found in the input text`);
|
|
465
606
|
}
|
|
607
|
+
// Headerless input — a raw fragment, and every Splat listing — says where the function starts
|
|
608
|
+
// only by its first line.
|
|
609
|
+
const startAddr = (splat ? undefined : symbolStart(sliced)) ?? instrs[0].addr;
|
|
610
|
+
// An FP condition-code branch is a DIFFERENT gap from a branch-likely, and saying so is what lets
|
|
611
|
+
// each be worked on alone: `bc1fl` is both, and the condition code blocks it either way. Asked
|
|
612
|
+
// FIRST, before the likely rewrite, so that a function carrying both reports the FP gap rather
|
|
613
|
+
// than whatever the likely placement happens to say about the other branch.
|
|
614
|
+
for (const ins of instrs) {
|
|
615
|
+
if (isFpCondBranch(ins.mnemonic)) {
|
|
616
|
+
throw new FrontendUnsupportedError(
|
|
617
|
+
`cannot lift '${name}': floating-point condition-code branch '${ins.mnemonic}' at 0x${ins.addr.toString(16)} ` +
|
|
618
|
+
`— the FP condition code is not modelled`,
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
// BRANCH-LIKELY is rewritten to its ordinary branch FIRST, so everything downstream — jump-table
|
|
623
|
+
// recovery's block-boundary walk included — sees one branch vocabulary. What stays special is the
|
|
624
|
+
// slot, and `toBlocks` is where that is placed.
|
|
625
|
+
const likelyAddrs = normaliseBranchLikely(name, instrs, startAddr);
|
|
466
626
|
// Regime B: recover jump tables from the `jr`-dispatch idiom + the AsmData table. A recovered
|
|
467
627
|
// dispatch's `jr` is subsumed into a `switch_br` (emitted from its bounds block), so it is
|
|
468
628
|
// exempted from the loud-fail below; an UNrecovered `jr <non-ra>` still fails loud.
|
|
469
629
|
const jts = asmData ? recoverMipsJumpTables(instrs, asmData) : new Map<number, MipsJT>();
|
|
470
630
|
const recoveredJr = new Set([...jts.values()].map((j) => j.jrAddr));
|
|
471
|
-
//
|
|
472
|
-
//
|
|
473
|
-
//
|
|
631
|
+
// THE TWO WAYS A RECOVERED TABLE MEETS AN ANNULLED SLOT, neither of which `normaliseBranchLikely`
|
|
632
|
+
// can see: the table does not exist while it runs. (1) The dispatch's bounds branch emits a
|
|
633
|
+
// `switch_br`, which runs its delay slot unconditionally — a nullified slot is not that. (2) An
|
|
634
|
+
// arm of the table lands ON a nullified slot, which would run the slot with no branch
|
|
635
|
+
// conditioning it and then jump to that branch's target: `case 1:` would take the taken arm's
|
|
636
|
+
// value where the hardware runs the slot and falls on through. Neither idiom has ever been seen
|
|
637
|
+
// in the corpus; refuse rather than answer either one wrong.
|
|
638
|
+
const jtArms = new Set([...jts.values()].flatMap((jt) => [...jt.caseAddrs, jt.defaultAddr]));
|
|
639
|
+
for (const addr of likelyAddrs) {
|
|
640
|
+
if (jts.has(addr)) {
|
|
641
|
+
throw new FrontendUnsupportedError(
|
|
642
|
+
`cannot lift '${name}': branch-likely at 0x${addr.toString(16)} — a recovered switch's bounds branch cannot annul its delay slot`,
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
if (jtArms.has(addr + 4)) {
|
|
646
|
+
throw new FrontendUnsupportedError(
|
|
647
|
+
`cannot lift '${name}': branch-likely at 0x${addr.toString(16)} — a recovered switch arm lands on its delay slot, which would run it unconditioned`,
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
// Carry the object's relocations on the instructions they fill (objdump dialect only — Splat
|
|
652
|
+
// text spells the halves and writes its own records). Runs AFTER jump-table recovery, so that
|
|
653
|
+
// reads `.text` relocs pristine and a recovered table base is decided before any of this.
|
|
474
654
|
if (!splat && asmData) {
|
|
475
|
-
|
|
655
|
+
attachMipsRelocs(name, instrs, asmData);
|
|
476
656
|
}
|
|
477
657
|
// TRUSTWORTHINESS: fail LOUD on a control transfer this frontend cannot model — the `opaque`
|
|
478
658
|
// path cannot catch these (implicit or no register destination). `jal`/`jalr` clobber `v0`
|
|
@@ -492,18 +672,16 @@ export function lift(
|
|
|
492
672
|
}
|
|
493
673
|
// CATCH-ALL (mirrors the PPC denylist): an unmodelled control-transfer mnemonic would otherwise
|
|
494
674
|
// fall through to `emitOpaqueDest` and have its BRANCH silently dropped (no register dest for
|
|
495
|
-
// the opaque guard to catch).
|
|
496
|
-
|
|
497
|
-
// not a branch.
|
|
498
|
-
const isBranchish = (ins.mnemonic[0] === 'b' && ins.mnemonic !== 'break') || ins.mnemonic[0] === 'j';
|
|
499
|
-
if (isBranchish && !isXfer(ins) && ins.mnemonic !== 'jal' && ins.mnemonic !== 'jalr') {
|
|
675
|
+
// the opaque guard to catch). `break` is a trap, not a branch.
|
|
676
|
+
if (isControlTransfer(ins) && !isXfer(ins) && ins.mnemonic !== 'jal' && ins.mnemonic !== 'jalr') {
|
|
500
677
|
throw new FrontendUnsupportedError(
|
|
501
678
|
`cannot lift '${name}': unmodelled control transfer '${ins.mnemonic}' at 0x${ins.addr.toString(16)} ` +
|
|
502
|
-
`—
|
|
679
|
+
`— not a modelled branch form`,
|
|
503
680
|
);
|
|
504
681
|
}
|
|
505
682
|
}
|
|
506
|
-
|
|
683
|
+
checkDelaySlots(name, instrs);
|
|
684
|
+
const { blocks, succAddrs } = toBlocks(instrs, jts, likelyAddrs);
|
|
507
685
|
const idxOf = new Map(blocks.map((b, i) => [b.startAddr, i]));
|
|
508
686
|
|
|
509
687
|
// CFG predecessors by block index.
|
|
@@ -533,6 +711,20 @@ export function lift(
|
|
|
533
711
|
const RET = target.returnReg;
|
|
534
712
|
const ARG_REGS = target.argRegs;
|
|
535
713
|
|
|
714
|
+
// The pending `%hi` halves of this function's global addresses, keyed by the SSA VALUE each `lui`
|
|
715
|
+
// defines (frontend/high-half.ts holds the invariant and why a register-keyed map cannot answer
|
|
716
|
+
// the question; frontend/ppc.ts folds the same one). FUNCTION-scoped, because a value is: a pair
|
|
717
|
+
// split across blocks folds when SSA says the half reaches, and refuses when what arrives is the
|
|
718
|
+
// block parameter standing for a merge.
|
|
719
|
+
const highHalves = makeHighHalves({
|
|
720
|
+
hi: '%hi',
|
|
721
|
+
hiArticle: 'a',
|
|
722
|
+
lo: '%lo',
|
|
723
|
+
fail: (message) => {
|
|
724
|
+
throw new FrontendUnsupportedError(message);
|
|
725
|
+
},
|
|
726
|
+
});
|
|
727
|
+
|
|
536
728
|
// SOUNDNESS GUARD. The word stack-slot model (emitLoad/emitStore) is safe ONLY when every
|
|
537
729
|
// sp-relative access in the function is word-width. If a SUB-WORD sp access aliases a word slot
|
|
538
730
|
// (`sw a0,4(sp)` then `lbu v0,4(sp)`), routing the word store to an SSA slot while the sub-word
|
|
@@ -556,10 +748,6 @@ export function lift(
|
|
|
556
748
|
|
|
557
749
|
const fillBlock = (b: MipsBlock, bi: number) => {
|
|
558
750
|
const ops = irBlocks[bi].ops;
|
|
559
|
-
// Pending `lui rX, %hi(SYM)` relocations awaiting their consuming `%lo` (a load/store base or an
|
|
560
|
-
// `addiu`). Block-local: the pair is emitted adjacently, so a `%lo` with no matching in-scope
|
|
561
|
-
// `%hi` (a cross-block or gp-relative access) declines LOUD rather than fabricating a base.
|
|
562
|
-
const hiReloc = new Map<string, { sym: string; addend: number }>();
|
|
563
751
|
// Materialise the address of a named global — the same `gaddr` op the Thumb frontend emits; the
|
|
564
752
|
// structurer lowers a load/store through it to `SYM` (scalar) or `((T *)&SYM)[i]` (aggregate).
|
|
565
753
|
const emitGaddr = (sym: string): Value => {
|
|
@@ -571,16 +759,6 @@ export function lift(
|
|
|
571
759
|
if (isZero(r)) {
|
|
572
760
|
return constVal(0);
|
|
573
761
|
}
|
|
574
|
-
// A `%hi(SYM)` register read as DATA before its `%lo` completes the address is a split hi/lo
|
|
575
|
-
// relocation (the high half used alone) this frontend does not model — decline rather than
|
|
576
|
-
// treat the partial address as a value. The legit consumers (load/store/addiu `%lo`) validate
|
|
577
|
-
// `hiReloc` directly and never route the base through `read`, so this fires only on misuse.
|
|
578
|
-
const hr = hiReloc.get(r);
|
|
579
|
-
if (hr) {
|
|
580
|
-
throw new FrontendUnsupportedError(
|
|
581
|
-
`cannot lift '${name}': %hi(${hr.sym}) register used as data before a matching %lo — split hi/lo relocation not modelled`,
|
|
582
|
-
);
|
|
583
|
-
}
|
|
584
762
|
// Reading `sp` as a DATA operand means frame-pointer arithmetic or an address-taken local
|
|
585
763
|
// (`addiu a0,sp,8` = `&local`) — not modellable without a stack abstraction. Fabricating a
|
|
586
764
|
// value for `sp` invents a PHANTOM leading parameter that shifts every real argument — a
|
|
@@ -600,7 +778,9 @@ export function lift(
|
|
|
600
778
|
`cannot lift '${name}': gp used as data (PIC / small-data global access) — not supported`,
|
|
601
779
|
);
|
|
602
780
|
}
|
|
603
|
-
|
|
781
|
+
// A register holding the high half of an address is NOT a value. The legitimate consumers
|
|
782
|
+
// (`addiu %lo`, a `%lo` load/store base) reach the half through `foldLoHalf` instead.
|
|
783
|
+
return highHalves.guardRead(name, r, readVar(r, bi));
|
|
604
784
|
};
|
|
605
785
|
// `slt`-family results, so a following `beqz`/`bnez` can fold into one compare.
|
|
606
786
|
//
|
|
@@ -615,10 +795,6 @@ export function lift(
|
|
|
615
795
|
// rD,%hi(SYM)` deliberately reassigns rD's meaning without it).
|
|
616
796
|
const cmpDef = new Map<Value, { opcode: string; lhs: Value; rhs: Value }>();
|
|
617
797
|
const write = (r: string, v: Value) => {
|
|
618
|
-
// Writing a register clears any pending `%hi` it held — the high-half address is gone once the
|
|
619
|
-
// register is reassigned (e.g. `lw rHi, %lo(SYM)(rHi)` reuses the base as the load dest). A
|
|
620
|
-
// `%hi` NOT overwritten persists across multiple `%lo` uses (the read-modify-write idiom).
|
|
621
|
-
hiReloc.delete(r);
|
|
622
798
|
if (!isZero(r)) {
|
|
623
799
|
writeVar(r, bi, v);
|
|
624
800
|
}
|
|
@@ -643,8 +819,30 @@ export function lift(
|
|
|
643
819
|
cmpDef.set(v, { opcode: opc, lhs, rhs });
|
|
644
820
|
};
|
|
645
821
|
|
|
822
|
+
// Where a refusal about ONE INSTRUCTION starts, spelled as frontend/ppc.ts spells it — the
|
|
823
|
+
// artifact's decline text is read row by row across both ISAs.
|
|
824
|
+
const site = (ins: Instr) => `cannot lift '${name}': '${ins.mnemonic}' at 0x${ins.addr.toString(16)}`;
|
|
825
|
+
// Set by every case entitled to fold a relocation, cleared per instruction, and answered by
|
|
826
|
+
// `relocPlaceholder`.
|
|
827
|
+
let relocTaken = false;
|
|
828
|
+
// EVERY immediate this frontend turns into a value goes through here. objdump prints them as
|
|
829
|
+
// numbers, so a non-numeric one means the operand is not an immediate at all — a relocation
|
|
830
|
+
// spelling the reader failed to resolve, a label — and bare `parseImm` answers NaN, which
|
|
831
|
+
// `constVal` renders as the literal 0. That is the relocation fold's own failure one level
|
|
832
|
+
// down, so it refuses instead.
|
|
833
|
+
const imm = (ins: Instr, text: string | undefined): number => {
|
|
834
|
+
const v = parseImm(text ?? '');
|
|
835
|
+
if (!Number.isFinite(v)) {
|
|
836
|
+
throw new FrontendUnsupportedError(
|
|
837
|
+
`${site(ins)} has the non-numeric ` +
|
|
838
|
+
`immediate '${text ?? ''}' where a number belongs — this frontend will not read it as one`,
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
return v;
|
|
842
|
+
};
|
|
646
843
|
const decode = (ins: Instr) => {
|
|
647
844
|
const [d, s, t] = ins.ops;
|
|
845
|
+
relocTaken = false;
|
|
648
846
|
// The GOT/small-data base register `gp` is set up by IDO's PIC prologue (`lui gp; addiu gp,gp,lo;
|
|
649
847
|
// addu gp,gp,t9`, an `_gp_disp` HI16/LO16 pair). It is a RELOCATION base, never program data — and
|
|
650
848
|
// reading `t9` for the `addu` would fabricate a phantom leading parameter (like the sp/r1 guards).
|
|
@@ -660,30 +858,44 @@ export function lift(
|
|
|
660
858
|
write(d, read(s));
|
|
661
859
|
break; // pseudo: addu/or rD,rS,zero
|
|
662
860
|
case 'li':
|
|
663
|
-
write(d, constVal(
|
|
861
|
+
write(d, constVal(imm(ins, s)));
|
|
664
862
|
break; // pseudo: load immediate
|
|
665
863
|
// `lui rD, hi` loads the 16-bit immediate into the UPPER half (mirrors PPC `lis`). Alone it
|
|
666
864
|
// is the high half of a 32-bit literal; the following `ori`/`addiu` supplies the low half
|
|
667
865
|
// and raise/const.ts folds the const/const pair into one 32-bit const — the form that
|
|
668
866
|
// recompiles to this exact `lui;ori`.
|
|
669
867
|
case 'lui':
|
|
670
|
-
// `lui rD, %hi(SYM)` is the
|
|
671
|
-
//
|
|
672
|
-
// is
|
|
673
|
-
//
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
868
|
+
// `lui rD, %hi(SYM)` is the HIGH HALF of a global's address, so rD is defined as a
|
|
869
|
+
// PLACEHOLDER value rather than a number (frontend/high-half.ts) and the `gaddr` itself
|
|
870
|
+
// is emitted at the `%lo` that completes the address. Being REL, this half contributes
|
|
871
|
+
// `hi_imm << 16` to the addend.
|
|
872
|
+
if (ins.reloc?.type === 'R_MIPS_HI16') {
|
|
873
|
+
relocTaken = true;
|
|
874
|
+
const hi = mkValue(T.unk(32));
|
|
875
|
+
highHalves.record(hi, {
|
|
876
|
+
sym: ins.reloc.sym,
|
|
877
|
+
addend: imm(ins, s ?? '0') << 16,
|
|
878
|
+
addr: ins.addr,
|
|
879
|
+
mnemonic: ins.mnemonic,
|
|
880
|
+
});
|
|
881
|
+
write(d, hi);
|
|
684
882
|
break;
|
|
685
883
|
}
|
|
686
|
-
|
|
884
|
+
// `lui rD, 0x0` WITHOUT a relocation is not code any compiler wrote: `lui` of zero
|
|
885
|
+
// writes zero, which `move rD,zero` says in one instruction. What does print it is a
|
|
886
|
+
// relocatable object's UNRELOCATED high half, whose symbol lives in a record this lift
|
|
887
|
+
// was never handed — the side table is optional, and a caller may simply not pass one.
|
|
888
|
+
// Every refusal above covers a relocation that ARRIVED and was dropped; this covers the
|
|
889
|
+
// one that never arrived, so `*(T *)0` is not the answer either way. A non-zero `lui` is
|
|
890
|
+
// a genuine absolute address or literal high half (a linked dump, `lui;ori`).
|
|
891
|
+
if (imm(ins, s) === 0) {
|
|
892
|
+
throw new FrontendUnsupportedError(
|
|
893
|
+
`${site(ins)} loads the high half 0x0 with no relocation on it — that is an ` +
|
|
894
|
+
`unrelocated placeholder, not the value; the object's R_MIPS_HI16/LO16 records were not supplied ` +
|
|
895
|
+
`with this disassembly (pass the object's \`objdump -s -r -t\` side table)`,
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
write(d, constVal((imm(ins, s) << 16) >> 0));
|
|
687
899
|
break;
|
|
688
900
|
case 'addiu':
|
|
689
901
|
case 'addi':
|
|
@@ -693,29 +905,22 @@ export function lift(
|
|
|
693
905
|
if (isStackPtr(d)) {
|
|
694
906
|
break;
|
|
695
907
|
}
|
|
696
|
-
// `addiu rD, rHi, %lo(SYM)` completes a global's address
|
|
697
|
-
// rD = &SYM (+
|
|
698
|
-
if (
|
|
699
|
-
const
|
|
700
|
-
|
|
701
|
-
if (!lo || !hr || hr.sym !== lo.sym || hr.addend !== lo.addend) {
|
|
702
|
-
throw new FrontendUnsupportedError(
|
|
703
|
-
`cannot lift '${name}': %lo relocation '${t}' with no matching in-scope %hi — split/cross-block hi/lo not modelled`,
|
|
704
|
-
);
|
|
705
|
-
}
|
|
706
|
-
// `&SYM` (addend 0), or `&SYM + N` for a byte offset into the global. The `add` tree is
|
|
908
|
+
// `addiu rD, rHi, %lo(SYM)` completes a global's address begun by a `lui %hi(SYM)`:
|
|
909
|
+
// rD = &SYM (+ a byte offset into it). Emits the shared `gaddr` op.
|
|
910
|
+
if (ins.reloc?.type === 'R_MIPS_LO16') {
|
|
911
|
+
const { base: g, off } = foldLoHalf(ins, s, imm(ins, t));
|
|
912
|
+
// `&SYM` (offset 0), or `&SYM + N` for a byte offset into the global. The `add` tree is
|
|
707
913
|
// folded byte-correctly by memAccess when this address is a load/store base; if it
|
|
708
914
|
// instead ESCAPES as a value, `assertDerefsTyped` declines it (the byte offset would
|
|
709
915
|
// element-scale in C) — see the interior-global-pointer guard there.
|
|
710
|
-
|
|
711
|
-
lo.addend !== 0 ? emitBin('add', d, g, constVal(lo.addend)) : write(d, g);
|
|
916
|
+
off !== 0 ? emitBin('add', d, g, constVal(off)) : write(d, g);
|
|
712
917
|
break;
|
|
713
918
|
}
|
|
714
919
|
if (isZero(s)) {
|
|
715
|
-
write(d, constVal(
|
|
920
|
+
write(d, constVal(imm(ins, t)));
|
|
716
921
|
break;
|
|
717
922
|
} // li idiom
|
|
718
|
-
emitBin('add', d, read(s), constVal(
|
|
923
|
+
emitBin('add', d, read(s), constVal(imm(ins, t)));
|
|
719
924
|
break;
|
|
720
925
|
case 'addu':
|
|
721
926
|
case 'add':
|
|
@@ -775,19 +980,19 @@ export function lift(
|
|
|
775
980
|
emitBin('and', d, read(s), read(t));
|
|
776
981
|
break;
|
|
777
982
|
case 'andi':
|
|
778
|
-
emitBin('and', d, read(s), constVal(
|
|
983
|
+
emitBin('and', d, read(s), constVal(imm(ins, t)));
|
|
779
984
|
break;
|
|
780
985
|
case 'or':
|
|
781
986
|
isZero(t) ? write(d, read(s)) : emitBin('or', d, read(s), read(t));
|
|
782
987
|
break;
|
|
783
988
|
case 'ori':
|
|
784
|
-
emitBin('or', d, read(s), constVal(
|
|
989
|
+
emitBin('or', d, read(s), constVal(imm(ins, t)));
|
|
785
990
|
break;
|
|
786
991
|
case 'xor':
|
|
787
992
|
emitBin('xor', d, read(s), read(t));
|
|
788
993
|
break;
|
|
789
994
|
case 'xori':
|
|
790
|
-
emitBin('xor', d, read(s), constVal(
|
|
995
|
+
emitBin('xor', d, read(s), constVal(imm(ins, t)));
|
|
791
996
|
break;
|
|
792
997
|
// `nor rD, x, zero` / `nor rD, zero, x` = ~x (GCC emits the zero in EITHER operand — e.g.
|
|
793
998
|
// its branchless `x<0?0:x` uses `nor v0,zero,a0`; IDO tends to put zero second).
|
|
@@ -807,13 +1012,13 @@ export function lift(
|
|
|
807
1012
|
break;
|
|
808
1013
|
}
|
|
809
1014
|
case 'sll':
|
|
810
|
-
|
|
1015
|
+
kit.shImm('shl', d, read(s), imm(ins, t));
|
|
811
1016
|
break;
|
|
812
1017
|
case 'srl':
|
|
813
|
-
|
|
1018
|
+
kit.shImm('shr_u', d, read(s), imm(ins, t));
|
|
814
1019
|
break;
|
|
815
1020
|
case 'sra':
|
|
816
|
-
|
|
1021
|
+
kit.shImm('shr_s', d, read(s), imm(ins, t));
|
|
817
1022
|
break;
|
|
818
1023
|
// Variable shift `<op>v rD, rT, rS` = rD = rT <shift> rS: VALUE is rT (=s), AMOUNT is rS
|
|
819
1024
|
// (=t) — value-then-amount, unlike `slt rD,rS,rT`.
|
|
@@ -837,58 +1042,71 @@ export function lift(
|
|
|
837
1042
|
emitCmp('icmp_slt', d, read(s), read(t));
|
|
838
1043
|
break;
|
|
839
1044
|
case 'slti':
|
|
840
|
-
emitCmp('icmp_slt', d, read(s), constVal(
|
|
1045
|
+
emitCmp('icmp_slt', d, read(s), constVal(imm(ins, t)));
|
|
841
1046
|
break;
|
|
842
1047
|
case 'sltu':
|
|
843
1048
|
emitCmp('icmp_ult', d, read(s), read(t));
|
|
844
1049
|
break;
|
|
845
1050
|
case 'sltiu':
|
|
846
|
-
emitCmp('icmp_ult', d, read(s), constVal(
|
|
1051
|
+
emitCmp('icmp_ult', d, read(s), constVal(imm(ins, t)));
|
|
847
1052
|
break;
|
|
848
1053
|
// typed memory: `off(base)` addressing. Width/signedness come from the mnemonic; the
|
|
849
1054
|
// base is typed a pointer-to-element during recovery, mirroring the Thumb frontend.
|
|
850
1055
|
case 'lw':
|
|
851
|
-
emitLoad(d, s, 4, true);
|
|
1056
|
+
emitLoad(ins, d, s, 4, true);
|
|
852
1057
|
break;
|
|
853
1058
|
case 'lh':
|
|
854
|
-
emitLoad(d, s, 2, true);
|
|
1059
|
+
emitLoad(ins, d, s, 2, true);
|
|
855
1060
|
break;
|
|
856
1061
|
case 'lhu':
|
|
857
|
-
emitLoad(d, s, 2, false);
|
|
1062
|
+
emitLoad(ins, d, s, 2, false);
|
|
858
1063
|
break;
|
|
859
1064
|
case 'lb':
|
|
860
|
-
emitLoad(d, s, 1, true);
|
|
1065
|
+
emitLoad(ins, d, s, 1, true);
|
|
861
1066
|
break;
|
|
862
1067
|
case 'lbu':
|
|
863
|
-
emitLoad(d, s, 1, false);
|
|
1068
|
+
emitLoad(ins, d, s, 1, false);
|
|
864
1069
|
break;
|
|
865
1070
|
case 'sw':
|
|
866
|
-
emitStore(d, s, 4);
|
|
1071
|
+
emitStore(ins, d, s, 4);
|
|
867
1072
|
break; // d = source reg, s = off(base)
|
|
868
1073
|
case 'sh':
|
|
869
|
-
emitStore(d, s, 2);
|
|
1074
|
+
emitStore(ins, d, s, 2);
|
|
870
1075
|
break;
|
|
871
1076
|
case 'sb':
|
|
872
|
-
emitStore(d, s, 1);
|
|
1077
|
+
emitStore(ins, d, s, 1);
|
|
873
1078
|
break;
|
|
874
1079
|
default:
|
|
875
1080
|
emitOpaqueDest(ins);
|
|
876
1081
|
break; // unmodelled: an honest opaque, never a silent drop
|
|
877
1082
|
}
|
|
1083
|
+
// THE CHOKE POINT (mirrors the PPC frontend). Every case above either folded the relocation
|
|
1084
|
+
// or threw, so one still sitting here was DROPPED. An unmodelled `%lo` consumer (`lwc1`,
|
|
1085
|
+
// `swc1`, `ori`) lands here, which is what keeps "not modelled" from becoming "not emitted".
|
|
1086
|
+
if (ins.reloc && !relocTaken) {
|
|
1087
|
+
relocPlaceholder(ins);
|
|
1088
|
+
}
|
|
1089
|
+
};
|
|
1090
|
+
// A relocation no case folded. The printed immediate is a link-time placeholder — the literal 0
|
|
1091
|
+
// in a relocatable object — so finishing the lift would put that 0 where the symbol belongs.
|
|
1092
|
+
const relocPlaceholder = (ins: Instr): never => {
|
|
1093
|
+
const r = ins.reloc!;
|
|
1094
|
+
throw new FrontendUnsupportedError(
|
|
1095
|
+
`${site(ins)} carries '${r.type}' against ` +
|
|
1096
|
+
`'${r.sym}' but is not a modelled consumer of it — the printed immediate is a link-time ` +
|
|
1097
|
+
`placeholder, not the value`,
|
|
1098
|
+
);
|
|
878
1099
|
};
|
|
879
1100
|
// TRUSTWORTHINESS GUARD (mirrors the PPC frontend): an unmodelled instruction must not silently
|
|
880
1101
|
// drop its destination register — emit an honest `opaque`, which fails LOUD at assertResolved
|
|
881
1102
|
// whether or not anything reads that register (see frontend/opaque.ts for the policy).
|
|
882
1103
|
const emitOpaqueDest = (ins: Instr) => {
|
|
883
|
-
//
|
|
884
|
-
//
|
|
885
|
-
//
|
|
886
|
-
//
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
throw new FrontendUnsupportedError(
|
|
890
|
-
`cannot lift '${name}': unmodelled instruction '${ins.mnemonic}' with a %hi/%lo global operand — not modelled`,
|
|
891
|
-
);
|
|
1104
|
+
// THE RELOCATION FIRST. `opaqueDest` would refuse an unmodelled `%lo` consumer for the
|
|
1105
|
+
// lesser reason — "no register destination" — or, for a form that HAS one, degrade it to an
|
|
1106
|
+
// opaque and lose the global access with it. Naming the relocation says which capability is
|
|
1107
|
+
// missing.
|
|
1108
|
+
if (ins.reloc) {
|
|
1109
|
+
relocPlaceholder(ins);
|
|
892
1110
|
}
|
|
893
1111
|
// storeClass: unmodelled MIPS stores — incl. the unaligned pair swl/swr and the FPU stores,
|
|
894
1112
|
// whose FIRST token is a register (a SOURCE, not a dest) that would otherwise fabricate an
|
|
@@ -913,25 +1131,37 @@ export function lift(
|
|
|
913
1131
|
write(od.dst, res);
|
|
914
1132
|
};
|
|
915
1133
|
const emitUn = kit.un;
|
|
916
|
-
|
|
917
|
-
//
|
|
918
|
-
//
|
|
919
|
-
//
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
const
|
|
925
|
-
|
|
926
|
-
|
|
1134
|
+
// `%lo` completes the address a `lui %hi` began. The pairing asks SSA — through `readVar`, not
|
|
1135
|
+
// `read`, which refuses a half — what `rHi` holds HERE, so a pair separated by unrelated
|
|
1136
|
+
// instructions folds while a register reused between the halves does not, and a half that
|
|
1137
|
+
// arrives only through a merge or a loop header comes back as the block parameter standing for
|
|
1138
|
+
// the join, which is not a half and refuses.
|
|
1139
|
+
const foldLoHalf = (ins: Instr, rHi: string, loImm: number): { base: Value; off: number } => {
|
|
1140
|
+
relocTaken = true;
|
|
1141
|
+
const lo = ins.reloc!;
|
|
1142
|
+
const hi = highHalves.pair(site(ins), rHi, readVar(rHi, bi), lo.sym);
|
|
1143
|
+
// The addend is split across the two instruction immediates because MIPS is REL.
|
|
1144
|
+
const off = hi.addend + loImm;
|
|
1145
|
+
if (off < 0) {
|
|
927
1146
|
throw new FrontendUnsupportedError(
|
|
928
|
-
|
|
1147
|
+
`${site(ins)} completes '${lo.sym}' at ` +
|
|
1148
|
+
`byte offset ${off} — an address BELOW the symbol (an index-biased array base) is not yet modelled`,
|
|
929
1149
|
);
|
|
930
1150
|
}
|
|
931
|
-
return { base: emitGaddr(lo.sym), off
|
|
1151
|
+
return { base: emitGaddr(lo.sym), off };
|
|
932
1152
|
};
|
|
933
|
-
|
|
934
|
-
|
|
1153
|
+
// A displacement memory operand whose base is a `%hi` half: the global's address plus the
|
|
1154
|
+
// access offset. A non-`%lo` instruction returns null (the caller falls through to the ordinary
|
|
1155
|
+
// `off(base)` path).
|
|
1156
|
+
const globalBase = (ins: Instr, mem: string): { base: Value; off: number } | null => {
|
|
1157
|
+
if (ins.reloc?.type !== 'R_MIPS_LO16') {
|
|
1158
|
+
return null;
|
|
1159
|
+
}
|
|
1160
|
+
const { off, base } = parseMem(mem);
|
|
1161
|
+
return foldLoHalf(ins, base, off);
|
|
1162
|
+
};
|
|
1163
|
+
const emitLoad = (ins: Instr, d: string, mem: string, width: number, signed: boolean) => {
|
|
1164
|
+
const g = globalBase(ins, mem);
|
|
935
1165
|
if (g) {
|
|
936
1166
|
const res = mkValue(T.unk(32));
|
|
937
1167
|
ops.push(mkOp('load', { operands: [g.base], results: [res], attrs: { off: g.off, width, signed } }));
|
|
@@ -969,8 +1199,8 @@ export function lift(
|
|
|
969
1199
|
ops.push(mkOp('load', { operands: [read(base)], results: [res], attrs: { off, width, signed } }));
|
|
970
1200
|
write(d, res);
|
|
971
1201
|
};
|
|
972
|
-
const emitStore = (srcReg: string, mem: string, width: number) => {
|
|
973
|
-
const g = globalBase(mem);
|
|
1202
|
+
const emitStore = (ins: Instr, srcReg: string, mem: string, width: number) => {
|
|
1203
|
+
const g = globalBase(ins, mem);
|
|
974
1204
|
if (g) {
|
|
975
1205
|
ops.push(mkOp('store', { operands: [g.base, read(srcReg)], attrs: { off: g.off, width } }));
|
|
976
1206
|
return;
|
|
@@ -1022,11 +1252,17 @@ export function lift(
|
|
|
1022
1252
|
|
|
1023
1253
|
if (br && isCond(br)) {
|
|
1024
1254
|
const cond = condValue(br, ops, read, constVal, cmpDef);
|
|
1255
|
+
// A NULLIFIED slot is not decoded here: it is its own block on the taken edge, and the
|
|
1256
|
+
// not-taken edge skips past it (toBlocks). Every pass after this one sees ordinary
|
|
1257
|
+
// conditional execution.
|
|
1258
|
+
if (b.delayAnnulled) {
|
|
1259
|
+
ops.push(mkOp('cond_br', { operands: [cond], successors: [succ(br.addr + 4), succ(br.addr + 8)] }));
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1025
1262
|
if (b.delay) {
|
|
1026
1263
|
decode(b.delay);
|
|
1027
1264
|
}
|
|
1028
|
-
|
|
1029
|
-
ops.push(mkOp('cond_br', { operands: [cond], successors: [succ(br.target!), succ(fall)] }));
|
|
1265
|
+
ops.push(mkOp('cond_br', { operands: [cond], successors: [succ(br.target!), succ(br.addr + 8)] }));
|
|
1030
1266
|
return;
|
|
1031
1267
|
}
|
|
1032
1268
|
if (b.delay) {
|
|
@@ -1034,9 +1270,20 @@ export function lift(
|
|
|
1034
1270
|
} // unconditional / return: delay slot just executes first
|
|
1035
1271
|
|
|
1036
1272
|
if (!br || isReturn(br)) {
|
|
1037
|
-
|
|
1273
|
+
// A HIGH HALF IN v0 IS NOT A RETURN VALUE. `v0` is both MIPS's return register and an
|
|
1274
|
+
// ordinary caller-saved scratch, so a void function can end with a half still live in it
|
|
1275
|
+
// (`lui v0,%hi(g); lw v1,%lo(g)(v0); … ; sw v1,%lo(g)(v0)`). Counting that def hands the
|
|
1276
|
+
// placeholder to the `ret` op, and the whole function then declines at `assertNoneEscaped` —
|
|
1277
|
+
// loud, but for a merge that never happened. Rejecting it gives the honest void return.
|
|
1278
|
+
// PowerPC reads its return register through the guard, so the refusal it would get is already
|
|
1279
|
+
// the right one; it passes this same predicate to its call-arity count.
|
|
1280
|
+
const retOps = ssa.hasReachingDef(RET, bi, (v) => !highHalves.has(v)) ? [readVar(RET, bi)] : [];
|
|
1038
1281
|
if (!br) {
|
|
1039
|
-
|
|
1282
|
+
// A nullified slot's block leaves by its branch's TARGET, which need not be the next
|
|
1283
|
+
// address, so `fallthrough` is claimed only where control really does fall through.
|
|
1284
|
+
const to = succAddrs.get(b)![0];
|
|
1285
|
+
const next = (b.body[b.body.length - 1]?.addr ?? b.startAddr) + 4;
|
|
1286
|
+
ops.push(mkOp('br', { attrs: to === next ? { fallthrough: true } : {}, successors: [succ(to)] }));
|
|
1040
1287
|
} // fall-through
|
|
1041
1288
|
else {
|
|
1042
1289
|
ops.push(mkOp('ret', { operands: retOps }));
|
|
@@ -1051,7 +1298,9 @@ export function lift(
|
|
|
1051
1298
|
fillBlock(b, bi);
|
|
1052
1299
|
ssa.markFilled(bi);
|
|
1053
1300
|
});
|
|
1301
|
+
highHalves.assertAllConsumed(name);
|
|
1054
1302
|
ssa.finish();
|
|
1303
|
+
highHalves.assertNoneEscaped(name, irBlocks);
|
|
1055
1304
|
|
|
1056
1305
|
// ABI-ordered entry parameters (a0, a1, …) — a callee-saved copy can read a later argument
|
|
1057
1306
|
// register first. Only the true entry (no predecessors) is sorted; a loop header's phis are
|