2009-11-04 33 views
71

Ho questo pezzo di codice in c:Lo stack cresce verso l'alto o verso il basso?

int q = 10; 
int s = 5; 
int a[3]; 

printf("Address of a: %d\n", (int)a); 
printf("Address of a[1]: %d\n", (int)&a[1]); 
printf("Address of a[2]: %d\n", (int)&a[2]); 
printf("Address of q: %d\n", (int)&q); 
printf("Address of s: %d\n", (int)&s); 

Il risultato è:

Address of a: 2293584 
Address of a[1]: 2293588 
Address of a[2]: 2293592 
Address of q: 2293612 
Address of s: 2293608 

Quindi, vedo che dal a a a[2], indirizzi di memoria aumenta di 4 byte. Ma da q a s, gli indirizzi di memoria diminuiscono di 4 byte.

Mi chiedo 2 cose:

  1. Vuol pila crescere o verso il basso? (Sembra che sia per me in questo caso)
  2. Che succede tra gli indirizzi di memoria a[2] e q? Perché c'è una grande differenza di memoria lì? (20 byte).

Nota: questa non è una domanda per i compiti. Sono curioso di sapere come funziona lo stack. Grazie per qualsiasi aiuto.

+0

L'ordine è arbitrario. Probabilmente il gap è quello di memorizzare un risultato intermedio come & q o & s - guarda lo smontaggio e guarda tu stesso. –

+0

Sono d'accordo, leggere il codice assembly. Se stai facendo questo tipo di domande è ora di imparare a leggerlo. –

+0

questo codice non ha nulla a che fare con lo stack. – specializt

risposta

61

Il comportamento dello stack (crescente o crescente) dipende dall'interfaccia binaria dell'applicazione (ABI) e da come è organizzato lo stack di chiamate (ovvero il record di attivazione).

Durante la sua vita un programma è destinato a comunicare con altri programmi come OS. ABI determina come un programma può comunicare con un altro programma.

Lo stack per architetture diverse può crescere in entrambi i casi, ma per un'architettura sarà coerente. Si prega di controllare il link wiki this. Ma la crescita dello stack è decisa dall'ABI di quell'architettura.

Ad esempio, se si utilizza l'ABI MIPS, lo stack di chiamate viene definito come di seguito.

Consideriamo che la funzione 'fn1' chiama 'fn2'. Ora il frame dello stack visto da "fn2" è il seguente:

direction of  |         | 
    growth of  +---------------------------------+ 
    stack   | Parameters passed by fn1(caller)| 
from higher addr.|         | 
to lower addr. | Direction of growth is opposite | 
     |   | to direction of stack growth | 
     |   +---------------------------------+ <-- SP on entry to fn2 
     |   | Return address from fn2(callee) | 
     V   +---------------------------------+ 
       | Callee saved registers being | 
       | used in the callee function | 
       +---------------------------------+ 
       | Local variables of fn2   | 
       |(Direction of growth of frame is | 
       | same as direction of growth of | 
       |   stack)    | 
       +---------------------------------+ 
       | Arguments to functions called | 
       | by fn2       | 
       +---------------------------------+ <- Current SP after stack 
                 frame is allocated 

Ora è possibile vedere lo stack crescere verso il basso. Quindi, se le variabili sono allocate al frame locale della funzione, gli indirizzi della variabile crescono effettivamente verso il basso. Il compilatore può decidere l'ordine delle variabili per l'allocazione della memoria. (Nel tuo caso può essere 'q' o 's' che è la prima memoria allocata dello stack, ma generalmente il compilatore impila l'allocazione di memoria secondo l'ordine della dichiarazione delle variabili).

Ma nel caso degli array, l'allocazione ha un solo puntatore e la memoria che deve essere allocata sarà effettivamente puntata da un singolo puntatore. La memoria deve essere contigua per un array. Quindi, sebbene lo stack cresca verso il basso, per gli array lo stack cresce.

+2

Inoltre, se si desidera verificare se lo stack aumenta o diminuisce. Dichiarare una variabile locale nella funzione principale. Stampa l'indirizzo della variabile. Chiama un'altra funzione dal principale. Dichiarare una variabile locale nella funzione. Stampa il suo indirizzo. Sulla base degli indirizzi stampati possiamo dire che lo stack cresce o diminuisce. –

+0

grazie Ganesh, ho una piccola domanda: nella figura che hai disegnato, nel terzo blocco, intendevi "registro salvato in Calle utilizzato in CALLER" perché quando f1 chiama f2, dobbiamo memorizzare l'indirizzo f1 (che è il return addr for f2) e f1 (calleR) registers not f2 (callee) registers. Destra? – CSawy

-1

Non penso che sia deterministico in questo modo. La matrice sembra "crescere" perché quella memoria dovrebbe essere assegnata in modo contiguo. Tuttavia, dal momento che q e s non sono affatto correlati tra loro, il compilatore attacca semplicemente ognuno di essi in una posizione di memoria libera all'interno dello stack, probabilmente quelli che si adattano meglio a una dimensione intera.

Quello che è successo tra un [2] e q è che lo spazio attorno alla posizione di q non era abbastanza grande (cioè non era più grande di 12 byte) per allocare un array di 3 integer.

+0

se così, perché q, s, non hai una memoria contingente? (Es: Indirizzo di q: 2.293.612 Indirizzo di s: 2.293.608 Indirizzo di: 2.293.604) – root

+0

vedo un "gap" tra s ed una – root

+0

Poiché s e non sono stati assegnati insieme - i soli indicatori che devono essere contigui sono quelli nella matrice. L'altra memoria può essere allocata ovunque. – javanix

2

Su un x86, la "allocazione" di memoria di un frame di stack consiste semplicemente nel sottrarre il numero necessario di byte dal puntatore dello stack (credo che altre architetture siano simili). In questo senso, suppongo che lo stack cresca "in basso", in quanto gli indirizzi diventano progressivamente più piccoli quando si chiama più profondamente nello stack (ma immagino sempre che la memoria inizi con 0 in alto a sinistra e si ingrandiscano gli indirizzi mentre ci si sposta a destra e avvolto, così nella mia immagine mentale lo stack cresce ...). L'ordine delle variabili dichiarate potrebbe non avere alcun rilievo sui loro indirizzi - credo che lo standard permetta al compilatore di riordinarle, purché non causi effetti collaterali (qualcuno mi corregga se ho torto) . Sono semplicemente bloccati da qualche parte nel divario negli indirizzi utilizzati creati quando sottrae il numero di byte dal puntatore dello stack.

Lo spazio intorno all'array potrebbe essere una sorta di riempimento, ma per me è misterioso.

+1

Infatti, io _know_ il compilatore può riordinarli, perché è anche libero di non assegnarli affatto. Può semplicemente metterli in registri e non utilizzare alcuno spazio di stack di sorta. – rmeador

+0

Non può inserirli nei registri se si fa riferimento ai loro indirizzi. – florin

+0

buon punto, non l'aveva considerato. ma è comunque sufficiente a dimostrare che il compilatore può riordinarli, poiché sappiamo che può farlo almeno una volta. :) – rmeador

4

Non c'è niente nello standard che imponga come mai le cose sono organizzate nello stack. In effetti, è possibile creare un compilatore conforme che non abbia memorizzato elementi di array su elementi contigui nello stack, a condizione che avesse l'intelligenza di eseguire ancora correttamente l'aritmetica degli elementi dell'array (in modo che sapesse, ad esempio, che uno 1 fosse 1K di distanza da [0] e potrebbe regolarsi per quello).

Il motivo per cui si ottengono risultati diversi è perché, mentre lo stack può ridursi per aggiungere "oggetti", l'array è un singolo "oggetto" e potrebbe avere elementi di array in ordine crescente nell'ordine opposto. Ma non è sicuro affidarsi a questo comportamento poiché la direzione può cambiare e le variabili possono essere scambiate per una serie di motivi, tra cui, a titolo esemplificativo:

  • ottimizzazione.
  • allineamento.
  • i capricci della persona la parte di gestione dello stack del compilatore.

Vedere here per il mio eccellente trattato sulla direzione pila :-)

In risposta alle vostre domande specifiche:

  1. Vuol impilare crescere o verso il basso?
    Non ha importanza (in termini di standard) ma, dal momento che è stato chiesto, può crescere o in memoria, a seconda dell'implementazione.
  2. Cosa succede tra un [2] e q indirizzi di memoria? Perché c'è una grande differenza di memoria lì? (20 byte)?
    Non ha importanza (in termini di standard). Vedi sopra per possibili ragioni.
+0

Ti ho visto collegare la maggior parte dell'architettura della CPU ad adottare il metodo "grow down", sai se c'è qualche vantaggio nel farlo? –

+0

Nessuna idea, davvero. È possibile che qualcuno abbia pensato che il codice salga da 0, quindi lo stack dovrebbe andare verso il basso da highmem, in modo da minimizzare la possibilità di intersecare. Ma alcune CPU in particolare avviano il codice in esecuzione in posizioni diverse da zero, quindi potrebbe non essere il caso. Come con la maggior parte delle cose, forse è stato fatto in quel modo semplicemente perché era il primo modo in cui qualcuno pensava di farlo :-) – paxdiablo

+0

@lzprgmr: Ci sono alcuni lievi vantaggi nell'assegnare determinati tipi di allocazione dell'heap in ordine crescente, e ha storicamente era comune che stack e heap si trovassero alle estremità opposte di uno spazio di indirizzamento comune. A condizione che l'utilizzo combinato statico + heap + stack non superi la memoria disponibile, non ci si deve preoccupare esattamente della quantità di memoria dello stack utilizzata da un programma. – supercat

13

La direzione è la crescita di stack specifica dell'architettura. Detto questo, la mia comprensione è che solo poche architetture hardware hanno stack che crescono.

La direzione di crescita di uno stack è indipendente dal layout di un singolo oggetto. Quindi, mentre lo stack può essere ridotto, gli array no (cioè l'array & sarà sempre & array [n + 1]);

40

Questa è in realtà due domande. Uno riguarda in che modo the stack grows when one function calls another (quando viene allocato un nuovo frame) e l'altro riguarda il modo in cui le variabili sono disposte nel frame di una particolare funzione.

Né è specificato dallo standard C, ma le risposte sono un po 'diverso:

  • quale modo la pila crescere quando un nuovo frame viene allocato - se la funzione f() chiama la funzione g (), il puntatore del frame di f può essere maggiore o minore del puntatore del frame di g? Questo può andare in entrambi i modi - dipende dal particolare compilatore e dall'architettura (cercare "convenzione di chiamata"), ma è sempre coerente all'interno di una determinata piattaforma (con alcune eccezioni bizzarre, vedere i commenti). Verso il basso è più comune; è il caso in x86, PowerPC, MIPS, SPARC, EE e le SPU di celle.
  • Come sono disposte le variabili locali di una funzione all'interno del suo stack frame? Questo non è specificato e completamente imprevedibile; il compilatore è libero di organizzare le sue variabili locali, ma a lui piace ottenere il risultato più efficiente.
+5

"è sempre coerente all'interno di una determinata piattaforma" - non garantito. Ho visto una piattaforma senza memoria virtuale, in cui lo stack è stato esteso dinamicamente. I nuovi blocchi di stack erano in effetti montati su un malloced, il che significava che avresti dovuto "abbassare" un blocco di stack per un po ', poi improvvisamente "di lato" in un altro blocco. "Lateralmente" potrebbe significare un indirizzo maggiore o minore, interamente per la fortuna del sorteggio. –

+2

Per ulteriori dettagli al punto 2 - un compilatore può essere in grado di decidere che una variabile non debba mai essere in memoria (tenerla in un registro per la vita della variabile), e/o se la durata di due o più variabili non si sovrappone, il compilatore può decidere di utilizzare la stessa memoria per più di una variabile. –

+2

Penso che S/390 (IBM zSeries) abbia un ABI in cui i frame di chiamata sono collegati anziché crescere su uno stack. – ephemient

0

La mia pila sembra estendersi verso indirizzi con numeri più bassi.

Potrebbe essere diverso su un altro computer o anche sul mio computer se utilizzo un richiamo del compilatore diverso. ... o il compilatore muigt sceglie di non utilizzare affatto una pila (tutto in linea (funzioni e variabili se non ho preso l'indirizzo di esse)).

$ cat stack.c 
#include <stdio.h> 

int stack(int x) { 
    printf("level %d: x is at %p\n", x, (void*)&x); 
    if (x == 0) return 0; 
    return stack(x - 1); 
} 

int main(void) { 
    stack(4); 
    return 0; 
} 
 
$ /usr/bin/gcc -Wall -Wextra -std=c89 -pedantic stack.c 
$ ./a.out 
level 4: x is at 0x7fff7781190c 
level 3: x is at 0x7fff778118ec 
level 2: x is at 0x7fff778118cc 
level 1: x is at 0x7fff778118ac 
level 0: x is at 0x7fff7781188c 
0

Il compilatore è libero di ripartire variabili (auto) locali in qualsiasi posto sul telaio dello stack locale, non si può dedurre in modo affidabile la direzione crescente pila puramente da questo. È possibile dedurre la pila crescere direzione dal confronto degli indirizzi di stack frame nidificate, cioè confrontando l'indirizzo di una variabile locale all'interno del frame dello stack di una funzione rispetto ad esso è callee:

int f(int *x) 
{ 
    int a; 
    return x == NULL ? f(&a) : &a - x; 
} 

int main(void) 
{ 
    printf("stack grows %s!\n", f(NULL) < 0 ? "down" : "up"); 
    return 0; 
} 
+4

Sono abbastanza sicuro che è un comportamento indefinito sottrarre i puntatori a diversi oggetti stack: i puntatori che non fanno parte dello stesso oggetto non sono confrontabili. Ovviamente anche se non si bloccherà su nessuna architettura "normale". –

0

Lo stack cresce verso il basso (su x86). Tuttavia, lo stack viene allocato in un blocco quando la funzione viene caricata e non si ha una garanzia sull'ordine in cui gli elementi saranno in pila.

In questo caso, ha allocato lo spazio per due ints e un array three-int nello stack. Inoltre allocato ulteriori 12 byte dopo la matrice, in modo che appaia così:

un [12 bytes]
imbottitura [12 bytes]
s [4 bytes]
q [4 byte] (?)

Per qualsiasi motivo, il compilatore ha deciso di allocare 32 byte per questa funzione, e possibilmente di più. È opaco per te come programmatore C, non sai perché.

Se si vuole sapere perché, compilare il codice in linguaggio assembly, credo che sia -S su gcc e/S sul compilatore C di MS. Se osservi le istruzioni di apertura di quella funzione, vedrai il vecchio puntatore dello stack che viene salvato e poi 32 (o qualcos'altro!) Da esso sottratto. Da lì, puoi vedere come il codice accede al blocco di memoria a 32 byte e capire cosa sta facendo il tuo compilatore. Alla fine della funzione, puoi vedere il puntatore dello stack da ripristinare.

0

Dipende dal sistema operativo e dal compilatore.

+0

Non so perché la mia risposta è stata sottovalutata. Dipende davvero dal tuo sistema operativo e dal compilatore. Su alcuni sistemi lo stack cresce verso il basso, ma su altri cresce verso l'alto. E su * alcuni * sistemi, non esiste un vero stack di frame push-down, ma piuttosto è simulato con un'area riservata di memoria o set di registri. –

+3

Probabilmente perché le asserzioni a frase singola non sono buone risposte. –

1

Prima di tutto, i suoi 8 byte di spazio inutilizzato in memoria (non è 12, ricorda che lo stack cresce verso il basso, quindi lo spazio che non è allocato va da 604 a 597). e perché?. Perché ogni tipo di dati occupa spazio in memoria a partire dall'indirizzo divisibile per la sua dimensione. Nel nostro caso un array di 3 numeri interi occupa 12 byte di spazio di memoria e 604 non è divisibile per 12. Quindi lascia spazi vuoti finché non incontra un indirizzo di memoria che è divisibile per 12, è 596.

Quindi lo spazio di memoria allocata all'array è compresa tra 596 e 584. Tuttavia, poiché l'allocazione dell'array è in continuazione, il primo elemento dell'array inizia dall'indirizzo 584 e non da 596.

0

Lo stack si riduce. Quindi f (g (h())), lo stack allocato per h avrà inizio all'indirizzo inferiore, g e g sarà inferiore quindi a f. Ma variabili all'interno della pila devono seguire le specifiche C,

http://c0x.coding-guidelines.com/6.5.8.html

1206 Se gli oggetti indicavano sono membri dello stesso oggetto aggregato, puntatori di strutturare i membri dichiarati successivamente confrontare maggiore di puntatori ai membri dichiarati in precedenza in la struttura e i puntatori agli elementi di matrice con valori di indice più grandi confrontano maggiori dei puntatori agli elementi della stessa matrice con valori di indice inferiori.

& a [0] < & un [1], deve essere sempre vero, indipendentemente da come 'un' è attribuito

1

cresce verso il basso e questo è a causa del piccolo norma ordine byte endian quando si tratta di l'insieme di dati in memoria.

Un modo in cui è possibile osservare è che lo stack aumenta verso l'alto se si guarda la memoria da 0 dall'alto e dal massimo dal basso.

Il motivo per cui lo stack cresce verso il basso deve essere in grado di dereferenziare dalla prospettiva della pila o del puntatore di base.

Ricordare che il dereferenziazione di qualsiasi tipo aumenta dall'indirizzo più basso a quello più alto. Dal momento che lo Stack cresce verso il basso (dal più alto al più basso) questo ti consente di trattare la pila come la memoria dinamica.

Questo è uno dei motivi per cui così tanti linguaggi di programmazione e scripting utilizzano una macchina virtuale basata su stack anziché una basata su registri.

+0

'Il motivo per cui lo stack cresce verso il basso è quello di essere in grado di dereferenziare dal punto di vista dello stack o del puntatore di base. Molto bello il ragionamento – user3405291

Problemi correlati