Perché continuiamo a far crescere la pila all'indietro?


46

Quando si compila il codice C e si guarda l'assemblaggio, tutto ha lo stack crescere all'indietro in questo modo:

_main:
    pushq   %rbp
    movl    $5, -4(%rbp)
     popq    %rbp
    ret

-4(%rbp)- significa che il puntatore di base o il puntatore dello stack si stanno effettivamente spostando verso il basso gli indirizzi di memoria invece di salire? Perché?

Ho cambiato $5, -4(%rbp)a $5, +4(%rbp), compilato e fatto funzionare il codice e non ci sono errori. Quindi perché dobbiamo ancora tornare indietro nello stack di memoria?


2
Nota che -4(%rbp)non sposta affatto il puntatore di base e che +4(%rbp)non avrebbe potuto funzionare.
Margaret Bloom,

14
" perché dobbiamo ancora andare indietro " - quale pensi che sarebbe il vantaggio di andare avanti? In definitiva, non importa, devi solo sceglierne uno.
Bergi,

31
"perché facciamo crescere la pila all'indietro?" - perché se non lo facessimo qualcun altro ci chiederebbe perché fa malloccrescere il mucchio all'indietro
slebetman

2
@MargaretBloom: Apparentemente sulla piattaforma dell'OP, al codice di avvio CRT non importa se mainblocca il suo RBP. Questo è certamente possibile. (E sì, la scrittura 4(%rbp)farebbe un passo sul valore RBP salvato). In realtà, questo principale non lo fa mai mov %rsp, %rbp, quindi l'accesso alla memoria è relativo all'RBP del chiamante , se è quello che l'OP ha effettivamente testato !!! Se questo è stato effettivamente copiato dall'output del compilatore, alcune istruzioni sono state escluse!
Peter Cordes,

1
Mi sembra che "indietro" o "avanti" (o "giù" e "su") dipende dal tuo punto di vista. Se hai schematizzato la memoria come una colonna con indirizzi bassi nella parte superiore, allora aumentare lo stack diminuendo un puntatore dello stack sarebbe analogo a uno stack fisico.
jamesdlin

Risposte:


86

Questo significa che il puntatore di base o il puntatore dello stack si stanno effettivamente spostando verso il basso gli indirizzi di memoria anziché salire? Perché?

Sì, le pushistruzioni diminuiscono il puntatore dello stack e scrivono nello stack, mentre popfanno il contrario, leggono dallo stack e incrementano il puntatore dello stack.

Questo è un po 'storico in quanto per le macchine con memoria limitata, lo stack è stato posizionato in alto e cresciuto verso il basso, mentre l'heap è stato posizionato in basso e cresciuto verso l'alto. C'è solo una lacuna di "memoria libera" - tra l'heap e lo stack, e questa lacuna è condivisa, o uno può crescere nel divario secondo necessità individuali. Pertanto, il programma esaurisce la memoria solo quando lo stack e l'heap si scontrano senza lasciare memoria libera. 

Se lo stack e l'heap crescono entrambi nella stessa direzione, allora ci sono due spazi vuoti e lo stack non può davvero crescere nello spazio dell'heap (il viceversa è anche problematico).

Inizialmente, i processori non avevano istruzioni di gestione dello stack dedicate. Tuttavia, poiché il supporto dello stack è stato aggiunto all'hardware, ha assunto questo modello di crescita verso il basso e i processori seguono ancora questo modello oggi.

Si potrebbe sostenere che su una macchina a 64 bit vi è spazio di indirizzi sufficiente per consentire più spazi vuoti - e come prova, più spazi vuoti sono necessariamente il caso in cui un processo ha più thread. Sebbene questa non sia una motivazione sufficiente per cambiare le cose, dal momento che con i sistemi a gap multipli, la direzione della crescita è probabilmente arbitraria, quindi tradizione / compatibilità punta sulla scala.


Dovreste modificare le istruzioni della CPU di gestione dello stack in modo da cambiare la direzione della pila, oppure rinunciare a uso degli dedicati spingere e popping istruzioni (ad esempio push, pop, call, ret, altri).

Si noti che l'architettura del set di istruzioni MIPS non ha dedicato push& pop, quindi è pratico far crescere lo stack in entrambe le direzioni - potresti comunque desiderare un layout di memoria a spazio vuoto per un singolo processo di thread, ma potresti far crescere lo stack verso l'alto e l'heap verso il basso. Se lo hai fatto, tuttavia, alcuni codici C varargs potrebbero richiedere una regolazione nel passaggio dei parametri sorgente o sottostante.

(In effetti, poiché non esiste una gestione dello stack dedicata su MIPS, potremmo utilizzare il pre o post increment o pre o post decrement per spingere sullo stack fintanto che abbiamo usato il contrario esatto per saltar fuori dallo stack e anche supponendo che il il sistema operativo rispetta il modello di utilizzo dello stack scelto. In effetti, in alcuni sistemi embedded e in alcuni sistemi educativi, lo stack MIPS è cresciuto verso l'alto.)


32
Non è solo pushe popsulla maggior parte delle architetture, ma anche di gran lunga più importante di interruzione-gestione, call, ret, e qualsiasi altra cosa si è cotto-in interazione con lo stack.
Deduplicatore il

3
ARM può avere tutti e quattro i tipi di stack.
Margaret Bloom,

14
Per quello che vale, non credo che "la direzione della crescita sia arbitraria", nel senso che entrambe le scelte sono ugualmente buone. La riduzione ha la proprietà che traboccare alla fine di un buffer blocca i frame stack precedenti, inclusi gli indirizzi di ritorno salvati. La crescita ha la proprietà che traboccare alla fine di un buffer blocca solo l'archiviazione nello stesso o successivo (se il buffer non è nell'ultimo, potrebbero esserci quelli successivi) chiama frame, e forse anche solo spazio inutilizzato (tutti assumendo una guardia pagina dopo lo stack). Quindi, dal punto di vista della sicurezza, crescere sembra preferibile
R ..

6
@R ..: crescere non elimina gli exploit di sovraccarico del buffer, perché le funzioni vulnerabili di solito non sono funzioni foglia: chiamano altre funzioni, posizionando un indirizzo di ritorno sopra il buffer. Le funzioni foglia che ottengono un puntatore dal chiamante potrebbero diventare vulnerabili alla sovrascrittura del proprio indirizzo di ritorno. ad esempio, se una funzione alloca un buffer nello stack e lo passa a gets(), o se strcpy()non lo fa inline, il ritorno in quelle funzioni della libreria utilizzerà l'indirizzo di ritorno sovrascritto. Attualmente con stack in crescita verso il basso, è quando il loro chiamante ritorna.
Peter Cordes,

5
@PeterCordes: In effetti il ​​mio commento ha osservato che i frame dello stesso livello o più recenti rispetto al buffer overflow sono ancora potenzialmente bloccabili, ma è molto meno. Nel caso in cui la funzione clobbering sia una funzione foglia chiamata direttamente dalla funzione di cui si trova il buffer (ad es. strcpy), Su un arco in cui l'indirizzo di ritorno è tenuto in un registro a meno che non debba essere versato, non è possibile accedere a clobber al ritorno indirizzo.
R ..

8

Nel tuo sistema specifico lo stack inizia da un indirizzo di memoria elevato e "cresce" verso il basso fino a indirizzi di memoria insufficiente. (esiste anche il caso simmetrico dal più basso al più alto)

E poiché sei passato da -4 e +4 e ha funzionato, ciò non significa che sia corretto. La disposizione della memoria di un programma in esecuzione è più complessa e dipende da molti altri fattori che potrebbero aver contribuito al fatto che non si è verificato immediatamente un arresto anomalo di questo programma estremamente semplice.


1

Il puntatore dello stack punta al confine tra memoria dello stack allocata e non allocata. La sua crescita verso il basso significa che punta all'inizio della prima struttura nello spazio di stack allocato, con altri oggetti allocati che seguono indirizzi più grandi. Avere puntatori che indicano l'inizio delle strutture allocate è molto più comune rispetto al contrario.

Ora su molti sistemi al giorno d'oggi, esiste un registro separato per i frame dello stack che può essere in qualche modo risolto in modo affidabile per capire la catena di chiamate, con l'archiviazione locale variabile intervallata. Il modo in cui questo registro dello stack frame è impostato su alcune architetture significa che finisce puntando dietro l'archiviazione variabile locale rispetto al puntatore dello stack prima di esso. Quindi l'utilizzo di questo registro di stack frame richiede quindi indicizzazione negativa.

Si noti che i frame di stack e la loro indicizzazione sono un aspetto secondario dei linguaggi informatici compilati, quindi è il generatore di codice del compilatore che deve fare i conti con la "innaturalità" piuttosto che con un programmatore di linguaggio assembly scadente.

Quindi, sebbene esistessero buone ragioni storiche per scegliere gli stack da crescere verso il basso (e alcuni di essi vengono mantenuti se si programma nel linguaggio assembly e non si preoccupa di impostare un frame stack adeguato), sono diventati meno visibili.


2
"Ora su molti sistemi al giorno d'oggi, esiste un registro separato per i frame di stack" che sei dietro i tempi. Al giorno d'oggi, formati di informazioni di debug più ricchi hanno ampiamente eliminato la necessità di puntatori a frame.
Peter Green,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.