Struttura dello stack di una chiamata di funzione

3

Ho letto il testo di Aleph One su Smashing the Stack for Fun and Profit. Ho annotato example1.c dal suo lavoro, l'ho modificato un po 'per vedere come appare lo stack sul mio sistema.

Sto utilizzando Ubuntu (64 bit) su una VM su Intel i5 M 480.

Il documento dice che una pila avrà la seguente struttura. Dice anche che la dimensione della parola è di 4 byte. Ho letto le dimensioni delle parole e ho stabilito che su un sistema operativo a 64 bit che non è "abilitato a lungo", la dimensione della parola è ancora di 32 bit o 4 byte.

Tuttavia,quandoeseguoilmiocodicepersonalizzato:

voidfunction(inta,intb,intc){charbuffer1[5];charbuffer2[10];memset(buffer1,"\xaa", sizeof(buffer1));
}

void main() {
    function(1, 2, 3);
}

Non ho la stessa struttura di stack della carta. Sì, sono a conoscenza del fatto che il documento è stato pubblicato nel 1998 ma non ho trovato nessun articolo su Internet che affermi che la struttura dello stack sia stata modificata notevolmente. Ecco come appare il mio stack (sto anche caricando screenshot GDB per la verifica, nel caso in cui abbia interpretato erroneamente lo stack):

    Lower memory                                                Higher memory
    -------------------------------------------------------------------------   
    | int c   | int b   | int a   | buffer1  | buffer2  | RBP     | RET     |
    | 4 bytes | 4 bytes | 4 bytes | 16 bytes | 16 bytes | 8 bytes | 8 bytes |
    -------------------------------------------------------------------------

Ora per le mie domande:

  1. Perché la struttura dello stack è cambiata?
  2. Che cos'è lo spazio extra assegnato a buffer1 e buffer2? Secondo la carta dovrebbero avere solo 8 byte e 12 byte assegnati. Tuttavia, buffer2 ottiene 6 byte in più e solo allora inizia il buffer1 e persino il buffer1 è assegnato a 16 byte. Mi sto perdendo qualcosa qui? Ho letto che lo spazio allentato viene dato come meccanismo di protezione, vero?

[EDIT]

L'ABI x64 dice che lo stack è sempre allineato a otto byte (pagina 17, nota a piè di pagina numero 9).

Quindi, se i parametri ei buffer di 2 caratteri vengono inseriti nello stack, lo spazio totale occupato dallo stack è di 48 byte (o 0x30 byte). Dato che è 8 byte per 3 interi che ammontano a un totale e 24 byte e 8 byte per buffer1 e 16 byte per buffer2, arriviamo al nostro totale complessivo di 48 byte che è corroborato dallo screenshot di GDB.

Tuttavia, lo stack GDB mostra anche che gli interi (a, b e c) sono stati assegnati solo 4 byte ciascuno. I buffer occupano anche la dimensione dichiarata nel programma (ovviamente). È per questo che vedo il gioco?

Slack = 48 - (5 + 10 + 4 + 4 + 4) = 21 byte

Ho ragione?

    
posta Torcellite 29.01.2015 - 07:26
fonte

1 risposta

8

La differenza è spiegata principalmente dalla differenza di ABI:

  • la carta spiega cosa succede in x86 (32 bit);

  • stai usando x86_64.

In x86n viene utilizzato ABI x86

  • I parametri vengono passati nello stack dal chiamante. Ciò significa che i 3 valori in alto saranno a, b e c.

  • L'istruzione call spinge l'IP (il campo ret ).

  • Solitamente, il puntatore del frame del chiamante viene inviato dal callee (il campo sfp ).

  • Tutti questi campi sono fissi su questa architettura dall'ABI.

  • La parte inferiore dello stack (variabili locali, registri salvati, ecc.) non viene riparata dall'ABI e il compilatore può decidere come usarlo.

Lower memory                                                Higher memory
-------------------------------------------------------------------------   
| buffer2 | buffer2 | sfp     | ret      | a        | b       | c       |
-------------------------------------------------------------------------
<----------(3)------><--(2)--><--------------------(1)------------------>

(1): Pushed by the caller for the callee, fixed by the ABI.
(2): Pushed by the callee, fixed by the ABI.
     Technically, this is optional as for x86 but it is generally used.
(3): Pushed by the callee, in any order.

In x86_64, viene utilizzato l' AMD-64 / x86_64 ABI:

  • I parametri vengono generalmente passati dai registri : le variabili a , b , c che trovi nello stack sono copie dei parametri eseguite dal destinatario (perché probabilmente non hai abilitato le ottimizzazioni). Questo è il motivo per cui sono più bassi nello stack rispetto all'indirizzo di ritorno. Ciò significa che il compilatore è libero di inserirli in qualsiasi ordine (e non è necessario averli in pila).

  • Inoltre, il codice di solito non è compilato con i puntatori dei frame: il push rbp; mov rbp, rsp viene generalmente omesso.

Lower memory                                                Higher memory
-------------------------------------------------------------------------   
| int c   | int b   | int a   | buffer1  | buffer2  | RBP     | RET     |
| 4 bytes | 4 bytes | 4 bytes | 16 bytes | 16 bytes | 8 bytes | 8 bytes |
-------------------------------------------------------------------------
<-------------(2)--------------------------------------------> <---(1)-->

(1): Pushed by the caller for the callee, fixed by the ABI.
(2): Pushed by the callee, in any order.

In breve:

  • Su x86, i parametri sono inseriti nello stack dal chiamante : le loro posizioni nello stack sono corrette dall'ABI. Sono più alti dell'indirizzo di ritorno perché sono spinti dal chiamante .

  • Su x86_64, i parametri sono passati nei registri (a meno che tu ne abbia troppi). Il callee è libero di spingerli in pila se è necessario in qualsiasi ordine. Poiché sono spinti dal callee , sono inferiori all'indirizzo di ritorno nello stack (e possono essere combinati con variabili locali, registri salvati).

  • In entrambi gli ABI, l'utilizzo di %rbp come frame base è facoltativo, ma in genere viene utilizzato per x86 e spesso non viene utilizzato in x86_64.

Nota: l'ABI di Windows x86_64 è diverso.

Alcuni riferimenti:

risposta data 29.01.2015 - 09:12
fonte

Leggi altre domande sui tag