In quella che dovrebbe essere l'ultima esecuzione del ciclo, scrivi array[10]
, ma ci sono solo 10 elementi nell'array, numerati da 0 a 9. La specifica del linguaggio C dice che si tratta di "comportamento indefinito". Ciò significa in pratica che il tuo programma tenterà di scrivere sul int
pezzo di memoria di grandi dimensioni che si trova immediatamente dopo array
nella memoria. Ciò che accade dipende da ciò che, in effetti, si trova lì, e questo dipende non solo dal sistema operativo, ma anche dal compilatore, dalle opzioni del compilatore (come le impostazioni di ottimizzazione), dall'architettura del processore, dal codice circostante , ecc. Potrebbe anche variare da un'esecuzione all'altra, ad esempio a causa della randomizzazione dello spazio degli indirizzi (probabilmente non su questo esempio di giocattolo, ma accade nella vita reale). Alcune possibilità includono:
- La posizione non è stata utilizzata. Il loop termina normalmente.
- La posizione è stata usata per qualcosa che aveva il valore 0. Il ciclo termina normalmente.
- La posizione conteneva l'indirizzo di ritorno della funzione. Il ciclo termina normalmente, ma il programma si arresta in modo anomalo perché tenta di passare all'indirizzo 0.
- La posizione contiene la variabile
i
. Il ciclo non termina mai perché si i
riavvia su 0.
- La posizione contiene qualche altra variabile. Il ciclo termina normalmente, ma poi accadono cose "interessanti".
- La posizione è un indirizzo di memoria non valido, ad es. Perché si
array
trova alla fine di una pagina di memoria virtuale e la pagina successiva non è mappata.
- I demoni volano fuori dal tuo naso . Fortunatamente alla maggior parte dei computer manca l'hardware necessario.
Quello che hai osservato su Windows è che il compilatore ha deciso di posizionare la variabile i
immediatamente dopo l'array in memoria, quindi ha array[10] = 0
finito per assegnare a i
. Su Ubuntu e CentOS, il compilatore non è stato inserito i
lì. Quasi tutte le implementazioni C raggruppano le variabili locali in memoria, su uno stack di memoria , con una grande eccezione: alcune variabili locali possono essere inserite interamente nei registri . Anche se la variabile è nello stack, l'ordine delle variabili è determinato dal compilatore e può dipendere non solo dall'ordine nel file di origine ma anche dai loro tipi (per evitare di sprecare memoria per vincoli di allineamento che lascerebbero buchi) , sui loro nomi, su alcuni valori di hash utilizzati nella struttura dati interna di un compilatore, ecc.
Se vuoi scoprire cosa ha deciso di fare il tuo compilatore, puoi dirlo per mostrarti il codice dell'assemblatore. Oh, e impara a decifrare l'assemblatore (è più facile che scriverlo). Con GCC (e alcuni altri compilatori, specialmente nel mondo Unix), passa l'opzione -S
per produrre il codice assembler anziché un binario. Ad esempio, ecco lo snippet dell'assemblatore per il loop dalla compilazione con GCC su amd64 con l'opzione di ottimizzazione -O0
(nessuna ottimizzazione), con i commenti aggiunti manualmente:
.L3:
movl -52(%rbp), %eax ; load i to register eax
cltq
movl $0, -48(%rbp,%rax,4) ; set array[i] to 0
movl $.LC0, %edi
call puts ; printf of a constant string was optimized to puts
addl $1, -52(%rbp) ; add 1 to i
.L2:
cmpl $10, -52(%rbp) ; compare i to 10
jle .L3
Qui la variabile i
è di 52 byte sotto la parte superiore dello stack, mentre l'array inizia 48 byte sotto la parte superiore dello stack. Quindi questo compilatore sembra essere stato posizionato i
poco prima dell'array; sovrascriveresti i
se ti capitasse di scrivere array[-1]
. Se cambi array[i]=0
a array[9-i]=0
, otterrai un loop infinito su questa particolare piattaforma con queste particolari opzioni del compilatore.
Ora compiliamo il tuo programma con gcc -O1
.
movl $11, %ebx
.L3:
movl $.LC0, %edi
call puts
subl $1, %ebx
jne .L3
È più corto! Il compilatore non ha solo rifiutato di allocare una posizione dello stack per i
- è sempre e solo memorizzato nel registro ebx
- ma non si è preoccupato di allocare memoria array
o di generare codice per impostare i suoi elementi, perché ha notato che nessuno degli elementi sono mai usati.
Per rendere più chiaro questo esempio, assicuriamoci che le assegnazioni di array vengano eseguite fornendo al compilatore qualcosa che non è in grado di ottimizzare. Un modo semplice per farlo è usare l'array da un altro file - a causa della compilazione separata, il compilatore non sa cosa succede in un altro file (a meno che non ottimizzi al momento del collegamento, che gcc -O0
o gcc -O1
no). Crea un file sorgente use_array.c
contenente
void use_array(int *array) {}
e cambia il tuo codice sorgente in
#include <stdio.h>
void use_array(int *array);
int main()
{
int array[10],i;
for (i = 0; i <=10 ; i++)
{
array[i]=0; /*code should never terminate*/
printf("test \n");
}
printf("%zd \n", sizeof(array)/sizeof(int));
use_array(array);
return 0;
}
Compila con
gcc -c use_array.c
gcc -O1 -S -o with_use_array1.c with_use_array.c use_array.o
Questa volta il codice dell'assemblatore è simile al seguente:
movq %rsp, %rbx
leaq 44(%rsp), %rbp
.L3:
movl $0, (%rbx)
movl $.LC0, %edi
call puts
addq $4, %rbx
cmpq %rbp, %rbx
jne .L3
Ora l'array è nello stack, 44 byte dall'alto. Che dire i
? Non appare da nessuna parte! Ma il contatore di loop viene mantenuto nel registro rbx
. Non è esattamente i
, ma l'indirizzo del array[i]
. Il compilatore ha deciso che dal momento che il valore di i
non è mai stato usato direttamente, non ha senso eseguire l'aritmetica per calcolare dove memorizzare 0 durante ogni esecuzione del ciclo. Invece quell'indirizzo è la variabile del ciclo e l'aritmetica per determinare i confini è stata eseguita in parte in fase di compilazione (moltiplicare 11 iterazioni per 4 byte per elemento dell'array per ottenere 44) e in parte in fase di esecuzione ma una volta per tutte prima dell'inizio del ciclo ( eseguire una sottrazione per ottenere il valore iniziale).
Anche su questo esempio molto semplice, abbiamo visto come cambiare le opzioni del compilatore (attivare l'ottimizzazione) o cambiare qualcosa di minore ( array[i]
in array[9-i]
) o persino cambiare qualcosa apparentemente non correlato (aggiungendo la chiamata a use_array
) può fare una differenza significativa rispetto a ciò che il programma eseguibile ha generato dal compilatore fa. Le ottimizzazioni del compilatore possono fare molte cose che potrebbero apparire non intuitive sui programmi che invocano comportamenti indefiniti . Ecco perché il comportamento indefinito viene lasciato completamente indefinito. Quando ti allontani leggermente dalle tracce, nei programmi del mondo reale, può essere molto difficile capire la relazione tra ciò che fa il codice e ciò che avrebbe dovuto fare, anche per programmatori esperti.