Il problema principale nel far crescere lo stack è che deve sedersi all'inizio della memoria e avere un limite di dimensione superiore forzato, oppure sedersi nella parte superiore della memoria e avere una posizione forzata. Entrambe queste situazioni limitano l'utilizzo massimo della memoria. Se sia la pila che l'heap crescono l'una verso l'altra da estremità opposte, si esaurisce la memoria solo quando non ne rimane letteralmente nessuna. In altre configurazioni potresti esaurire lo stack o l'heap pur avendo memoria gratuita.
L'idea di "stack indietro" è stata lanciata da un po 'di tempo e generalmente non è vista come una soluzione solida. Per cominciare, ti imbatterai in problemi se stai modificando i buffer dello stack che si trovano nei frame precedenti. Inizialmente questo non sembra essere comune, ma considera di allocare i buffer per molte funzioni prima che le passi:
void foo(char* src)
{
char dest[20]; // local allocated within frame of foo
strcpy(dest, src); // new stack frame, so return ptr AFTER dest
// blah blah rest of code...
if (dest[0] == 'X')
printf("First char is X\n");
}
Si noti che il frame dello stack per strcpy
si posizionerà numericamente sopra il frame dello stack per foo
. In un sistema tipico, strcpy
scriverà oltre il buffer, sopra il puntatore di ritorno di foo
, portando al controllo del puntatore di istruzioni dopo che foo
ritorna. Con uno stack all'indietro, strcpy
scriverà, attraverso la fine dello stack frame di foo
, nella parte inferiore dello stack frame di strcpy
, portando a una sovrascrittura dell'indirizzo di ritorno di strcpy
, e di nuovo dando il controllo del puntatore di istruzioni.
Una soluzione considerata da molti abbastanza sicura consiste nell'utilizzare un'architettura con uno stack per locali e parametri (lo stack di dati) e un'altra per i frame dello stack e i puntatori di ritorno (lo stack di controllo). Questo isolamento garantisce che i puntatori di ritorno non possano essere sovrascritti casualmente quando un buffer locale viene sovraccaricato. Ha di nuovo problemi con la gestione della memoria, anche se solo uno stack deve avere una dimensione massima fissa - l'altro può crescere nell'heap.
Naturalmente, anche questa architettura non è completamente robusta dal punto di vista della sicurezza. Considera quanto segue:
int int_sorter( const void *val_a, const void *val_b )
{
// this code isn't important here
int first = *(int*)val_a;
int second = *(int*)val_b;
if ( first == second )
return 0;
else return (first < second) ? -1 : 1;
}
void bar(char* message, int (*sorter)(const void*,const void*))
{
int array[10];
char dest[32];
// do something with array
// ...
strcpy(dest, message);
qsort(array, 10, sizeof( int ), sorter);
}
In questo caso, supponendo una coppia di classiche pile in crescita in un'architettura a doppio stack, message
sovrasta dest
sullo stack di dati, copiando il puntatore di funzione sorter
che è stato premuto come secondo parametro a bar
. La chiamata a strcpy
ritorna normalmente (lo stack di controllo è intatto), ma la chiamata a qsort
contiene un'istruzione per passare alla funzione sorter
, che di nuovo porta al controllo sul puntatore di istruzioni.
Alla fine della giornata, non troverai una soluzione completa al problema. Il meglio che puoi fare è usare buone pratiche di codifica (o insegnare ai tuoi sviluppatori di farlo) e abilitare protezioni come ASLR (a.k.a. PIE), DEP / NX, stack di canarini, ecc.