Qual è esattamente il puntatore di base e il puntatore dello stack? A cosa indicano?

Usando questo esempio proveniente da wikipedia, in cui DrawSquare () chiama DrawLine (),

alt text

(Si noti che questo diagramma ha indirizzi alti in basso e indirizzi in alto.)

Qualcuno potrebbe spiegarmi cosa sono ebp ed esp in questo contesto?

Da quello che vedo, direi che il puntatore dello stack punta sempre in cima allo stack e il puntatore di base all’inizio della funzione corrente? O cosa?


modifica: intendo questo nel contesto dei programmi Windows

edit2: E come funziona anche eip ?

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 parole d’ordine, prendendo quindi 4 byte ciascuno. Quindi posso vedere che c’è una lacuna 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?


(ndr: rimosso una lunga citazione dalla risposta di Michael, che non appartiene alla domanda, ma è stata modificata una domanda successiva):

Questo perché il stream 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 ciò che accade dall’istante in cui compro gli argomenti della funzione che voglio richiamare fino alla fine del prologo? Voglio sapere come l’ebp, in particolare, si è evoluto durante quei momentjs (ho già capito come funziona il prologo, voglio solo sapere cosa succede dopo aver spinto gli argomenti in pila e prima del prologo).

esp è come dici tu, la cima della pila.

ebp è solitamente impostato su esp all’inizio della funzione. I parametri di funzione e le variabili locali sono accessibili aggiungendo e sottraendo, rispettivamente, un offset costante da ebp . Tutte le convenzioni di chiamata x86 definiscono ebp come conservato tra le chiamate di funzione. ebp stesso ebp punta al puntatore di base del frame precedente, il che abilita lo stack walking in un debugger e la visualizzazione di altre variabili locali dei frame.

La maggior parte dei prologhi di funzioni assomiglia a qualcosa:

 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 del puntatore FPO o del frame che è ansible abilitare in realtà eliminerà questo e userà ebp come un altro registro e accederà ai locals direttamente da esp , ma questo rende il debug un po ‘più difficile poiché il debugger non può più accedere direttamente ai frame dello stack della funzione precedente chiamate.

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 stream della chiamata di funzione è:

  • Parametri push ( hInstance , ecc.)
  • Funzione di chiamata, che spinge l’indirizzo di ritorno
  • Premere ebp
  • Assegna spazio per i locali

ESP è il puntatore dello stack corrente, che cambierà ogni volta che una parola o un indirizzo viene inserito o estratto 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 dal compilatore al compilatore), tutti gli argomenti di una funzione chiamata vengono inseriti nello stack (di solito nell’ordine inverso in cui vengono dichiarati nel prototipo della funzione, ma questo varia). Quindi viene chiamata la funzione, che inserisce l’indirizzo di ritorno (EIP) sullo stack.

All’ingresso della funzione, il vecchio valore EBP viene inserito nello stack e EBP viene impostato sul valore di ESP. Quindi l’ESP viene decrementato (poiché lo stack cresce in modo discendente nella memoria) per allocare spazio per le variabili locali e i temporanei della funzione. Da quel momento in poi, durante l’esecuzione della funzione, gli argomenti della funzione si trovano nello stack con offset positivi da EBP (perché sono stati inseriti prima della chiamata alla funzione) e le variabili locali si trovano in offset negativi da EBP (perché sono stati assegnati in pila dopo l’inserimento della funzione). Questo è il motivo per cui l’EBP è chiamato il puntatore del frame , perché punta al centro del frame di chiamata della funzione .

All’uscita, tutta la funzione deve essere impostata su ESP sul valore di EBP (che rilascia le variabili locali dallo stack e espone la voce EBP in cima allo stack), quindi inserisce il vecchio valore EBP dallo stack, e quindi la funzione ritorna (scatta l’indirizzo di ritorno in EIP).

Hai ragione. Il puntatore dello stack punta all’elemento in cima allo stack e il puntatore di base punta al “precedente” in cima allo stack prima che la funzione venisse 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 escono dall’ambito. Lo fai impostando il puntatore dello stack sul puntatore di base (che era il precedente “precedente” prima della chiamata alla funzione).

L’allocazione della memoria in questo modo è molto , molto veloce ed efficiente.

Prima di tutto, il puntatore dello stack punta alla parte inferiore dello stack poiché gli stack x86 vengono creati da valori di indirizzi elevati a valori di indirizzo inferiori. Il puntatore dello stack è il punto in cui la prossima chiamata a premere (o chiamare) posizionerà il valore successivo. La sua operazione è 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 indica in genere il tuo indirizzo di ritorno. ebp + 4 punti al primo parametro della funzione (o il valore di un metodo di class). ebp-4 punta alla prima variabile locale della tua funzione, di solito il vecchio valore di ebp in modo da poter ripristinare il precedente puntatore del frame.

MODIFICA: per una descrizione migliore, vedere Disinstallazione / Funzioni x86 e cornici dello stack in un WikiBook sull’assemblaggio x86. Provo ad aggiungere alcune informazioni che potrebbero essere interessate all’utilizzo di Visual Studio.

Memorizzare l’EBP del chiamante come prima variabile locale è chiamato frame stack standard e può essere utilizzato per quasi tutte le convenzioni di chiamata su Windows. Esistono differenze se il chiamante o il chiamato disgiunge i parametri passati e quali parametri sono passati nei registri, ma questi sono ortogonali al problema dello stack frame standard.

Parlando di programmi Windows, potresti probabilmente usare Visual Studio per compilare il tuo codice C ++. Si noti che Microsoft utilizza un’ottimizzazione chiamata Frame Pointer Omission, che rende quasi imansible eseguire lo stack senza utilizzare la libreria dbghlp e il file PDB dell’eseguibile.

Questa omissione di Pointer frame indica che il compilatore non memorizza il vecchio EBP su un posto standard e utilizza il registro EBP per qualcos’altro, pertanto è difficile trovare l’EIP del chiamante senza sapere quanto spazio le variabili locali necessitano per una determinata funzione. Ovviamente Microsoft fornisce un’API che ti permette di fare passeggiate stack anche in questo caso, ma la ricerca del database delle tabelle dei simboli nei file PDB richiede troppo tempo per alcuni casi d’uso.

Per evitare FPO nelle unità di compilazione, è necessario evitare l’uso di / O2 o aggiungere esplicitamente / Oy- alle flag di compilazione C ++ nei progetti. Probabilmente ti colleghi al runtime C o C ++, che usa FPO nella configurazione Release, quindi avrai difficoltà a fare stack walk senza dbghlp.dll.

Molto tempo da quando ho fatto la programmazione Assembly, ma questo link potrebbe essere utile …

Il processore ha una collezione di registri che vengono utilizzati per memorizzare i dati. Alcuni di questi sono valori diretti mentre altri puntano a un’area all’interno della RAM. I registri tendono ad essere utilizzati per determinate azioni specifiche e ogni operando in assembly richiederà una certa 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à scaricato prima nello stack, seguito dall’indirizzo di ritorno in modo che il sistema saprà dove tornare una volta che gli verrà detto di tornare. Il puntatore dello stack punterà nella prossima posizione in cui i nuovi dati possono essere inseriti nello stack, dove rimarrà fino a quando non viene nuovamente visualizzato.

Registri di base o registri di segmento puntano semplicemente allo spazio di indirizzamento di una grande quantità di dati. Combinato con un secondo registratore, il puntatore Base dividerà la memoria in enormi blocchi mentre il secondo registro punterà su un object all’interno di questo blocco. I puntatori di base quindi puntano alla base di blocchi di dati.

Tieni presente che Assembly è molto specifico della CPU. La pagina che ho collegato fornisce informazioni sui diversi tipi di CPU.

esp sta per “Extended Stack Pointer” ….. ebp per “Something Base Pointer” …. e eip per “Something Instruction Pointer” …… Il puntatore stack punta all’indirizzo di offset del segmento stack . Il puntatore di base punta all’indirizzo di offset del segmento extra. Il puntatore di istruzioni punta all’indirizzo di offset del segmento di codice. Ora, riguardo ai segmenti … sono piccole divisioni di 64 KB dell’area di memoria dei processori ….. Questo processo è noto come Segmentazione della memoria. Spero che questo post sia stato utile.

Modifica Sì, questo è principalmente sbagliato. Descrive qualcosa di completamente diverso nel caso qualcuno sia interessato 🙂

Sì, il puntatore dello stack punta in cima allo stack (indipendentemente dal fatto che sia la prima posizione dello stack vuoto o l’ultimo pieno di cui non sono sicuro). Il puntatore di base punta alla posizione di memoria dell’istruzione che viene eseguita. Questo è a livello di opcode – le istruzioni più elementari che puoi ottenere su un computer. Ogni opcode e i suoi parametri sono memorizzati in una posizione di memoria. Una riga C o C ++ o C # può essere tradotta in un opcode o in una sequenza di due o più a seconda di quanto sia complessa. Questi vengono scritti nella memoria del programma in modo sequenziale ed eseguiti. In circostanze normali il puntatore di base è incrementato di una istruzione. Per il controllo del programma (GOTO, IF, ecc.) Può essere incrementato più volte o semplicemente sostituito con il successivo indirizzo di memoria.

In questo contesto, le funzioni sono memorizzate nella memoria di programma a un determinato indirizzo. Quando viene richiamata la funzione, determinate informazioni vengono inserite nello stack che consente al programma di trovarne il ritorno al punto da cui è stata richiamata la funzione nonché i parametri della funzione, quindi l’indirizzo della funzione nella memoria del programma viene inserito nel puntatore di base. Al successivo ciclo di clock 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 continua da lì.