Ci ho provato:
;# As you have already noticed, the compiler wants to align the stack
;# pointer on a 16 byte boundary before it pushes anything. That's
;# because certain instructions' memory access needs to be aligned
;# that way.
;# So in order to first save the original offset of esp (+4), it
;# executes the first instruction:
lea ecx,[esp+0x4]
;# Now alignment can happen. Without the previous insn the next one
;# would have made the original esp unrecoverable:
and esp,0xfffffff0
;# Next it pushes the return addresss and creates a stack frame. I
;# assume it now wants to make the stack look like a normal
;# subroutine call:
push DWORD PTR [ecx-0x4]
push ebp
mov ebp,esp
;# Remember that ecx is still the only value that can restore the
;# original esp. Since ecx may be garbled by any subroutine calls,
;# it has to save it somewhere:
push ecx
Questa operazione viene eseguita per mantenere lo stack allineato a un limite di 16 byte. Alcune istruzioni richiedono che determinati tipi di dati siano allineati su un limite massimo di 16 byte. Per soddisfare questo requisito, GCC si assicura che lo stack sia inizialmente allineato a 16 byte e alloca lo spazio dello stack in multipli di 16 byte. Questo può essere controllato usando l'opzione -mpreferred-stack-boundary=num . Se utilizzi -mpreferred-stack-boundary=2 (per un allineamento 2=4 byte), questo codice di allineamento non verrà generato perché lo stack è sempre allineato almeno a 4 byte. Tuttavia potresti avere problemi se il tuo programma utilizza tipi di dati che richiedono un allineamento più forte.
Secondo il manuale di gcc:
Su Pentium e PentiumPro, i valori double e long double dovrebbero essere allineati a un limite di 8 byte (vedere -malign-double) o subire significative penalizzazioni delle prestazioni in fase di esecuzione. Su Pentium III, il tipo di dati SSE (Streaming SIMD Extension) __m128 potrebbe non funzionare correttamente se non è allineato a 16 byte.
Per garantire il corretto allineamento di questi valori nello stack, il limite dello stack deve essere allineato come quello richiesto da qualsiasi valore memorizzato nello stack. Inoltre, ogni funzione deve essere generata in modo tale da mantenere lo stack allineato. Pertanto, chiamare una funzione compilata con un limite di stack preferito più alto da una funzione compilata con un limite di stack preferito inferiore molto probabilmente disallineerà lo stack. Si raccomanda che le librerie che utilizzano i callback utilizzino sempre l'impostazione predefinita.
Questo allineamento aggiuntivo consuma spazio aggiuntivo nello stack e generalmente aumenta la dimensione del codice. Il codice sensibile all'utilizzo dello spazio dello stack, come i sistemi embedded e i kernel del sistema operativo, potrebbe voler ridurre l'allineamento preferito a -mpreferred-stack-boundary=2.
Il lea
carica il puntatore dello stack originale (da prima della chiamata a main
) in ecx
, poiché il puntatore dello stack sta per essere modificato. Viene utilizzato per due scopi:
- per accedere agli argomenti del
main
funzione, poiché sono relativi al puntatore dello stack originale - per ripristinare il puntatore dello stack al suo valore originale al ritorno da
main
lea ecx,[esp+0x4] ; I assume this is for getting the adress of the first argument of the main...why ?
and esp,0xfffffff0 ; ??? is the compiler trying to align the stack pointer on 16 bytes ???
push DWORD PTR [ecx-0x4] ; I understand the assembler is pushing the return adress....why ?
push ebp
mov ebp,esp
push ecx ;why is ecx pushed too ??
Anche se ogni istruzione funzionasse perfettamente senza penalità di velocità nonostante gli operandi arbitrariamente allineati, l'allineamento aumenterebbe comunque le prestazioni. Immagina un ciclo che fa riferimento a una quantità di 16 byte che si sovrappone solo a due righe della cache. Ora, per caricare quel piccolo wchar nella cache, devono essere rimosse due intere righe di cache, e cosa succede se ne hai bisogno nello stesso ciclo? La cache è così tremendamente più veloce della RAM che le prestazioni della cache sono sempre fondamentali.
Inoltre, di solito c'è una penalità di velocità per spostare gli operandi disallineati nei registri. Dato che lo stack viene riallineato, dobbiamo naturalmente salvare il vecchio allineamento per attraversare i frame dello stack per i parametri e tornare.
ecx è un registro temporaneo quindi deve essere salvato. Inoltre, a seconda del livello di ottimizzazione, alcune delle operazioni di collegamento dei frame che non sembrano strettamente necessarie per eseguire il programma potrebbero essere importanti per impostare una catena di frame pronta per la traccia.