Lo stack di chiamate potrebbe anche essere chiamato stack di frame.
Le cose che vengono impilate dopo il principio LIFO non sono le variabili locali ma l'intero stack frame ("chiamate") delle funzioni chiamate . Le variabili locali vengono inserite e inserite insieme a quei frame nel cosiddetto prologo della funzione e nell'epilogo , rispettivamente.
All'interno del frame l'ordine delle variabili è completamente non specificato; I compilatori "riordinano" le posizioni delle variabili locali all'interno di un frame in modo appropriato per ottimizzare il loro allineamento in modo che il processore possa recuperarle il più rapidamente possibile. Il fatto cruciale è che l'offset delle variabili rispetto a un indirizzo fisso è costante per tutta la durata del frame , quindi è sufficiente prendere un indirizzo di ancoraggio, ad esempio l'indirizzo del frame stesso, e lavorare con gli offset di quell'indirizzo per le variabili. Un tale indirizzo di ancoraggio è effettivamente contenuto nella cosiddetta base o frame pointerche è memorizzato nel registro EBP. Gli offset, d'altra parte, sono chiaramente noti in fase di compilazione e sono quindi codificati nel codice macchina.
Questo grafico da Wikipedia mostra come è strutturato il tipico stack di chiamate 1 :
Aggiungiamo l'offset di una variabile a cui vogliamo accedere all'indirizzo contenuto nel frame pointer e otteniamo l'indirizzo della nostra variabile. Detto così brevemente, il codice vi accede direttamente tramite offset costanti del tempo di compilazione dal puntatore di base; È semplice aritmetica del puntatore.
Esempio
#include <iostream>
int main()
{
char c = std::cin.get();
std::cout << c;
}
gcc.godbolt.org ci fornisce
main:
pushq %rbp
movq %rsp, %rbp
subq $16, %rsp
movl std::cin, %edi
call std::basic_istream<char, std::char_traits<char> >::get()
movb %al, -1(%rbp)
movsbl -1(%rbp), %eax
movl %eax, %esi
movl std::cout, %edi
call [... the insertion operator for char, long thing... ]
movl $0, %eax
leave
ret
.. per main
. Ho diviso il codice in tre sottosezioni. Il prologo della funzione è costituito dalle prime tre operazioni:
- Il puntatore di base viene inserito nella pila.
- Il puntatore dello stack viene salvato nel puntatore di base
- Il puntatore allo stack viene sottratto per fare spazio alle variabili locali.
Quindi cin
viene spostato nel registro EDI 2 e get
viene chiamato; Il valore restituito è in EAX.
Fin qui tutto bene. Ora accade la cosa interessante:
Il byte di ordine inferiore di EAX, designato dal registro a 8 bit AL, viene preso e memorizzato nel byte subito dopo il puntatore di base : ovvero -1(%rbp)
, l'offset del puntatore di base è -1
. Questo byte è la nostra variabilec
. L'offset è negativo perché lo stack cresce verso il basso su x86. L'operazione successiva viene memorizzata c
in EAX: EAX viene spostato in ESI, cout
viene spostato in EDI e quindi viene chiamato l'operatore di inserimento con cout
e c
essendo gli argomenti.
Finalmente,
- Il valore restituito di
main
è memorizzato in EAX: 0. Ciò è dovuto return
all'istruzione implicita . Potresti anche vedere xorl rax rax
invece di movl
.
- lasciare e tornare al sito della chiamata.
leave
sta abbreviando questo epilogo e implicitamente
- Sostituisce il puntatore dello stack con il puntatore di base e
- Apre il puntatore di base.
Dopo che questa operazione ret
è stata eseguita, il frame è stato effettivamente rimosso, sebbene il chiamante debba ancora ripulire gli argomenti poiché stiamo usando la convenzione di chiamata cdecl. Altre convenzioni, ad esempio stdcall, richiedono al chiamato di riordinare, ad esempio passando la quantità di byte a ret
.
Omissione del puntatore del fotogramma
È anche possibile non utilizzare offset dal puntatore base / frame ma invece dallo stack pointer (ESB). Ciò rende il registro EBP che altrimenti conterrebbe il valore del puntatore del frame disponibile per un uso arbitrario, ma può rendere impossibile il debug su alcune macchine e sarà implicitamente disattivato per alcune funzioni . È particolarmente utile quando si compila per processori con solo pochi registri, incluso x86.
Questa ottimizzazione è nota come FPO (frame pointer omission) e impostata da -fomit-frame-pointer
in GCC e -Oy
in Clang; si noti che è implicitamente attivato da ogni livello di ottimizzazione> 0 se e solo se il debugging è ancora possibile, poiché non ha costi a parte questo. Per ulteriori informazioni vedere qui e qui .
1 Come sottolineato nei commenti, il puntatore del frame è presumibilmente destinato a puntare all'indirizzo dopo l'indirizzo del mittente.
2 Notare che i registri che iniziano con R sono le controparti a 64 bit di quelli che iniziano con E. EAX designa i quattro byte di ordine inferiore di RAX. Ho usato i nomi dei registri a 32 bit per chiarezza.