Kısım I · Makine · Bölüm 9
İlk Gerçek Dil: C
C ve derleyici (C and the compiler)
Neden burada: Önceki bölümlerde işlemcinin yalnızca bit desenlerinden oluşan komutları çalıştırdığını, assembly'nin bu desenlere verilmiş insanca okunur adlar olduğunu gördün. Ama kimse bir hukuk uygulamasını mov ve add komutlarıyla yazmaz. Bu bölüm bir kat yukarı çıkar: insanın okuyup yazabildiği bir dil (C) ve o dili makine koduna çeviren program (derleyici). Kitabın geri kalanında göreceğin her dil (JavaScript, TypeScript, Go) bu fikrin bir akrabasıdır; C ise merdivenin ilk gerçek basamağıdır, çünkü altındaki makineye en yakın duran dildir.
Neye dayanıyor: Bölüm 7: makine kodunun bit dizisi olması ve assembly'nin onun okunur hali; derleyici bu iki ucu birbirine bağlar. Bölüm 1.3: fonksiyon, koşul, Boole ifadesi, döngü ve değişken. Burada aynı beş yapı taşını bu kez gerçek bir dilin sözdizimiyle yazacaksın. Ayrıca Bölüm 2'deki ASCII ve ikilik sayı bilgisini tiplerde kullanacağız.
- Kat 6–10Gömülü yazılım, işletim sistemi, ağ, web, uygulamaHepsi bu bölümde gösterilen türden dillerle yazılmıştır.
- Kat 5Programlama dilleri ve derleyiciİnsanın okuyabildiği kodu makine koduna çeviren araçlar. Bölüm 9–11.
- Kat 4Makine dili ve assemblyİşlemcinin doğrudan anladığı komutlar. Bölüm 7.
- Kat 0–3Elektrik, kapılar, devreler, işlemci ve bellekHer şeyin altındaki fiziksel makine. Bölüm 2–6, 8.
Bu bölümün sonunda şunları yapabileceksin: bir C dosyasının kaynak koddan çalıştırılabilir dosyaya hangi dört aşamadan geçtiğini sayabilmek (ve aşamaların her birinin gerçek çıktısını görmüş olmak), temel bir C programını satır satır okuyup ne yaptığını söyleyebilmek, bir hata mesajını okuyunca hatanın hangi türden olduğunu ayırt edebilmek ve aynı fikirlerin JavaScript'te nasıl yazıldığını bilmek. Hedef C programcısı olmak değil. Hedef, bir kat aşağıya baktığında orada ne olduğunu bilmek; böylece agent'ın ürettiği kodda ya da bir hata mesajında "bu hangi aşamanın şikâyeti?" diye sorabilmek.
9.1 Kaynak kod, derleyici, derleme, çalıştırılabilir dosya
Bir programı insan yazar; işlemci ise yalnızca makine kodunu çalıştırır. Bu ikisi arasındaki boşluğu doldurmak için iki ayrı kavram gerekir. İnsanın yazdığı, okunabilir komut listesine kaynak kod (source code) denir. İşlemcinin çalıştırdığı bit desenine ise (Bölüm 7'den hatırla) makine kodu denir. Kaynak kodu makine koduna çeviren programa derleyici (compiler), bu çeviri işine de derleme (compiling) denir.1 CS50 bunu üç kutuluk bir şemayla gösterir: kaynak kod girer, derleyici (içi henüz bilinmeyen bir kara kutu) çalışır, makine kodu çıkar.1 Bölüm 1'deki girdi–işlem–çıktı resminin kendisidir; bu sefer girdi ve çıktı birer program.
Derlemenin çıktısı, diskte duran ve işletim sisteminin belleğe yükleyip çalıştırabileceği bir dosyadır: çalıştırılabilir dosya (executable). Bottom-Up CS kitabı bunu şöyle koyar: bir program belleğe doğrudan yüklenebilmek için düz bir ikilik (binary) biçimde olmalıdır; kaynak koddan bu biçime dönüştürme işine derleme, bunu yapan programa derleyici denir.3 Yani çalıştırılabilir dosya da sonunda bir bayt dizisidir (Bölüm 2.8'deki ders: anlamı, onu okuyan kuraldan gelir; işletim sistemi bu dosyayı "program" kuralıyla okur).
Benzetme: yeminli tercüman
Derleyici, Kerem'in hukuk dünyasından tanıdığı bir figüre benzer: yeminli tercüman. Tercüman metnin anlamını değiştirmez; bir dildeki metni, karşı tarafın okuyabildiği dile çevirir. Kaynak kod "Türkçe metin", işlemcinin dili "karşı tarafın dili"dir. Ama benzetme burada biter, çünkü derleyici bir de katı bir redaktördür: metinde yazım kuralına aykırı bir yer varsa çeviriyi hiç yapmaz ve hatanın yerini gösterir (9.6).
Yazdığın dil ile çalışan dil farklıdır
Bir programlama dili, bir derleyici ve bir işlemci aynı şey değildir. C'yi bilen bir derleyici Mac'in ARM işlemcisi için de, Windows PC'nin x86-64 işlemcisi için de makine kodu üretebilir; kaynak aynı, çıktı farklıdır (bunu Bölüm 6.6'da "aynı program neden Mac'te ve PC'de ayrı derlenir" diye görmüştün). Bu bölümde göstereceğim çıktılar bir Linux makinesinde, x86-64 işlemci ve gcc 15.2.0 derleyicisiyle üretildi; Apple silicon bir Mac'te aynı komutlar farklı bir assembly verir. CS50 ise clang adlı başka bir C derleyicisini kullanır.2 Derleyici markası değişir, fikir aynı kalır.
Not: Her dil derlenmez. Bazı diller bir yorumlayıcı (interpreter) tarafından satır satır çalıştırılır; Bottom-Up CS bunu "her değişiklikte derleyiciyi çağırma zahmetinden doğan mantıksal bir uzantı" olarak anlatır.3 JavaScript'in tarayıcıda nasıl çalıştığını Bölüm 21 ve 25'te göreceğiz; şimdilik C'nin "önce derle, sonra çalıştır" modelinde kal.
9.2 hello, world: sözdiziminin ilk hali
Her programlama dersinin ilk programı ekrana bir selam yazdırır. CS50'nin C'deki ilk programı da budur:1
#include <stdio.h>
int main(void)
{
printf("hello, world\n");
}
Bu altı satırın her karakterinin bir görevi var; CS50'nin dediği gibi biri yanlış yazılırsa program çalışmaz.1 Her bir parçayı açalım:
#include <stdio.h>: "şu başlık dosyasını (header file) programıma kat."stdio.h(standard input/output), ekrana yazma ve klavyeden okuma gibi hazır işlevlerin adlarını ve nasıl çağrılacaklarını bildiren bir dosyadır. Bu bildirim olmadan derleyiciprintfkelimesini tanımaz. Başlık dosyaları bir kütüphaneyi (başkasının yazıp hazır bıraktığı kod koleksiyonu) kullanma izni gibidir.1 Neden "studio" değil destdioolduğunu soran olursa: standard input/output'un kısaltması.int main(void): programın giriş noktası.mainadlı fonksiyon, işletim sistemi programı başlattığında çalışan ilk yerdir. Baştakiint, bu fonksiyonun bir tam sayı döndüreceğini, parantez içindekivoidise hiçbir şey almadığını söyler. (Dönüş değerini 9.4'te açıklıyorum.){ ... }: fonksiyonun gövdesini çerçeveler; içindeki satırlar sırayla çalışır.printf("hello, world\n");:printffonksiyonunu çağırır; tırnak içindeki metin ona verilen girdidir.\nyeni satır karakteridir: ters eğik çizgi, derleyiciye "sonraki karakter özel" diyen bir kaçış işaretidir ("tırnak,\ters eğik çizgi için kullanılır).1 Satır;ile biter. C'de noktalı virgül, cümlenin noktası gibidir.
Bir dilin bu tür yazım kurallarının tümüne sözdizimi (syntax) denir. Sözdizimi hem insan hem derleyici için ortak bir sözleşmedir; tıpkı bir dilekçenin "başlık, ilgi, açıklama, sonuç ve istem" bölümlerinin belirli biçimde dizilmesi gibi. Derleyici bu sözleşmeye hiç esneklik tanımaz.
Üç komutla çalıştırmak
CS50'de program şu üç adımla yazılıp çalıştırılır: code hello.c (dosyayı yaz), make hello (derle), ./hello (çalıştır).1 make bir derleme aracıdır (build tool): içeride derleyiciyi uygun seçeneklerle çağırır. Aynı işi doğrudan derleyiciyle yapmak da mümkündür: CS50'de clang -o hello hello.c.2 Komutu parçalayalım: clang derleyicinin adı, -o hello "çıktıyı hello adlı dosyaya yaz", hello.c ise girdi dosyası. Klasörde bundan sonra iki dosya görürsün: hello.c insanın okuduğu kaynak kod, hello ise bilgisayarın doğrudan çalıştırabildiği makine kodu.1 Sondaki ./, "bulunduğum klasördeki dosyayı çalıştır" demektir. (Terminalin ayrıntılarını Bölüm 18'de göreceğiz.)
9.3 Değişken, tip, atama
Bölüm 1.3'te değişkeni "adı olan bir saklama yeri" diye tanımlamıştık. Bölüm 5'te bu saklama yerinin RAM'deki hücreler olduğunu öğrendin. C'de bir değişken, belleğin belli bir miktarını ayırıp ona ad vermektir:
int dogru = 7; // 7'yi "dogru" adlı bir tam sayı değişkeninde sakla
int toplam = 10;
char harf = 'B'; // tek bir karakter
Üç yeni kavram var. Başta yazılan kelime (int, char) değişkenin tipidir (type): o bellek bölgesindeki bitlerin hangi kuralla okunacağını söyler. Bölüm 2.8'de "anlam bağlamdadır" demiştik; tip, bu bağlamı programın içinde açıkça yazmaktır. = işareti atama (assignment) işlemidir: sağdaki değeri soldaki değişkene koyar. Matematikteki "eşittir" değildir; "sağdakini soldakine yaz" komutudur. Eşitlik sorusu için iki işaret kullanılır: ==.1 Üçüncüsü, // ile başlayan satır sonu notlarıdır: derleyici bunları atlar, insana yöneliktir.
C'nin başlıca tipleri
Her tip bellekten sabit miktarda yer kaplar. CS50'nin kullandığı ortamdaki tipik boyutlar şöyledir:2
| Tip | Ne saklar | Boyut |
|---|---|---|
bool | doğru/yanlış | 1 byte |
char | tek karakter (ASCII kodu) | 1 byte |
int | tam sayı | 4 byte |
long | daha büyük tam sayı | 8 byte |
float | ondalıklı sayı | 4 byte |
double | daha duyarlı ondalıklı sayı | 8 byte |
Bu tablo Bölüm 2'nin doğrudan sonucudur. Bir int 4 byte, yani 32 bittir; Bölüm 2.2'de öğrendiğin gibi 32 bit 232 = 4.294.967.296 farklı durum demektir. Bu durumlardan yarısı negatif sayılar için ayrıldığında (işaretli sayılar, Bölüm 4.3'teki ikiye tümleyen düzeni), CS50'nin ifadesiyle işaretli bir int'in en büyük değeri tipik olarak 2.147.483.647, işaretsiz bir int'in en büyüğü 4.294.967.295'tir.1 (Bunu bu makinede doğruladım: INT_MAX 2147483647, UINT_MAX 4294967295 çıktı.)
CS50, bir oyunla bunu gösterir: bir program her turda dolar miktarını ikiye katlar. Bir int bir noktada sınırı aşar ve değer "sarar": sonuç bozuk, hatta negatif çıkar.1 Buna taşma (overflow) dendiğini ve gerçek hayatta felaketlere yol açtığını Bölüm 4.3'te görmüştün. Çözüm olarak long kullanılır; ama CS50'nin notu şudur: taşmayı ortadan kaldırmaz, ertelenmesini sağlar.1 Tip seçmek, "bu değer en fazla ne kadar büyüyebilir?" sorusunu cevaplamaktır. Uygulamamızda puan, seri ve soru sayısı için bu sorunun cevabı bellidir; yine de bir kullanıcı kimliği (ileride milyarları geçebilir) gibi büyüyen sayılarda aynı soru ciddileşir.
Tam sayı bölmesi ve ondalıklı sayılar
İki int bölündüğünde sonuç yine int olur ve ondalık kısım atılır: CS50'nin örneğiyle 7 / 2 işleminin sonucu 3'tür, 3,5 değil.1 Ondalıklı sonuç isteniyorsa sayıyı önce float'a dönüştürmek gerekir; buna tip dönüştürme (cast) denir ve (float) x / y biçiminde yazılır.1 Ayrıca ondalıklı sayıların kesin olmadığını, Bölüm 4.4'te 0.1 + 0.2'nin neden 0.30000000000000004 çıktığıyla görmüştün; CS50 de aynı noktaya, %.50f ile 50 basamak yazdırıp son basamakların oynadığını göstererek değinir.1 Aşağıdaki küçük programı derleyip çalıştırdım:
#include <stdbool.h>
#include <stdio.h>
int main(void)
{
int dogru = 7;
int toplam = 10;
float oran = (float) dogru / toplam; // 0.7, tam sayı bölmesi olsaydı 0
char harf = 'B';
bool gecti = oran >= 0.6;
printf("oran: %f, secilen: %c\n", oran, harf);
}
Çıktı: oran: 0.700000, secilen: B. printf içindeki %f ve %c yer tutuculardır (format kodu, format code): %i int, %f float, %c char, %s string, %li long için kullanılır; tırnaktan sonra gelen değerler sırayla bu yerlere yerleşir.1 Bir not daha: 'B' tek tırnakla yazılır ve bellekte 66 sayısı olarak durur, çünkü Bölüm 2.5'teki ASCII tablosunda B = 66'dır. Karakter ile sayı aynı bitlerdir; tip, onu nasıl okuyacağını söyler.
Yazarken tembellik etme: sabit ve işleçler
const int n = 3; değeri değişmeyecek bir değişken (sabit) tanımlar; ikinci kez değiştirmeye kalkarsan derleyici seni durdurur.1 Aritmetik işleçler (operators) + - * / %'dir; % bölümden kalanı verir. seri++, seri = seri + 1 işleminin kısa yazılışıdır.1 Uygulamamızın seri sayacı (streak) tam olarak böyle bir değişkendir: gün tamamlandıkça bir artar, bir gün atlanınca sıfırlanır.
9.4 Koşul, döngü, fonksiyon, parametre, dönüş değeri
Bölüm 1.3'ün dört yapı taşını artık C ile yazabiliriz. CS50'nin bunlar için verdiği yazımlar şöyledir:1
Koşul ve Boole ifadesi
if (x < y)
{
printf("x, y'den küçük\n");
}
else if (x > y)
{
printf("x, y'den büyük\n");
}
else
{
printf("eşit\n");
}
Parantez içindeki x < y bir Boole ifadesidir: sonucu doğru ya da yanlıştır (Bölüm 3.3). Yukarıdan aşağıya sırayla denenir; ilk doğru çıkan dal çalışır ve gerisi atlanır. CS50 bir nokta daha vurgular: üçüncü dal için else if (x == y) yazmak gereksizdir, çünkü ilk ikisi yanlışsa geriye eşitlik zaten kalmıştır; else yeter.1 Boole ifadeleri && (VE), || (VEYA), ! (DEĞİL) ile birleşir: c == 'Y' || c == 'y' hem büyük hem küçük harfi kabul eder.1 Bunlar Bölüm 3.5'te söylediğimiz köprünün ta kendisidir: mantık kapılarının dilbilgisi hâli.
Döngü
for (int i = 0; i < 3; i++)
{
printf("meow\n");
}
for döngüsünün parantezi üç bölümlüdür: başlangıç (int i = 0), devam koşulu (i < 3), her turdan sonra yapılacak adım (i++).1 Bilgisayar bilimciler genelde sıfırdan saydığı için "üç kez" demek i'nin 0, 1, 2 değerlerini alması demektir. Aynı işi while (koşul) ile de yazabilirsin; bir kez çalışıp sonra koşula bakmak istiyorsan do { ... } while (koşul) kullanılır.1 Koşul hiç yanlış olmazsa döngü sonsuza dek döner; Ctrl-C programı durdurur.1 Bölüm 7.3'te gördüğün atlama (jump) komutları, döngünün makinedeki gerçek halidir: bir for sonunda "başa dön" diyen bir atlamaya ve "koşul yanlışsa çık" diyen bir dallanmaya dönüşür.
Fonksiyon, parametre, dönüş değeri
int puan_hesapla(int dogru_sayisi, int soru_basina); // önce bildirim (prototip)
int main(void)
{
printf("puan: %i\n", puan_hesapla(7, 10));
}
int puan_hesapla(int dogru_sayisi, int soru_basina)
{
return dogru_sayisi * soru_basina;
}
Bu programı derledim ve çıktı puan: 70 oldu. Parçalar şunlar: Fonksiyon adının önündeki int, dönüş değerinin (return value) tipidir; return o değeri çağıran yere geri verir. Parantez içindeki int dogru_sayisi, int soru_basina parametrelerdir (parameter): fonksiyonun içinde kullanılan girdi adları. Çağrıda verilen gerçek değerlere (7, 10) argüman (argument) denir. Hiçbir şey döndürmeyen fonksiyon void olarak yazılır.1 Satır içindeki ilk satır prototiptir: derleyici dosyayı yukarıdan aşağı okuduğu için, çağrıdan önce fonksiyonun adını ve imzasını duyurmak gerekir; gövde altta olabilir.1 Yani main da aslında bir fonksiyondur ve int döndürür: programın dışarıya, işletim sistemine bildirdiği sonuç (0 genelde "sorunsuz bitti", 1 "hata var" demektir).2
Önemli bir ayrıntı: fonksiyon argümanları kopyalanarak geçer. CS50'nin notuna göre, main içindeki n değişkeni yalnızca main'in kapsamındadır (scope); meow fonksiyonu n'in orijinalini değil, kendi kopyasını kullanır.1 Bu cümleyi aklına yaz: Bölüm 10'da bunun neden önemli olduğunu ve kopya yerine "adres" verilirse ne olduğunu göreceksin. Fonksiyon, Bölüm 1.4'teki soyutlamanın en küçük örneğidir: puan_hesapla'yı çağıran, içinde çarpma yapıldığını bilmek zorunda değildir.
Hukuk uygulamamızda bir bölümü bitirince puan hesaplayan bir kural düşün: doğru cevap sayısı çarpı soru başına puan. Yukarıdaki puan_hesapla o kuralın C'deki hali. Seriyi artıran bir for ya da bir quizin tüm sorularını dolaşan döngü, aynı yapı taşlarının tekrarıdır. Kerem'in prototipinin JavaScript dosyasında muhtemelen buna benzer fonksiyonlar, if koşulları ve bir soru listesini dolaşan bir döngü vardır; 9.7'de iki dili yan yana koyacağız. Uygulamayı Expo + TypeScript ve Go ile yazarken de aynı beş yapı taşı kullanılacak; yalnızca yazım kuralları değişecek.
9.5 Derlemenin aşamaları: ön işleme, derleme, assembly, bağlama
"Derleme" tek bir adım gibi görünür ama CS50 onu dört aşamaya ayırır: ön işleme, derleme, assembly, bağlama.2 Bottom-Up CS kitabı da aynı zinciri anlatır: derleyici önce kaynak kodu assembly'ye çevirir, assembler assembly'yi nesne koduna, bağlayıcı da nesne dosyalarını tek bir çalıştırılabilir dosyaya birleştirir.3 Bir dosyayı her aşamada durdurup bakmak mümkündür. Bunu hello.c üzerinde gerçekten yaptım ve çıktılar şunlar:
Aşama 1: ön işleme (preprocessing)
Ön işleme (preprocessing), kaynağın derleyiciye verilmeden önce metin olarak hazırlanmasıdır. Bottom-Up CS'in deyişiyle ön işlemci özünde bir metin değiştirme programıdır.3 En çok işi #include satırlarıdır: CS50'nin anlatımıyla başlık dosyalarının içeriği, olduğu gibi kopyalanıp senin dosyanın içine yapıştırılır.2 Komut: gcc -E hello.c -o hello.i (-E: yalnızca ön işlemeyi yap ve dur). 6 satırlık hello.c, ön işlemeden sonra 844 satır oldu; çünkü stdio.h'ın içindeki bildirimler araya girdi. Ortasında şu satır duruyor: extern int printf (const char *__restrict __format, ...);. Yani "printf diye bir fonksiyon var; bir yazı alır" bildirimi bu. En altta ise senin yazdığın 4 satır (int main ile gövdesi) kalır; kendi #include satırın başlığın içeriğiyle yer değiştirmiştir.
Aşama 2: derleme (compiling), çıktı assembly
Ön işlenmiş metin, kelimelerine ve yapısına ayrıştırılarak (buna ayrıştırma, parsing denir; derleyici yorumları ve boşlukları atar3) assembly'ye çevrilir. Komut: gcc -S hello.c -o hello.s. Bu makinede çıktının ana kısmı şudur:
main:
endbr64
pushq %rbp
movq %rsp, %rbp
leaq .LC0(%rip), %rax # "hello, world" metninin adresini al
movq %rax, %rdi # onu fonksiyona verilecek yere koy
call puts@PLT # puts fonksiyonunu çağır
movl $0, %eax # dönüş değeri: 0
popq %rbp
ret
Bu, Bölüm 7'de tanıştığın assembly. Gördüğün gibi tek bir printf(...) satırı birkaç komuta dönüştü: adresi al, argümanı yerleştir, fonksiyona atla, 0 döndür. Bir ilginç ayrıntı: derleyici sana sormadan printf'i daha basit olan puts'a çevirdi, çünkü metinde yer tutucu (%) yok. Derleyici çevirmenken aynı zamanda iyileştirir; bu yüzden "yazdığım kodun birebir aynısı çalışıyor" varsayımı yanlıştır (anlam korunur, ama komutlar değişebilir).
Aşama 3: assembly, çıktı nesne kodu
Assembler, assembly'deki her komutu tablodaki karşılığı olan makine koduna çevirir; Bottom-Up CS'in ifadesiyle bu, her komutun ve ikilik karşılığının (işlem kodunun) büyük bir tablosunu tutmaktır.3 Sonuç, nesne kodu (object code): makine kodu ama henüz çalıştırılabilir değil.3 Komut: gcc -c hello.c -o hello.o. Dosya 1.496 byte. İçine bakınca (objdump -d) komutların ikilik halini ve assembly karşılıklarını yan yana görürsün:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 48 8d 05 00 00 00 00 lea 0x0(%rip),%rax
f: 48 89 c7 mov %rax,%rdi
12: e8 00 00 00 00 call 17 <main+0x17>
17: b8 00 00 00 00 mov $0x0,%eax
1c: 5d pop %rbp
1d: c3 ret
Soldaki onaltılık sayılar (Bölüm 2.4) gerçek makine kodudur: örneğin 55 baytı "push %rbp" komutudur (Bölüm 7.1'deki işlem kodu). Dikkat edilecek iki satır var: call komutunun işleneni 00 00 00 00. Yani assembler, puts fonksiyonunun nerede olduğunu henüz bilmiyor ve boş bırakıyor. O adresi son aşama dolduracak.
Aşama 4: bağlama (linking)
Bağlama (linking), nesne dosyalarını ve kütüphanelerdeki hazır kodu tek bir çalıştırılabilir dosyada birleştirir. Bottom-Up CS'in belirttiği gibi tüm programın tek dosyaya sığması bile bağlamayı gereksiz kılmaz: printf gibi çağrılar bir kütüphanede durur ve yürütülebilir dosyaya bağlanması gerekir.4 CS50'nin ifadesiyle bağlama sırasında kütüphanelerdeki önceden derlenmiş makine kodu seninkiyle birleştirilir ve nihai dosya çıkar.2 Komut: gcc hello.o -o hello. Sonuç 15.952 byte'lık bir dosya; file komutu onu "ELF 64-bit executable, x86-64, dynamically linked" olarak tanımlıyor ve ldd komutu libc.so.6'ya (C'nin standart kütüphanesi) bağlı olduğunu gösteriyor. "Dinamik" bağlama, kütüphanenin kodunun dosyanın içine kopyalanmayıp program başlarken belleğe getirilmesi demektir; bunu Bölüm 15 ve 18 civarında daha iyi anlayacaksın.
| Aşama | Girdi → çıktı | Bu makinede boyut |
|---|---|---|
| 1. Ön işleme | hello.c → hello.i (metin) | 6 satır → 844 satır |
| 2. Derleme | hello.i → hello.s (assembly) | 37 satır |
| 3. Assembly | hello.s → hello.o (nesne kodu) | 1.496 byte |
| 4. Bağlama | hello.o + kütüphane → hello | 15.952 byte |
Başlık dosyası (stdio.h), "printf diye bir fonksiyon var, şöyle çağrılır" diyen bir bildirimdir; ön işleme aşamasında dosyana girer. Kütüphane (library) ise o fonksiyonların gerçek, derlenmiş kodudur; bağlama aşamasında dosyana bağlanır. İkisini ayırmak hata mesajlarını okumayı kolaylaştırır: derleme sırasında "böyle bir fonksiyon bilmiyorum" derse başlığı eklememişsindir; bağlama sırasında "undefined reference" (tanımsız başvuru) derse bildirim var ama gerçek kod bulunamamıştır.
9.6 Hata türleri ve hata ayıklama
Bir program üç farklı biçimde yanlış olabilir ve her birinin tespit edildiği zaman da farklıdır. Bunları gerçek çıktılarla gösteriyorum.
Sözdizimi hatası (syntax error)
Kural ihlali. Derleyici aşama 1–2'de yakalar ve program hiç oluşmaz. printf satırının sonundaki noktalı virgülü sildim:
e1.c: In function 'main':
e1.c:4:29: error: expected ';' before '}' token
4 | printf("hello, world\n")
| ^
| ;
Mesajı soldan sağa oku: dosya (e1.c), satır (4), sütun (29), hata türü (error), açıklama ("} işaretinden önce ; bekleniyordu"). Derleyici, eksik yerin ayrıca altını işaretliyor. Ders: hatanın gerçek yeri bazen bir önceki satırdır; derleyici ancak "şimdi anladım ki bir şey eksik" noktasında şikâyet edebilir. Birden fazla hata varsa ilkinden başla: sonrakiler çoğu zaman ilkinin yan etkisidir.
Bağlama hatası
Fonksiyon bildirilmiş ama hiçbir yerde tanımlanmamışsa derleme geçer, bağlama durur. Bu makinede ters(3) çağırıp ters'i hiç yazmayınca şu çıktı geldi: undefined reference to 'ters' ve ardından ld returned 1 exit status (ld bağlayıcının adıdır). Hatayı veren derleyici değil bağlayıcıdır; 9.5'teki dört aşamayı bilmek bu mesajı "koda bak" yerine "bir tanım eksik" diye okumanı sağlar.
Çalışma zamanı hatası (runtime error)
Program derlenmiş ve başlamıştır ama çalışırken bir şey ters gider. Örnek: tam sayıyı sıfıra bölmek.
int x = 10, y = 0;
printf("%i\n", x / y);
Derleme sorunsuz geçer; çalıştırınca işletim sistemi programı durdurur ve kabuk "Floating point exception" yazar (çıkış durumu 136). Mesajın adı yanıltıcı (burada ondalıklı sayı yok, tam sayı bölmesi var) ama fikir şudur: işlemci bu işlemi yapamadı ve işletim sistemi programı sonlandırdı. (Bu mekanizmayı Bölüm 13'te kullanıcı alanı ve çekirdek ayrımıyla anlayacaksın.) Bölüm 10'da bir başka sık çalışma zamanı hatası göreceğiz: izin verilmeyen belleğe dokunmak (segmentation fault).
Mantık hatası (logic error)
Derleme geçer, program çalışır, çökmez; ama yanlış sonuç verir. CS50'nin örneği: üç tuğla (#) basmak isteyen ama i <= 3 yazan döngü dört tuğla basar.2 Bunu çalıştırdım: çıktı 4 satır. Hiçbir araç "yanlış" demez, çünkü bilgisayar senden ne istediğini değil, ne yazdığını yapar. Hukuk benzetmesiyle: dilekçe şekil olarak tamdır ve mahkemece kabul edilir, ama talebin kendisi istediğin sonucu vermeyecek biçimde yazılmıştır. En tehlikeli hata türü budur, çünkü uyarı vermez.
Hata ayıklama (debugging)
Hata ayıklama (debugging) hatayı bulup düzeltme işidir. CS50 dört yöntem sayar:2
printfile bakmak: şüpheli yereprintf("i is %i\n", i);koyup değişkenin değerini görmek. CS50'nin örneğinde bu, döngünün 0, 1, 2 ve 3 değerlerini aldığını gösterir ve hatayı bulduran budur.2- Hata ayıklayıcı (debugger): programı satır satır yürütmeni sağlayan araç. Bir satıra kesme noktası (breakpoint) koyarsın, program orada durur, yerel değişkenlerin değerlerini görürsün ve adım adım ilerlersin.2 Bu, Bölüm 6'daki getir–çöz–çalıştır döngüsünü yavaş çekimde izlemek gibidir.
- Kauçuk ördek: sorunu ve attığın adımları bir ördeğe (ya da bir insana) yüksek sesle anlatmak; anlatırken kendi hatanı fark edersin.2
- Yapay zekâ öğretmeni: CS50 kendi ördeğini ("CS50 Duck") de yardım olarak sayar.2 Aynı mantıkla sen de hata mesajını ve ilgili kodu yapay zekâ öğretmenine yapıştırıp "bu hangi türden hata, hangi aşamada oluştu?" diye sorabilirsin.
Agent'ın yazdığı kodda bir hata aldığında ilk sorun şu olsun: hangi aşama şikâyet ediyor? Kod hiç çalışmadan reddediliyorsa bu bir sözdizimi ya da derleme sorunudur (ileride TypeScript'te de tür denetiminden önce görülecek). Program çalışıp patlıyorsa çalışma zamanı hatasıdır. Çalışıp yanlış puan gösteriyorsa mantık hatasıdır ve çözümü agent'a "şu girdi için şu sonucu bekliyordum, bu çıktı geldi" demektir. Bu üç sınıf, her dilde ve her katta geçerli; Bölüm 42'de test yazmayı bu yüzden mantık hatalarına karşı bir ağ olarak göreceğiz.
9.7 Aynı kavramlar JavaScript'te
Yukarıdaki yapı taşlarını JavaScript'te yazarsan şuna benzer (Node ile çalıştırdım):
const dogru = 7, toplam = 10;
const oran = dogru / toplam; // 0.7
const gecti = oran >= 0.6;
function puanHesapla(dogruSayisi, soruBasina) {
return dogruSayisi * soruBasina;
}
if (gecti) console.log("gecti, puan:", puanHesapla(dogru, 10));
else console.log("tekrar dene");
for (let i = 1; i <= 3; i++) console.log("gun", i);
| Kavram | C | JavaScript |
|---|---|---|
| Değişken | int dogru = 7; (tip yazılır) | const dogru = 7; / let seri = 0; (tip yazılmaz) |
| Fonksiyon | int f(int a) { return a; } | function f(a) { return a; } |
| Koşul | if (x < y) { ... } else { ... } | Aynı yazım |
| Döngü | for (int i = 0; i < 3; i++) | for (let i = 0; i < 3; i++) |
| Mantıksal VE/VEYA/DEĞİL | && || ! | Aynı işaretler |
| Ekrana yazma | printf("%i\n", x); | console.log(x); |
| Yer tutucular | Gerekli (%i, %s…) | Gerekmez; değer kendi tipini taşır |
| Tam sayı bölmesi | 7 / 2 → 3 | 7 / 2 → 3.5 |
| Ne zaman çalışır | Önce derlenir, sonra çalıştırılır | Tarayıcı ya da Node kodu çalıştırırken okur |
Üç fark önemli. Birincisi: JavaScript'te tip, değişkende değil değerde durur (typeof 5 "number", typeof "5" "string" verir; Bölüm 2.8'deki "5" + 1 örneği). Yani C'de tip derleme öncesi yazılan bir söz, JavaScript'te çalışma anında değere bakılarak belirlenen bir kuraldır; Bölüm 30'da TypeScript'in neden bu eksiği kapatmaya çalıştığını göreceksin. İkincisi: JavaScript'in tek bir sayı tipi var (ondalıklı bir tip, Bölüm 4.4), bu yüzden 7 / 2 3,5 verir; C'deki sessiz tam sayı bölmesi yok. Bunun bedeli, tam sayıların da ondalıklı biçimde saklanması ve bu yüzden güvenle temsil edilebilen en büyük tam sayının 9.007.199.254.740.991 olması (Node'da Number.MAX_SAFE_INTEGER bu değeri verdi). Üçüncüsü: JavaScript'te açıkça gördüğün bir "derleme" adımı yok; kodu çalıştıran motor işi senin yerine yapar (ayrıntı Bölüm 21 ve 25).
Prototipinin JavaScript dosyasında muhtemelen şunlar var: sorular için bir dizi ya da nesne listesi, doğru cevabı kontrol eden bir if, puanı artıran bir değişken ve belki bir function. Bunların her biri yukarıdaki tablonun sağ sütunundaki bir satır. Dosyayı agent'a gösterip "burada hangi yapı taşları var, C'deki karşılığı ne olurdu?" diye sorman iyi bir alıştırma. İleride Go'da yazacağımız arka uç, sözdizimi olarak C'ye JavaScript'ten çok daha yakın olacak: tipler açıkça yazılacak ve program önce derlenecek. Go'nun C'den nasıl farklı olduğunu Bölüm 36'da göreceğiz.
Bölüm 7'de makine kodu ile assembly'nin bire bir eşleştiğini söylemiştik: bir assembly satırı bir makine komutuna denk gelir. C'de ise bir satır birçok komuta açılır (9.5'te tek bir printf satırı dokuz komut oldu). İşte "üst kat" demek buydur: aşağıdaki katın ayrıntılarını gizleyip bir adım başına daha çok iş yapabilen bir yüz sunmak. Derleyici, bu yüzün altındaki ayrıntıları senin yerine halleder; gerekirse -S bayrağıyla altına bakabilirsin. Soyutlama (Bölüm 1.4) burada da "sızabilir": derleyici bazen kodunu değiştirir (printf → puts gibi), ve bellekle ilgili hatalar (Bölüm 10) alt katı yüzeye çıkarır.
Bu bölümde neler kuruldu
İnsanın yazdığı kaynak kod, derleyici tarafından makine koduna çevrilir; çıktı bir çalıştırılabilir dosyadır. Derleme dört aşamadır: ön işleme (başlık dosyaları metin olarak eklenir), derleme (assembly üretilir), assembly (nesne kodu), bağlama (kütüphanelerle birleştirme). C'de #include, main ve printf'li bir program sözdizimi kurallarıyla yazılır. Değişkenlerin bir tipi vardır (int, char, float, bool…), bu tip boyutunu ve sınırını belirler; atama sağdakini soldakine yazar. Koşul, döngü ve fonksiyon (parametre, dönüş değeri) Bölüm 1.3'ün yapı taşlarıdır; argümanlar kopyalanarak geçer. Hata üç türdür: sözdizimi hatası (derleyici yakalar), çalışma zamanı hatası (program çöker), mantık hatası (yanlış sonuç). Hata ayıklama yöntemleri: printf, hata ayıklayıcı, kauçuk ördek. Aynı kavramlar JavaScript'te benzer biçimde yazılır ama tip değerde durur.
Terimler sözlükte, Bölüm 9 başlığı altında.
CS50x 2026, Lecture 1 ders notları, şu başlıklar: Hello World, Header Files and CS50 Manual Pages, Conditionals, Types, Loops and meow.c, Functions, Operators. Derleme aşamaları için aynı dersin ikinci haftası: Lecture 2 ders notları, Debugging ve Compiling başlıkları. Derinleşmek istersen: Computer Science from the Bottom Up, Bölüm 7 (The Toolchain).
Takıldığın her yeri yapay zekâ öğretmenine sor. Bir öneri: kendi bilgisayarına gcc ya da clang kurup (ya da çevrimiçi bir C derleyicisi açıp) hello.c'yi yaz; sonra ondan 9.5'teki dört komutu (-E, -S, -c, ardından bağlama) sana adım adım çalıştırtmasını iste ve her aşamanın çıktısını kendi gözünle gör.
Bölüm sonu soruları
Kitaba bakmadan cevapla. Kâğıt kalem serbest.
#include <stdio.h> satırı ne yapar?7 / 2 işlemi neden 3 verir?int bölündüğünde sonuç da int olur ve ondalık kısım atılır. Ondalıklı sonuç için sayılardan biri float'a dönüştürülür. Bkz. 9.3.int kabaca neden 2,1 milyar civarında bir sınıra sahiptir?undefined reference to 'ters' mesajı geliyor. Hangi aşama şikâyet ediyor?i <= 3 yazıldığı için üç yerine dört tuğla basıyor. Bu hangi türden hatadır?Örnek cevabı göster
Ön işleme: #include satırlarındaki başlık dosyalarının içeriği kaynak koda metin olarak eklenir. Derleme: bu metin assembly'ye çevrilir. Assembly: assembler assembly'yi nesne koduna, yani ikilik makine koduna dönüştürür; henüz başka dosyadaki fonksiyonların adresleri boştur. Bağlama: nesne kodu, printf gibi fonksiyonların bulunduğu kütüphanelerle birleştirilir ve boş adresler doldurulur; ortaya çalıştırılabilir dosya çıkar. Bağlama gereklidir çünkü bir program kendi yazdığın kodun yanında hazır kütüphane kodunu da kullanır ve ikisi tek bir çalıştırılabilir dosyada buluşmak zorundadır.
Cevabı göster
(a) Sözdizimi (ya da bağlama) hatası: derleyicinin ilk hata mesajına, satır ve sütun numarasına bak; ilk hatadan başla, çünkü sonrakiler onun yan etkisi olabilir. Mesajda "undefined reference" geçiyorsa bir tanım eksiktir. (b) Çalışma zamanı hatası: çöküşün mesajına ve hangi girdiyle olduğuna bak; printf ya da hata ayıklayıcıyla değişkenlerin çökme anındaki değerlerini incele. (c) Mantık hatası: beklenen ile gerçek sonucu bir örnek girdiyle karşılaştır; printf ile ara değerleri yazdır ya da hata ayıklayıcıyla adım adım izle.
Kaynaklar
- CS50x 2026, Lecture 1 ders notları. Kaynak: CS50 Hafta 1 notları ve transkripti; kaynak kod ve derleyici,
hello, world, başlık dosyaları ve kütüphane, koşul, tipler ve biçim kodları, değişken, döngü, fonksiyon ve kapsam, işleçler, taşma, tam sayı bölmesi, tip dönüştürme. - CS50x 2026, Lecture 2 ders notları. Kaynak: CS50 Hafta 2 notları ve transkripti; Debugging (
printf, hata ayıklayıcı, kauçuk ördek) ve Compiling (clang, derlemenin dört aşaması, tip boyutları); çıkış durumu. - Ian Wienand, Computer Science from the Bottom Up, Bölüm 7: The Toolchain (alt bölümler: Compiling, Assembler, Linker): derleyici, yorumlayıcı, ön işlemci, nesne kodu.
- Ian Wienand, Computer Science from the Bottom Up, Bölüm 7.5: Linker: nesne dosyalarını tek bir çalıştırılabilir dosyada birleştirme; tek dosyalık programların bile sistem kütüphanelerine bağlanması.
Bu bölümdeki derleme çıktıları (hello.i satır sayısı, assembly, nesne dosyası, bayt boyutları, hata mesajları, INT_MAX, JavaScript çıktıları) yazar tarafından Linux x86-64 üzerinde gcc 15.2.0 ve Node ile gerçekten çalıştırılarak elde edilmiştir; başka bir makinede komutlar ve boyutlar farklı olabilir.