Sonsuzluk kavramıyla ilk defa matematik dersinde karşılaşmadığımızı düşünüyorum. Kendi adıma hatırladığım ilk sonsuz kavramı sorgulaması, “Sonsuza kadar yaşamak nasıl oluyordu?” üzerine olmuştu. Galiba hep merak ettiğim nokta, toplama işlemi üzerinden gidersek, 1+1=2 ise 1+1+1=3 oluyor fakat bunu nasıl devam ettiremiyoruz oluyordu. Al bak 100 oldu, 100 000 oldu, 1000….000 oldu diyebiliyorken aniden sonsuz oluveriyor sayılar. İnternetten hızlıca baktığımız zaman içinde 2703 hane sıfır içeren sayıyı “Noncentilyon” olarak okuyoruz ve bunun bir fazlası yine sonsuz oluyor.

Bilgisayar mühendisliği galiba bu soruma zoraki olarak cevap verdi. Programlamaya başlarken yanlış veya eksik kurgulanmış döngülerin sonsuza kadar çalışacağı söylenir ve zaten bu uyarıyı dikkate almayınca başımıza gelen durum pek zevkli sayılmaz. Bir program çalıştığında ihtiyacı kadar RAM tüketiyor dersek üstün körü bir tanım olarak, sonsuza kadar çalışan bir program ne kadar RAM tüketir? Malesef cevap sonsuz değil. Burada fiziksel limitler devreye giriyor ve bu limit yakın bir zamana kadar benim için 4GB gibi az bir boyuttaydı. Aslında noncentilyon da benim için 4GB’dan bir bit bile yukarda değil diyebilirim.

Dennis Ritchie tarafından tasarlanan C programlama dilinde 5 farklı sonsuz döngü kodlayabildim. temel olarak tüm programlar tek bir fonksiyondan oluşuyor, int main(){ } kod bloğunda gözüken kısım tüm programların başlangıç yeri, referans noktası veya Greenwich’i diyebiliriz. 5 farklı kodu dowhile.c,for.c,goto.c,main.c ve while.c olarak isimlendirdim. Bunlar dışında while döngüsünü define makrosunda kullanarak define.c kodunu da test etmek istedim. Aslında dowhile.c,define.c,for.c ve while.c kodlarının hepsi döngülerden oluşuyor, bu bakımdan birbirine yakın sayabiliriz. Kodları irdelemeye başlamadan önce 5.3.0 kernel versiyonlu 64 bit GNU/Linux kurulu bir bilgisayarda 9.2.1 versiyonlu gcc derleyicisini kullanacağımı belirtmekte fayda var.

Öncelikle incelemek ve denemek isteyenler olursa diye kodları blok olarak iliştiriyorum buraya:

define.c:

#define forever while(1)
int main(){
	forever{}
	return 0;
}

dowhile.c:

int main(){
	do{}while(1);
	return 0;
}

for.c:

int main(){
	for(;;);
	return 0;
}

goto.c:

int main(){
	loop:
		goto loop;
	return 0;
}

main.c:

int main(){
	main();
	return 0;
}

while.c:

int main(){
	while(1){}
	return 0;
}

gcc kullanarak bu kodları derlerken aynı zamanda assembly kodlarını da elde ettim ve elimde 6 tane .s uzantılı 6 tane assembly kodu oldu. Örnek: gcc -S for.c komutu for.s dosyasında assembly kodunu üretiyor. Alfabetik olarak bu dosyaları 1’den 6’ya kadar linkledim ve birbirleri ile diff tool kullanarak karşılaştırdım. main.s haricinde tüm karşılaştırmaların ortak olmayan tek bir satırı olduğunu gördüm ve daha öncesinde gcc for.c -o for.o şeklinde oluşturduğum .o uzantılı programları çalıştırınca sadece main.o‘nun Segmentation fault hatasını almasından farklı bir çalışma beklediğimi söyleyebilirim.

diff 1.s 2.s
1c1
< 	.file	"define.c"
---
> 	.file	"dowhile.c"

for.s dosyasında bulunan kodu örnek alarak, 5 farklı programda gcc tarafından aynı üretilen assembly kodunu incelemek istedim.

	.file	"for.c"
	.text
	.globl	main
	.type	main, @function
main:
.LFB0:
	.cfi_startproc
	endbr64
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
.L2:
	jmp	.L2
	.cfi_endproc
.LFE0:
	.size	main, .-main
	.ident	"GCC: (Ubuntu 9.2.1-9ubuntu2) 9.2.1 20191008"
	.section	.note.GNU-stack,"",@progbits
	.section	.note.gnu.property,"a"
	.align 8
	.long	 1f - 0f
	.long	 4f - 1f
	.long	 5
0:
	.string	 "GNU"
1:
	.align 8
	.long	 0xc0000002
	.long	 3f - 2f
2:
	.long	 0x3
3:
	.align 8
4:

Aynı şekilde main.s dosyasını karşılaştırmak istediğimde karşımıza çıkan sonuç üretilen kodun mantığının da farklı olduğunu ortaya koyuyor. main.s:

	.file	"main.c"
	.text
	.globl	main
	.type	main, @function
main:
.LFB0:
	.cfi_startproc
	endbr64
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	movl	$0, %eax
	call	main
	movl	$0, %eax
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE0:
	.size	main, .-main
	.ident	"GCC: (Ubuntu 9.2.1-9ubuntu2) 9.2.1 20191008"
	.section	.note.GNU-stack,"",@progbits
	.section	.note.gnu.property,"a"
	.align 8
	.long	 1f - 0f
	.long	 4f - 1f
	.long	 5
0:
	.string	 "GNU"
1:
	.align 8
	.long	 0xc0000002
	.long	 3f - 2f
2:
	.long	 0x3
3:
	.align 8
4:

diff 1.s 5.s:

1c1
< 	.file	"define.c"
---
> 	.file	"main.c"
14,15c14,19
< .L2:
< 	jmp	.L2
---
> 	movl	$0, %eax
> 	call	main
> 	movl	$0, %eax
> 	popq	%rbp
> 	.cfi_def_cfa 7, 8
> 	ret

Temel farkın call main satırından kaynaklandığını görünce biraz araştırma ile main’in global bir constructor olan “_main”in genel adı olduğunu ve bu yüzden kod derlenirken hata almadığımı fakat programı çalıştırmaya çalışırken SegFault hatası aldığımı anladım. Bunun sebebinin bir program bir kere çalışmaya başlayabilir. Yani entry point dedikleri fonksiyon yazılımcı tarafından çağrılamaz, derleyici tarafından koda eklenemez. Entry point call main satırı demek oluyor ve bunu ‘_compile time‘da değil ‘run time‘da kontrol edebildiği için derleyici biz mantıken doğru kod yazdığımızı düşünürken, aslında hatalı bir program üretmiş oluyoruz.

Bir programlama dili ile uğraşırken, aslında iki temel konuyu sorgulayabilmeyi bu işi fazlasıyla zevkli kılan noktalar olarak görüyorum. Bunlar, bir kere başlayabilmek ve sonsuzluk olsa bile bir yerlerde, limitlerin olduğunu hatırlamak. Bir kere dünyaya geldiğimizi ve bir gün dünyadan gideceğimizi düşündürten bir bakış açısıyla programlama dili tasarlamaya zekice demek hafif bir iltifat olarak kalsa da, bizim programımız çalışmaya başladı ve çalışmaya devam ettiği süre boyunca segmentation fault yememek dileğiyle.