Since i,j, and k are on the stack, they will be located relative to the stack pointer.
Prima di tutto, questa affermazione è chiaramente sbagliata in un caso comune con i compilatori moderni. Sembra che tu ti abitui a guardare solo a compilatori molto vecchi, che hanno davvero funzionato nel modo in cui assegnano le posizioni sullo stack, mappano le variabili e utilizzano i registri solo quando richiesto esplicitamente (con register
parola chiave) o per valori temporanei. (Realizzi qualche compito con i compilatori MS-DOS e 16-bit?) Questo è un modo molto antico.
I compilatori moderni (GCC, Clang / LLVM, ecc.) utilizzano SSA e le tecniche di accompagnamento. Con SSA, non ci sono "variabili" internamente; ci sono valori quasi immutabili (eccetto i punti di fusione del percorso di esecuzione), ognuno di essi può ottenere un proprio spazio, in un registro o in pila, a seconda della frequenza con cui viene utilizzato e di quanto verrà utilizzato nuovamente. Se hai chiamato una variabile "int i", può essere in EAX in una parte della funzione, in EDI in un'altra, allo stack quando viene scambiata dal pool di registri ...
A volte, una variabile viene completamente ottimizzata, se il compilatore presuppone che sia più facile utilizzarne un'altra o anche una costante. A volte, è presente, ma modificato (ad esempio, il ciclo da i
da 1 a N, se accede a un array come x [i-1], viene modificato in ciclo da 0 a N-1 e accesso come x [i]) . E così via e così via, la lista di ottimizzazione è quasi infinita.
Puoi essere certo che una variabile ha il suo posto nello stack solo quando ottieni il suo indirizzo. Tuttavia, è un indirizzo variabile solo quando questo indirizzo è valido. Se chiami un'altra funzione come g(&i)
, i
viene posta sullo stack prima di chiamare g
, ma può essere immediatamente spostata nel registro o utilizzata come argomento di espressione quando g () finisce, perché questo indirizzo non viene mai più utilizzato .
Esempio. La funzione C è:
int f(int i, int j, int k) {
return i + j + k;
}
È tradotto per x86_64 / Unix per:
addl %esi, %edi
leal (%rdi,%rdx), %eax
ret
Nessuna pila è usata affatto. La convenzione di chiamata specifica i registri per l'argomento e il posizionamento dei risultati, ed è così che viene implementato. (Si noti che la stessa aggiunta è add
e quindi lea
- questo è il flusso di istruzioni che si estende tra diverse unità di esecuzione.)
Aggiungi un'azione esterna per i
:
void g(int*);
int f(int i, int j, int k) {
g(&i);
return i + j + k;
}
Output di montaggio:
pushq %rbp
movl %esi, %ebp
pushq %rbx
movl %edx, %ebx
subq $24, %rsp
movl %edi, 12(%rsp) ; <-- storing i onto stack
leaq 12(%rsp), %rdi ; <-- getting &i
call g
addl 12(%rsp), %ebp ; <-- using possibly modified i
addq $24, %rsp
leal 0(%rbp,%rbx), %eax
popq %rbx
popq %rbp
ret
Qui, i
arriva in RDI, quindi viene collocato sullo stack prima di g(&i)
, e poi usato come sommatore direttamente dallo stack, senza tornare a un registro. (Inoltre, il puntatore del frame è regolato perché questa non è più una funzione foglia.)
Considera di nuovo che stai utilizzando la macchina del tempo e / o un ambiente molto integrato. SSA è una tecnica costosa. La mappatura della variabile set to stack (o, prima, memoria) è il modo in cui i compilatori sono stati elaborati fin dall'inizio, ovvero le prime versioni di Fortran. Per quel caso, hai ragione. E la domanda in questo caso è come tenere traccia delle possibili modifiche agli indirizzi.
Su S / 360, un compilatore utilizza un blocco di memoria dopo il corpo della funzione per le sue variabili e utilizza la seguente sequenza di istruzioni per ottenere un indirizzo di base per questo blocco:
BALR 15,0
USING *,15
il registro 15 è convenzionale per questo obiettivo e dopo questo riceve l'indirizzo subito dopo il comando. Quindi, l'accesso alla memoria basato su offset viene utilizzato per le variabili, ad esempio:
LR 3,196(15)
qui il valore a 4 byte da questa base più 196 viene caricato nel registro 3. Per l'intera esecuzione della funzione, i
sarà a 196 (15).
Si noti che questo approccio non è basato sullo stack; una funzione non è rientranti.
Con PDP-11, ma ancora non Fortran basato su stack, questo può essere fatto con accesso basato su puntatore di istruzioni, come
MOV 196(PC), R3
perché non è necessario allocare un registro per tale accesso alla memoria. Ma gli offset varieranno (un'istruzione immediatamente successiva dovrà usare 192 invece di 196).
Con stack, questo cambia in accesso basato su SP e nessuna area variabile vicino al corpo della funzione:
MOV 16(SP), R3
ma vedi sotto per la variazione dell'offset quando SP cambia.
Con x86 prima di i386, l'accesso allo stack basato su BP era obbligatorio; poiché i386, è ancora utilizzabile, ma non unico. Dopo l'impostazione di BP al prologo di funzione, gli offset rimangono fissi, quindi è possibile utilizzare la stessa espressione, come [EBP-20] (Intel), -20 (% ebp) (AT & T), per una variabile durante la durata della chiamata di funzione. Con accesso basato su ESP / RSP (dal i386), gli offset vengono modificati con qualsiasi push / pop. Se i
era [ESP + 20], una singola spinta di 4 byte "la converte" in [ESP + 24]; "converte" significa che l'indirizzo di memoria è lo stesso, ma la formula per esso è cambiata con la regolazione ESP. Ad esempio, se viene chiamato g(i,&i,&i)
, un compilatore può produrre la seguente sequenza per formare l'elenco degli argomenti (le convenzioni di chiamata x86_32 sono principalmente basate sullo stack):
## assume i is now at 24(%esp)
leal 24(%esp), %eax ; &i to eax
pushl %eax
leal 28(%esp), %eax ; &i to eax
pushl %eax
movl 32(%esp), %eax ; i to eax
pushl %eax
(È ovviamente non ottimale, ma che esprime.)