Sto preparando alcuni materiali di formazione in C e voglio che i miei esempi si adattino al tipico modello di stack.
In che direzione cresce uno stack C in Linux, Windows, Mac OSX (PPC e x86), Solaris e gli Unix più recenti?
Sto preparando alcuni materiali di formazione in C e voglio che i miei esempi si adattino al tipico modello di stack.
In che direzione cresce uno stack C in Linux, Windows, Mac OSX (PPC e x86), Solaris e gli Unix più recenti?
Risposte:
La crescita dello stack di solito non dipende dal sistema operativo stesso, ma dal processore su cui è in esecuzione. Solaris, ad esempio, funziona su x86 e SPARC. Mac OSX (come hai detto) funziona su PPC e x86. Linux gira su tutto, dal mio grande clamoroso System z al lavoro a un piccolo orologio da polso .
Se la CPU fornisce qualsiasi tipo di scelta, la convenzione ABI / chiamata utilizzata dal sistema operativo specifica quale scelta è necessario fare se si desidera che il codice chiami il codice di tutti gli altri.
I processori e la loro direzione sono:
Mostrando la mia età su quegli ultimi, il 1802 era il chip utilizzato per controllare le prime navette (percependo se le porte erano aperte, sospetto, in base alla potenza di elaborazione che aveva :-) e il mio secondo computer, il COMX-35 ( seguendo il mio ZX80 ).
Dettagli PDP11 raccolti da qui , dettagli 8051 da qui .
L'architettura SPARC utilizza un modello di registro a finestra scorrevole. I dettagli architettonicamente visibili includono anche un buffer circolare di finestre di registro che sono valide e memorizzate nella cache internamente, con trappole in caso di overflow / underflow. Vedi qui per i dettagli. Come spiega il manuale di SPARCv8, le istruzioni SAVE e RESTORE sono come le istruzioni ADD più la rotazione della finestra di registro. L'uso di una costante positiva invece del solito negativo darebbe uno stack in crescita.
La suddetta tecnica SCRT è un'altra: il 1802 utilizzava alcuni o sedici registri a 16 bit per SCRT (tecnica di chiamata e ritorno standard). Uno era il contatore del programma, puoi usare qualsiasi registro come il PC con l' SEP Rn
istruzione. Uno era il puntatore dello stack e due erano sempre impostati per puntare all'indirizzo del codice SCRT, uno per la chiamata, uno per il ritorno. Nessun registro è stato trattato in modo speciale. Tieni presente che questi dettagli provengono dalla memoria, potrebbero non essere completamente corretti.
Ad esempio, se R3 fosse il PC, R4 era l'indirizzo di chiamata SCRT, R5 era l'indirizzo di ritorno SCRT e R2 era lo "stack" (virgolette poiché è implementato nel software), SEP R4
imposterebbe R4 come PC e inizierebbe a eseguire SCRT codice di chiamata.
Quindi memorizzerebbe R3 sullo "stack" R2 (penso che R6 sia stato utilizzato per l'archiviazione temporanea), regolandolo verso l'alto o verso il basso, prenda i due byte dopo R3, li carichi in R3, quindi SEP R3
esegua ed esegua al nuovo indirizzo.
Per tornare, ciò SEP R5
estrarrebbe il vecchio indirizzo dallo stack R2, aggiungerne due (per saltare i byte dell'indirizzo della chiamata), caricarlo in R3 e SEP R3
iniziare a eseguire il codice precedente.
Inizialmente molto difficile da capire dopo tutto il codice basato sullo stack 6502/6809 / z80, ma comunque elegante in un modo che sbatte la testa contro il muro. Inoltre una delle caratteristiche più vendute del chip era una suite completa di 16 registri a 16 bit, nonostante il fatto che ne abbiate subito perso 7 (5 per SCRT, due per DMA e interrupt dalla memoria). Ahh, il trionfo del marketing sulla realtà :-)
System z è in realtà abbastanza simile, utilizzando i suoi registri R14 e R15 per la chiamata / ritorno.
In C ++ (adattabile a C) stack.cc :
static int
find_stack_direction ()
{
static char *addr = 0;
auto char dummy;
if (addr == 0)
{
addr = &dummy;
return find_stack_direction ();
}
else
{
return ((&dummy > addr) ? 1 : -1);
}
}
static
per questo. Invece potresti passare l'indirizzo come argomento a una chiamata ricorsiva.
static
, se lo chiami più di una volta, le chiamate successive potrebbero non riuscire ...
Il vantaggio di crescere verso il basso è che nei sistemi più vecchi lo stack era tipicamente in cima alla memoria. I programmi in genere riempivano la memoria partendo dal basso, quindi questo tipo di gestione della memoria riduceva al minimo la necessità di misurare e posizionare il fondo dello stack in un posto ragionevole.
In MIPS e in molte moderne architetture RISC (come PowerPC, RISC-V, SPARC ...) non ci sono istruzioni push
e pop
. Queste operazioni vengono eseguite esplicitamente regolando manualmente il puntatore dello stack, quindi caricando / memorizzando il valore relativamente al puntatore regolato. Tutti i registri (eccetto il registro zero) sono di uso generale, quindi in teoria qualsiasi registro può essere un puntatore allo stack e lo stack può crescere in qualsiasi direzione il programmatore desidera
Detto questo, lo stack in genere si riduce sulla maggior parte delle architetture, probabilmente per evitare il caso in cui i dati dello stack e del programma o dei dati dell'heap crescano e si scontrino tra loro. Ci sono anche i grandi motivi di indirizzamento menzionati nella risposta di sh- . Alcuni esempi: MIPS ABI cresce verso il basso e usa $29
(AKA $sp
) come puntatore dello stack, anche RISC-V ABI cresce verso il basso e usa x2 come puntatore dello stack
In Intel 8051 lo stack cresce, probabilmente perché lo spazio di memoria è così piccolo (128 byte nella versione originale) che non c'è heap e non è necessario mettere lo stack in cima in modo che venga separato dall'heap in crescita dal basso
Puoi trovare ulteriori informazioni sull'utilizzo dello stack in varie architetture in https://en.wikipedia.org/wiki/Calling_convention
Guarda anche
Solo una piccola aggiunta alle altre risposte, che per quanto posso vedere non hanno toccato questo punto:
Se lo stack cresce verso il basso, tutti gli indirizzi all'interno dello stack hanno un offset positivo rispetto al puntatore dello stack. Non c'è bisogno di offset negativi, poiché indicherebbero solo lo spazio dello stack inutilizzato. Ciò semplifica l'accesso alle posizioni dello stack quando il processore supporta l'indirizzamento relativo allo stackpointer.
Molti processori hanno istruzioni che consentono l'accesso con un offset solo positivo relativo ad alcuni registri. Questi includono molte architetture moderne, così come alcune vecchie. Ad esempio, ARM Thumb ABI fornisce accessi relativi allo stackpointer con un offset positivo codificato all'interno di una singola parola di istruzione a 16 bit.
Se lo stack crescesse verso l'alto, tutti gli offset utili relativi allo stackpointer sarebbero negativi, il che è meno intuitivo e meno conveniente. Inoltre è in contrasto con altre applicazioni di indirizzamento relativo al registro, ad esempio per accedere ai campi di una struttura.
Sulla maggior parte dei sistemi, lo stack si riduce e il mio articolo su https://gist.github.com/cpq/8598782 spiega PERCHÉ cresce. È semplice: come disporre due blocchi di memoria in crescita (heap e stack) in un blocco fisso di memoria? La soluzione migliore è metterli alle estremità opposte e lasciarli crescere l'uno verso l'altro.
Cresce verso il basso perché la memoria allocata al programma ha i "dati permanenti", cioè il codice per il programma stesso in basso, quindi l'heap nel mezzo. Hai bisogno di un altro punto fisso da cui fare riferimento alla pila, in modo da rimanere in cima. Ciò significa che lo stack cresce fino a diventare potenzialmente adiacente agli oggetti nell'heap.
Questa macro dovrebbe rilevarlo in fase di esecuzione senza UB:
#define stk_grows_up_eh() stk_grows_up__(&(char){0})
_Bool stk_grows_up__(char *ParentsLocal);
__attribute((__noinline__))
_Bool stk_grows_up__(char *ParentsLocal) {
return (uintptr_t)ParentsLocal < (uintptr_t)&ParentsLocal;
}