Decompiler Construction: Chapter 17 Lowering IR to Readable and Executable Code
Translation
Lowering is a translation, not a pass. If the output is ugly, the problem is upstream or it needs output helpers.
This is the last stage of the pipeline. The IR has reached a fixed point, control flow has been recovered, types have been refined, and memory has been inferred. All that is left is printing it.
That sounds simple and it is where a lot of things can fall apart because there are two goals here and they pull in opposite directions:
- Readability -> A person can understand what the original code did.
- Executability -> The output recompiles and behaves identically.
You cannot maximize both at once.
If the IR is broken your output will be broken! This step does not check for correctness we only want a possible direct translation.
The Core Idea
Given any keyword we want an identical and safe logical format we can express it in with any given syntax. Sometimes it may be impossible to implement this in every language but we can utalize any IR special keywords that allow the user to mutate it on there own so we can still use the same IR.
Statement Emitting
A lot of common statements will have an almost direct translation to the given target:
- if x then -> C: if (x) {
- end -> C++: }
- call(x) -> lua: call(x)
- **memset
(x)** -> lua: memset(t, x)
Any memory operation should be wrapped in a helper function to minimize compatibility and read errors.
Expression Emitting
A lot of IR expression keywords have almost a direct translation. Follows statement emissions from above.
Limitations
Some IR may not have a 1:1 translation to the target output so its best practice to keep the input IR statements either use a direct IR type for logic or contruct the logic with primitive types.
Example
Given input IR string reprensentation:
1
2
3
4
r = 0;
while (r < 100) do
r = 9;
end
We can represent this in many different languages:
Lua
1
2
3
4
r = 0;
while (r < 100) do
r = 9;
end
LuaU
1
2
3
4
r = 0;
while (r < 100) do
r = 9;
end
C
1
2
3
4
r = 0;
while (r < 100) {
r = 9;
}
C++
1
2
3
4
r = 0;
while (r < 100) {
r = 9;
}
Rust
1
2
3
4
r = 0;
while r < 100 {
r = 9;
}
The IR gives us flags about each expression like if its constant or declared or not. We assumed in this example it is not constant and has been pre-defined.