18. Registri salvati

La parte finale dell’analisi dello stack frame prevede il rilevamento dei registri salvati.

I registri salvati sono quei registri che sono obbligati da una certa convenzione di chiamata a essere preservati attraverso le chiamate di funzione. Questo concetto è molto importante per un compilatore (e quindi per un decompilatore) per poter allocare variabili ai registri ed evitare di dover salvare in memoria quei registri ogni volta che c’è una chiamata ad una funzione che potrebbe essere stata generata da un altro compilatore.

Vale la pena notare che diverse convenzioni di chiamata definiscono diversi insiemi di registri da conservare tra le chiamate di funzione. Questo perché finché entrambi i lati, il lato chiamante e il lato della funzione chiamata, concordano su quali informazioni devono essere conservate, il compilatore può generare il codice più efficiente per quella particolare chiamata.

Nel contesto di un decompilatore, dobbiamo rilevare quali registri sono conservati da ciascuna funzione, in modo da poter calcolare esattamente le informazioni di attività per ciascun registro.

Il codice di base per salvare i registri viene generato dai compilatori come parte del prologo della funzione dopo aver impostato lo stack frame. Corrispondentemente, ogni registro viene ripristinato nell’epilogo della funzione, prima della deallocazione dello stack frame. Per esempio:

Framed functions              Frame-less functions

   PUSH FP
   FP = SP
   SP -= #local_size            SP -= #local_size
   PUSH R1                      PUSH R1
   PUSH R2                      PUSH R2
   ...                          ...
   POP  R2                      POP R2
   POP  R1                      POP R1
   SP = FP                      SP += #local_size
   RET                          RET

In che modo le informazioni sui registri salvate influiscono sulla compressione delle espressioni? Considera la seguente sequenza di codice:

   R1 = 1
   R2 = 2            R2 = 2
   PUSH R1
   CALL FUNC         FUNC(R1)           FUNC(1)
   ADD SP, #4
   M1400 = R2        M1400 = R2         M1400 = 2?

Come possiamo essere sicuri che il valore 2 sia ancora nel registro R2 dopo la chiamata alla funzione? Solo se sappiamo che FUNC() non utilizza né modifica R2, possiamo comprimere R2 = 2 e M1400 = R2 in un’unica istruzione. Se FUNC() utilizza R2 come uno dei suoi valori di input, non possiamo rimuovere l’assegnazione R2 = 2. Se FUNC() modifica R2, è possibile che R2 sia in realtà un output della funzione e potremmo effettivamente dover tradurre la chiamata in M1400 = FUNC(R1).

La compressione di R2 = 2 e M1400 = R2 può essere eseguita utilizzando le informazioni sull’attività, ma solo se le informazioni sull’attività sono accurate per FUNC(). Ciò significa che dobbiamo tradurre FUNC() prima di tradurre la funzione che chiama FUNC(). Come decidiamo l’ordine di analisi delle funzioni? Possiamo utilizzare una tecnica collaudata, ovvero calcolare il depth-first order (DFO) per ciascuna funzione e quindi visitare prima le funzioni con l’ID DFO più alto.

Il seguente algoritmo calcola l’ID DFO per ogni funzione:

   id = procList.length
   computeDFO(entryFunction)
   qsort(procList, by_DFO_id)
   for each function in procList in reverse order
      computeInputOutputRegisters(function)

computeDFO(function){
    int succ;

    function.visited = true
    for each destFunc in function.callsTo
        if destFunc.visited
            continue
        computeDFO(destFunc)
    }
    function.dfoID = id
    --id
} 

Dopo questo algoritmo, ogni funzione avrà un ID DFO, con funzioni foglia (funzioni che non chiamano nessun’altra funzione) con ID più alti, mentre le funzioni più vicine al punto di ingresso avranno ID più bassi.

Possiamo quindi analizzare le funzioni con l’id DFO più alto. Quelle saranno inizialmente funzioni foglia. Poiché non avranno alcuna chiamata di funzione, possiamo calcolare con precisione l’insieme di registri di input, registri di output e registri salvati. Mentre continuiamo ad analizzare le funzioni con ID DFO inferiori, possiamo essere sicuri che tutte le funzioni che chiamano saranno già state analizzate, quindi possiamo essere sicuri di quali registri sono attivi in ​​un dato punto del programma.

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