Skip to content

Commit 0b13ff7

Browse files
committed
Decompiler: convert flag arithmetic to conditional jumps
1 parent 0d98cd5 commit 0b13ff7

7 files changed

Lines changed: 413 additions & 6 deletions

File tree

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using Cpp2IL.Core.Analysis;
4+
using Cpp2IL.Core.Graphs;
5+
using Cpp2IL.Core.ISIL;
6+
7+
namespace Cpp2IL.Core.Tests.Analysis;
8+
9+
public class FlagConditionRecoveryTests
10+
{
11+
private static readonly LocalVariable A = new("a", new Register(null, "a"));
12+
private static readonly LocalVariable B = new("b", new Register(null, "b"));
13+
14+
private static LocalVariable Flag(string name) => new(name, new Register(null, name));
15+
16+
/// <summary>
17+
/// Builds a graph from a flag-cluster + conditional jump, resolving the jump target like the
18+
/// lifter does, and returns the instruction defining <paramref name="condition"/> after recovery.
19+
/// </summary>
20+
private static Instruction RecoverAndGetConditionDef(List<Instruction> flagAndBranch, LocalVariable condition)
21+
{
22+
// Append two trivial return blocks so the conditional jump has a real target/fallthrough.
23+
var index = flagAndBranch.Count;
24+
var instructions = new List<Instruction>(flagAndBranch)
25+
{
26+
new(index, OpCode.Return),
27+
new(index + 1, OpCode.Return),
28+
};
29+
30+
// The conditional jump's target (operand 0) is the last instruction's index.
31+
var conditionalJump = instructions.First(i => i.OpCode == OpCode.ConditionalJump);
32+
conditionalJump.Operands[0] = instructions[index + 1];
33+
34+
var graph = new ISILControlFlowGraph(instructions);
35+
FlagConditionRecovery.Run(graph);
36+
37+
return graph.Blocks.SelectMany(b => b.Instructions)
38+
.First(i => i.Destination is LocalVariable d && ReferenceEquals(d, condition));
39+
}
40+
41+
[Test]
42+
public void RecoversEquality()
43+
{
44+
// cmp a, b ; je -> ZF = (a - b) == 0 ; if ZF
45+
var t1 = Flag("TEMP1");
46+
var zf = Flag("ZF");
47+
48+
var def = RecoverAndGetConditionDef(new List<Instruction>
49+
{
50+
new(0, OpCode.Subtract, t1, A, B),
51+
new(1, OpCode.CheckEqual, zf, t1, 0),
52+
new(2, OpCode.ConditionalJump, 0, zf),
53+
}, zf);
54+
55+
Assert.That(def.OpCode, Is.EqualTo(OpCode.CheckEqual));
56+
Assert.That(def.Operands[1], Is.EqualTo(A));
57+
Assert.That(def.Operands[2], Is.EqualTo(B));
58+
}
59+
60+
[Test]
61+
public void RecoversInequality()
62+
{
63+
// cmp a, b ; jne -> ZF = (a - b) == 0 ; cond = !ZF ; if cond
64+
var t1 = Flag("TEMP1");
65+
var zf = Flag("ZF");
66+
var cond = Flag("TEMP");
67+
68+
var def = RecoverAndGetConditionDef(new List<Instruction>
69+
{
70+
new(0, OpCode.Subtract, t1, A, B),
71+
new(1, OpCode.CheckEqual, zf, t1, 0),
72+
new(2, OpCode.Not, cond, zf),
73+
new(3, OpCode.ConditionalJump, 0, cond),
74+
}, cond);
75+
76+
Assert.That(def.OpCode, Is.EqualTo(OpCode.CheckNotEqual));
77+
Assert.That(def.Operands[1], Is.EqualTo(A));
78+
Assert.That(def.Operands[2], Is.EqualTo(B));
79+
}
80+
81+
[Test]
82+
public void RecoversSignedLessThan()
83+
{
84+
// cmp a, b ; jl -> full flag cluster ; cond = !(SF == OF) ; if cond
85+
var t1 = Flag("TEMP1");
86+
var t2 = Flag("TEMP2");
87+
var t3 = Flag("TEMP3");
88+
var t4 = Flag("TEMP4");
89+
var of = Flag("OF");
90+
var sf = Flag("SF");
91+
var sfEqOf = Flag("TEMP_a");
92+
var cond = Flag("TEMP_b");
93+
94+
var def = RecoverAndGetConditionDef(new List<Instruction>
95+
{
96+
new(0, OpCode.Subtract, t1, A, B),
97+
new(1, OpCode.Xor, t2, A, B),
98+
new(2, OpCode.Xor, t3, A, t1),
99+
new(3, OpCode.And, t4, t2, t3),
100+
new(4, OpCode.CheckLess, of, t4, 0),
101+
new(5, OpCode.CheckLess, sf, t1, 0),
102+
new(6, OpCode.CheckEqual, sfEqOf, sf, of),
103+
new(7, OpCode.Not, cond, sfEqOf),
104+
new(8, OpCode.ConditionalJump, 0, cond),
105+
}, cond);
106+
107+
Assert.That(def.OpCode, Is.EqualTo(OpCode.CheckLess));
108+
Assert.That(def.Operands[1], Is.EqualTo(A));
109+
Assert.That(def.Operands[2], Is.EqualTo(B));
110+
}
111+
112+
[Test]
113+
public void RecoversSignedGreaterOrEqual()
114+
{
115+
// cmp a, b ; jge -> cond = (SF == OF) ; if cond
116+
var t1 = Flag("TEMP1");
117+
var t2 = Flag("TEMP2");
118+
var t3 = Flag("TEMP3");
119+
var t4 = Flag("TEMP4");
120+
var of = Flag("OF");
121+
var sf = Flag("SF");
122+
var cond = Flag("TEMP");
123+
124+
var def = RecoverAndGetConditionDef(new List<Instruction>
125+
{
126+
new(0, OpCode.Subtract, t1, A, B),
127+
new(1, OpCode.Xor, t2, A, B),
128+
new(2, OpCode.Xor, t3, A, t1),
129+
new(3, OpCode.And, t4, t2, t3),
130+
new(4, OpCode.CheckLess, of, t4, 0),
131+
new(5, OpCode.CheckLess, sf, t1, 0),
132+
new(6, OpCode.CheckEqual, cond, sf, of),
133+
new(7, OpCode.ConditionalJump, 0, cond),
134+
}, cond);
135+
136+
Assert.That(def.OpCode, Is.EqualTo(OpCode.CheckGreaterOrEqual));
137+
Assert.That(def.Operands[1], Is.EqualTo(A));
138+
Assert.That(def.Operands[2], Is.EqualTo(B));
139+
}
140+
141+
[Test]
142+
public void LeavesUnrecognisedConditionsAlone()
143+
{
144+
// A conditional jump on a plain boolean local with no flag pattern must be untouched.
145+
var cond = Flag("someBool");
146+
var def = RecoverAndGetConditionDef(new List<Instruction>
147+
{
148+
new(0, OpCode.Move, cond, 1),
149+
new(1, OpCode.ConditionalJump, 0, cond),
150+
}, cond);
151+
152+
Assert.That(def.OpCode, Is.EqualTo(OpCode.Move));
153+
}
154+
}
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
using System.Collections.Generic;
2+
using Cpp2IL.Core.Graphs;
3+
using Cpp2IL.Core.ISIL;
4+
using Cpp2IL.Core.Model.Contexts;
5+
6+
namespace Cpp2IL.Core.Analysis;
7+
8+
/// <summary>
9+
/// Recovers high-level relational conditions from the explicit EFLAGS computations the lifter emits.
10+
///
11+
/// The x86 lifter models a <c>cmp</c>/<c>test</c> as a cluster of flag pseudo-registers
12+
/// (ZF = (a-b)==0, SF = (a-b)&lt;0, OF = signed overflow, ...) and lowers each <c>jcc</c> into a
13+
/// boolean expression over those flags. This pass recognises those canonical shapes for every
14+
/// <see cref="OpCode.ConditionalJump"/> condition and rewrites the condition's defining instruction
15+
/// into a single relational comparison (==, !=, &lt;, &lt;=, &gt;, &gt;=) on the original compare
16+
/// operands. The now-orphaned flag arithmetic is removed by the dead-code pass that runs next.
17+
///
18+
/// Runs in SSA form, where each flag/temporary has a single, version-stable definition, so the
19+
/// operands referenced at the branch are provably the ones captured at the compare.
20+
///
21+
/// Note: the lifter lowers the unsigned conditions (ja/jae/jb/jbe) with the same flag expressions as
22+
/// their signed counterparts, so they are recovered as signed comparisons too - matching the
23+
/// existing (signed) behaviour rather than introducing a new inaccuracy.
24+
/// </summary>
25+
public static class FlagConditionRecovery
26+
{
27+
public static void Run(MethodAnalysisContext method) => Run(method.ControlFlowGraph!);
28+
29+
public static void Run(ISILControlFlowGraph cfg)
30+
{
31+
var defOf = BuildDefMap(cfg);
32+
33+
foreach (var block in cfg.Blocks)
34+
{
35+
foreach (var instruction in block.Instructions)
36+
{
37+
if (instruction.OpCode != OpCode.ConditionalJump)
38+
continue;
39+
40+
if (instruction.Operands[1] is not LocalVariable condition)
41+
continue;
42+
43+
if (!TryClassify(condition, defOf, out var relop, out var op0, out var op1))
44+
continue;
45+
46+
// Rewrite the condition's defining instruction in place into a single comparison.
47+
// Its destination (the condition local the branch reads) is preserved.
48+
var definition = defOf[condition];
49+
definition.OpCode = relop;
50+
definition.Operands = new List<object> { definition.Operands[0], op0!, op1! };
51+
}
52+
}
53+
}
54+
55+
private static Dictionary<LocalVariable, Instruction> BuildDefMap(ISILControlFlowGraph cfg)
56+
{
57+
var defs = new Dictionary<LocalVariable, Instruction>();
58+
59+
foreach (var block in cfg.Blocks)
60+
foreach (var instruction in block.Instructions)
61+
if (instruction.Destination is LocalVariable destination)
62+
defs[destination] = instruction;
63+
64+
return defs;
65+
}
66+
67+
private static bool TryClassify(LocalVariable condition, Dictionary<LocalVariable, Instruction> defOf,
68+
out OpCode relop, out object? op0, out object? op1)
69+
{
70+
relop = default;
71+
72+
// ZF on its own => a == b (je)
73+
if (IsZeroFlag(condition, defOf, out op0, out op1)) { relop = OpCode.CheckEqual; return true; }
74+
// SF on its own => a < b (js; exact for the common test-against-self case)
75+
if (IsSignFlag(condition, defOf, out op0, out op1)) { relop = OpCode.CheckLess; return true; }
76+
77+
var definition = Def(condition, defOf);
78+
if (definition == null)
79+
return false;
80+
81+
switch (definition.OpCode)
82+
{
83+
case OpCode.Not:
84+
var inner = AsLocal(definition.Operands[1]);
85+
if (IsZeroFlag(inner, defOf, out op0, out op1)) { relop = OpCode.CheckNotEqual; return true; } // !ZF => != (jne)
86+
if (IsSignFlag(inner, defOf, out op0, out op1)) { relop = OpCode.CheckGreaterOrEqual; return true; } // !SF => >= (jns)
87+
if (IsSignEqualsOverflow(inner, defOf, out op0, out op1)) { relop = OpCode.CheckLess; return true; } // !(SF==OF) => < (jl/jb)
88+
return false;
89+
90+
case OpCode.CheckEqual:
91+
// SF == OF => a >= b (jge/jae)
92+
if (IsSignFlag(AsLocal(definition.Operands[1]), defOf, out op0, out op1)) { relop = OpCode.CheckGreaterOrEqual; return true; }
93+
return false;
94+
95+
case OpCode.And:
96+
// (SF==OF) && !ZF => a > b (jg/ja)
97+
if (IsSignGreater(definition, defOf, out op0, out op1)) { relop = OpCode.CheckGreater; return true; }
98+
return false;
99+
100+
case OpCode.Or:
101+
// !(SF==OF) || ZF => a <= b (jle/jbe)
102+
if (IsSignLessOrEqual(definition, defOf, out op0, out op1)) { relop = OpCode.CheckLessOrEqual; return true; }
103+
return false;
104+
105+
default:
106+
return false;
107+
}
108+
}
109+
110+
// ZF: local := CheckEqual(t, 0) where t := Subtract(a, b)
111+
private static bool IsZeroFlag(LocalVariable? local, Dictionary<LocalVariable, Instruction> defOf, out object? op0, out object? op1)
112+
{
113+
op0 = op1 = null;
114+
var def = Def(local, defOf);
115+
if (def is not { OpCode: OpCode.CheckEqual } || !IsZeroConstant(def.Operands[2]))
116+
return false;
117+
return IsSubtraction(AsLocal(def.Operands[1]), defOf, out op0, out op1);
118+
}
119+
120+
// SF: local := CheckLess(t, 0) where t := Subtract(a, b)
121+
private static bool IsSignFlag(LocalVariable? local, Dictionary<LocalVariable, Instruction> defOf, out object? op0, out object? op1)
122+
{
123+
op0 = op1 = null;
124+
var def = Def(local, defOf);
125+
if (def is not { OpCode: OpCode.CheckLess } || !IsZeroConstant(def.Operands[2]))
126+
return false;
127+
return IsSubtraction(AsLocal(def.Operands[1]), defOf, out op0, out op1);
128+
}
129+
130+
// local := Subtract(a, b)
131+
private static bool IsSubtraction(LocalVariable? local, Dictionary<LocalVariable, Instruction> defOf, out object? op0, out object? op1)
132+
{
133+
op0 = op1 = null;
134+
var def = Def(local, defOf);
135+
if (def is not { OpCode: OpCode.Subtract })
136+
return false;
137+
op0 = def.Operands[1];
138+
op1 = def.Operands[2];
139+
return true;
140+
}
141+
142+
// local := CheckEqual(SF, OF) - the signed "not less" test. Operands are taken from the SF side.
143+
private static bool IsSignEqualsOverflow(LocalVariable? local, Dictionary<LocalVariable, Instruction> defOf, out object? op0, out object? op1)
144+
{
145+
op0 = op1 = null;
146+
var def = Def(local, defOf);
147+
if (def is not { OpCode: OpCode.CheckEqual })
148+
return false;
149+
return IsSignFlag(AsLocal(def.Operands[1]), defOf, out op0, out op1);
150+
}
151+
152+
// local := Not(CheckEqual(SF, OF))
153+
private static bool IsNotSignEqualsOverflow(LocalVariable? local, Dictionary<LocalVariable, Instruction> defOf, out object? op0, out object? op1)
154+
{
155+
op0 = op1 = null;
156+
var def = Def(local, defOf);
157+
if (def is not { OpCode: OpCode.Not })
158+
return false;
159+
return IsSignEqualsOverflow(AsLocal(def.Operands[1]), defOf, out op0, out op1);
160+
}
161+
162+
// local := Not(ZF)
163+
private static bool IsNotZeroFlag(LocalVariable? local, Dictionary<LocalVariable, Instruction> defOf)
164+
{
165+
var def = Def(local, defOf);
166+
return def is { OpCode: OpCode.Not } && IsZeroFlag(AsLocal(def.Operands[1]), defOf, out _, out _);
167+
}
168+
169+
// And((SF==OF), !ZF), in either operand order
170+
private static bool IsSignGreater(Instruction and, Dictionary<LocalVariable, Instruction> defOf, out object? op0, out object? op1)
171+
{
172+
var left = AsLocal(and.Operands[1]);
173+
var right = AsLocal(and.Operands[2]);
174+
175+
if (IsSignEqualsOverflow(left, defOf, out op0, out op1) && IsNotZeroFlag(right, defOf))
176+
return true;
177+
if (IsSignEqualsOverflow(right, defOf, out op0, out op1) && IsNotZeroFlag(left, defOf))
178+
return true;
179+
180+
op0 = op1 = null;
181+
return false;
182+
}
183+
184+
// Or(!(SF==OF), ZF), in either operand order
185+
private static bool IsSignLessOrEqual(Instruction or, Dictionary<LocalVariable, Instruction> defOf, out object? op0, out object? op1)
186+
{
187+
var left = AsLocal(or.Operands[1]);
188+
var right = AsLocal(or.Operands[2]);
189+
190+
if (IsNotSignEqualsOverflow(left, defOf, out op0, out op1) && IsZeroFlag(right, defOf, out _, out _))
191+
return true;
192+
if (IsNotSignEqualsOverflow(right, defOf, out op0, out op1) && IsZeroFlag(left, defOf, out _, out _))
193+
return true;
194+
195+
op0 = op1 = null;
196+
return false;
197+
}
198+
199+
private static Instruction? Def(LocalVariable? local, Dictionary<LocalVariable, Instruction> defOf)
200+
=> local != null && defOf.TryGetValue(local, out var def) ? def : null;
201+
202+
private static LocalVariable? AsLocal(object operand) => operand as LocalVariable;
203+
204+
private static bool IsZeroConstant(object operand) =>
205+
operand switch
206+
{
207+
int v => v == 0,
208+
long v => v == 0,
209+
uint v => v == 0,
210+
ulong v => v == 0,
211+
short v => v == 0,
212+
byte v => v == 0,
213+
sbyte v => v == 0,
214+
_ => false
215+
};
216+
}

Cpp2IL.Core/ISIL/Instruction.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ public object? Destination
5656
case OpCode.CheckEqual:
5757
case OpCode.CheckGreater:
5858
case OpCode.CheckLess:
59+
case OpCode.CheckNotEqual:
60+
case OpCode.CheckGreaterOrEqual:
61+
case OpCode.CheckLessOrEqual:
5962
if (newDestination != null)
6063
Operands[0] = newDestination;
6164
return IsConstantValue(Operands[0]) ? null : Operands[0];
@@ -80,6 +83,7 @@ or OpCode.And or OpCode.Or or OpCode.Xor
8083
OpCode.Call => Operands.Skip(2).ToList(),
8184
OpCode.CallVoid or OpCode.Phi => Operands.Skip(1).ToList(),
8285
OpCode.CheckEqual or OpCode.CheckGreater or OpCode.CheckLess
86+
or OpCode.CheckNotEqual or OpCode.CheckGreaterOrEqual or OpCode.CheckLessOrEqual
8387
=> [Operands[1], Operands[2]],
8488

8589
_ => []

0 commit comments

Comments
 (0)