Döngünün son çalıştırılmasında ne yazılır array[10]
, ancak dizide 0 ile 9 arasında numaralandırılmış yalnızca 10 öğe vardır. C dili belirtimi bunun “tanımlanmamış davranış” olduğunu söyler. Uygulamada bunun anlamı, programınızın int
hemen bellekte kalan büyük boyutlu bir belleğe yazmaya çalışacağıdır array
. O zaman ne olur, aslında orada yatan şeylere bağlıdır ve bu sadece işletim sistemine değil, derleyiciye, derleyici seçeneklerine (optimizasyon ayarları gibi), işlemci mimarisine, çevre koduna bağlıdır. Örneğin adres alanı rasgeleleştirmesi nedeniyle yürütmeden yürütmeye bile değişebilir (muhtemelen bu oyuncak örneğinde değil, ancak gerçek hayatta gerçekleşir). Bazı olasılıklar:
- Konum kullanılmadı. Döngü normal olarak sona erer.
- Konum, 0 değerine sahip olan bir şey için kullanıldı. Döngü normal olarak sona erer.
- Konum, işlevin dönüş adresini içeriyordu. Döngü normal olarak sona erer, ancak program 0 adresine atlamaya çalıştığı için çöker.
- Konum değişkeni içerir
i
. Döngü asla sona ermez, çünkü i
0'da yeniden başlar.
- Konum başka bir değişken içeriyor. Döngü normal olarak sona erer, ancak daha sonra “ilginç” şeyler olur.
- Konum geçersiz bir bellek adresidir, örneğin
array
bir sanal bellek sayfasının hemen sonunda olduğundan ve sonraki sayfa eşlenmediğinden.
- Şeytanlar burnundan uçar . Neyse ki çoğu bilgisayar gerekli donanıma sahip değildir.
Ne Windows üzerinde gözlenen derleyici değişkeni yerleştirmek için karar olmasıydı i
, bellekte dizinin hemen sonra öylesine array[10] = 0
için atama sona erdi i
. Ubuntu ve CentOS'ta derleyici i
oraya yerleştirilmedi. Hemen hemen tüm C uygulamaları, bellekteki yerel değişkenleri bir bellek yığınında gruplandırır ve büyük bir istisnadır: bazı yerel değişkenler tamamen kayıtlara yerleştirilebilir . Değişken yığın üzerinde olsa bile, değişkenlerin sırası derleyici tarafından belirlenir ve yalnızca kaynak dosyadaki sıraya değil, aynı zamanda türlerine de bağlı olabilir (delik bırakacak hizalama kısıtlamalarına bellek israfını önlemek için) , isimlerinde, bir derleyicinin dahili veri yapısında kullanılan bazı karma değerlerde vb.
Derleyicinizin ne yapmaya karar verdiğini öğrenmek istiyorsanız, bunu derleyici kodunu göstermesini söyleyebilirsiniz. Oh, ve montajcıyı deşifre etmeyi öğren (yazmaktan daha kolay). GCC (ve özellikle Unix dünyasındaki diğer derleyiciler) ile -S
ikili yerine montajcı kodu üretme seçeneğini iletin. Örneğin, burada döngü için derleyici snippet'i, optimizasyon seçeneği -O0
(optimizasyon yok) ile amd64 üzerinde GCC ile derleme ve yorumların manuel olarak eklenmesi:
.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
Burada, değişken i
yığının üst kısmının altında 52 bayt, dizi ise yığının üstünden 48 bayt altında başlar. Yani bu derleyici i
diziden hemen önce yer alıyor; Üzerine yazmak istiyorum i
size yazmaya olduysa array[-1]
. Eğer değiştirirseniz array[i]=0
için array[9-i]=0
, bu belirli derleyici seçenekleri ile bu özel platformda sonsuz döngü elde edersiniz.
Şimdi programınızı derleyelim gcc -O1
.
movl $11, %ebx
.L3:
movl $.LC0, %edi
call puts
subl $1, %ebx
jne .L3
Bu daha kısa! Derleyici yalnızca bir yığın konumu ayırmayı reddetmedi i
- sadece kayıtta saklandı ebx
- ancak array
öğelerini ayarlamak için bellek ayırmak veya kod oluşturmak için uğraşmadı , çünkü öğelerin hiçbirinin hiç kullanılmaz.
Bu örneği daha iyi anlatmak için, derleyiciye optimize edemediği bir şey sağlayarak dizi atamalarının gerçekleştirildiğinden emin olalım. Bunu yapmanın kolay bir yolu, diziyi başka bir dosyadan kullanmaktır - ayrı derleme nedeniyle, derleyici başka bir dosyada ne olduğunu bilmiyor (bağlantı zamanında optimize edilmedikçe, hangisi gcc -O0
yoksa veya gcc -O1
etmiyorsa). Aşağıdakileri use_array.c
içeren bir kaynak dosya oluşturun
void use_array(int *array) {}
ve kaynak kodunuzu
#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;
}
Şununla derleyin:
gcc -c use_array.c
gcc -O1 -S -o with_use_array1.c with_use_array.c use_array.o
Bu kez montajcı kodu şöyle görünür:
movq %rsp, %rbx
leaq 44(%rsp), %rbp
.L3:
movl $0, (%rbx)
movl $.LC0, %edi
call puts
addq $4, %rbx
cmpq %rbp, %rbx
jne .L3
Şimdi dizi yığında, üstten 44 bayt. Ne olmuş i
? Hiçbir yerde görünmüyor! Ancak döngü sayacı kayıtta tutulur rbx
. Tam olarak i
değil, adresi array[i]
. Derleyici, değerinin i
hiçbir zaman doğrudan kullanılmadığından, döngünün her çalışması sırasında 0'ın nerede depolanacağını hesaplamak için aritmetik gerçekleştirmenin bir anlamı olmadığına karar verdi . Bunun yerine bu adres döngü değişkendir ve sınırları belirlemek için aritmetik kısmen derleme zamanında gerçekleştirildi (44 elde etmek için 11 yinelemeyi dizi öğesi başına 4 bayt ile çarpın) ve kısmen çalışma zamanında ancak döngü başlamadan önce bir kez ve hepsi için ( başlangıç değerini almak için bir çıkarma işlemi yapın).
Bu çok basit örnekte bile, derleyici seçeneklerini değiştirmenin (optimizasyonu aç) veya küçük bir şeyi değiştirmeyi ( array[i]
to array[9-i]
) veya hatta görünüşte ilgisiz bir şeyi değiştirmeyi (çağrıyı ekleyerek use_array
) yürütülebilir programın oluşturduğu şeyde nasıl önemli bir fark yaratabildiğini gördük. derleyici tarafından yapar. Derleyici optimizasyonları, tanımlanmamış davranışları çağıran programlarda sezgisel görünmeyebilir . Bu yüzden tanımsız davranış tamamen tanımsız bırakılır. Gerçek dünya programlarında, parçalardan çok az saptığınız zaman, deneyimli programcılar için bile kodun ne yaptığı ve ne yapılması gerektiği arasındaki ilişkiyi anlamak çok zor olabilir.