16. Funzioni con stack frame
Le funzioni che utilizzano un frame pointer sono facilmente riconoscibili, poiché impostano il frame nel loro codice di prologo (il codice generato per l’apertura “{”) e rimuovono il frame nel loro codice di epilogo (il codice generato per la chiusura “}” ). Diversi processori possono persino avere istruzioni specializzate per impostare e rimuovere il telaio:
Tuttavia, è perfettamente legale per il compilatore utilizzare istruzioni più semplici per impostare il frame, e questo è in effetti ciò che accade con i compilatori moderni, che tengono conto della velocità di esecuzione di diverse istruzioni (per qualche motivo Intel ha deciso di non ottimizzare il istruzioni di entrata e uscita, preferendo ottimizzare istruzioni individuali e più semplici). Pertanto, un decompilatore dovrebbe essere in grado di riconoscere ogni possibile sequenza di codice che imposta e rimuove il frame. In realtà le istruzioni specializzate di cui sopra possono essere suddivise in istruzioni che hanno lo stesso comportamento, come nella tabella seguente:
Sequenze orientate ai frame x86:
PUSH EBP
EBP = ESP
ESP = ESP - ##
ESP = EBP
POP EBP
La traduzione delle istruzioni specializzate in sequenze di istruzioni equivalenti può essere lasciata all’analizzatore del flusso di istruzioni, in modo che le altre parti del decompilatore non debbano mai vedere le istruzioni specializzate.
La rimozione di queste istruzioni è importante per alcuni motivi:
queste istruzioni non possono essere generate direttamente dal codice utente. Sono implicitamente generati dai { e } della funzione.
l’impostazione del frame di solito indica quanto spazio è riservato alle variabili locali, quindi è utile rilevare queste informazioni per la fase successiva (sostituzione delle variabili locali)
riducendo il numero di istruzioni, in particolare le istruzioni che rimuovono lo stack, si apre la porta a ulteriori riduzioni, come la sostituzione di un " goto " all’ultimo blocco con un’istruzione " return “.
L’ultimo caso è esemplificato nella seguente sequenza di trasformazioni:
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
} }
con la rimozione dell’etichetta L2 e di 2 istruzioni goto .
Poiché lo stack pointer e il frame pointer sono concetti generici, usati da molti compilatori e processori, ha senso implementare la rimozione del prologo e dell’epilogo nella parte indipendente dal processore del decompilatore. Un codice come il seguente potrebbe essere facilmente implementato:
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
Riconoscimento variabili locali
L’aver stabilito che una funzione usa il frame pointer ci permette automaticamente di determinare quale variabile è una variabile locale della funzione e quale è un parametro in ingresso (eccetto per i parametri passati nei registri, che saranno discussi più avanti). Alle variabili locali si accede con offset negativi dal puntatore del frame, mentre agli argomenti si accede con offset positivi:
R1 = *(FP + 4) // argument (e.g. "arg4")
R1 = *(FP - 12) // local variable (e.g. "local12")
Analizzando la sequenza di istruzioni e sostituendo ogni accesso utilizzando l’FP in una variabile sintetica, è possibile rimuovere i riferimenti al puntatore del frame e “creare” variabili o argomenti locali:
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
}
Notare una ruga nel codice precedente: i riferimenti di memoria, come (FP + 4) (che sono indirizzi), vengono convertiti in variabili denominate (che per impostazione predefinita sono valori)! Questo non è corretto , poiché viene aggiunto un indirizzamento implicito quando si accede a una variabile denominata. Pertanto abbiamo bisogno di aggiungere un ulteriore operatore " AddressOf " per mantenere la semantica delle istruzioni originali, nel caso in cui le istruzioni originali stessero calcolando l’indirizzo di una variabile locale o di un argomento. In altre parole:
(FP + 4) == &arg4
*(FP + 4) == *(&arg4) == arg4
(FP - 12) == &local12
*(FP - 12) == *(&local12) == local12
Quindi le precedenti istruzioni n = var dovrebbero essere sostituite con:
n = new Node(AddressOf)
n.op1 = var
La sequenza *(&var) viene sostituita con var da un passaggio di pulizia che semplifica le espressioni per renderle più belle quando vengono emesse nell’output decompilato.