Kısım I · Makine · Bölüm 9

İlk Gerçek Dil: C

C ve derleyici (C and the compiler)

Neden burada · Neye dayanıyor

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.

  1. 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.
  2. Kat 5Programlama dilleri ve derleyiciİnsanın okuyabildiği kodu makine koduna çeviren araçlar. Bölüm 9–11.
  3. Kat 4Makine dili ve assemblyİşlemcinin doğrudan anladığı komutlar. Bölüm 7.
  4. 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:

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

TipNe saklarBoyut
booldoğru/yanlış1 byte
chartek karakter (ASCII kodu)1 byte
inttam sayı4 byte
longdaha büyük tam sayı8 byte
floatondalıklı sayı4 byte
doubledaha 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ı.)

Tipin sınırı vardır: taşma

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.

Uygulamamızda

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şamaGirdi → çıktıBu makinede boyut
1. Ön işlemehello.c → hello.i (metin)6 satır → 844 satır
2. Derlemehello.i → hello.s (assembly)37 satır
3. Assemblyhello.s → hello.o (nesne kodu)1.496 byte
4. Bağlamahello.o + kütüphane → hello15.952 byte
Kütüphane ile başlık dosyası aynı şey değildir

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

Uygulamamızda

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);
KavramCJavaScript
Değişkenint dogru = 7; (tip yazılır)const dogru = 7; / let seri = 0; (tip yazılmaz)
Fonksiyonint f(int a) { return a; }function f(a) { return a; }
Koşulif (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 yazmaprintf("%i\n", x);console.log(x);
Yer tutucularGerekli (%i, %s…)Gerekmez; değer kendi tipini taşır
Tam sayı bölmesi7 / 2 → 37 / 2 → 3.5
Ne zaman çalışırÖnce derlenir, sonra çalıştırılırTarayı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).

Uygulamamızda

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 ile bağlantı

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.

Birincil okuma

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.

1. Derleyicinin işi nedir?
Derleyici kaynak kodu (insanın okuduğu) makine koduna (işlemcinin çalıştırdığı) çevirir; hatayı düzeltmez, yalnızca bildirir. Bkz. 9.1.
2. #include <stdio.h> satırı ne yapar?
Başlık dosyası printf gibi fonksiyonların adlarını ve çağrılış biçimlerini bildirir; ön işleme aşamasında dosyanın içine eklenir. Fonksiyonun gerçek kodu ise bağlama sırasında gelir. Bkz. 9.2 ve 9.5.
3. C'de 7 / 2 işlemi neden 3 verir?
İki 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.
4. 32 bitlik işaretli bir int kabaca neden 2,1 milyar civarında bir sınıra sahiptir?
32 bit 2³² farklı durum demektir; işaretli sayılarda bunların yarısı negatif değerlere ayrıldığından en büyük pozitif değer yaklaşık 2,1 milyardır. Bkz. 9.3 ve Bölüm 2.2.
5. Derleme sırasında undefined reference to 'ters' mesajı geliyor. Hangi aşama şikâyet ediyor?
"Undefined reference" bağlayıcının (ld) mesajıdır: fonksiyon bildirilmiş ama gerçek kodu hiçbir nesne dosyasında ya da kütüphanede yok. Bkz. 9.5 ve 9.6.
6. Döngü i <= 3 yazıldığı için üç yerine dört tuğla basıyor. Bu hangi türden hatadır?
Program geçerli yazılmış, derlenmiş ve çökmeden bitmiştir; ama istenenden farklı sonuç vermiştir. Bu bir mantık hatasıdır ve hiçbir araç otomatik uyarı vermez. Bkz. 9.6.
7. Derlemenin dört aşaması doğru sırayla hangisidir?
Önce başlıklar metin olarak eklenir (ön işleme), sonra assembly üretilir (derleme), sonra nesne koduna çevrilir (assembly), en sonda kütüphanelerle birleştirilir (bağlama). Bkz. 9.5.
8. C'de bir fonksiyona argüman verildiğinde fonksiyon neyi alır?
Argümanlar kopyalanarak geçer; fonksiyon kendi kopyasını kullanır ve çağıranın değişkeni değişmez. Bölüm 10'da adres vererek bunun nasıl aşıldığını göreceğiz. Bkz. 9.4.
9. Kendi cümlelerinle: bir C dosyasının çalıştırılabilir dosyaya dönüşürken geçtiği dört aşamayı sırayla, her birinin ne yaptığını bir cümleyle anlat. Bağlama aşaması neden gereklidir?
Ö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.

10. Agent'ın yazdığı bir programda üç ayrı sorun çıktı: (a) hiç derlenmiyor, (b) derleniyor ama çalışırken çöküyor, (c) çalışıyor ama puanı yanlış hesaplıyor. Her birinin hata türünü söyle ve her biri için ilk neye bakacağını yaz.
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

  1. 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.
  2. 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.
  3. 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.
  4. 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.