Post

Decompiler Construction: Chapter 19 - Practical Decompilation Results and Pipeline Evaluation

To stress-test the process, I have included this program, which includes:

  • State-dependent control flow with continuously mutating variables
  • Mixed arithmetic and bitwise operations (XOR, shifts, rotation patterns)
  • Early loop terminations
  • Pointers
  • Redundant computations that should be simplified Note. The following implementation used is closed source as of 2026.

The following input program is compiled and passed through the decompilation process:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdint.h>
volatile int32_t sink;
typedef struct {
    int32_t a; int32_t b; int32_t c;
} Node;
int32_t func(int32_t *arr, const Node *n, const int32_t x) {
	
    int32_t acc = 0;
    int32_t i = 0;
    int32_t state = (x ^ 0x5A) + (n->a << 1);
    while (i < 10) {
        state = state ^ (i * 3);
        if ((state & 1) == 0) { acc += arr[i] + n->b; } else { acc += arr[i] ^ n->c; }
        if (acc > 100) { break; }
        ++i;
        if (!(i % 3)) { continue; }
        state = (state << 1) | (state >> 31);
    }
    int32_t tmp = acc * 2;
    tmp = tmp - acc;
    if (x < 0) { tmp = tmp ^ 0xDEADBEEF; }
    sink = tmp;
    return acc + state;
}
int32_t main(void) {
    int32_t arr = 9;
    for (int32_t t = 0; t < 1000; ++t) { 
        Node n = { .a = t, .b = t ^ 0xAA, .c = t + 3};  func(&arr, &n, 19); }
    return 0;
}

To verify our output is right the original code was ran 5 times with printf after func call

1
printf("OUTPUT arr(%d), n->a(%d), n->b(%d), n->c(%d)\n", arr, n.a, n.b, n.c);

Output:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
OUTPUT arr(9), n->a(0), n->b(170), n->c(3)
OUTPUT arr(9), n->a(1), n->b(171), n->c(4)
OUTPUT arr(9), n->a(2), n->b(168), n->c(5)
OUTPUT arr(9), n->a(3), n->b(169), n->c(6)
OUTPUT arr(9), n->a(4), n->b(174), n->c(7)
OUTPUT arr(9), n->a(5), n->b(175), n->c(8)
OUTPUT arr(9), n->a(6), n->b(172), n->c(9)
OUTPUT arr(9), n->a(7), n->b(173), n->c(10)
OUTPUT arr(9), n->a(8), n->b(162), n->c(11)
OUTPUT arr(9), n->a(9), n->b(163), n->c(12)
OUTPUT arr(9), n->a(10), n->b(160), n->c(13)
OUTPUT arr(9), n->a(11), n->b(161), n->c(14)
OUTPUT arr(9), n->a(12), n->b(166), n->c(15)
OUTPUT arr(9), n->a(13), n->b(167), n->c(16)
....

Typically this step you would use DBI but for for this example we have already compiled and extracted machine code.

Assembly, this was compiled with CLANG.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
0x0: push rbp
0x1: mov rbp, rsp
0x4: sub rsp, 0x20
0x8: mov dword ptr [rbp - 4], 0
0xf: mov dword ptr [rbp - 8], 9
0x16: mov dword ptr [rbp - 0xc], 0
0x1d: cmp dword ptr [rbp - 0xc], 0x3e8
0x24: jge 0x5d
0x26: mov eax, dword ptr [rbp - 0xc]
0x29: mov dword ptr [rbp - 0x18], eax
0x2c: mov eax, dword ptr [rbp - 0xc]
0x2f: xor eax, 0xaa
0x34: mov dword ptr [rbp - 0x14], eax
0x37: mov eax, dword ptr [rbp - 0xc]
0x3a: add eax, 3
0x3d: mov dword ptr [rbp - 0x10], eax
0x40: lea rdi, [rbp - 8]
0x44: lea rsi, [rbp - 0x18]
0x48: mov edx, 0x13
0x4d: call 0x65
....

Download Full Assembly

We will lift it to IL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
107 SHRN r264, r263, 32 
108 XOR r265, r263, r264 
109 MOVE r263, r265 
110 SHRN r266, r263, 16 
111 XOR r267, r263, r266 
112 MOVE r263, r267 
113 SHRN r268, r263, 8 
114 XOR r269, r263, r268 
115 MOVE r263, r269 
116 SHRN r270, r263, 4 
117 XOR r271, r263, r270 
118 MOVE r263, r271 
119 SHRN r272, r263, 2 
120 XOR r273, r263, r272 
121 MOVE r263, r273 
122 SHRN r274, r263, 1 
123 XOR r275, r263, r274 
124 MOVE r263, r275 
125 ANDN r276, r263, 1 
126 BITNOT r277, r276 
127 ANDN r278, r277, 1 
128 FLAGSET 16, r278 
129 BITCAST r250, r250, 64, 0, false 
130 MOVE r44, r250 
131 NOP 
132 BITCAST r248, r248, 32, 0, false 
133 LOADINT r249, 0 
134 MOVE r248, r249 
135 BITCAST r250, r250, 32, 0, false
.... 

Download Full IL

We will then lift the IL to IR

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
r260 = memread<int32_t>(r250)
r259 = (r260 - r255)
r259 = int32_t(r259)
r262 = memread<int32_t>(r250)
r261 = (r262 & 15)
r261 = int32_t(r261)
r263 = (r253 & 15)
r263 = int32_t(r263)
r264 = (r261 + r263)
r264 = int32_t(r264)
r265 = (r264 > 15)
r265 = int32_t(r265)
r266 = (r259 & 15)
r266 = int32_t(r266)
r268 = memread<int32_t>(r250)
r267 = (r268 & 15)
r267 = int32_t(r267)
r269 = (r266 < r267)
r269 = int32_t(r269)
r270 = (r265 | r269)
r270 = int32_t(r270)
r270 = int32_t(r270)
....

Download Full IR

Generated CFG: UNOPTCFG Graph

Download GraphViz Example

The IR currently is unsimplified. We will use various passes to simplify it, here are the most common ones:

  • Constant folding
  • Page constructions
  • Dead code elimination
  • Jump threading
  • Loop reconstruction
  • Expression canonicalization
  • Loop simplification
  • Constant Propagation
  • Dead store elimination Note. Full list as of 2026 is proprietary

Completely Safe IR:

1
2
3


C Code Geneation


Prev Chapter: Chapter 18 - Lowering IR to Readable and Executable Code

This post is licensed under CC BY 4.0 by the author.