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.

Biografia

Sono uno specialista in materia di sicurezza informatica, scrittore, contributore per il progetto Monero, una criptovaluta che è focalizzata nel proteggere le informazioni sulle transazioni. Il libro che ho pubblicato Mastering Monero è diventato una delle migliori risorse per padroneggiare Monero. Più informazioni su di me

Seguimi su Twitter o scrivimi una e-mail. Le donazioni sono molto apprezzate, mi permettono di continuare a lavorare e a scrivere.

Mastering Monero Book