Domanda Errore o eccezione di overflow dello stack?


Perché il seguente risultato non ha errori?

void func()
{
   func();
}

int main()
{
   func();
}

10
2018-03-30 23:29


origine


risposte:


In teoria, potrebbe sovraccaricare lo stack (perché, anche se non vengono utilizzate variabili locali, ogni chiamata aggiungerebbe il precedente indirizzo di ritorno nello stack); in pratica, con le ottimizzazioni abilitate, non ha overflow a causa di ottimizzazione della chiamata di coda, che in realtà evita qualsiasi consumo di risorse trasformando la chiamata in un salto, quindi non consumando lo stack.

Questo può essere visto facilmente da esaminando l'assemblaggio ottimizzato generato dal codice OP:

func():
.L2:
        jmp     .L2
main:
.L4:
        jmp     .L4

func è ottimizzato per un ciclo infinito, sia la "versione indipendente" che la chiamata in linea main.

Si noti che questo è coerente con lo standard C ++ per la regola "as if": il programma compilato deve essere eseguito come se era quello che hai richiesto nel codice (in termini di effetto), e dal momento che la dimensione dello stack è solo un limite di implementazione, il codice generato che utilizza un call e quello che usa a jmp sono equivalenti.

Ma: questo è un caso ancora più particolare, poiché lo standard dice anche che il ciclo infinito (definito come "non terminare e non avere alcuni effetti collaterali") è in realtà un comportamento indefinito, quindi in teoria il compilatore potrebbe omettere quella chiamata completamente.


23
2018-03-30 23:36



Probabilmente, il compilatore l'ha ottimizzato e lo ha trasformato in a while(true){} costruire.


8
2018-03-30 23:34



esso fa finire con a Difetto di segmentazione sul mio sistema Linux - Valgrind indica un possibile overflow dello stack, che è ovviamente vero, poiché per ogni chiamata di funzione è richiesto un nuovo stack frame.

però, abilitare le ottimizzazioni nel compilatore riduce questo intero programma a un ciclo infinito, che, naturalmente, non finisce affatto:

        .file   "so.c"
        .text
        .p2align 4,,15
.globl func
        .type   func, @function
func:
.LFB0:
        .cfi_startproc
        .p2align 4,,10
        .p2align 3
.L2:
        jmp     .L2
        .cfi_endproc
.LFE0:
        .size   func, .-func
        .p2align 4,,15
.globl main
        .type   main, @function
main:
.LFB1:
        .cfi_startproc
        .p2align 4,,10
        .p2align 3
.L5:
        jmp     .L5
        .cfi_endproc
.LFE1:
        .size   main, .-main
        .ident  "GCC: (GNU) 4.4.3"
        .section        .note.GNU-stack,"",@progbits

Ecco la parte interessante:

.L5:
        jmp     .L5

5
2018-03-30 23:35



Se si sta eseguendo la compilazione e l'esecuzione su Windows in una finestra di comando, è possibile che si verifichi un arresto anomalo ma senza commenti dal sistema operativo. (Costruiamo un compilatore divertente e ci imbattiamo molto in questo problema). L'affermazione di Microsoft è che quando il programma fa cose molto brutte, non possono recuperare ... quindi semplicemente uccidono il processo e riavviano il prompt dei comandi. In questo caso, dopo aver riacquistato il limite dello stack, quando il gestore trap tenta di fare qualcosa (come lo stato push trap nello stack) non c'è spazio e Windows arresta il processo.

Personalmente penso che questo sia un comportamento imperdonabile. Se il mio processo fa qualcosa di male, il sistema operativo dovrebbe sempre lamentarsi. Potrebbe dire "processo terminato con pregiudizio", insieme a qualche tipo di indicazione ("hai esaurito lo stack nel gestore dell'ultimo errore") ma dovrebbe dire qualcosa.

Multics ha fatto bene nel 1966. È un peccato che non abbiamo applicato queste lezioni in oltre 40 anni.


3
2018-03-30 23:39



Sulla mia macchina termina con un segfault (come dovrebbe essere la ricorsione infinita).

Forse la tua shell non sta segnalando il segfault. Quale sistema operativo stai usando?


1
2018-03-30 23:35



Tornando ai tempi antichi, quando volevi ottimizzare troppo un programma ASM, c'era una pratica: può accadere che una funzione termini una chiamata con un'altra funzione (che poi ritorna). Assomiglierebbe a qualcosa:

somefunc:

    ; do some things

    CALL someotherfunc
    RET

someotherfunc:

    ; do some other things

    RET

In questo modo quando CALL someotherfunc è successo l'indirizzo della prossima istruzione (il RET) viene salvato nello stack e quindi someotherfunc restituito solo per eseguire il ritorno. Esattamente gli stessi risultati possono essere raggiunti con a JMP a someotherfunc. In questo modo lo stack non conterrà l'indirizzo dell'ultima istruzione, ma conterrà l'indirizzo del chiamante originale. Cosi quando someotherfunc lo rende RET il programma continuerà al chiamante originale.

Quindi il codice ottimizzato sarebbe simile a:

somefunc:

    ; do some things

    JMP someotherfunc

someotherfunc:

    ; do some other things

    RET

E se somefunc si sta definendo come l'ultima istruzione (infatti questa è l'unica istruzione), sembrerebbe davvero:

somefunc:

    JMP somefunc

1
2018-04-04 13:27