Decompiler Design - Stack Frames |
|
Prev: The Stack
Functions that use a frame pointer are easily recognizable, since they set up the frame in their prologue code (the code generated for the opening "{") and they remove the frame in their epilogue code (the code generated for the closing "}"). Different processors may even have specialized instructions to set up and remove the frame:
x86 frame-oriented instructions | 68K frame-oriented instructions | |
ENTER ## LEAVE |
LINK A6, ## UNLK A6 |
Set up the frame (function's "{") Remove the frame (function's "}") |
However, it's perfectly legal for the compiler to use simpler instructions to set up the frame, and that in fact is what happens with modern compilers, which take into account the speed of execution of different instructions (for some reason Intel decided to not optimize the enter and leave instructions, preferring to optimize individual, simpler instructions). Therefore, a decompiler should be able to recognize any possible code sequence that sets up and removes the frame. Actually the above specialized instructions can be broken down into instructions that have the same behavior, as in the following table:
x86 frame-oriented sequences | |
PUSH EBP EBP = ESP ESP = ESP - ## ESP = EBP POP EBP |
Set up the frame (function's "{") ## = size of locals Remove the frame (function's "}") |
The translation of the specialized instructions into equivalent instruction sequences can be left to the instruction stream analyzer, so that the other parts of the decompiler won't ever have to see the specialized instructions.
Removing these instructions is important for a few motives:
The last case is exemplified in the following sequence of transformations:
PUSH FP
PUSH(FP) {
{
FP = SP FP = SP
...
...
...
...
CMP R1,#0 if(R1 == 0)
if(R1 == 0) if(R1 == 0)
JEQ L2
goto L2
goto L2
return
...
...
...
...
CMP R2,#0 if(R2 > 0)
if(R2 > 0) if(R2 > 0)
JGT L2
goto L2
goto L2
return
...
...
...
...
L2: L2:
L2:
SP = FP SP = FP
POP FP
POP(FP)
RET
return
return
}
}
with the removal of the L2 label and of 2 goto statements.
Since the stack pointer and the frame pointer are generic concepts, used by many compilers and processors, it makes sense to implement the removal of the prologue and epilogue in the processor-independent part of the decompiler. Code such as the following could be easily implemented:
proc.isFramed = false // by
default, not framed
FPreg = processor.getFramePtrReg
SPreg = processor.getStackPtrReg
top = entry_block.firstInstruction
bottom = exit_block.lastInstruction
if bottom.op == Return
bottom = bottom.previousInstruction
topNode = top.expr
bottomNode = bottom.expr
if !topNode.isPush(FPreg) || !bottomNode.isPop(FPreg)
return
// not a framed function
top = top.nextInstruction
entry_block.Remove(top.previousInstruction) // remove
PUSH FP
bottom = bottom.previousInstruction
exit_block.Remove(bottom.nextInstruction)
// remove POP FP
topNode = top.expr
bottomNode = bottom.expr
if !topNode.isAssignment(FPreg, SPreg) ||
!bottomNode.isAssignment(SPreg, FPreg)
return
// not a framed function
top = top.nextInstruction
entry_block.Remove(top.previousInstruction) // remove
FP = SP
exit_block.Remove(bottom)
// remove SP = FP
proc.isFramed = true
proc.sizeOfLocals = 0
topNode = top.expr
if !topNode.isAssignment(SPreg) ||
!topNode.op2.isSubtract(SPreg,
ConstantValue)
return
// framed, but no local variables
proc.sizeOfLocals = -topNode.op2.value
Having determined that a function uses the frame pointer automatically allows us to determine which variable is a local variable of the function and which is an incoming parameter (except for parameters passed in registers, which will be discussed later). Local variables are accessed at negative offsets from the frame pointer, while arguments are accessed at positive offsets:
R1 = *(FP + 4) // argument (e.g. "arg4")
R1 = *(FP - 12) // local variable (e.g.
"local12")
By scanning the instruction sequence and replacing each access using the FP into a synthetic variable allows us to remove references to the frame pointer and "create" local variables or arguments:
for each instr in proc
replaceFramePtr(instr.expr)
replaceFramePtr(Node n)
{
if n.op1 != null
replaceFramePtr(n.op1)
if n.op2 != null
replaceFramePtr(n.op2)
if n.isAdd(FPreg, ConstantValue)
name = "arg" + n.op2.value
var = proc.arguments.find(name)
if var == null
var = new
Variable(name)
proc.arguments.add(var)
n = var
// replace (FP + #) with var
else if n.isSub(FPreg, ConstantValue)
name = "local" + n.op2.value
var = proc.locals.find(name)
if var == null
var = new
Variable(name)
proc.locals.add(var)
n = var
// replace (FP - #) with var
}
Note one wrinkle in the above code: memory references, such as (FP + 4) (which are addresses), are being converted to named variables (which by default are values)! This is incorrect, as an implicit indirection is added when accessing a named variable. Therefore we need to add an additional "AddressOf" operator to keep the semantic of the original instructions, in case the original instructions were computing the address of a local variable or argument. In other words:
(FP + 4) ==
&arg4
*(FP + 4) == *(&arg4)
== arg4
(FP - 12) ==
&local12
*(FP - 12) == *(&local12) == local12
Thus the above n = var statements should be replaced with:
n = new Node(AddressOf)
n.op1 = var
The sequence *(&var) is replaced with var by a clean-up pass that simplifies expressions to make them better-looking when emitted to the decompiled output.
Other resources:
http://unixwiz.net/techtips/win32-callconv-asm.html
Next: Frame-less functions