Lo stack cresce verso l'alto o verso il basso?


89

Ho questo pezzo di codice in c:

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);

L'output è:

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 da aa a[2], gli indirizzi di memoria aumentano di 4 byte ciascuno. Ma da qa s, gli indirizzi di memoria diminuiscono di 4 byte.

Mi chiedo 2 cose:

  1. Lo stack cresce o diminuisce? (Mi sembrano entrambe le cose in questo caso)
  2. Cosa succede tra a[2]e qindirizzi di memoria? 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.


L'ordinamento è arbitrario. Il divario è probabilmente quello di memorizzare un risultato intermedio come & q o & s - guarda lo smontaggio e guarda tu stesso.
Tom Leys

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

Risposte:


74

Il comportamento dello stack (in crescita o in diminuzione) dipende dall'interfaccia binaria dell'applicazione (ABI) e da come è organizzato lo stack di chiamate (noto anche come record di attivazione).

Per tutta 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 modi, ma per un'architettura sarà coerente. Si prega di controllare questo collegamento wiki. Ma la crescita dello stack è decisa dall'ABI di quell'architettura.

Ad esempio, se prendi l'ABI MIPS, lo stack di chiamate è definito come di seguito.

Consideriamo che la funzione "fn1" chiama "fn2". Ora lo stack frame 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 puoi vedere che lo stack cresce verso il basso. Quindi, se le variabili sono allocate al frame locale della funzione, gli indirizzi della variabile aumentano 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" la prima memoria di stack allocata. Ma, generalmente il compilatore esegue l'allocazione di memoria di stack secondo l'ordine della dichiarazione delle variabili).

Ma nel caso degli array, l'allocazione ha un solo puntatore e la memoria da allocare 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.


5
Inoltre, se vuoi controllare se lo stack cresce verso l'alto o verso il basso. Dichiara una variabile locale nella funzione principale. Stampa l'indirizzo della variabile. Chiama un'altra funzione da main. Dichiarare una variabile locale nella funzione. Stampa il suo indirizzo. In base agli indirizzi stampati, possiamo dire che lo stack aumenta o diminuisce.
Ganesh Gopalasubramanian

grazie Ganesh, ho una piccola domanda: nella figura che hai disegnato, nel terzo blocco, intendevi "calleR registro salvato utilizzato in CALLER" perché quando f1 chiama f2, dobbiamo memorizzare l'indirizzo f1 (che è l'addr di ritorno per f2) e f1 (calleR) registri non f2 (chiamato). Destra?
CSawy

44

In realtà sono due domande. Uno riguarda il modo in cui lo stack cresce quando una funzione ne chiama un'altra (quando viene allocato un nuovo frame), e l'altro riguarda il modo in cui le variabili sono disposte nel frame di una particolare funzione.

Nessuno dei due è specificato dallo standard C, ma le risposte sono leggermente diverse:

  • In che modo lo stack cresce quando viene allocato un nuovo frame - se la funzione f () chiama la funzione g (), fil puntatore del frame sarà maggiore o minore del gpuntatore del frame di? Questo può andare in entrambi i modi - dipende dal particolare compilatore e architettura (cerca "convenzione di chiamata"), ma è sempre coerente all'interno di una data piattaforma (con alcune bizzarre eccezioni, vedi i commenti). Verso il basso è più comune; è il caso di x86, PowerPC, MIPS, SPARC, EE e Cell SPU.
  • 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 comunque gli piaccia per ottenere il risultato più efficiente.

7
"è sempre coerente all'interno di una data piattaforma" - non garantito. Ho visto una piattaforma senza memoria virtuale, in cui lo stack è stato esteso in modo dinamico. I nuovi blocchi dello stack sono stati in effetti mallocati, il che significa che dovresti andare "giù" di un blocco dello stack per un po ', quindi improvvisamente "lateralmente" a un blocco diverso. "Sideways" potrebbe significare un indirizzo maggiore o minore, interamente dovuto alla fortuna del sorteggio.
Steve Jessop,

2
Per ulteriori dettagli sull'elemento 2: un compilatore potrebbe essere in grado di decidere che una variabile non deve mai essere in memoria (conservandola in un registro per la durata della variabile), e / o se la durata di due o più variabili non lo fa ' Se si sovrappongono, il compilatore può decidere di utilizzare la stessa memoria per più di una variabile.
Michael Burr,

2
Penso che S / 390 (IBM zSeries) abbia un'ABI in cui i frame di chiamata sono collegati invece di crescere su uno stack.
effimero

2
Correggere su S / 390. Una chiamata è "BALR", filiale e registro di collegamento. Il valore restituito viene inserito in un registro anziché inserito in uno stack. La funzione di ritorno è un ramo del contenuto di quel registro. Man mano che lo stack diventa più profondo, lo spazio viene allocato nell'heap e vengono concatenati insieme. È qui che l'equivalente MVS di "/ bin / true" prende il nome: "IEFBR14". La prima versione aveva un'unica istruzione: "BR 14", che si diramava al contenuto del registro 14 che conteneva l'indirizzo del mittente.
gennaio

1
E alcuni compilatori su processori PIC eseguono analisi dell'intero programma e assegnano posizioni fisse per le variabili automatiche di ciascuna funzione; lo stack effettivo è piccolo e non è accessibile dal software; è solo per gli indirizzi di ritorno.
gennaio

13

La direzione in cui crescono gli stack è specifica dell'architettura. Detto questo, la mia comprensione è che solo pochissime architetture hardware hanno stack che crescono.

La direzione in cui cresce una pila è indipendente dal layout di un singolo oggetto. Quindi, sebbene lo stack possa aumentare, gli array non lo faranno (cioè & array [n] sarà sempre <& array [n + 1]);


4

Non c'è niente nello standard che imponga come le cose sono organizzate in pila. In effetti, è possibile creare un compilatore conforme che non memorizzi affatto gli elementi dell'array in elementi contigui sullo stack, a condizione che avesse l'astuzia di eseguire ancora correttamente l'aritmetica degli elementi dell'array (in modo che sapesse, ad esempio, che un 1 era 1K di distanza da uno [0] e potrebbe adattarsi a questo).

Il motivo per cui potresti ottenere risultati diversi è perché, sebbene lo stack possa aumentare per aggiungere "oggetti" ad esso, l'array è un singolo "oggetto" e potrebbe avere elementi di array crescenti nell'ordine opposto. Ma non è sicuro fare affidamento su quel comportamento poiché la direzione può cambiare e le variabili potrebbero essere scambiate per una serie di motivi tra cui, ma non limitati a:

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

Vedi qui per il mio eccellente trattato sulla direzione dello stack :-)

In risposta alle tue domande specifiche:

  1. Lo stack cresce o diminuisce?
    Non importa affatto (in termini di standard) ma, da quando hai chiesto, può aumentare o diminuire nella memoria, a seconda dell'implementazione.
  2. Cosa succede tra a [2] eq indirizzi di memoria? Perché c'è una grande differenza di memoria lì? (20 byte)?
    Non importa affatto (in termini di standard). Vedi sopra per possibili ragioni.

Ti ho visto collegare che la maggior parte dell'architettura della CPU adotta il modo "crescere verso il basso", sai se ci sono dei vantaggi nel farlo?
Baiyan Huang

Nessuna idea, davvero. È possibile che qualcuno abbia pensato che il codice vada verso l'alto da 0, quindi lo stack dovrebbe andare verso il basso da highmem, in modo da ridurre al minimo la possibilità di intersecare. Ma alcune CPU iniziano specificamente a eseguire il codice in posizioni diverse da zero, quindi potrebbe non essere il caso. Come con la maggior parte delle cose, forse è stato fatto in questo modo semplicemente perché è stato il primo modo in cui qualcuno ha pensato di farlo :-)
paxdiablo

@lzprgmr: ci sono alcuni piccoli vantaggi nell'avere certi tipi di allocazione dell'heap eseguiti in ordine crescente, ed è storicamente comune che lo stack e l'heap si trovino alle estremità opposte di uno spazio di indirizzamento comune. A condizione che l'utilizzo combinato statico + heap + stack non superasse la memoria disponibile, non ci si doveva preoccupare esattamente della quantità di memoria dello stack utilizzata da un programma.
supercat

3

Su un x86, l '"allocazione" della memoria di uno stack frame consiste semplicemente nel sottrarre il numero necessario di byte dallo stack pointer (credo che altre architetture siano simili). In questo senso, immagino che lo stack cresca "verso il basso", in quanto gli indirizzi diventano progressivamente più piccoli mentre chiami più in profondità nello stack (ma immagino sempre che la memoria inizi con 0 in alto a sinistra e riceva indirizzi più grandi man mano che ti sposti a destra e avvolgere verso il basso, quindi nella mia immagine mentale la pila cresce ...). L'ordine delle variabili dichiarate potrebbe non avere alcuna influenza sui loro indirizzi - credo che lo standard consenta al compilatore di riordinarle, purché non causi effetti collaterali (qualcuno per favore correggimi se sbaglio) . Essi'

Il divario attorno all'array potrebbe essere una sorta di imbottitura, ma per me è misterioso.


1
In effetti, so che il compilatore può riordinarli, perché è anche libero di non allocarli affatto. Può semplicemente inserirli nei registri e non utilizzare alcuno spazio di stack.
rmeador

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

buon punto, non l'avevo considerato. ma è comunque sufficiente come prova che il compilatore possa riordinarli, dato che sappiamo che può farlo almeno qualche volta :)
rmeador

1

Prima di tutto, i suoi 8 byte di spazio inutilizzato in memoria (non sono 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 dato occupa spazio in memoria a partire dall'indirizzo divisibile per la sua dimensione. Nel nostro caso un array di 3 numeri interi richiede 12 byte di spazio di memoria e 604 non è divisibile per 12. Quindi lascia spazi vuoti finché non incontra un indirizzo di memoria divisibile per 12, è 596.

Quindi lo spazio di memoria allocato all'array va da 596 a 584. Ma poiché l'allocazione dell'array è in continuazione, il primo elemento dell'array inizia dall'indirizzo 584 e non da 596.


1

Il compilatore è libero di allocare variabili locali (auto) in qualsiasi punto del frame dello stack locale, non è possibile dedurre in modo affidabile la direzione di crescita dello stack esclusivamente da questo. È possibile dedurre la direzione di crescita dello stack confrontando gli indirizzi di stack frame annidati, ovvero confrontando l'indirizzo di una variabile locale all'interno dello stack frame di una funzione rispetto al suo chiamato:

#include <stdio.h>
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;
}

5
Sono abbastanza sicuro che sia un comportamento indefinito sottrarre puntatori a diversi oggetti dello stack: i puntatori che non fanno parte dello stesso oggetto non sono confrontabili. Ovviamente però non andrà in crash su nessuna architettura "normale".
Steve Jessop,

@SteveJessop C'è un modo per risolvere questo problema per ottenere la direzione dello stack a livello di codice?
xxks-kkk

@ xxks-kkk: in linea di principio no, perché un'implementazione C non è richiesta per avere una "direzione dello stack". Ad esempio, non violerebbe lo standard avere una convenzione di chiamata in cui un blocco dello stack viene allocato in anticipo e quindi viene utilizzata una routine di allocazione della memoria interna pseudo-casuale per saltare al suo interno. In pratica funziona effettivamente come descrive Matja.
Steve Jessop

0

Non penso che sia deterministico in questo modo. L'array sembra "crescere" perché quella memoria dovrebbe essere allocata in modo contiguo. Tuttavia, poiché q e s non sono affatto correlate tra loro, il compilatore inserisce ciascuna di esse in una posizione di memoria libera arbitraria all'interno dello stack, probabilmente quelle che si adattano meglio a una dimensione intera.

Quello che è successo tra a [2] eq è che lo spazio intorno alla posizione di q non era abbastanza grande (cioè, non era più grande di 12 byte) per allocare un array di 3 interi.


in caso affermativo, perché q, s, a non hanno memoria contingente? (Es: Indirizzo di q: 2293612 Indirizzo di s: 2293608 Indirizzo di a: 2293604)

Vedo un "divario" tra se a

Poiché se a non sono stati allocati insieme, gli unici puntatori che devono essere contigui sono quelli dell'array. L'altra memoria può essere allocata ovunque.
javanix,

0

Il mio stack sembra estendersi verso indirizzi con numeri inferiori.

Potrebbe essere diverso su un altro computer, o anche sul mio computer se utilizzo una chiamata di compilatore diversa. ... oppure il compilatore deve scegliere di non usare affatto uno stack (inline tutto (funzioni e variabili se non ne ho preso l'indirizzo)).

$ 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
livello 4: x è a 0x7fff7781190c
livello 3: x è a 0x7fff778118ec
livello 2: x è a 0x7fff778118cc
livello 1: x è a 0x7fff778118ac
livello 0: x è a 0x7fff7781188c

0

Lo stack si riduce (su x86). Tuttavia, lo stack viene allocato in un blocco quando la funzione viene caricata e non si ha alcuna garanzia sull'ordine in cui gli articoli saranno nello stack.

In questo caso, ha allocato spazio per due int e un array di tre int nello stack. Ha anche assegnato ulteriori 12 byte dopo l'array, quindi assomiglia a questo:

a [12 byte]
riempimento (?) [12 byte]
s [4 byte]
q [4 byte]

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

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


0

Dipende dal tuo sistema operativo e dal tuo compilatore.


Non so perché la mia risposta è stata negata. Dipende davvero dal tuo sistema operativo e dal tuo 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 viene simulato con un'area riservata di memoria o un set di registri.
David R Tribble

3
Probabilmente perché le affermazioni di una sola frase non sono buone risposte.
Gare di leggerezza in orbita il

0

Lo stack cresce. Quindi f (g (h ())), lo stack allocato per h inizierà da un indirizzo inferiore, quindi g e g saranno inferiori a f. Ma le variabili all'interno dello stack devono seguire la specifica C,

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

1206 Se gli oggetti puntati sono membri dello stesso oggetto aggregato, i puntatori ai membri della struttura dichiarati in seguito vengono confrontati più grandi dei puntatori ai membri dichiarati in precedenza nella struttura ei puntatori agli elementi dell'array con valori di pedice più grandi confrontano maggiori dei puntatori agli elementi della stessa matrice con valori di pedice inferiori.

& a [0] <& a [1], deve sempre essere vero, indipendentemente da come viene allocato "a"


Sulla maggior parte delle macchine, la pila cresce verso il basso, ad eccezione di quelle in cui cresce verso l'alto.
Jonathan Leffler

0

cresce verso il basso e questo è dovuto allo standard dell'ordine dei byte little endian quando si tratta del set di dati in memoria.

Un modo in cui potresti guardarlo è che lo stack CRESCE verso l'alto se guardi la memoria da 0 dall'alto e il massimo dal basso.

Il motivo per cui lo stack cresce verso il basso è quello di poter dereferenziare dalla prospettiva dello stack o del puntatore di base.

Ricorda che la dereferenziazione di qualsiasi tipo aumenta dall'indirizzo più basso a quello più alto. Poiché lo Stack cresce verso il basso (indirizzo dal più alto al più basso), questo ti consente di trattare lo stack come una 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.


The reason for the stack growing downward is to be able to dereference from the perspective of the stack or base pointer.Ragionamento molto carino
user3405291

0

Dipende dall'architettura. Per controllare il tuo sistema, usa questo codice da GeeksForGeeks :

// C program to check whether stack grows 
// downward or upward. 
#include<stdio.h> 

void fun(int *main_local_addr) 
{ 
    int fun_local; 
    if (main_local_addr < &fun_local) 
        printf("Stack grows upward\n"); 
    else
        printf("Stack grows downward\n"); 
} 

int main() 
{ 
    // fun's local variable 
    int main_local; 

    fun(&main_local); 
    return 0; 
} 
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.