@asmlift/core 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/package.json +1 -1
- package/src/backend/cfamily.ts +154 -5
- package/src/backend/cpp.ts +3 -1
- package/src/backend/pascal.ts +11 -0
- package/src/contracts.ts +37 -5
- package/src/declare.ts +251 -0
- package/src/frontend/frontend.ts +12 -2
- package/src/frontend/mips.ts +24 -23
- package/src/frontend/opaque.ts +39 -2
- package/src/frontend/ssa.ts +32 -53
- package/src/frontend/thumb.ts +420 -32
- package/src/ir/opcodes.ts +44 -0
- package/src/ir/simplify.ts +72 -0
- package/src/l3/argbase.ts +216 -0
- package/src/l3/ast.ts +126 -6
- package/src/l3/basecse.ts +3 -40
- package/src/l3/coalesce.ts +146 -0
- package/src/l3/dce.ts +2 -23
- package/src/l3/hoist.ts +65 -0
- package/src/l3/reindex.ts +7 -0
- package/src/l3/scopebase.ts +436 -0
- package/src/l3/symbol-refs.ts +61 -0
- package/src/l3/tailmerge.ts +120 -0
- package/src/l3/typing.ts +4 -0
- package/src/macros.ts +335 -0
- package/src/pattern/engine.ts +99 -6
- package/src/pipeline.ts +20 -6
- package/src/proto.ts +55 -0
- package/src/raise/divpow2.ts +226 -0
- package/src/raise/gvn.ts +141 -0
- package/src/raise/pre-recovery.ts +37 -3
- package/src/raise/recover.ts +24 -7
- package/src/raise/retsink.ts +36 -7
- package/src/raise/shortcircuit.ts +264 -22
- package/src/raise/structs.ts +12 -2
- package/src/rank.ts +370 -79
- package/src/structure/analysis.ts +42 -1
- package/src/structure/structure.ts +852 -67
- package/src/structure/switch-recover.ts +21 -3
- package/src/symbols.ts +541 -0
- package/src/target.ts +4 -2
- package/src/trace.ts +17 -2
package/src/frontend/thumb.ts
CHANGED
|
@@ -19,7 +19,9 @@ import type { Opcode } from '../ir/opcodes';
|
|
|
19
19
|
import { T } from '../ir/types';
|
|
20
20
|
import { type Prototypes, protoArity } from '../proto';
|
|
21
21
|
import { RUNTIME_HELPERS } from '../raise/softdiv';
|
|
22
|
+
import { type SymbolMap, lookupInterior, lookupSymbol } from '../symbols';
|
|
22
23
|
import type { TargetDescription } from '../target';
|
|
24
|
+
import type { AsmData } from './asmdata';
|
|
23
25
|
import { pushSwitchBr } from './emit';
|
|
24
26
|
import { FrontendUnsupportedError } from './errors';
|
|
25
27
|
import { assertInputFormat } from './format';
|
|
@@ -28,14 +30,74 @@ import { opaqueDest } from './opaque';
|
|
|
28
30
|
import { abiSortEntryParams, fallbackArgc, makeSsaBuilder } from './ssa';
|
|
29
31
|
|
|
30
32
|
interface Instr {
|
|
33
|
+
/** the CANONICAL spelling — legacy names are normalised (see LEGACY_MNEMONICS) so that every
|
|
34
|
+
* consumer matches one name. */
|
|
31
35
|
mnemonic: string;
|
|
32
36
|
ops: string[];
|
|
37
|
+
/** the spelling the input file actually used, present only when normalisation changed it.
|
|
38
|
+
* Messages must use this: a decline naming `ldrsh` for a file containing `ldsh` sends the
|
|
39
|
+
* reader looking for an instruction that is not there. */
|
|
40
|
+
asWritten?: string;
|
|
33
41
|
}
|
|
34
42
|
interface AsmBlock {
|
|
35
43
|
label: string;
|
|
36
44
|
instrs: Instr[];
|
|
37
45
|
}
|
|
38
46
|
|
|
47
|
+
// Alternative mnemonic spellings, normalised at the single point where an instruction enters the
|
|
48
|
+
// IR. Each maps to a name the decode switch below already handles.
|
|
49
|
+
//
|
|
50
|
+
// These are not "similar" instructions — each pair is ONE instruction with two accepted spellings.
|
|
51
|
+
// The ARM7TDMI Technical Reference Manual (ARM DDI 0029G) gives a single encoding for each:
|
|
52
|
+
// Figure 1-6 "Thumb instruction set formats" lists Format 08 "Load and store sign-extended byte and
|
|
53
|
+
// halfword" (0101 H S 1 Ro Rb Rd) and Format 15 "Multiple load and store" (1100 L Rb Rlist), and
|
|
54
|
+
// Table 1-7 "Thumb instruction set summary" spells them `LDRSH Rd, [Rb, Ro]`, `LDRSB Rd, [Rb, Ro]`,
|
|
55
|
+
// `LDMIA Rb!, <reglist>` and `STMIA Rb!, <reglist>`. Older ARM7TDMI documentation used LDSH/LDSB,
|
|
56
|
+
// which is where the short spellings come from; the stack-suffix forms (FD, EA) are the same
|
|
57
|
+
// instructions named after the stack discipline they implement.
|
|
58
|
+
//
|
|
59
|
+
// Confirmed with this project's own toolchain — same encoding, and gba-kit executes them with the
|
|
60
|
+
// same architectural effect (sign extension, transfers, base writeback):
|
|
61
|
+
//
|
|
62
|
+
// ldsh / ldrsh 885e / 885e ldm / ldmia / ldmfd 01c9 / 01c9 / 01c9
|
|
63
|
+
// ldsb / ldrsb 8856 / 8856 stm / stmia / stmea 01c1 / 01c1 / 01c1
|
|
64
|
+
//
|
|
65
|
+
// Measured on the Klonoa: Empire of Dreams disassembly (luvdis, 469 .s files): `ldsh` 292 and
|
|
66
|
+
// `ldsb` 180 against `ldrsh` 0 and `ldrsb` 12 — the same tool emits both spellings for the signed
|
|
67
|
+
// byte load — and `ldm` 12 / `stm` 34 against `ldmia` 0 / `stmia` 0. The UAL names this frontend
|
|
68
|
+
// cased for the multiple forms never appear in that corpus at all.
|
|
69
|
+
//
|
|
70
|
+
// These are PURE SYNONYMS — identical operands — which is why they belong in a table here rather
|
|
71
|
+
// than in decode arms like MIPS's `move` or PPC's `slwi`. That distinction, and why there is no
|
|
72
|
+
// shared alias helper across the three frontends, is written up once in ./opaque.ts.
|
|
73
|
+
//
|
|
74
|
+
// The LOAD aliases matter more than the store ones, and the asymmetry is worth knowing: an
|
|
75
|
+
// unrecognised `stm*` matches `opaquePolicy.storeClass` and fails LOUD, but an unrecognised `ldm*`
|
|
76
|
+
// does not — it reaches opaqueDest, which takes ops[0] (the BASE) as the destination, so the opaque
|
|
77
|
+
// is dead, DCE removes it, and the load silently vanishes. Measured: `ldmfd r1, {r0}; bx lr` lifted
|
|
78
|
+
// to `return a0;` where the answer is `return *a0;`. Every load spelling ARMv4T Thumb accepts is
|
|
79
|
+
// therefore listed. (`ldmed`/`ldmea` are NOT: they mean IB/DB, which Thumb-1 does not have, and
|
|
80
|
+
// `as` rejects them — so they cannot appear.)
|
|
81
|
+
//
|
|
82
|
+
// `stmfd` is deliberately absent, and the asymmetry is real rather than an oversight: `stmfd` is
|
|
83
|
+
// `stmdb`, and ARMv4T Thumb has neither — `as` rejects both with "selected processor does not
|
|
84
|
+
// support ... in Thumb mode". There is nothing to normalise it TO.
|
|
85
|
+
//
|
|
86
|
+
// Null-prototype so that an inherited key (`constructor`, `toString`) cannot be mistaken for an
|
|
87
|
+
// entry. Unreachable from real assembly, but the lookup should not depend on that.
|
|
88
|
+
const LEGACY_MNEMONICS: Readonly<Record<string, string>> = Object.assign(Object.create(null), {
|
|
89
|
+
ldsh: 'ldrsh',
|
|
90
|
+
ldsb: 'ldrsb',
|
|
91
|
+
ldm: 'ldmia',
|
|
92
|
+
ldmfd: 'ldmia',
|
|
93
|
+
stm: 'stmia',
|
|
94
|
+
stmea: 'stmia',
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
function canonicalMnemonic(mn: string): string {
|
|
98
|
+
return LEGACY_MNEMONICS[mn] ?? mn;
|
|
99
|
+
}
|
|
100
|
+
|
|
39
101
|
// Map a Thumb conditional-branch mnemonic to the icmp opcode for "branch taken". The signed forms
|
|
40
102
|
// (`blt`/`ble`/`bgt`/`bge`) follow a signed `cmp`; the UNSIGNED forms carry the carry/borrow sense:
|
|
41
103
|
// `bhi` = unsigned > (higher), `bls` = unsigned <= (lower-or-same), `bcc`/`blo` = unsigned <
|
|
@@ -117,6 +179,48 @@ const imm = (s: string) => parseInt(s.replace(/^#/, ''), s.includes('0x') ? 16 :
|
|
|
117
179
|
// detection sees them, and any consumer that needs the exact list rejects the leftover `-` token
|
|
118
180
|
// loudly rather than treating the fused range as one phantom register.
|
|
119
181
|
const REG_NUM: Record<string, number> = { sp: 13, lr: 14, pc: 15 };
|
|
182
|
+
|
|
183
|
+
// Thumb-1 data-processing mnemonics that write the condition flags when their destination is a LOW
|
|
184
|
+
// register — which is all of them on this ISA, `s`-suffix or not (the assembler picks the encoding).
|
|
185
|
+
// Used to invalidate a pending compare: see the decode loop. `cmp`/`cmn`/`tst` are absent on purpose
|
|
186
|
+
// — they set flags but define no register, and `cmp` is the very instruction that seeds the pending
|
|
187
|
+
// compare. Loads, stores, push/pop, `bl` and the high-register forms leave the flags alone.
|
|
188
|
+
const FLAG_SETTING = new Set([
|
|
189
|
+
'mov',
|
|
190
|
+
'movs',
|
|
191
|
+
'add',
|
|
192
|
+
'adds',
|
|
193
|
+
'sub',
|
|
194
|
+
'subs',
|
|
195
|
+
'lsl',
|
|
196
|
+
'lsls',
|
|
197
|
+
'lsr',
|
|
198
|
+
'lsrs',
|
|
199
|
+
'asr',
|
|
200
|
+
'asrs',
|
|
201
|
+
'neg',
|
|
202
|
+
'negs',
|
|
203
|
+
'rsb',
|
|
204
|
+
'rsbs',
|
|
205
|
+
'mvn',
|
|
206
|
+
'mvns',
|
|
207
|
+
'bic',
|
|
208
|
+
'bics',
|
|
209
|
+
'ror',
|
|
210
|
+
'rors',
|
|
211
|
+
'mul',
|
|
212
|
+
'muls',
|
|
213
|
+
'and',
|
|
214
|
+
'ands',
|
|
215
|
+
'orr',
|
|
216
|
+
'orrs',
|
|
217
|
+
'eor',
|
|
218
|
+
'eors',
|
|
219
|
+
'adc',
|
|
220
|
+
'adcs',
|
|
221
|
+
'sbc',
|
|
222
|
+
'sbcs',
|
|
223
|
+
]);
|
|
120
224
|
const regNum = (r: string) => (r[0] === 'r' ? Number(r.slice(1)) : REG_NUM[r]);
|
|
121
225
|
function expandRegList(tokens: string[]): string[] {
|
|
122
226
|
const out: string[] = [];
|
|
@@ -171,10 +275,15 @@ function splitOperands(s: string): string[] {
|
|
|
171
275
|
// Parse a Thumb memory addressing operand `[base]` or `[base, #off]` into base register +
|
|
172
276
|
// constant byte offset. (Register-scaled indices like `[base, r1, lsl #2]` are not handled
|
|
173
277
|
// yet — agbcc materialises those as explicit add/lsl before the load in the cases we target.)
|
|
174
|
-
function parseAddr(operand: string): { base: string; off: number } {
|
|
278
|
+
function parseAddr(operand: string): { base: string; off: number; regOff?: string } {
|
|
175
279
|
const inner = operand.replace(/[[\]]/g, '').trim();
|
|
176
280
|
const parts = inner.split(',').map((s) => s.trim());
|
|
177
281
|
const base = parts[0];
|
|
282
|
+
// `[rB, rX]` — REGISTER-offset addressing. Surfaced to the caller so load/store DECLINE
|
|
283
|
+
// loud: silently reading `[rB]` (the old behavior) dropped the index — a silent miscompile.
|
|
284
|
+
if (parts[1] !== undefined && !parts[1].startsWith('#')) {
|
|
285
|
+
return { base, off: 0, regOff: parts[1] };
|
|
286
|
+
}
|
|
178
287
|
const off = parts[1]?.startsWith('#') ? imm(parts[1]) : 0;
|
|
179
288
|
return { base, off };
|
|
180
289
|
}
|
|
@@ -298,7 +407,14 @@ function decode(name: string, asm: string): { blocks: AsmBlock[]; dataWords: Map
|
|
|
298
407
|
continue;
|
|
299
408
|
}
|
|
300
409
|
dataLabel = null; // a real instruction ends a data run
|
|
301
|
-
|
|
410
|
+
const canon = canonicalMnemonic(m[1]);
|
|
411
|
+
flat.push({
|
|
412
|
+
instr: {
|
|
413
|
+
mnemonic: canon,
|
|
414
|
+
ops: m[2] ? splitOperands(m[2]) : [],
|
|
415
|
+
...(canon === m[1] ? {} : { asWritten: m[1] }),
|
|
416
|
+
},
|
|
417
|
+
});
|
|
302
418
|
}
|
|
303
419
|
if (
|
|
304
420
|
armLabels.has(name) ||
|
|
@@ -601,6 +717,41 @@ function decode(name: string, asm: string): { blocks: AsmBlock[]; dataWords: Map
|
|
|
601
717
|
`cannot lift '${name}': block '${mixed.label}' interleaves raw data (.${subwordData.get(mixed.label)}) with instructions`,
|
|
602
718
|
);
|
|
603
719
|
}
|
|
720
|
+
// Two labels on the same instruction (`.LCB80:` immediately followed by `.L7:`) make the first
|
|
721
|
+
// an ALIAS of the second, not a block of its own — agbcc emits exactly that when a long-jump
|
|
722
|
+
// helper label lands on an existing one. The empty block is dropped just below, so a branch
|
|
723
|
+
// naming the alias would afterwards resolve to nothing and decline as a dangling target. Point
|
|
724
|
+
// those branches at the block the label actually names, before anything reads the CFG.
|
|
725
|
+
// A label naming DATA is emphatically NOT an alias, and this is the guard the whole pass turns
|
|
726
|
+
// on. Decode pushes an empty block for a literal-pool / jump-table label too, so aliasing them
|
|
727
|
+
// blindly would silently retarget `beq .Lpool` at whatever code happens to follow the pool —
|
|
728
|
+
// marker-free, plausible, wrong C where the frontend used to decline. Every agbcc pool is a
|
|
729
|
+
// label on data, so that is the common case, not an exotic one. A data label therefore neither
|
|
730
|
+
// aliases nor is aliased THROUGH: scanning past one for a later code block would silently jump
|
|
731
|
+
// over the data.
|
|
732
|
+
const isDataLabel = (l: string) => dataWords.has(l) || subwordData.has(l);
|
|
733
|
+
const aliasOf = new Map<string, string>();
|
|
734
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
735
|
+
if (blocks[i].instrs.length > 0 || isDataLabel(blocks[i].label)) {
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
let j = i + 1;
|
|
739
|
+
while (j < blocks.length && blocks[j].instrs.length === 0 && !isDataLabel(blocks[j].label)) {
|
|
740
|
+
j++;
|
|
741
|
+
}
|
|
742
|
+
const next = blocks[j];
|
|
743
|
+
if (next && next.instrs.length > 0) {
|
|
744
|
+
aliasOf.set(blocks[i].label, next.label);
|
|
745
|
+
} // otherwise a trailing or data-fronted label: left dangling so a branch to it still declines
|
|
746
|
+
}
|
|
747
|
+
for (const b of aliasOf.size ? blocks : []) {
|
|
748
|
+
for (const ins of b.instrs) {
|
|
749
|
+
const k = ins.ops.length - 1;
|
|
750
|
+
if ((ins.mnemonic === 'b' || COND_OPCODE[ins.mnemonic]) && k >= 0) {
|
|
751
|
+
ins.ops[k] = aliasOf.get(ins.ops[k]) ?? ins.ops[k];
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
604
755
|
let live = blocks.filter((b) => b.instrs.length > 0);
|
|
605
756
|
// Alignment-pad NOPs a splitter emits around returns and literal pools: `lsls r0, r0, #0`
|
|
606
757
|
// is the 0x0000 halfword, `mov r8, r8` is 0x46C0, plus a literal `nop`. A block made ONLY
|
|
@@ -724,6 +875,37 @@ function poolRef(operand: string, dataWords: Map<string, string[]>): PoolRef | n
|
|
|
724
875
|
return { kind: 'unmodelled', why: `pool word '${w}' is a symbol offset or code label` };
|
|
725
876
|
}
|
|
726
877
|
|
|
878
|
+
/** Does this function's literal pool name at least one EXTERNAL symbol?
|
|
879
|
+
*
|
|
880
|
+
* agbcc emits a pool word symbolically exactly when the source expression named a linker symbol,
|
|
881
|
+
* and numerically when it did not (`*(vu16 *)0x4000130`, an address-cast macro). So within one
|
|
882
|
+
* function, a numeric word sitting alongside a symbolic one is numeric *by the source's choice* —
|
|
883
|
+
* which is what lets {@link liftThumb} refuse to invent a name for it.
|
|
884
|
+
*
|
|
885
|
+
* The witness is required because that inference only holds for asm that KEPT its symbols. A
|
|
886
|
+
* linked-ROM disassembly resolves every relocation to a number, and there "numeric" says nothing
|
|
887
|
+
* about the source; vetoing on it would disable the map's naming for the users who need it most.
|
|
888
|
+
* A function whose pool names nothing external is therefore left alone (both spellings still
|
|
889
|
+
* enumerate, and the differ referees) rather than being read as evidence of anything.
|
|
890
|
+
*
|
|
891
|
+
* Labels DEFINED in this same asm — the jump-table pointer word, a pret-style `_08012358` pool
|
|
892
|
+
* label — are not external symbols and never witness: they survive disassembly whether or not
|
|
893
|
+
* relocations did. */
|
|
894
|
+
function poolNamesASymbol(dataWords: Map<string, string[]>, blockLabels: Set<string>): boolean {
|
|
895
|
+
for (const [, words] of dataWords) {
|
|
896
|
+
for (const raw of words) {
|
|
897
|
+
const w = raw.trim();
|
|
898
|
+
if (!/^[A-Za-z_]\w*$/.test(w) || w.startsWith('.L')) {
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
if (!dataWords.has(w) && !blockLabels.has(w)) {
|
|
902
|
+
return true;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
return false;
|
|
907
|
+
}
|
|
908
|
+
|
|
727
909
|
// Recover an agbcc Thumb jump-table dispatch. Given a dispatch block `disp` ending in `mov pc, rV`
|
|
728
910
|
// and its unique bounds predecessor `bounds` ending in `cmp rX,#(N-1); bhi DEF`, verify the exact
|
|
729
911
|
// idiom and read the inline table — else return null (→ the indirect-jump loud-fail fires). The
|
|
@@ -746,21 +928,47 @@ function recoverJumpTable(
|
|
|
746
928
|
disp: AsmBlock,
|
|
747
929
|
dataWords: Map<string, string[]>,
|
|
748
930
|
blockLabels: Set<string>,
|
|
931
|
+
longDefault?: string,
|
|
749
932
|
): JumpTable | null {
|
|
750
|
-
// bounds: last two instrs
|
|
933
|
+
// bounds: last two instrs are `cmp rX,#M` then the out-of-range guard, in one of two spellings.
|
|
934
|
+
//
|
|
935
|
+
// direct cmp rX,#M ; bhi DEF → fall through to the dispatch
|
|
936
|
+
// long jump cmp rX,#M ; bls DISP ; b DEF → branch TO the dispatch, long-branch the default
|
|
937
|
+
//
|
|
938
|
+
// The second is what agbcc emits whenever the default is out of a conditional branch's reach —
|
|
939
|
+
// Thumb-1 `B<cond>` carries a signed 8-bit HALFWORD offset, so ±256 BYTES, about 128
|
|
940
|
+
// instructions — which on a real switch it usually is: five of the six benchmark
|
|
941
|
+
// functions with a table use it, and only the sixth uses the direct form. `longDefault` is the
|
|
942
|
+
// target of that trailing `b`, read by the caller from the block after `bounds`.
|
|
751
943
|
const bi = bounds.instrs;
|
|
752
|
-
const
|
|
944
|
+
const guard = bi[bi.length - 1],
|
|
753
945
|
cmp = bi[bi.length - 2];
|
|
754
|
-
if (!
|
|
946
|
+
if (!guard || !cmp || cmp.mnemonic !== 'cmp') {
|
|
755
947
|
return null;
|
|
756
948
|
}
|
|
949
|
+
let defaultLabel: string;
|
|
950
|
+
if (longDefault === undefined) {
|
|
951
|
+
if (guard.mnemonic !== 'bhi') {
|
|
952
|
+
return null;
|
|
953
|
+
}
|
|
954
|
+
defaultLabel = guard.ops[0];
|
|
955
|
+
} else {
|
|
956
|
+
// The `bls` must name THIS dispatch block, or the guard belongs to some other branch and the
|
|
957
|
+
// `b` we picked up is not its default.
|
|
958
|
+
if (guard.mnemonic !== 'bls' || guard.ops[0] !== disp.label) {
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
961
|
+
defaultLabel = longDefault;
|
|
962
|
+
}
|
|
757
963
|
const scrutReg = cmp.ops[0];
|
|
758
964
|
const m = cmp.ops[1];
|
|
759
965
|
if (!m?.startsWith('#')) {
|
|
760
966
|
return null;
|
|
761
967
|
}
|
|
762
968
|
const n = imm(m) + 1; // cases 0..M → N = M+1
|
|
763
|
-
|
|
969
|
+
if (n < 1) {
|
|
970
|
+
return null; // a bound that admits no case at all is not a dispatch — fail closed
|
|
971
|
+
}
|
|
764
972
|
|
|
765
973
|
// disp: exactly the 5-op idiom, threading a single index register from `lsl rY,rX,#2`.
|
|
766
974
|
const d = disp.instrs;
|
|
@@ -797,11 +1005,28 @@ function recoverJumpTable(
|
|
|
797
1005
|
}
|
|
798
1006
|
|
|
799
1007
|
// Read the table: the ldr loads a POINTER word (PTR: .word TABLE); the table is TABLE: .word C0…
|
|
800
|
-
|
|
801
|
-
|
|
1008
|
+
// Note the case labels are matched against `blockLabels` as WRITTEN: the adjacent-label aliasing in
|
|
1009
|
+
// `decode` rewrites branch operands, not `.word` entries, so a table naming an aliased label would
|
|
1010
|
+
// decline here rather than dispatch anywhere. Loud, and no corpus instance — left as a known edge
|
|
1011
|
+
// rather than fixed speculatively.
|
|
1012
|
+
//
|
|
1013
|
+
// The pointer word is addressed the same way every other pool load in this frontend is —
|
|
1014
|
+
// `LABEL[+N]`, selecting word N/4 — because a literal pool is a POOL: agbcc packs the dispatch
|
|
1015
|
+
// pointer in beside whatever else the function needed, and which slot it lands in is an artifact
|
|
1016
|
+
// of emission order. Reading only a bare label whose pool held exactly ONE word declined six real
|
|
1017
|
+
// benchmark functions whose table pointer merely sat later in the pool. Same fix m2c made in
|
|
1018
|
+
// `a7c5c2d`, and the same shared POOL_LABEL grammar the const/gaddr resolvers use, so the three
|
|
1019
|
+
// cannot disagree about what `.L21+0x4` addresses.
|
|
1020
|
+
const pm = ptrLabel.match(POOL_LABEL);
|
|
1021
|
+
const ptrWords = pm ? dataWords.get(pm[1]) : undefined;
|
|
1022
|
+
if (!pm || !ptrWords) {
|
|
802
1023
|
return null;
|
|
803
1024
|
}
|
|
804
|
-
const
|
|
1025
|
+
const ptrOff = pm[2] ? Number(pm[2]) : 0;
|
|
1026
|
+
if (ptrOff % 4 !== 0 || ptrOff / 4 >= ptrWords.length) {
|
|
1027
|
+
return null; // misaligned or past the end of the pool — not a word this pool holds
|
|
1028
|
+
}
|
|
1029
|
+
const caseLabels = dataWords.get(ptrWords[ptrOff / 4].trim());
|
|
805
1030
|
if (!caseLabels || caseLabels.length !== n) {
|
|
806
1031
|
return null;
|
|
807
1032
|
} // table length must equal the bound
|
|
@@ -816,7 +1041,14 @@ function recoverJumpTable(
|
|
|
816
1041
|
/** Lift decoded asm → an L1 Fn with block-argument SSA. `prototypes` supplies each callee's
|
|
817
1042
|
* declared parameter count (from the project's headers); it is authoritative for recovering
|
|
818
1043
|
* how many argument registers a `bl` passes (falling back to a heuristic when absent). */
|
|
819
|
-
export function lift(
|
|
1044
|
+
export function lift(
|
|
1045
|
+
name: string,
|
|
1046
|
+
asm: string,
|
|
1047
|
+
target: TargetDescription,
|
|
1048
|
+
prototypes: Prototypes = {},
|
|
1049
|
+
_asmData?: AsmData,
|
|
1050
|
+
symbols?: SymbolMap,
|
|
1051
|
+
): Fn {
|
|
820
1052
|
assertInputFormat('thumb', 'gnu-as', asm);
|
|
821
1053
|
const { blocks: rawBlocks, dataWords } = decode(name, asm);
|
|
822
1054
|
|
|
@@ -825,28 +1057,54 @@ export function lift(name: string, asm: string, target: TargetDescription, proto
|
|
|
825
1057
|
// dispatch block is ELIDED from the CFG. A `mov pc` that is NOT a recognised table falls through
|
|
826
1058
|
// to the loud-fail below.
|
|
827
1059
|
const blockLabels = new Set(rawBlocks.map((b) => b.label));
|
|
1060
|
+
// Whether THIS asm preserves symbol names in its literal pools — the witness the numeric-pool
|
|
1061
|
+
// naming veto needs (see poolNamesASymbol).
|
|
1062
|
+
const poolNamesSymbols = poolNamesASymbol(dataWords, blockLabels);
|
|
828
1063
|
// Any label referenced as a branch target (so we can tell if an elided dispatch block has a SECOND
|
|
829
1064
|
// predecessor — a `b disp` from elsewhere — which would dangle after elision; decline if so).
|
|
830
|
-
|
|
1065
|
+
// How many branches name each label — not just whether any does, because the long-jump bounds
|
|
1066
|
+
// form legitimately branches to its own dispatch block exactly once.
|
|
1067
|
+
const branchRefs = new Map<string, number>();
|
|
831
1068
|
for (const b of rawBlocks) {
|
|
832
1069
|
for (const ins of b.instrs) {
|
|
833
1070
|
if ((ins.mnemonic === 'b' || COND_OPCODE[ins.mnemonic]) && ins.ops.length) {
|
|
834
|
-
|
|
1071
|
+
const t = ins.ops[ins.ops.length - 1];
|
|
1072
|
+
branchRefs.set(t, (branchRefs.get(t) ?? 0) + 1);
|
|
835
1073
|
}
|
|
836
1074
|
}
|
|
837
1075
|
}
|
|
838
1076
|
const tables = new Map<AsmBlock, JumpTable>(); // bounds block → recovered table
|
|
839
|
-
const elided = new Set<AsmBlock>(); // dispatch blocks removed from the CFG
|
|
1077
|
+
const elided = new Set<AsmBlock>(); // dispatch (and long-jump default) blocks removed from the CFG
|
|
840
1078
|
rawBlocks.forEach((d, i) => {
|
|
841
1079
|
const last = d.instrs[d.instrs.length - 1];
|
|
842
|
-
if (last
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
1080
|
+
if (!last || last.mnemonic !== 'mov' || last.ops[0] !== 'pc' || last.ops[1] === 'lr') {
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
const refs = branchRefs.get(d.label) ?? 0;
|
|
1084
|
+
const prev = rawBlocks[i - 1];
|
|
1085
|
+
// Direct form: the dispatch is reached ONLY by falling through from its bounds predecessor. A
|
|
1086
|
+
// `b disp` from anywhere else would leave a dangling edge after elision, so decline (→ loud-fail).
|
|
1087
|
+
if (prev && refs === 0) {
|
|
1088
|
+
const jt = recoverJumpTable(prev, d, dataWords, blockLabels);
|
|
1089
|
+
if (jt) {
|
|
1090
|
+
tables.set(prev, jt);
|
|
1091
|
+
elided.add(d);
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
// Long-jump form: `bounds` (cmp; bls DISP), then a lone `b DEF` block, then the dispatch. The
|
|
1096
|
+
// dispatch is entered by exactly that one `bls` and nothing else, and the `b DEF` block —
|
|
1097
|
+
// synthetically labelled, so unnameable and unreachable once the bounds block dispatches — is
|
|
1098
|
+
// elided WITH it. Leaving it would make it a parameterless predecessor of the default block,
|
|
1099
|
+
// and wiring a phi through it fabricates an entry parameter (the phantom-param miscompile).
|
|
1100
|
+
const boundsB = rawBlocks[i - 2];
|
|
1101
|
+
const prevNamed = prev ? (branchRefs.get(prev.label) ?? 0) > 0 : false;
|
|
1102
|
+
if (refs === 1 && prev && boundsB && !prevNamed && prev.instrs.length === 1 && prev.instrs[0].mnemonic === 'b') {
|
|
1103
|
+
const jt = recoverJumpTable(boundsB, d, dataWords, blockLabels, prev.instrs[0].ops[0]);
|
|
847
1104
|
if (jt) {
|
|
848
|
-
tables.set(
|
|
1105
|
+
tables.set(boundsB, jt);
|
|
849
1106
|
elided.add(d);
|
|
1107
|
+
elided.add(prev);
|
|
850
1108
|
}
|
|
851
1109
|
}
|
|
852
1110
|
});
|
|
@@ -980,7 +1238,7 @@ export function lift(name: string, asm: string, target: TargetDescription, proto
|
|
|
980
1238
|
// adjustments have no low-register data destination, so they fall through harmlessly;
|
|
981
1239
|
// terminators are handled in the terminator section below.
|
|
982
1240
|
const isThumbReg = (s: string | undefined): s is string => /^r\d+$/.test(s ?? '');
|
|
983
|
-
const emitOpaqueDest = (ins: { mnemonic: string; ops: string[] }) => {
|
|
1241
|
+
const emitOpaqueDest = (ins: { mnemonic: string; ops: string[]; asWritten?: string }) => {
|
|
984
1242
|
// storeClass: unmodelled Thumb stores are str*/stm* — `stmia rN!, {…}`'s dest token `r0!`
|
|
985
1243
|
// fails isReg, so without this it would be skipped as "no reg dest", silently deleting the
|
|
986
1244
|
// memory writes AND the base writeback. push/pop stay transparent frame ops (they don't match).
|
|
@@ -992,6 +1250,7 @@ export function lift(name: string, asm: string, target: TargetDescription, proto
|
|
|
992
1250
|
storeClass: /^(str|stm)/,
|
|
993
1251
|
skipSafe: /^(push|pop|nop)$/,
|
|
994
1252
|
context: name,
|
|
1253
|
+
display: ins.asWritten,
|
|
995
1254
|
});
|
|
996
1255
|
if (!od) {
|
|
997
1256
|
return;
|
|
@@ -999,7 +1258,7 @@ export function lift(name: string, asm: string, target: TargetDescription, proto
|
|
|
999
1258
|
const operands = od.srcRegs.map((r) => readVar(r, bi));
|
|
1000
1259
|
const res = mkValue(T.unk(32));
|
|
1001
1260
|
// carry the mnemonic so annotate mode can name the gap (`ASMLIFT_ERROR("unmodelled 'rsb'")`)
|
|
1002
|
-
irb.ops.push(mkOp('opaque', { operands, results: [res], attrs: { mnemonic: ins.mnemonic } }));
|
|
1261
|
+
irb.ops.push(mkOp('opaque', { operands, results: [res], attrs: { mnemonic: ins.asWritten ?? ins.mnemonic } }));
|
|
1003
1262
|
writeVar(od.dst, bi, res);
|
|
1004
1263
|
};
|
|
1005
1264
|
// 2-operand ALU form `op rD, op2` (rD = rD ⟨op⟩ op2). `op2` is an immediate (`#N`) or a
|
|
@@ -1028,6 +1287,22 @@ export function lift(name: string, asm: string, target: TargetDescription, proto
|
|
|
1028
1287
|
if (classifyXfer(ins)) {
|
|
1029
1288
|
continue;
|
|
1030
1289
|
}
|
|
1290
|
+
// A Thumb-1 data-processing instruction on LOW registers writes the condition flags whether or
|
|
1291
|
+
// not the mnemonic carries the `s` (agbcc spells `adds r0,r0,r3` as `add r0,r0,r3`, and the
|
|
1292
|
+
// assembler picks the flag-setting encoding) — so an instruction between a `cmp` and its branch
|
|
1293
|
+
// REPLACES the flags the branch will test. Folding the earlier `cmp` in anyway would emit a
|
|
1294
|
+
// condition on the wrong operands: silently wrong C with no marker. Drop the pending compare
|
|
1295
|
+
// and let the terminator's existing "no reaching compare in its block" decline fire — the loud
|
|
1296
|
+
// answer, since modelling arithmetic flags is a capability asmlift does not have.
|
|
1297
|
+
//
|
|
1298
|
+
// The HIGH-register forms (`mov rD,rH`, `add rD,rH`) do NOT set flags and stay transparent,
|
|
1299
|
+
// which is what keeps agbcc's callee-saved shuffling from tripping this. Measured free: across
|
|
1300
|
+
// every agbcc row in the benchmark, no conditional-branch block has ANY instruction between its
|
|
1301
|
+
// compare and the branch — compilers keep the pair adjacent. The inhabitant this guards is
|
|
1302
|
+
// hand-written asm in the playground, where there is no oracle to catch a lie.
|
|
1303
|
+
if (FLAG_SETTING.has(ins.mnemonic) && /^r[0-7]$/.test(reg(ins.ops[0] ?? ''))) {
|
|
1304
|
+
pendingCmp = null;
|
|
1305
|
+
}
|
|
1031
1306
|
const [a, b, c] = ins.ops;
|
|
1032
1307
|
switch (ins.mnemonic) {
|
|
1033
1308
|
case 'mov':
|
|
@@ -1167,9 +1442,18 @@ export function lift(name: string, asm: string, target: TargetDescription, proto
|
|
|
1167
1442
|
// rejoin below also tolerates a split list defensively. Thumb-1 LDMIA skips the
|
|
1168
1443
|
// writeback when rN is itself in the list (the loaded value wins) — modelled; any
|
|
1169
1444
|
// malformed shape degrades to the loud opaque.
|
|
1170
|
-
//
|
|
1171
|
-
//
|
|
1172
|
-
//
|
|
1445
|
+
// There is NO no-writeback form in Thumb-1, so the `!` is decoration and must not drive
|
|
1446
|
+
// the model. Four sources agree:
|
|
1447
|
+
// * ARM DDI 0029G Table 1-7 gives the canonical syntax as `LDMIA Rb!, <reglist>` and
|
|
1448
|
+
// `STMIA Rb!, <reglist>` — the `!` is part of the mnemonic, not an option, and
|
|
1449
|
+
// Figure 1-6 Format 15 has no bit that could encode its absence;
|
|
1450
|
+
// * GNU as assembles `ldm r1,{r0}` and `ldm r1!,{r0}` to the same halfword, 0xc901,
|
|
1451
|
+
// and warns "this instruction will write back the base register";
|
|
1452
|
+
// * gba-kit executes both with the base advanced by 4;
|
|
1453
|
+
// * GBATEK, THUMB.15: "Both STM and LDM are incrementing the Base Register".
|
|
1454
|
+
// An earlier version of this comment called the `!`-less spelling "the valid
|
|
1455
|
+
// no-writeback form — same transfers, base unchanged", which is false, and the code
|
|
1456
|
+
// below acted on it. A missing register list is malformed → loud opaque.
|
|
1173
1457
|
const baseTok = a;
|
|
1174
1458
|
const writeback = !!baseTok?.endsWith('!');
|
|
1175
1459
|
if (baseTok === undefined || b === undefined || !b.startsWith('{')) {
|
|
@@ -1196,6 +1480,30 @@ export function lift(name: string, asm: string, target: TargetDescription, proto
|
|
|
1196
1480
|
emitOpaqueDest(ins);
|
|
1197
1481
|
break;
|
|
1198
1482
|
}
|
|
1483
|
+
// An STM whose base is in its own list, but is not the LOWEST entry, stores a value this
|
|
1484
|
+
// frontend must not guess — because the available references DISAGREE about what it is.
|
|
1485
|
+
//
|
|
1486
|
+
// ARM: UNPREDICTABLE, "the stored value cannot be relied upon".
|
|
1487
|
+
// GNU as: warns "value stored for rN is UNKNOWN".
|
|
1488
|
+
// GBATEK: version-specific — "Store OLD base if Rb is FIRST entry in Rlist,
|
|
1489
|
+
// otherwise store NEW base (STM/ARMv4), always store OLD base (STM/ARMv5)".
|
|
1490
|
+
// mGBA: stores the OLD base unconditionally, on an ARMv4T core — its STM_LOOP
|
|
1491
|
+
// reads gprs[i] during the loop and the writeback runs after it.
|
|
1492
|
+
//
|
|
1493
|
+
// So GBATEK's ARMv4 rule and the reference emulator's behaviour do not agree, and no
|
|
1494
|
+
// hardware test result was found either way. This frontend used to emit the old base,
|
|
1495
|
+
// i.e. it silently picked one side of that disagreement. Declining is the contract:
|
|
1496
|
+
// where the architecture declines to define a value, so do we.
|
|
1497
|
+
//
|
|
1498
|
+
// (One site in the Klonoa corpus, in unreachable code after a `pop`/`bx`, and it already
|
|
1499
|
+
// declines for an unrelated pc-relative-pool reason — so this costs nothing today.)
|
|
1500
|
+
if (ins.mnemonic === 'stmia' && list.some((r) => reg(r) === baseReg) && reg(list[0]) !== baseReg) {
|
|
1501
|
+
throw new FrontendUnsupportedError(
|
|
1502
|
+
`cannot lift '${name}': stm with the base register in its own list, not as the lowest ` +
|
|
1503
|
+
`entry — the value stored for that register is UNPREDICTABLE and differs between ` +
|
|
1504
|
+
`ARMv4 (new base) and ARMv5 (old base)`,
|
|
1505
|
+
);
|
|
1506
|
+
}
|
|
1199
1507
|
// SNAPSHOT the base ONCE: hardware performs every transfer from the ORIGINAL base, but
|
|
1200
1508
|
// a base-in-list ldmia overwrites that register mid-list — re-reading it per iteration
|
|
1201
1509
|
// loaded the siblings from the freshly-loaded value instead (silent wrong addresses,
|
|
@@ -1212,10 +1520,12 @@ export function lift(name: string, asm: string, target: TargetDescription, proto
|
|
|
1212
1520
|
irb.ops.push(mkOp('store', { operands: [base0, readData(reg(r), bi)], attrs: { off: 4 * i, width: 4 } }));
|
|
1213
1521
|
}
|
|
1214
1522
|
});
|
|
1215
|
-
// Writeback advances the base by 4×count
|
|
1216
|
-
//
|
|
1523
|
+
// Writeback advances the base by 4×count. It is suppressed ONLY for an ldmia whose base
|
|
1524
|
+
// is in its own list — the loaded value wins. GBATEK, THUMB.15: "no writeback
|
|
1525
|
+
// (LDM/ARMv4/ARMv5; at this point, THUMB opcodes work different than ARM opcodes)".
|
|
1526
|
+
// The `!` is NOT what decides it: see above, there is no encoding without writeback.
|
|
1217
1527
|
const wroteBase = ins.mnemonic === 'ldmia' && list.some((r) => reg(r) === baseReg);
|
|
1218
|
-
if (
|
|
1528
|
+
if (!wroteBase) {
|
|
1219
1529
|
const adv = mkValue(T.unk(32));
|
|
1220
1530
|
irb.ops.push(mkOp('add', { operands: [base0, constVal(4 * list.length, bi)], results: [adv] }));
|
|
1221
1531
|
writeVar(baseReg, bi, adv);
|
|
@@ -1278,6 +1588,54 @@ export function lift(name: string, asm: string, target: TargetDescription, proto
|
|
|
1278
1588
|
if (ins.mnemonic === 'ldr' && b !== undefined) {
|
|
1279
1589
|
const pr = poolRef(b, dataWords);
|
|
1280
1590
|
if (pr?.kind === 'const') {
|
|
1591
|
+
// Numeric-pool PROMOTION (symbols.ts): a pool-loaded word whose value the
|
|
1592
|
+
// project's symbol map knows becomes the NAMED global's address — the same
|
|
1593
|
+
// `gaddr` the symbol-pool path emits, so everything downstream is the existing
|
|
1594
|
+
// named-global machinery. Only pool-loaded words promote (an address built by
|
|
1595
|
+
// arithmetic never reaches here); a promoted `code` symbol carries `code: true`
|
|
1596
|
+
// so the structurer spells it `(u32)Name`, not `&Name`.
|
|
1597
|
+
//
|
|
1598
|
+
// VETOED when this asm's pool names other symbols (poolNamesASymbol): agbcc would
|
|
1599
|
+
// have emitted THIS word symbolically too had the source named it, so promoting it
|
|
1600
|
+
// spells a name the source did not use.
|
|
1601
|
+
//
|
|
1602
|
+
// …but the veto is really about RELOCATION, not about naming. An `extern` name makes
|
|
1603
|
+
// the compiler emit a relocated pool word, which contradicts the numeric word the
|
|
1604
|
+
// target shows. An address-cast MACRO expands to that same numeric literal, so it is
|
|
1605
|
+
// COMPATIBLE with the evidence by construction and is never vetoed — indeed it is
|
|
1606
|
+
// the spelling the numeric word is evidence FOR (klonoa's true source reaches these
|
|
1607
|
+
// cells through exactly such macros). Nothing is guessed in either case: a vetoed
|
|
1608
|
+
// word stays the raw constant the target says it is.
|
|
1609
|
+
const found = symbols ? lookupSymbol(symbols, pr.value) : null;
|
|
1610
|
+
const si = found && (!poolNamesSymbols || found.macroBody !== undefined) ? found : null;
|
|
1611
|
+
if (si) {
|
|
1612
|
+
const res = mkValue(T.unk(32));
|
|
1613
|
+
irb.ops.push(
|
|
1614
|
+
mkOp('gaddr', {
|
|
1615
|
+
results: [res],
|
|
1616
|
+
attrs: { sym: si.name, ...(si.kind === 'code' ? { code: true } : {}) },
|
|
1617
|
+
}),
|
|
1618
|
+
);
|
|
1619
|
+
writeVar(reg(a), bi, res);
|
|
1620
|
+
break;
|
|
1621
|
+
}
|
|
1622
|
+
// INTERIOR attribution: a value strictly inside a sized data symbol becomes
|
|
1623
|
+
// `gaddr sym + offset` — the `&gSym + K` tree structure.ts already lowers (and,
|
|
1624
|
+
// with a struct layout, spells as the named field). Sized symbols only; an
|
|
1625
|
+
// unattributed address stays a raw const — nothing guesses.
|
|
1626
|
+
// Interior attribution is always an `&gSym + K` spelling — extern-shaped, hence
|
|
1627
|
+
// relocated — so the veto applies to it without the macro exemption above.
|
|
1628
|
+
const interior = symbols && !poolNamesSymbols ? lookupInterior(symbols, pr.value) : null;
|
|
1629
|
+
if (interior) {
|
|
1630
|
+
const g = mkValue(T.unk(32));
|
|
1631
|
+
const k = mkValue(T.unk(32));
|
|
1632
|
+
const res = mkValue(T.unk(32));
|
|
1633
|
+
irb.ops.push(mkOp('gaddr', { results: [g], attrs: { sym: interior.info.name } }));
|
|
1634
|
+
irb.ops.push(mkOp('const', { results: [k], attrs: { value: interior.offset } }));
|
|
1635
|
+
irb.ops.push(mkOp('add', { operands: [g, k], results: [res] }));
|
|
1636
|
+
writeVar(reg(a), bi, res);
|
|
1637
|
+
break;
|
|
1638
|
+
}
|
|
1281
1639
|
const res = mkValue(T.unk(32));
|
|
1282
1640
|
irb.ops.push(mkOp('const', { results: [res], attrs: { value: pr.value } }));
|
|
1283
1641
|
writeVar(reg(a), bi, res);
|
|
@@ -1303,9 +1661,19 @@ export function lift(name: string, asm: string, target: TargetDescription, proto
|
|
|
1303
1661
|
}
|
|
1304
1662
|
const width = /b/.test(ins.mnemonic) ? 1 : /h/.test(ins.mnemonic) ? 2 : 4;
|
|
1305
1663
|
const signed = ins.mnemonic === 'ldr' || /s/.test(ins.mnemonic.slice(3));
|
|
1306
|
-
const { base, off } = parseAddr(b);
|
|
1664
|
+
const { base, off, regOff } = parseAddr(b);
|
|
1665
|
+
// `[rB, rX]` register-offset: lower EXACTLY as `rB + rX` then a load at offset 0 —
|
|
1666
|
+
// the same address arithmetic the encoding performs. (parseAddr used to silently
|
|
1667
|
+
// read `[rB]`, dropping the index — a silent miscompile; ldrsh exists ONLY in this
|
|
1668
|
+
// form in Thumb-1, so every ldrsh went through here.)
|
|
1669
|
+
let baseVal = readData(base, bi);
|
|
1670
|
+
if (regOff !== undefined) {
|
|
1671
|
+
const sum = mkValue(T.unk(32));
|
|
1672
|
+
irb.ops.push(mkOp('add', { operands: [baseVal, readData(regOff, bi)], results: [sum] }));
|
|
1673
|
+
baseVal = sum;
|
|
1674
|
+
}
|
|
1307
1675
|
const res = mkValue(T.unk(32));
|
|
1308
|
-
irb.ops.push(mkOp('load', { operands: [
|
|
1676
|
+
irb.ops.push(mkOp('load', { operands: [baseVal], results: [res], attrs: { off, width, signed } }));
|
|
1309
1677
|
writeVar(reg(a), bi, res);
|
|
1310
1678
|
break;
|
|
1311
1679
|
}
|
|
@@ -1318,8 +1686,15 @@ export function lift(name: string, asm: string, target: TargetDescription, proto
|
|
|
1318
1686
|
break;
|
|
1319
1687
|
}
|
|
1320
1688
|
const width = /b/.test(ins.mnemonic) ? 1 : /h/.test(ins.mnemonic) ? 2 : 4;
|
|
1321
|
-
const { base, off } = parseAddr(b);
|
|
1322
|
-
|
|
1689
|
+
const { base, off, regOff } = parseAddr(b);
|
|
1690
|
+
let storeBase = readData(base, bi);
|
|
1691
|
+
if (regOff !== undefined) {
|
|
1692
|
+
// register-offset store: same exact `rB + rX` lowering as the load path above
|
|
1693
|
+
const sum = mkValue(T.unk(32));
|
|
1694
|
+
irb.ops.push(mkOp('add', { operands: [storeBase, readData(regOff, bi)], results: [sum] }));
|
|
1695
|
+
storeBase = sum;
|
|
1696
|
+
}
|
|
1697
|
+
irb.ops.push(mkOp('store', { operands: [storeBase, readData(reg(a), bi)], attrs: { off, width } }));
|
|
1323
1698
|
break;
|
|
1324
1699
|
}
|
|
1325
1700
|
case 'bl':
|
|
@@ -1364,7 +1739,20 @@ export function lift(name: string, asm: string, target: TargetDescription, proto
|
|
|
1364
1739
|
irb.ops.push(mkOp('br', { successors: [succ(fallLabel(bi))] }));
|
|
1365
1740
|
} else if (kind === 'return') {
|
|
1366
1741
|
// bx lr / pop {…,pc} / mov pc,lr
|
|
1367
|
-
|
|
1742
|
+
//
|
|
1743
|
+
// A `bx rN` BRANCHES THROUGH rN, so at that instruction rN holds the RETURN ADDRESS. When rN
|
|
1744
|
+
// is the return-VALUE register the two uses collide, and the address wins by definition —
|
|
1745
|
+
// whatever value was in r0 is gone, so the function cannot be returning one. agbcc spells an
|
|
1746
|
+
// interworking return that way (`push {lr}` … `pop {r0}; bx r0`), and reading r0 as a value
|
|
1747
|
+
// there invents a return the machine provably cannot make: a phantom `return`, a non-`void`
|
|
1748
|
+
// signature that would contradict the project's own prototype, and a live range that keeps
|
|
1749
|
+
// otherwise-dead computation alive.
|
|
1750
|
+
//
|
|
1751
|
+
// The other return forms are untouched, because none of them writes the return register:
|
|
1752
|
+
// `bx lr` and `bx r1`/`bx r2` branch through a different one, and `pop {…,pc}` / `mov pc,lr`
|
|
1753
|
+
// load PC directly. Only the register actually branched through is disqualified.
|
|
1754
|
+
const viaReturnReg = last.mnemonic === 'bx' && last.ops[0] === target.returnReg;
|
|
1755
|
+
irb.ops.push(mkOp('ret', { operands: viaReturnReg ? [] : [readVar(target.returnReg, bi)] }));
|
|
1368
1756
|
} else if (kind === 'uncond') {
|
|
1369
1757
|
irb.ops.push(mkOp('br', { successors: [succ(last.ops[0])] }));
|
|
1370
1758
|
} else if (kind === 'cond') {
|