I motivi principali sono che è scomodo, produce un codice non ottimale, richiede una conoscenza preliminare delle dimensioni dello stack di funzione, limita le prestazioni, viola le specifiche di molte convenzioni di chiamata e non impedisce in realtà il superamento degli overflow.
Il solito modo in cui una funzione chiama in un'altra funzione, almeno su x86, è (ovviamente) con l'istruzione call
. Questa istruzione spinge l'indirizzo di ritorno nello stack e reindirizza il puntatore di istruzioni all'indirizzo di destinazione. Lo stack è un concetto che il processore stesso comprende e facilita e non possiamo modificare il comportamento del processore.
Per facilitare l'inserimento del puntatore di ritorno prima delle allocazioni dello stack, dovremmo allocare lo spazio di stack della funzione prima l'istruzione call
viene eseguita. Ciò richiede la funzione di chiamata (il chiamante) per allocare lo stack per la funzione chiamata (il chiamato), e quindi richiede implicitamente che il chiamante pulisca la pila del destinatario. Questo è possibile chiamando convenzioni come cdecl
(anche alcuni casi con thiscall
), che utilizzano la pulizia del chiamante, ma è una violazione delle specifiche della convenzione di chiamata per stdcall
, fastcall
, vectorcall
, pascal
e un numero di altre convenzioni.
Ci si potrebbe chiedere perché è importante che l'allocazione dello stack sia fatta dal chiamante o dal chiamante. Bene, in generale, esiste una funzione perché contiene codice che viene utilizzato in più di una posizione (questo è il motivo per cui abbiamo ottimizzato le ottimizzazioni!). In un modello di pulizia del callee, il codice per impostare lo spazio di allocazione dello stack della funzione e pulirlo alla fine fa parte della funzione di chiamata stessa. La funzione di chiamata non ha bisogno di conoscere i requisiti di memoria dello stack della funzione chiamata. Questo è utile perché non si finisce per duplicare la logica di gestione dello stack per ogni chiamata in quella funzione. Immagina, ad esempio, un programma che ha chiamato memcpy
in 10.000 posizioni diverse: per la pulizia di tutto, c'è solo un'istanza del codice di gestione dello stack per la funzione memcpy
, mentre per la pulizia del chiamante ci sono ora 10.000 copie. Anche se questo è solo 10 o 20 byte di codice ogni volta che sono già centinaia di kilobyte di codice ridondante.
Un altro problema è la crescita dello stack. Hai detto che il compilatore sa quanta pila userà una funzione, questo non è assolutamente vero. Le allocazioni dello stack dinamico sono abbastanza comuni e il tuo modello proposto racchiude lo stack del callee tra il frame dello stack della funzione chiamante e l'indirizzo di ritorno del callee:
| return addr | saved *bp | stackalloc space | return addr | saved bp | ...
| (callee) | (callee) | (callee) | (caller) | (caller) |
*bp+0 *bp+N *bp+2N *bp+?N ...
Questo significa che se la funzione richiede che il suo spazio di stack cresca dinamicamente, deve spostare il puntatore di ritorno e il puntatore dello stack frame (salvato *bp
) quando lo fa, e anche modificare il puntatore del frame dello stack nel processo. Questo è inutilmente complesso e introduce alcuni problemi di prestazioni.
Potresti anche aver capito perché questa non è una soluzione per i buffer overflow. Superando un buffer nello spazio stackalloc non sovrascrivi l'indirizzo di ritorno del callee, ma puoi sovrascrivere l'indirizzo di ritorno del chiamante. Questo non complica davvero lo sfruttamento.