Che cos'è esattamente il puntatore di base e il puntatore dello stack? A cosa indicano?


225

Utilizzando questo esempio proveniente da Wikipedia, in cui DrawSquare () chiama DrawLine (),

testo alternativo

(Nota che questo diagramma ha indirizzi alti in basso e indirizzi bassi in alto.)

Qualcuno potrebbe spiegarmi cosa ebpe cosa espsono in questo contesto?

Da quello che vedo direi che il puntatore dello stack punta sempre all'inizio dello stack e il puntatore di base all'inizio della funzione corrente? O cosa?


modifica: intendo questo nel contesto dei programmi Windows

edit2: E anche come eipfunziona?

edit3: ho il seguente codice da MSVC ++:

var_C= dword ptr -0Ch
var_8= dword ptr -8
var_4= dword ptr -4
hInstance= dword ptr  8
hPrevInstance= dword ptr  0Ch
lpCmdLine= dword ptr  10h
nShowCmd= dword ptr  14h

Tutti sembrano essere dwords, prendendo così 4 byte ciascuno. Quindi posso vedere che c'è un gap da hInstance a var_4 di 4 byte. Quali sono? Presumo che sia l'indirizzo di ritorno, come si può vedere nella foto di Wikipedia?


(nota del redattore: rimossa una lunga citazione dalla risposta di Michael, che non appartiene alla domanda, ma una domanda di follow-up è stata modificata in):

Questo perché il flusso della chiamata di funzione è:

* Push parameters (hInstance, etc.)
* Call function, which pushes return address
* Push ebp
* Allocate space for locals

La mia domanda (ultimo, spero!) Ora è: che cosa è esattamente cosa succede dall'istante in cui apro gli argomenti della funzione che voglio chiamare fino alla fine del prologo? Voglio sapere come si evolve l'ebp, esp in quei momenti (ho già capito come funziona il prologo, voglio solo sapere cosa sta succedendo dopo aver spinto gli argomenti in pila e prima del prologo).


23
Una cosa importante da notare è che lo stack cresce "verso il basso" in memoria. Ciò significa che per spostare il puntatore dello stack verso l'alto si riduce il suo valore.
BS

4
Un suggerimento per differenziare ciò che fanno EBP / ESP ed EIP: EBP ed ESP gestiscono i dati, mentre EIP si occupa del codice.
mmmmmmmm,

2
Nel tuo grafico, ebp (di solito) è il "puntatore al frame", in particolare il "puntatore dello stack". Ciò consente di accedere ai locali tramite [ebp-x] e di impilare i parametri tramite [ebp + x] in modo coerente, indipendentemente dal puntatore dello stack (che cambia frequentemente all'interno di una funzione). L'indirizzo potrebbe essere effettuato tramite ESP, liberando EBP per altre operazioni - ma in questo modo, i debugger non possono dire lo stack di chiamate o i valori dei locali.
peterchen,

4
@ Ben. Non negligentemente. Alcuni compilatori inseriscono i frame dello stack nell'heap. Il concetto di stack in crescita è proprio questo, un concetto che lo rende facile da capire. L'implementazione dello stack può essere qualsiasi cosa (l'uso di blocchi casuali dell'heap rende gli hack che sovrascrivono parti dello stack molto più difficili in quanto non sono così deterministici).
Martin York,

1
in due parole: il puntatore dello stack consente alle operazioni push / pop di funzionare (quindi push and pop sa dove inserire / ottenere i dati). il puntatore di base consente al codice di fare riferimento in modo indipendente ai dati che sono stati precedentemente inseriti nello stack.
Tigrou,

Risposte:


229

esp è come dici tu, la parte superiore della pila.

ebpdi solito è impostato espall'inizio della funzione. È possibile accedere ai parametri di funzione e alle variabili locali aggiungendo e sottraendo, rispettivamente, un offset costante da ebp. Tutte le convenzioni di chiamata x86 vengono definite ebpcome conservate tra le chiamate di funzione. ebpstesso in realtà punta al puntatore di base del frame precedente, che consente lo stack walking in un debugger e la visualizzazione delle variabili locali di altri frame per funzionare.

La maggior parte dei prologhi di funzioni assomigliano a:

push ebp      ; Preserve current frame pointer
mov ebp, esp  ; Create new frame pointer pointing to current stack top
sub esp, 20   ; allocate 20 bytes worth of locals on stack.

Quindi più avanti nella funzione potresti avere un codice simile (presumendo che entrambe le variabili locali siano 4 byte)

mov [ebp-4], eax    ; Store eax in first local
mov ebx, [ebp - 8]  ; Load ebx from second local

L' ottimizzazione dell'omissione dell'FPO o del puntatore al frame che è possibile abilitare eliminerà effettivamente questo e userà ebpcome un altro registro e accederà ai locali direttamente esp, ma ciò rende il debug un po 'più difficile poiché il debugger non può più accedere direttamente ai frame dello stack delle precedenti chiamate di funzione.

MODIFICARE:

Per la tua domanda aggiornata, le due voci mancanti nello stack sono:

var_C = dword ptr -0Ch
var_8 = dword ptr -8
var_4 = dword ptr -4
*savedFramePointer = dword ptr 0*
*return address = dword ptr 4*
hInstance = dword ptr  8h
PrevInstance = dword ptr  0C
hlpCmdLine = dword ptr  10h
nShowCmd = dword ptr  14h

Questo perché il flusso della chiamata di funzione è:

  • Parametri push ( hInstance, ecc.)
  • Funzione di chiamata, che invia l'indirizzo di ritorno
  • spingere ebp
  • Allocare spazio per i locali

1
Grazie per la spiegazione! Ma ora sono un po 'confuso. Supponiamo che io chiami una funzione e sono nella prima riga del suo prologo, ancora senza aver eseguito una sola riga da essa. A quel punto, qual è il valore di ebp? Lo stack ha qualcosa a quel punto oltre agli argomenti push? Grazie!
divorò l'elisio il

3
L'EBP non viene modificato magicamente, quindi fino a quando non avrai stabilito un nuovo EBP per la tua funzione, avrai comunque il valore dei chiamanti. E oltre agli argomenti, lo stack conterrà anche il vecchio EIP (indirizzo di ritorno)
MSalters

3
Bella risposta. Anche se non può essere completo senza menzionare cosa c'è nell'epilogo: istruzioni "lasciare" e "rip".
Calmarius,

2
Penso che questa immagine aiuterà a chiarire alcune cose su quale sia il flusso. Inoltre, tieni presente che la pila cresce verso il basso. ocw.cs.pub.ro/courses/_media/so/laboratoare/call_stack.png
Andrei-Niculae Petre

Sono io o mancano tutti i segni meno nello snippet di codice sopra?
BarbaraKwarc,

96

ESP è il puntatore dello stack corrente, che cambierà ogni volta che una parola o un indirizzo viene inserito o rimosso dallo stack. EBP è un modo più conveniente per il compilatore di tenere traccia dei parametri di una funzione e delle variabili locali rispetto all'utilizzo diretto dell'ESP.

Generalmente (e questo può variare da compilatore a compilatore), tutti gli argomenti di una funzione chiamata vengono inseriti nello stack dalla funzione chiamante (di solito nell'ordine inverso sono dichiarati nel prototipo della funzione, ma questo varia) . Quindi viene chiamata la funzione, che inserisce l'indirizzo di ritorno (EIP) nello stack.

All'accesso alla funzione, il vecchio valore EBP viene inserito nello stack ed EBP viene impostato sul valore di ESP. Quindi l'ESP viene ridotto (poiché lo stack cresce verso il basso in memoria) per allocare spazio per le variabili e i temporali locali della funzione. Da quel momento in poi, durante l'esecuzione della funzione, gli argomenti della funzione si trovano nello stack in offset positivi da EBP (perché sono stati spinti prima della chiamata di funzione) e le variabili locali si trovano in offset negativi da EBP (perché sono stati allocati nello stack dopo l'immissione della funzione). Ecco perché l'EBP è chiamato puntatore al frame , perché punta al centro del frame della chiamata di funzione .

All'uscita, tutto ciò che la funzione deve fare è impostare ESP sul valore di EBP (che sposta le variabili locali dallo stack ed espone la voce EBP nella parte superiore dello stack), quindi pop il vecchio valore EBP dallo stack, e quindi la funzione ritorna (inserendo l'indirizzo di ritorno in EIP).

Al ritorno alla funzione chiamante, può quindi aumentare l'ESP al fine di rimuovere gli argomenti della funzione che ha inserito nello stack appena prima di chiamare l'altra funzione. A questo punto, lo stack è tornato nello stesso stato in cui era prima di invocare la funzione chiamata.


15

Hai ragione. Il puntatore dello stack punta all'elemento superiore dello stack e il puntatore di base punta all'inizio della "precedente" dello stack prima che la funzione fosse chiamata.

Quando si chiama una funzione, qualsiasi variabile locale verrà memorizzata nello stack e il puntatore dello stack verrà incrementato. Quando si ritorna dalla funzione, tutte le variabili locali nello stack non rientrano nell'ambito. Puoi farlo riportando il puntatore dello stack sul puntatore di base (che era il "precedente" in alto prima della chiamata di funzione).

Fare l'allocazione di memoria in questo modo è molto , molto veloce ed efficiente.


14
@Robert: quando dici "precedente" in cima allo stack prima che la funzione fosse chiamata, stai ignorando entrambi i parametri, che vengono inseriti nello stack appena prima di chiamare la funzione e l'EIP del chiamante. Questo potrebbe confondere i lettori. Diciamo che in un frame stack standard, EBP punta allo stesso punto in cui ESP puntava subito dopo aver inserito la funzione.
parrucca,

7

EDIT: per una migliore descrizione, vedi Disassemblaggio / Funzioni x86 e Stack Frame in un WikiBook sull'assemblaggio x86. Provo ad aggiungere alcune informazioni che potrebbero interessarti utilizzando Visual Studio.

La memorizzazione dell'EBP del chiamante come prima variabile locale è chiamata frame di stack standard e può essere utilizzata per quasi tutte le convenzioni di chiamata su Windows. Esistono differenze se il chiamante o il chiamante trasferiscono i parametri passati e quali parametri vengono passati nei registri, ma questi sono ortogonali al problema del frame dello stack standard.

Parlando di programmi Windows, potresti probabilmente usare Visual Studio per compilare il tuo codice C ++. Tenere presente che Microsoft utilizza un'ottimizzazione denominata Frame Pointer Omission, che rende quasi impossibile eseguire lo stack senza utilizzare la libreria dbghlp e il file PDB per l'eseguibile.

Questo Frame Pointer Omission significa che il compilatore non memorizza il vecchio EBP in un posto standard e usa il registro EBP per qualcos'altro, quindi hai difficoltà a trovare l'EIP del chiamante senza sapere di quanto spazio hanno bisogno le variabili locali per una determinata funzione. Ovviamente Microsoft fornisce un'API che consente di eseguire stack-walk anche in questo caso, ma la ricerca del database della tabella dei simboli nei file PDB richiede troppo tempo per alcuni casi d'uso.

Per evitare FPO nelle unità di compilazione, è necessario evitare di utilizzare / O2 o aggiungere esplicitamente / Oy- ai flag di compilazione C ++ nei progetti. Probabilmente ti collegherai al runtime C o C ++, che utilizza FPO nella configurazione della versione, quindi avrai difficoltà a fare passeggiate nello stack senza dbghlp.dll.


Non capisco come EIP sia archiviato nello stack. Non dovrebbe essere un registro? Come può un registro essere in pila? Grazie!
divorò elisio l'

Il chiamante EIP viene inserito nello stack dall'istruzione CALL stessa. L'istruzione RET recupera semplicemente la parte superiore dello stack e la inserisce nell'EIP. Se si hanno sovraccarichi del buffer, questo fatto potrebbe essere utilizzato per passare al codice utente da un thread privilegiato.
parrucca,

@devouredelysium Il contenuto (o il valore ) del registro EIP viene inserito (o copiato) nello stack, non nel registro stesso.
BarbaraKwarc,

@BarbaraKwarc Grazie per l' input di valore . Non riuscivo a vedere cosa mancava all'OP dalla mia risposta. In effetti, i registri rimangono dove sono, solo il loro valore viene inviato alla RAM dalla CPU. Nella modalità amd64, questo diventa un po 'più complesso, ma lascialo a un'altra domanda.
parrucca

Che dire di quel amd64? Sono curioso.
BarbaraKwarc,

6

Prima di tutto, il puntatore dello stack punta verso la parte inferiore dello stack poiché gli stack x86 vengono compilati da valori di indirizzo elevati a valori di indirizzo inferiori. Il puntatore dello stack è il punto in cui la chiamata successiva da premere (o chiamata) posizionerà il valore successivo. Il suo funzionamento è equivalente all'istruzione C / C ++:

 // push eax
 --*esp = eax
 // pop eax
 eax = *esp++;

 // a function call, in this case, the caller must clean up the function parameters
 move eax,some value
 push eax
 call some address  // this pushes the next value of the instruction pointer onto the
                    // stack and changes the instruction pointer to "some address"
 add esp,4 // remove eax from the stack

 // a function
 push ebp // save the old stack frame
 move ebp, esp
 ... // do stuff
 pop ebp  // restore the old stack frame
 ret

Il puntatore di base è in cima al fotogramma corrente. ebp generalmente indica il tuo indirizzo di ritorno. ebp + 4 indica il primo parametro della funzione (o questo valore di un metodo di classe). ebp-4 punta alla prima variabile locale della tua funzione, di solito il vecchio valore di ebp in modo da poter ripristinare il puntatore del frame precedente.


2
No, ESP non punta alla fine dello stack. Lo schema di indirizzamento della memoria non ha nulla a che fare con esso. Non importa se lo stack cresce a indirizzi inferiori o superiori. Il "top" dello stack è sempre il punto in cui verrà spostato il valore successivo (posto in cima allo stack) o, su altre architetture, dove è stato inserito l'ultimo valore spinto e dove si trova attualmente. Pertanto, ESP punta sempre in cima allo stack.
BarbaraKwarc,

1
Il fondo o la base della pila, invece, è dove è stato inserito il primo (o più vecchio ) valore, e quindi coperto da valori più recenti. Ecco da dove deriva il nome "puntatore di base" per EBP: doveva puntare alla base (o alla base) dell'attuale stack locale di una subroutine.
BarbaraKwarc,

Barbara, nell'Intel x86, lo stack è UPSIDE DOWN. La parte superiore della pila contiene il primo oggetto inserito nella pila e ogni oggetto dopo viene spinto SOTTO l'elemento superiore. La parte inferiore della pila è dove vengono posizionati i nuovi oggetti. I programmi vengono messi in memoria a partire da 1k e crescono all'infinito. Lo stack inizia all'infinito, realisticamente massimo mem meno ROM e cresce verso 0. ESP punta a un indirizzo il cui valore è inferiore al primo indirizzo inviato.
jmucchiello,

1

Molto tempo da quando ho fatto la programmazione dell'Assemblea, ma questo link potrebbe essere utile ...

Il processore ha una raccolta di registri che vengono utilizzati per archiviare i dati. Alcuni di questi sono valori diretti mentre altri puntano verso un'area all'interno della RAM. I registri tendono ad essere utilizzati per determinate azioni specifiche e ogni operando nell'assieme richiederà una determinata quantità di dati in registri specifici.

Il puntatore dello stack viene utilizzato principalmente quando si chiamano altre procedure. Con i moderni compilatori, un gruppo di dati verrà prima scaricato nello stack, seguito dall'indirizzo di ritorno in modo che il sistema sappia dove tornare una volta che gli viene detto di tornare. Il puntatore dello stack punterà nella posizione successiva in cui i nuovi dati possono essere inseriti nello stack, dove rimarranno finché non verranno nuovamente visualizzati.

I registri di base o i registri di segmenti indicano semplicemente lo spazio degli indirizzi di una grande quantità di dati. Combinato con un secondo registratore, il puntatore Base divide la memoria in blocchi enormi mentre il secondo registro punta a un elemento all'interno di questo blocco. I puntatori di base puntano quindi alla base di blocchi di dati.

Tieni presente che Assembly è molto specifico per la CPU. La pagina a cui ho collegato fornisce informazioni sui diversi tipi di CPU.


I registri di segmento sono separati su x86: sono gs, cs, ss e, a meno che tu non stia scrivendo un software di gestione della memoria, non li tocchi mai.
Michael,

ds è anche un registro di segmento e ai tempi di MS-DOS e codice a 16 bit, era assolutamente necessario modificare occasionalmente questi registri di segmento, poiché non potevano mai indicare più di 64 KB di RAM. Tuttavia DOS poteva accedere alla memoria fino a 1 MB perché utilizzava puntatori di indirizzi a 20 bit. Successivamente abbiamo ottenuto sistemi a 32 bit, alcuni con registri di indirizzi a 36 bit e ora registri a 64 bit. Quindi al giorno d'oggi non avrai più bisogno di cambiare questi registri di segmento.
Wim ten Brink,

Nessun sistema operativo moderno utilizza 386 segmenti
Ana Betts,

@Paul: SBAGLIATO! SBAGLIATO! SBAGLIATO! I segmenti a 16 bit sono sostituiti da segmenti a 32 bit. In modalità protetta, ciò consente la virtualizzazione della memoria, sostanzialmente consentendo al processore di mappare gli indirizzi fisici a quelli logici. Tuttavia, all'interno dell'applicazione, le cose sembrano ancora piatte, poiché il sistema operativo ha virtualizzato la memoria per te. Il kernel funziona in modalità protetta, consentendo alle applicazioni di funzionare in un modello di memoria piatta. Vedi anche en.wikipedia.org/wiki/Protected_mode
Wim ten Brink,

@Workshop ALex: questo è un tecnicismo. Tutti i sistemi operativi moderni impostano tutti i segmenti su [0, FFFFFFFF]. Questo non conta davvero. E se leggessi la pagina collegata, vedrai che tutte le cose fantasiose sono fatte con pagine, che sono molto più dettagliate rispetto ai segmenti.
MSalters,

-4

Modifica Sì, questo è per lo più sbagliato. Descrive qualcosa di completamente diverso nel caso qualcuno sia interessato :)

Sì, il puntatore dello stack punta in cima allo stack (se si tratta della prima posizione dello stack vuota o dell'ultima piena di cui non sono sicuro). Il puntatore di base punta alla posizione di memoria dell'istruzione che viene eseguita. Questo è a livello di codici operativi: le istruzioni di base che puoi ottenere su un computer. Ogni codice operativo e i relativi parametri sono memorizzati in una posizione di memoria. Una riga C o C ++ o C # può essere tradotta in un codice operativo o in una sequenza di due o più a seconda della complessità. Questi vengono scritti nella memoria del programma in sequenza ed eseguiti. In circostanze normali il puntatore di base viene incrementato di un'istruzione. Per il controllo del programma (GOTO, IF, ecc.) Può essere incrementato più volte o semplicemente sostituito con l'indirizzo di memoria successivo.

In questo contesto, le funzioni sono memorizzate nella memoria del programma a un determinato indirizzo. Quando viene chiamata la funzione, alcune informazioni vengono inviate nello stack che consente al programma di ritrovare la posizione da cui è stata richiamata la funzione, nonché i parametri della funzione, quindi l'indirizzo della funzione nella memoria del programma viene inserito nella puntatore di base. Al successivo ciclo dell'orologio il computer inizia a eseguire le istruzioni da quell'indirizzo di memoria. Quindi ad un certo punto RITORNERÀ nella posizione di memoria DOPO l'istruzione che ha chiamato la funzione e proseguirà da lì.


Sto avendo un po 'di problemi a capire cos'è l'ebp. Se abbiamo 10 righe di codice MASM, ciò significa che mentre scendiamo in esecuzione quelle righe, ebp aumenterà sempre?
divorò elisio l'

1
@Devoured - No. Questo non è vero. eip sarà in aumento.
Michael,

Vuoi dire che quello che ho detto è giusto ma non per EBP, ma per IEP, vero?
divorato elisio l'

2
Sì. EIP è il puntatore dell'istruzione e viene implicitamente modificato dopo l'esecuzione di ciascuna istruzione.
Michael,

2
Oooh mio cattivo. Sto pensando a un puntatore diverso. Penso che andrò a lavarmi il cervello.
Stephen Friederichs,

-8

esp sta per "Extended Stack Pointer" ..... ebp per "Something Base Pointer" .... ed eip per "Something Instruction Pointer" ...... Lo stack Pointer punta all'indirizzo di offset del segmento dello stack . Il puntatore di base punta all'indirizzo di offset del segmento aggiuntivo. Il puntatore a istruzioni punta all'indirizzo di offset del segmento di codice. Ora, riguardo ai segmenti ... sono piccole divisioni da 64 KB dell'area di memoria dei processori ..... Questo processo è noto come Segmentazione della memoria. Spero che questo post sia stato utile.


3
Questa è una vecchia domanda, tuttavia, sp sta per puntatore stack, bp sta per puntatore base e ip per puntatore istruzione. La e all'inizio di tutti sta solo dicendo che è un puntatore a 32 bit.
Hyden,

1
La segmentazione è irrilevante qui.
BarbaraKwarc,
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.