15. I dati sullo stack

Finora abbiamo considerato solo accessi di codice e memoria a variabili e registri globali. Tuttavia, molte variabili sono allocate nello stack come variabili locali alle funzioni. Per scoprire queste variabili dobbiamo analizzare le istruzioni che accedono allo stack.

Ma lo stack non viene utilizzato solo per memorizzare variabili locali. I suoi scopi includono:

  • fornire l’archiviazione per le variabili locali alle funzioni, inclusa la creazione di più copie delle stesse variabili in caso di funzioni ricorsive.
  • fornire un luogo in cui salvare i registri che non possono essere modificati durante una chiamata di funzione, come stabilito dalle convenzioni di chiamata per quel processore, compilatore e sistema operativo.
  • fornire un luogo in cui salvare il puntatore del frame e l’indirizzo di ritorno, nonché gli argomenti passati alla funzione che non possono essere passati tramite i registri.
  • A volte, lo stack viene utilizzato anche per fornire memoria temporanea per restituire strutture o qualsiasi valore che non può essere restituito tramite registri.
  • Infine, lo stack può essere utilizzato per salvare e ripristinare i registri durante la valutazione di espressioni complesse, quando non ci sono abbastanza registri per contenere tutti gli operandi dell’espressione.

Tutte queste attività vengono eseguite a volte con pochissimi indizi lasciati dal compilatore nel flusso di istruzioni. La ricostruzione degli accessi allo stack aggiunge una notevole complessità all’analizzatore di espressioni e merita un proprio modulo specializzato all’interno dell’architettura del decompilatore. Infatti, più di un modulo può essere dedicato alla ricostruzione dello stack, quindi dedicheremo un po’ di tempo a descrivere come funzionano questi moduli per le diverse informazioni che sono collegate allo stack.

Si noti che lo stack non è direttamente visibile a un linguaggio di alto livello come C o C++ (e ancor meno a linguaggi come Java e C#), tranne per il fatto che alcune variabili sono dichiarate come variabili automatiche (con l’opzionale e obsoleto C parola chiave “auto”). Pertanto un decompilatore dovrebbe rimuovere tutti i riferimenti allo stack trovati nel flusso di istruzioni poiché non erano certamente presenti nel programma originale di alto livello.

Funzioni con frame e senza frame

Per capire come rimuovere i riferimenti allo stack è utile capire come lo stack è partizionato dal compilatore. La maggior parte dei programmi non ottimizzati genera codice che utilizza un registro fisso per accedere allo stack, chiamato frame pointer (spesso abbreviato in FP, sebbene nell’architettura x86 il registro EBP sia solitamente dedicato a questo compito. Useremo principalmente FP, poiché è il processore neutrale, tranne quando si mostra il codice x86.)

Il puntatore del frame viene utilizzato per puntare all’inizio di un elenco collegato di frame. Ogni frame contiene dati specifici per una chiamata di funzione. L’uso di un frame pointer semplifica il compito del compilatore di generare codice (rendendo compilazioni più veloci), e specialmente il lavoro del debugger di localizzare variabili locali, registri salvati e indirizzi di ritorno.

È importante notare che esiste un frame per ogni chiamata attiva di una funzione, quindi se stai scrivendo un debugger dovresti presumere che potrebbero esserci più frame per la stessa funzione, nel caso in cui la funzione sia ricorsiva. Poiché un decompilatore non si occupa delle informazioni di runtime, questo non è un problema per lo scrittore del decompilatore, tranne in un caso di cui parleremo nella sezione avanzata.

Tuttavia, per i programmi compilati da un compilatore di ottimizzazione, il frame di solito non viene generato. Ciò ha il vantaggio che il registro FP è libero di essere utilizzato per altri scopi e che il codice generato è più breve e più veloce, poiché il codice di ingresso e di uscita non ha bisogno di creare una voce nell’elenco collegato di puntatori di frame. Sfortunatamente questo crea molti problemi ai decompilatori (così come ai debugger), perché ora il decompilatore non sa quanto è grande l’area delle variabili locali per ogni funzione.

Per comprendere meglio il ruolo dello stack, consideriamo prima le funzioni con frame e poi passiamo alle più complesse funzioni senza frame.

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