2013-04-09 9 views
7

Si consideri il seguente programma:Il compilatore è autorizzato a ottimizzare l'utilizzo della memoria dello stack riordinando le variabili locali?

#include <stdio.h> 

void some_func(char*, int*, char*); 

void stack_alignment(void) { 
    char a = '-'; 
    int i = 1337; 
    char b = '+'; 
    some_func(&a, &i, &b); // to prevent the compiler from removing the local variables 
    printf("%c|%i|%c", a, i, b); 
} 

Si genera il seguente assemblaggio (commenti aggiunto da me, io sono un novizio completo per il montaggio):

$ vim stack-alignment.c 
$ gcc -c -S -O3 stack-alignment.c 
$ cat stack-alignment.s 
     .file "stack-alignment.c" 
     .section .rdata,"dr" 
LC0: 
     .ascii "%c|%i|%c\0" 
     .text 
     .p2align 2,,3 
     .globl _stack_alignment 
     .def _stack_alignment;  .scl 2;  .type 32;  .endef 
_stack_alignment: 
LFB7: 
     .cfi_startproc 
     subl $44, %esp 
     .cfi_def_cfa_offset 48 
     movb $45, 26(%esp) // local variable 'a' 
     movl $1337, 28(%esp) // local variable 'i' 
     movb $43, 27(%esp) // local variable 'b' 
     leal 27(%esp), %eax 
     movl %eax, 8(%esp) 
     leal 28(%esp), %eax 
     movl %eax, 4(%esp) 
     leal 26(%esp), %eax 
     movl %eax, (%esp) 
     call _some_func 
     movsbl 27(%esp), %eax 
     movl %eax, 12(%esp) 
     movl 28(%esp), %eax 
     movl %eax, 8(%esp) 
     movsbl 26(%esp), %eax 
     movl %eax, 4(%esp) 
     movl $LC0, (%esp) 
     call _printf 
     addl $44, %esp 
     .cfi_def_cfa_offset 4 
     ret 
     .cfi_endproc 
LFE7: 
     .def _some_func;  .scl 2;  .type 32;  .endef 
     .def _printf;  .scl 2;  .type 32;  .endef 

Come potete vedere ci sono 3 locali variabili (a, i e b) con le dimensioni di 1 byte, 4 byte e 1 byte. Compreso il padding questo sarebbe 12 byte (assumendo che il compilatore si allinea a 4 byte).

Non sarebbe più efficiente la memoria se il compilatore modificasse l'ordine delle variabili in (a, b, i)? Quindi sarebbero necessari solo 8 byte.

Qui una rappresentazione "grafico":

3 bytes unused     3 bytes unused 
    vvvvvvvvvvv      vvvvvvvvvvv 
+---+---+---+---+---+---+---+---+---+---+---+---+ 
| a | | | | i    | b | | | | 
+---+---+---+---+---+---+---+---+---+---+---+---+ 

       | 
       v 

+---+---+---+---+---+---+---+---+ 
| a | b | | | i    | 
+---+---+---+---+---+---+---+---+ 
     ^^^^^^^ 
     2 bytes unused 

è il compilatore permesso di fare questa ottimizzazione (dal ecc serie C)?

  • Se no (come penso l'uscita dell'assieme mostra), perché?
  • Se sì, perché non succede sopra?
+0

Supponendo che sia consentito dagli standard ecc, allora sarebbe completamente fino alla singola implementazione del compilatore, indipendentemente dal fatto che lo facciano o no. Immagino che sarebbe controllato dai livelli di ottimizzazione in fase di compilazione. – John3136

+0

Il compilatore/ottimizzatore è libero di posizionare i locali dove lo desidera, purché non interrompa il programma. È libero di posizionare due variabili nello stesso posto se è sicuro che non vengano mai utilizzate contemporaneamente. – mah

+0

Hai provato a compilare con diverse opzioni di ottimizzazione? Forse hai compilato le ottimizzazioni. –

risposta

4

Il compilatore è autorizzato a eseguire questa ottimizzazione (dallo standard C ecc.)?

Sì.

Se sì, perché non succede sopra?

E 'accaduto.

Leggere attentamente l'uscita dell'assemblatore.

movb $45, 26(%esp) // local variable 'a' 
    movl $1337, 28(%esp) // local variable 'i' 
    movb $43, 27(%esp) // local variable 'b' 

variabile a è all'offset 26. variabile b è all'offset 27. variabile i è all'offset 28.

Utilizzando le immagini che hai fatto il layout è ora:

+---+---+---+---+---+---+---+---+ 
| | | a | b | i    | 
+---+---+---+---+---+---+---+---+ 
^^^^^^^ 
2 bytes unused 
+0

Davvero non ho notato il numero nel secondo argomento del comando mov (l | b)>. <, Grazie per quello. Beh, ancora divertente perché gcc organizza questi comandi in questo ordine, sarebbe stato più facile notare se fosse il contrario. – MarcDefiant

+1

Non ho idea del perché le istruzioni siano ordinate in quel modo. Se dovessi speculare o si tratta di due istruzioni 'movb' che toccano la stessa parola di memoria che potrebbe bloccarsi (anche se abbiamo un buffer di memoria per un motivo, quindi è improbabile) o più probabile: non importa, quindi sono stati generati nell'ordine in cui il compilatore si trovava ad averli nell'albero di sintassi interno. – Art

7

Il compilatore è libero di impaginare le variabili locali come vuole. Non ha nemmeno bisogno di usare la pila.

Può memorizzare le variabili locali in un ordine non correlato all'ordine di dichiarazione nello stack se utilizza lo stack.

Il compilatore è autorizzato a eseguire questa ottimizzazione (dallo standard C ecc.)?

  • Se sì, perché non succede sopra?

Bene, si tratta di un'ottimizzazione?

Non è chiaro. Utilizza un paio di byte in meno, ma raramente conta. Ma su alcune architetture, potrebbe essere più veloce leggere un char se è stato archiviato allineato alle parole. Quindi, mettere gli char uno vicino all'altro forzerebbe almeno uno di loro a non essere allineato a parole e rendere la lettura più lenta.

+1

Questo non risponde completamente alla domanda. Un paio di byte sullo stack di chiamate possono essere importanti quando si implementa un algoritmo ricorsivo. C è la lingua di programmazione dei sistemi, quindi risparmiare memoria ove possibile * è * una preoccupazione. La domanda è se GCC sta perdendo un'opportunità di ottimizzazione o sta scegliendo un lato di un compromesso. È quasi certamente vero che su * alcune * architetture è più veloce leggere un 'char' da una posizione allineata; ma l'assemblaggio è generato per un'architettura concreta. Se questa architettura non impone una penalità per letture non allineate, manca l'ottimizzazione. – user4815162342

0

Generalmente nei sistemi normali in cui la velocità è importante, la lettura delle parole è più veloce della lettura del carattere. La perdita di memoria rispetto al guadagno di velocità viene ignorata. Ma nel caso del sistema in cui la memoria è importante, come in diversi cross compilatori che generano eseguibili (in un significato molto generico) per una determinata piattaforma di destinazione, l'immagine potrebbe essere completamente diversa. Il compilatore può riunirli insieme, persino controllarne la durata e gli usi, a seconda che riduca la larghezza di banda, ecc. Ecc. In pratica, dipende in larga misura dalla necessità. Ma in generale ogni compilatore ti dà la flessibilità se vuoi che le tue "pack" siano strettamente collegate.Si può guardare in manuale che

2

Non sarebbe più efficiente della memoria se il compilatore avrebbe cambiato l'ordine delle variabili

Non c'è modo di raccontare senza parlare di una specifica CPU, un sistema operativo specifico e un compilatore specifico. In generale, il compilatore funziona in modo ottimale. Per ottimizzare il codice in modo significativo, è necessaria una conoscenza approfondita del sistema specifico.

Nel tuo caso, il compilatore è probabilmente impostato per ottimizzare la velocità in questo caso. Sembra che il compilatore abbia deciso che gli indirizzi allineati per ogni variabile danno il codice più efficiente. In alcuni sistemi non è solo più veloce, ma anche obbligatoria per allocare indirizzi pari, perché alcune CPU possono gestire solo accesso allineati.

Il compilatore è autorizzato a eseguire questa ottimizzazione (dallo standard C ecc.)?

Sì, lo standard C non richiede nemmeno l'assegnazione delle variabili. Il compilatore è completamente gratuito per gestire questa situazione in alcun modo che vuole e non ha bisogno di documentare come o perché. Si può allocare le variabili da nessuna parte, si potrebbe ottimizzare il loro via del tutto, o di destinarli all'interno registri della CPU, o sullo stack, o in una piccola scatola di legno sotto la scrivania.

0

I compilatori con protezione da sovraccarico del buffer per lo stack (/GS per il compilatore Microsoft) possono riordinare le variabili come funzionalità di sicurezza. Ad esempio, se le variabili locali sono un array di caratteri (buffer) di dimensioni costanti e un puntatore di funzione, un utente malintenzionato che può sovraccaricare il buffer potrebbe sovrascrivere anche il puntatore della funzione. Pertanto, le variabili locali vengono riordinate in modo tale che il buffer si trovi vicino al canarino. In questo modo, un attaccante non può (direttamente) compromette il puntatore a funzione e il buffer overflow è (si spera) rilevata dal canarino distrutta.

Avvertenza: tali caratteristiche non impediscono il compromesso, ma alzano le barriere per un attaccante, ma un abile attaccante normalmente trova la sua strada.