Mikroişlemciler · Ders Notu

Bölüm 3.1: Programlama Dilleri

İşlemci yalnız bitleri tanır. Belleğinden okuduğu her bayt ya bir komutun parçasıdır ya da bir veridir; hangisi olduğunu programın akışı belirler. İnsan ise “a ile b'yi topla, sonucu x'e yaz” diye düşünür. Programlama dilleri bu iki dünya arasındaki uzaklığı kapatmak için doğdu. Not, bu uzaklığı aşağıdan yukarıya doğru katlar hâlinde ele alır. En altta işlemcinin doğrudan çalıştırdığı makine dili vardır; 8086'nın birkaç baytının bellekte nasıl getirilip yürütüldüğü adım adım izlenir. Küçük, uydurma bir komut biçimi üzerinde komutlar elle kodlanır ve çözülür. Ardından assembly dili, assembler'ın iki geçişi, bağlayıcı ve yükleyici gelir. Aynı “Merhaba” programı altı ayrı ortamda yazılarak assembly kodunun hem işlemciye hem işletim sistemine ne kadar bağlı olduğu gösterilir. Son kesimler yüksek seviyeli dilleri derlenen, yorumlanan ve hibrit diye ayırır. Bir sayının tek mi çift mi olduğunu yazan aynı program on dört dilde ve sonunda 8086 assembly ile okunur.

Öğrenme çıktıları

Dil seviyelerini ayırmak
Makine dili, assembly dili ve yüksek seviyeli diller arasındaki farkı soyutlama, taşınabilirlik ve donanım denetimi açısından açıklamak.
Makine kodunu okumak ve yazmak
Verilen bir komut biçimine göre ikili komutları kodlamak ve çözmek; 8086'nın A1 00 02 gibi baytlarının bellekte nasıl getirilip yürütüldüğünü izlemek.
Araç zincirini anlatmak
Assembler'ın iki geçişini, sembol tablosunu, bağlayıcıyı ve yükleyiciyi açıklamak; liste çıktısını okumak.
Platform bağımlılığını göstermek
Aynı programın x86, 68000, ARM, PowerPC ve farklı işletim sistemlerinde neden farklı yazıldığını sistem çağrısı ve çağrı kuralı üzerinden açıklamak.
Derleme ve yorumlamayı karşılaştırmak
Derleyici, bağlayıcı, yorumlayıcı, ara kod ve JIT kavramlarını hız, taşınabilirlik ve hata yakalama açısından karşılaştırmak.
Diller arasında okumak
Aynı algoritmayı farklı dillerde ve 8086 assembly ile izlemek; for ve if yapılarının karşılaştırma ve koşullu atlamaya nasıl dönüştüğünü göstermek.

3.1.1Yazılım ve dil seviyeleri

3.1.1.1Yazılımın görevi ve katmanlar

Bir bilgisayarın donanımı tek başına hiçbir iş yapmaz. İşlemci, bellek ve giriş/çıkış birimleri elektrik verildiği anda çalışmaya hazırdır, ama neyi hangi sırayla yapacaklarını bilmezler. Bu bilgiyi yazılım (software) verir. Yazılım, donanımın düzgün, eşgüdümlü ve istenen biçimde çalışmasını sağlayan komutların ve verilerin bütünüdür. Elektronik bir devrede sinyallerin sırasını bir denetim birimi belirler; bilgisayarda bu sırayı bellekteki program belirler.

Yazılımın en temel parçası işletim sistemidir (operating system). İşletim sistemi donanımı yönetir: belleği programlara paylaştırır, diske ve ekrana erişimi düzenler, aynı anda çalışan programların birbirini bozmasını engeller. Uygulama programları donanıma doğrudan dokunmaz; bir dosya açmak ya da ekrana yazı yazmak istediklerinde işletim sisteminden hizmet ister. Bu isteğin adı sistem çağrısıdır; ayrıntıları “Merhaba” programlarında görülecek. İşletim sistemi de donanıma, aygıtın kendine özgü ayrıntılarını bilen sürücüler (driver) aracılığıyla ulaşır.

Şekil 3.1.1 bu düzeni dört katman olarak gösterir. Kullanıcı en üstte, donanım en alttadır. Her katman yalnız hemen altındakinin sunduğu hizmetleri kullanır. Tarayıcı, derleyici ya da oyun gibi uygulamalar işletim sistemini, işletim sistemi de işlemciyi, belleği ve giriş/çıkış birimlerini kullanır.

Kullanıcı Uygulamalartarayıcı, derleyici, oyun İşletim sistemisistem çağrıları, sürücüler DonanımCPU, bellek, G/Ç
Şekil 3.1.1: Bilgisayar sisteminin katmanları. Uygulamalar donanıma işletim sistemi üzerinden ulaşır; işletim sistemi sistem çağrılarını karşılar ve sürücülerle donanımı yönetir.

Uygulamaları, işletim sistemini ve sürücüleri yazmak için programlama dilleri kullanılır. Bir programlama dili, bir işi adım adım tarif etmeye yarayan, kuralları kesin olarak tanımlanmış yapay bir dildir. Bugün yüzlerce dil vardır. Bazıları işletim sistemi yazmaya, bazıları sayısal hesaba, bazıları web sayfalarına yöneliktir. Dillerin tarihi, bilgisayar teknolojisinin tarihini de yansıtır. İlk bilgisayarlar kablolarla ve anahtarlarla programlanıyordu. Bellek büyüyüp işlemciler hızlandıkça programcının yükünü makineye devreden diller ortaya çıktı.

3.1.1.2Üç dil seviyesi

Programlama dilleri, donanıma ne kadar yakın olduklarına göre üç seviyede toplanır. Şekil 3.1.2 bu seviyeleri üst üste dizer; yukarı çıkıldıkça soyutlama artar, yani programcı donanımın ayrıntılarını daha az görür.

DerlenenC, Rust, Go YorumlananPython, Ruby Yüksek seviyeli diller Assembly diliMOV AL, 5 Makine dili1011 0000 0000 0101 soyutlama artar donanım (işlemci)
Şekil 3.1.2: Dil seviyeleri. En altta işlemcinin doğrudan çalıştırdığı makine dili, onun üstünde makine komutlarının sembolik yazımı olan assembly dili, en üstte derlenen ve yorumlanan yüksek seviyeli diller.
Tanım 3.1.1

Makine dili (machine language), işlemcinin hiçbir çeviri olmadan doğrudan çalıştırdığı ikili komutlardır. Assembly dili (assembly language), her makine komutunu MOV, ADD gibi akılda kalır bir adla, yani bir anımsatıcıyla (mnemonic) yazan dildir. Bir assembly satırı çoğunlukla tek bir makine komutuna karşılık gelir. Yüksek seviyeli dil (high-level language), işlemcinin komutlarından bağımsız olarak değişkenler, ifadeler, döngüler ve fonksiyonlarla yazılan dildir; bir satırı genellikle birden çok makine komutuna dönüşür.

Yüksek seviyeli diller de kendi içinde ikiye ayrılır. Derlenen dillerde (C, Rust, Go) kaynak kod, program çalışmadan önce bütünüyle makine diline çevrilir. Ortaya çıkan dosya doğrudan işlemcide çalışır. Yorumlanan dillerde (Python, Ruby) kaynak kod çalışma anında bir yorumlayıcı programca okunup işlenir. Şekildeki örnekler bu ayrımı gösterir: MOV AL, 5 satırı assembly dilinde, onun karşılığı olan 1011 0000 0000 0101 bitleri makine dilindedir.

Seviye yükseldikçe üç şey birlikte değişir. Birincisi okunabilirliktir. x = a + b satırını okuyan herkes ne yapıldığını anlar; aynı işin makine kodunu çözmek için komut tablosu gerekir. İkincisi taşınabilirliktir. C ile yazılmış bir program x86 için de ARM için de derlenebilir. 8086 makine kodu ise yalnız 8086 ailesinde çalışır. Üçüncüsü doğrudan denetimdir ve bu ters yönde değişir. Yüksek seviyeli dilde değişkenin hangi yazmaçta tutulacağına, bir döngünün hangi atlama komutuyla kurulacağına derleyici karar verir. Assembly yazan programcı ise her yazmacı, her bayrağı ve her bellek erişimini kendisi seçer. Aygıt sürücüsü, kesme yordamı ya da önyükleyici gibi donanıma yakın işlerde bu denetim gereklidir.

Elektrik-elektronik mühendisliği açısından bu seviyeler tanıdık bir benzetmeyle de düşünülebilir. Makine dili, bir sayısal devrenin denetim girişlerine uygulanan bit örüntüleridir. Assembly dili, bu örüntülere ad veren bir sembol listesidir. Yüksek seviyeli dil ise bir donanım tanımlama dilinde davranışı tarif edip ayrıntıyı sentez aracına bırakmaya benzer. Her seviyede iş aynıdır; değişen, ayrıntıyı kimin doldurduğudur.

3.1.1.3Aynı işlem üç seviyede

Seviyeler arasındaki farkı görmenin en iyi yolu aynı işi üç seviyede yazmaktır. İş basittir: a ile b değişkenlerini toplayıp sonucu x değişkenine yazmak. Üç değişken de 16 bitlik tamsayıdır.

C dilinde bu iş tek satırdır:

x = a + b;

Bu satır değişkenlerin bellekte nerede durduğunu, toplamanın hangi yazmaçta yapılacağını söylemez. Bu kararlar derleyiciye bırakılmıştır. 8086 assembly dilinde aynı iş üç satır tutar:

MOV AX, [a]     ; a'yı AX yazmacına yükle
ADD AX, [b]     ; AX = AX + b
MOV [x], AX     ; sonucu x'e yaz

Burada her şey açıktır. Toplama AX yazmacında yapılır. Köşeli parantez, değişkenin adresindeki bellek hücresinin içeriği anlamına gelir. 8086'da iki bellek işleneni tek bir komutta birlikte kullanılamaz. Bu yüzden önce a yazmaca alınır, sonra b eklenir, en son sonuç belleğe geri yazılır. Her satır tek bir makine komutudur.

Makine dilinde ise yalnız baytlar kalır. Değişkenlerin adresleri a = 0200h, b = 0202h, x = 0204h olsun. “h” soneki sayının onaltılık yazıldığını gösterir. Assembler bu üç satırı şu baytlara çevirir:

A1 00 02        ; MOV AX, [0200h]
03 06 02 02     ; ADD AX, [0202h]
A3 04 02        ; MOV [0204h], AX

Her komutun ilk baytı işlem kodudur (opcode): işlemciye hangi işin yapılacağını söyler. A1, “bellekteki bir sözcüğü AX'e yükle” demektir ve adres hemen arkasından gelir. A3 bunun tersidir: “AX'i belleğe yaz”. Bu iki komut AX'e özgü kısa biçimlerdir. 03 ise genel bir toplama komutudur: “16 bitlik bir yazmaca bir yazmacı ya da bellek sözcüğünü ekle”. Hangi yazmaç ve hangi bellek hücresi olduğu ikinci bayttan anlaşılır. Bu bayta ModR/M baytı denir. 06 = 00 000 110b; ortadaki 000 AX'i, kenarlardaki 00 ve 110 ise “adres komutun içinde doğrudan verilmiştir” durumunu kodlar.

Adres baytlarının sırası ilk bakışta tersmiş gibi görünür: 0200h adresi 00 02 diye yazılmıştır. 8086, çok baytlı sayıları bellekte düşük bayt önce (little-endian) saklar. 0200h sayısının düşük baytı 00h, yüksek baytı 02h'dir; önce 00h, sonra 02h gelir. Aynı kural verilere de uygulanır: a = 0015h ise 0200h adresinde 15h, 0201h adresinde 00h bulunur.

Sık yapılan hata

Makine kodundaki adres baytlarını yazıldığı sırayla okumak. A1 00 02 komutundaki adres 0002h değil, 0200h'dir. 8086'da komut içindeki 16 bitlik her sayı, adres de olsa sabit de olsa, düşük bayt önce yazılır. Bellek dökümünde bir sözcük okurken de ikinci bayt yüksek bayttır.

3.1.1.4Kod izleme: x = a + b bellekte

Bu üç komutun bellekte nasıl çalıştığını izlemek için onları küçük bir COM programına koyalım. COM programı, emu8086 ve DOS'un kullandığı en basit çalıştırılabilir dosya türüdür. Kod, veri ve yığın aynı 64 KB'lık kesimde durur; program kesimin 0100h adresinden başlar.

org 100h
mov ax, [0200h]   ; A1 00 02
add ax, [0202h]   ; 03 06 02 02
mov [0204h], ax   ; A3 04 02
ret               ; C3

org 100h bir direktiftir: makine kodu üretmez, yalnız assembler'a ilk baytın 0100h adresine yerleşeceğini söyler. Kod 0100h–010Ah aralığındaki 11 bayttır: üç komut 3 + 4 + 3 = 10 bayt, ret 1 bayt. Başlangıçta a = 0015h (21) ve b = 0027h (39) olsun; x'in değeri bilinmiyor.

İşlemci her komutu iki evrede işler. Getirme (fetch) evresinde komut baytları IP'nin gösterdiği adresten okunur ve IP komut uzunluğu kadar ilerler. Yürütme (execute) evresinde işlem kodu çözülür ve iş yapılır. 8086'da bu iki evre iki ayrı birime dağıtılmıştır. Veri yolu arabirim birimi (BIU, bus interface unit) bellekten komut baytlarını okuyup altı baytlık bir komut kuyruğuna doldurur. Yürütme birimi (EU, execution unit) baytları bu kuyruktan alıp çözer ve yürütür. Yürütme birimi bir işlemi yaparken veri yolu boştaysa BIU sıradaki baytları önceden getirir. İzleme tablosunda iki evre ayrı satırlarda gösterilir, ama gerçek işlemcide bir komutun yürütülmesi ile sonrakinin getirilmesi zaman içinde örtüşebilir.

Tablo 3.1.1: x = a + b makine kodunun izlemesi (a = 0015h, b = 0027h).
AdımEvreKomut baytlarıIPAX0204h–0205h (x)
1getirA1 00 020100h → 0103h?????? ??
2yürüt0200h'den sözcük oku: 15 000103h0015h?? ??
3getir03 06 02 020103h → 0107h0015h?? ??
4yürüt0202h'den 27 00; 15h + 27h0107h003Ch?? ??
5getirA3 04 020107h → 010Ah003Ch?? ??
6yürütAX'i 0204h'ye yaz010Ah003Ch3C 00
7getir, yürütC3 (RET)010Ah → dönüş003Ch3C 00

Adım 1'de IP = 0100h'dir. İşlemci A1 baytını okuyunca komutun üç bayt olduğunu anlar; arkadan gelen 00 02 baytları adresi oluşturur. Adres little-endian okunur: 0200h. IP 0103h'ye ilerler. Adım 2'de yürütme birimi veri kesiminin 0200h adresinden bir sözcük ister. BIU, 0200h'de duran 15h'yi ve 0201h'de duran 00h'yi okur; AX = 0015h olur. Bu okuma gerçek bir veri yolu çevrimidir. 8086'nın veri yolu 16 bit genişliğindedir ve 0200h çift bir adrestir. Bu yüzden sözcüğün iki baytı tek bir veri yolu çevriminde, yani en az dört saat çevriminde gelir. Sözcük tek adreste dursaydı iki ayrı çevrim gerekirdi.

Adım 3'te dört baytlık 03 06 02 02 getirilir. 03 toplama işlemini, 06 “hedef AX, kaynak doğrudan adres” bilgisini, 02 02 ise 0202h adresini taşır. Adım 4'te 0202h'den 0027h okunur ve aritmetik-mantık biriminde AX'e eklenir: 0015h + 0027h = 003Ch, ondalık olarak 21 + 39 = 60. Toplama bayrakları da günceller: sonuç sıfır olmadığından ZF = 0, en yüksek bit 0 olduğundan SF = 0 olur ve elde çıkmadığından CF = 0 kalır.

Adım 5'te A3 04 02 getirilir ve adım 6'da AX belleğe yazılır. Yazma da düşük bayt önce yapılır: 0204h adresine 3Ch, 0205h adresine 00h gider. Artık x = 003Ch = 60'tır. Adım 7'deki C3 baytı RET komutudur; COM programı denetimi kendisini çalıştıran sisteme geri verir. Şekil 3.1.3 bu izlemenin üç anını gösterir.

kod (0100h–010Ah)A1000203060202A30402C3IP = 0103hveri150200ha000201h270202hb000203h??0204hx??0205hAX0015hYÜRÜTkod (0100h–010Ah)A1000203060202A30402C3IP = 0107hveri150200ha000201h270202hb000203h??0204hx??0205hAX003ChYÜRÜTkod (0100h–010Ah)A1000203060202A30402C3IP = 010Ahveri150200ha000201h270202hb000203h3C0204hx000205hAX003ChYÜRÜT
Şekil 3.1.3: x = a + b makine kodunun üç anı. Üstte birinci komut yürütülmüş ve AX = 0015h olmuştur; ortada toplama yapılmış ve AX = 003Ch olmuştur; altta AX, 0204h ve 0205h adreslerine 3C 00 olarak yazılmıştır. Vurgulu kutular o adımda kullanılan baytlardır.

Bu izleme, C'deki tek satırın donanımda neye dönüştüğünü ortaya koyar. Satır üç komuta ve on bayt makine koduna dönüşmüştür. Bunlara üç veri erişimi ve komut baytlarını getiren veri yolu çevrimleri eklenir. 8086 kullanıcı kılavuzundaki zamanlama tablosu bu komutların sürelerini de verir. Bellekten AX'e yükleme 10 saat çevrimi sürer. Bellekteki bir sözcüğü yazmaca eklemek doğrudan adreslemede 9 + 6 = 15, AX'i belleğe yazmak yine 10 çevrimdir. 5 MHz'lik bir 8086'da bir saat çevrimi 200 ns olduğundan bu üç komut yaklaşık 35 × 200 ns = 7 µs tutar. Bu sürelere komut kuyruğu boşaldığında getirme için beklenen çevrimler eklenebilir.

Örnek 3.1.1

Aynı program a = 7FFFh, b = 0001h ile çalıştırılırsa x ve bayraklar ne olur?

Toplama 7FFFh + 0001h = 8000h verir. Belleğe 0204h ← 00h, 0205h ← 80h yazılır. Sonuç sıfır değildir (ZF = 0). En yüksek bit 1 olduğundan SF = 1 olur. İşaretsiz toplamda 16 bitten taşan elde yoktur (CF = 0). İşaretli sayılar açısından ise iki pozitif sayının toplamı negatif görünmüştür: +32767 + 1 sonucu −32768 çıkmıştır. Bu bir işaretli taşmadır ve OF = 1 olur. İşlemci taşmayı yalnız OF bayrağına yazar ve işine devam eder. C de işaretli taşmayı kendiliğinden bildirmez; dil standardı bu durumu tanımsız davranış sayar. Taşmayı denetlemek programcının işidir.

3.1.2Makine dili

3.1.2.1İşlemcinin tek anladığı dil

Makine dili, 0 ve 1'lerden oluşan en düşük seviyeli dildir. İşlemcinin denetim birimi bellekten okuduğu bit örüntüsünü çözer ve buna göre yazmaçları, aritmetik-mantık birimini ve veri yolunu sürer. Bu çözme işi kod çözücü devrelerle yapılır: belirli bir işlem kodu, belirli denetim hatlarını etkin kılar. Bu yüzden makine dili tamamen işlemciye ve donanıma bağımlıdır. Bir işlemci ailesi için yazılmış makine kodu başka bir ailenin kod çözücüsünde anlamsızdır ya da bambaşka bir iş yapar.

İlk genel amaçlı elektronik bilgisayarlardan ENIAC (1945) belleğe yüklenen bir programla çalışmıyordu. Program, birimler arasındaki kablolar takılarak ve anahtarlar ayarlanarak donanımın üzerine kuruluyordu. Yeni bir problem için kabloların yeniden düzenlenmesi günler sürebiliyordu. Programı da veri gibi belleğe koyma fikri bu sorunu çözdü. Bellekte duran komutlar artık sayılardı ve bu sayıların dili makine diliydi.

Günümüzde her işlemci ailesinin kendine özgü bir makine dili, başka bir deyişle bir komut kümesi (instruction set) vardır. Makine dili okumak ve yazmak için pratik değildir; bu yüzden programcılar onu doğrudan kullanmaz. Yine de bir mikroişlemci mühendisinin makine kodunu çözebilmesi gerekir. Bellek dökümünü okumak, bir mantık çözümleyicisinde veri yolundaki baytları anlamak ya da bir derleyicinin ne ürettiğini denetlemek bu beceriye dayanır.

Şekil 3.1.4, “AL yazmacına 5 yükle” işinin iki ayrı makine dilinde nasıl kodlandığını gösterir. 8086'da bu iş iki bayttır: B0 05. İlk baytın üst dört biti 1011, “yazmaca sabit yükle” komutunu belirtir. Sonraki bit w = 0, işlemin 8 bitlik olduğunu söyler. Son üç bit 000 hedef yazmacı seçer; 8 bitlik yazmaçlarda 000 AL'dir. İkinci bayt sabitin kendisidir: 00000101 = 5. Aynı komut 16 bitlik AX için yazılsaydı w = 1 olur ve sabit iki bayt tutardı: MOV AX, 5 = B8 05 00.

8086 · MOV AL, 51011MOV sabit0w=0000AL00000101sabit = 5= B0 05ARM · MOV r0, #51110 0011 1010 0000 0000 0000 0000 0101= E3A00005 (32 bit)
Şekil 3.1.4: Aynı iş iki makine dilinde. 8086'da MOV AL, 5 iki baytlık B0 05'tir: işlem kodu 1011, w biti, yazmaç alanı ve sabit. ARM'da MOV r0, #5 tek bir 32 bitlik sözcüktür: E3A00005.

ARM işlemcilerinde ise bütün komutlar 32 bit uzunluğundadır. MOV r0, #5 komutu E3A00005 sözcüğüne çevrilir. Bu sözcüğün en üstteki dört biti 1110 “her zaman çalıştır” koşuludur. Sonraki alanlar komutun bir veri işleme komutu olduğunu, işlemin MOV olduğunu, hedef yazmacın r0 olduğunu ve en alttaki sekiz bitte 5 sabitinin durduğunu kodlar. Aynı iş için 8086 iki bayt, ARM dört bayt kullanır. Alanların yeri ve anlamı da tamamen farklıdır. Bu fark, 8086 kodunun bir ARM işlemcide neden çalışmadığını tek örnekle açıklar.

Makine dilinin bu bağımlılığı assembly dilleriyle yumuşatılır. Assembly dili de işlemciye bağlıdır, ama programcıyı bit alanlarıyla uğraşmaktan kurtarır ve daha insan dostu bir programlama sağlar.

3.1.2.2Örnek bir komut biçimi

Gerçek bir işlemcinin komut kümesi yüzlerce komut ve onlarca biçim içerir. Makine dilinin ana fikrini görmek için çok daha küçük, uydurma bir işlemci tasarlamak yeterlidir. Bu işlemcinin dört yazmacı (R0–R3) olsun ve her komutu 8 bit tutsun. Şekil 3.1.5 komutun alanlarını gösterir.

o o o o h h k k bit 7bit 0 işlem kodu (4 bit) hedef (2 bit) kaynak (2 bit)
Şekil 3.1.5: Örnek işlemcinin 8 bitlik komut biçimi. Bit 7–4 işlem kodu, bit 3–2 hedef yazmaç, bit 1–0 kaynak yazmaçtır.
Tanım 3.1.2

Komut biçimi (instruction format), bir makine komutunun bitlerinin hangi alanlara ayrıldığını ve her alanın neyi kodladığını belirleyen düzendir. İşlem kodu (opcode) alanı yapılacak işlemi, işlenen (operand) alanları ise işlemin hangi yazmaç, bellek hücresi ya da sabit üzerinde yapılacağını belirtir.

Örnek işlemcide komutun anlamı her zaman aynı kalıba uyar: hedef ← hedef işlem kaynak. Sonuç hedef yazmaca yazılır; kaynak yazmaç değişmez. Yazmaçlar iki bitle kodlanır: R0 = 00, R1 = 01, R2 = 10, R3 = 11. İki bit dört yazmacı ayırt etmeye tam olarak yeter. İşlem kodları Tablo 3.1.2'de verilmiştir.

Tablo 3.1.2: Örnek işlemcinin işlem kodları.
İşlem koduAnımsatıcıAnlamı
0000HALTprogramı durdur
0001ADDhedef ← hedef + kaynak
0010SUBhedef ← hedef − kaynak
0011ANDhedef ← hedef VE kaynak (bit bit)
0100ORhedef ← hedef VEYA kaynak (bit bit)

İşlem kodu alanı 4 bit olduğu için en çok 24 = 16 farklı işlem tanımlanabilir. Bu tasarım yalnız beşini kullanıyor; kalan on bir kod sonradan eklenecek komutlar için boştur. Bir alanın genişliği, o alanın seçebileceği şey sayısını belirler: k bitlik bir alan 2k şeyden birini seçer. Tersine, n şeyden birini seçmek için en az ⌈log2 n⌉ bit gerekir.

Örnek bir komut: 0001 10 11. İlk dört bit 0001 = ADD, sonraki iki bit 10 = R2 (hedef), son iki bit 11 = R3 (kaynak). Komut ADD R2, R3'tür ve R2 ← R2 + R3 işini yapar. Onaltılık yazımda bu bayt 1Bh'dir. Onaltılık yazımın kolaylığı burada görülür: ilk onaltılık basamak doğrudan işlem kodudur, ikinci basamak ise hedef ve kaynak alanlarını birlikte taşır.

3.1.2.3Komutları kodlamak ve çözmek

Bir assembly satırını makine koduna çevirmeye kodlama, bir bayttan assembly satırını geri çıkarmaya çözme denir. Assembler birincisini, ters assembler (disassembler) ve işlemcinin kod çözücüsü ikincisini yapar. Çözmenin yolu bitleri alanlarına ayırmaktır. Tablo 3.1.3 dört komutluk bir programı alanlarına ayırır.

Tablo 3.1.3: Örnek programın alanlara ayrılması.
Makine koduOnaltılıkİşlemHedefKaynakAnlamı
0001 000111hADDR0R1R0 ← R0 + R1
0011 10113BhANDR2R3R2 ← R2 VE R3
0100 011046hORR1R2R1 ← R1 VEYA R2
0000 000000hHALT——programı durdur

Biçim, ilk iki bitlik yazmaç alanını hedef olarak tanımlar. Bu yüzden her komutun sonucu ilk yazılan yazmaca gider: birinci komutta R0'a, ikincide R2'ye, üçüncüde R1'e. HALT komutunda yazmaç alanları kullanılmaz; sıfır bırakılmıştır.

Örnek 3.1.2

SUB R2, R0 komutunu kodlayalım ve 4Dh baytını çözelim.

SUB'un işlem kodu 0010, R2 = 10, R0 = 00'dır. Alanlar yan yana yazılınca 0010 1000 elde edilir; onaltılık olarak 28h. R2 = 12 ve R0 = 5 ise komut R2 ← 12 − 5 = 7 yapar, R0 değişmez.

4Dh = 0100 1101. İşlem kodu 0100 = OR. Kalan dört bit 11 01'dir: hedef R3, kaynak R1. Bayt OR R3, R1 komutudur ve R3 ← R3 VEYA R1 işini yapar. R3 = 1010 ve R1 = 0011 ise sonuç 1011 = 11 olur.

3.1.2.4Kod izleme: örnek makine programı

Tablo 3.1.3'teki program bellekte 0, 1, 2 ve 3 adreslerinde durur. İşlemcinin program sayacı (PC), sıradaki komutun adresini tutar. 8086'da bu yazmacın adı IP'dir. Her komut getirildiğinde PC bir artar, çünkü her komut tek bayttır.

0001 0001   ; ADD R0, R1 → R0 ← R0 + R1
0011 1011   ; AND R2, R3 → R2 ← R2 AND R3
0100 0110   ; OR  R1, R2 → R1 ← R1 OR R2
0000 0000   ; HALT

Başlangıç değerleri R0 = 5, R1 = 3, R2 = 12, R3 = 10 olsun. Mantıksal işlemleri izleyebilmek için değerleri ikili de yazmak gerekir. Tablo 3.1.4 her komuttan sonraki durumu verir; değişen yazmaç kalın yazılmıştır.

Tablo 3.1.4: Örnek makine programının izlemesi.
AdımKomutPCR0R1R2R3
başlangıç—00000 0101 (5)0000 0011 (3)0000 1100 (12)0000 1010 (10)
1ADD R0, R110000 1000 (8)0000 0011 (3)0000 1100 (12)0000 1010 (10)
2AND R2, R320000 1000 (8)0000 0011 (3)0000 1000 (8)0000 1010 (10)
3OR R1, R230000 1000 (8)0000 1011 (11)0000 1000 (8)0000 1010 (10)
4HALT30000 1000 (8)0000 1011 (11)0000 1000 (8)0000 1010 (10)

Birinci komut ADD R0, R1'dir: 5 + 3 = 8 olur ve sonuç R0'a yazılır. Kaynak R1 değişmez. İkinci komut AND R2, R3'tür. Bit bit VE işleminde yalnız iki tarafta da 1 olan bit 1 kalır: 1100 VE 1010 = 1000, yani R2 = 8. Üçüncü komut OR R1, R2'dir. VEYA işleminde iki taraftan birinde 1 olan bit 1 olur: 0011 VEYA 1000 = 1011, yani R1 = 11. Bu komut R2'nin bir önceki adımda değişmiş değerini kullanır. Komutlar sırayla yürütüldüğünden her komut, öncekilerin bıraktığı durumu görür. Dördüncü komut HALT'tır ve işlemci durur. Bu tasarımda HALT getirildikten sonra PC artırılmaz ve 3'te kalır; gerçek işlemcilerde bu ayrıntı tasarıma göre değişir. Sonuç: R0 = 8, R1 = 11, R2 = 8, R3 = 10.

Bu küçük işlemci, 8086'nın da temel çalışma döngüsünü taşır: getir, PC'yi ilerlet, çöz, yürüt, sonucu yaz. İşlemci Simülatörü, benzer bir 8 bitlik işlemcide bu adımları PC, komut yazmacı, bellek adres ve veri yazmaçları üzerinde tek tek gösterir.

3.1.2.5Makine dilinin artıları ve eksileri

Makine dilinin artıları doğrudan donanıma yakınlığından gelir. Çeviri gerekmez; işlemci kodu olduğu gibi çalıştırır. Programcı donanım üzerinde tam denetime sahiptir: hangi bayt nereye yazılacak, hangi komut hangi sırayla gelecek, hepsi onun kararıdır. Doğru yazılırsa en küçük ve en hızlı kod da böyle elde edilir.

Eksileri ise ağırdır. Makine kodu okunmaz, yazılması ve hata ayıklaması çok zordur. Tek bir bitin yanlış olması komutu başka bir komuta çevirir; 0001 0001 (ADD R0, R1) yerine 0011 0001 yazılırsa toplama, VE işlemine dönüşür. Kod taşınabilir de değildir. Program her işlemci için baştan yazılmak zorundadır.

Belki de en can sıkıcı sorun adreslerin elle hesaplanmasıdır. Atlama komutları hedefin adresini taşır. Programın ortasına bir komut eklenince arkadan gelen bütün komutların adresi kayar, ama atlama komutlarının içindeki adresler eski hâlinde kalır. Şekil 3.1.6 bu durumu gösterir.

önceJMP 0105h0100hMOV …0103hhedef: ADD …0105h2 baytlık komut eklendiJMP 0105h0100hYENİ0103hMOV …0105hhedef: ADD …0107hJMP hâlâ 0105h’ye gider ✗
Şekil 3.1.6: Makine koduna komut eklemenin sonucu. Üstte JMP 0105h doğru hedefe gider. Altta araya iki baytlık bir komut eklenmiştir; hedef 0107h'ye kaymıştır, ama atlama hâlâ 0105h'ye gider.

Programcı bu durumda bütün atlama adreslerini yeniden bulup düzeltmek zorundadır. Büyük bir programda bu iş hem uzun sürer hem de hataya açıktır. Bu sorunları çözmek için önce assembly dilleri (1950'ler), ardından FORTRAN (1957) gibi yüksek seviyeli diller geliştirildi. Assembly dilinde atlama hedefi bir etiketle yazılır; adresi assembler her seferinde yeniden hesaplar.

Sık yapılan hata

Komut biçimindeki alanların sırasını karıştırmak. Örnek işlemcide hedef alanı kaynaktan önce gelir: 0010 11 00 SUB R3, R0'dır ve R3 ← R3 − R0 yapar. Alanlar ters okunursa SUB R0, R3 elde edilir; çıkarma değişmeli olmadığı için sonuç da başka bir yazmaca, başka bir değerle yazılır.

3.1.3Assembly dilleri ve assembler

3.1.3.1Anımsatıcılar

Assembly dilinin ilk kazancı adlardır. Programcı 10110000 00000101 yerine MOV AL, 5 yazar. MOV bir anımsatıcıdır: “taşı” (move) sözcüğünün kısaltması olarak komutun ne yaptığını hatırlatır. AL yazmacın adıdır, 5 ise ondalık yazılmış sabittir. Bu satırı makine koduna çeviren programa assembler denir. Şekil 3.1.7 çevirinin tek bir satır için ne yaptığını gösterir.

MOV AL, 5assemblerB0 05=10110000 00000101
Şekil 3.1.7: Assembler, MOV AL, 5 satırını B0 05 makine koduna, yani 10110000 00000101 bitlerine çevirir.

Assembly dili her işlemci mimarisi ve komut kümesi için ayrı tasarlanır. Intel x86, Motorola 68000, ARM, PIC ve AVR işlemcilerinin her birinin kendi assembly dili vardır. Bir dilde geçerli olan yazmaç adları ve komutlar ötekinde yoktur. Bir işlemci ailesi içinde ise uyum korunur. 8086 için yazılmış assembly programı, küçük farklarla 80286, 80386 ve günümüz x86 işlemcilerinde de çevrilip çalıştırılabilir. Mikrodenetleyicilere özel assembly dilleri gömülü sistemlerde ve elektronik uygulamalarda hâlâ sıkça kullanılır.

Assembly dili yüksek seviyeli dillerin yaygınlaştığı bugün de üç nedenle önemlidir. Birincisi, bazı kodlar yalnız assembly ile yazılabilir. Bilgisayar açıldığında ilk çalışan önyükleyici, kesme yordamları, aygıt sürücülerinin donanıma dokunan kısımları ve işletim sistemi çekirdeğinin bağlam değiştiren parçaları bu türdendir. İkincisi, işlemcinin gerçekte nasıl çalıştığını anlamanın yolu assembly'den geçer. Bir programın neden çöktüğü, bir hata ayıklayıcının gösterdiği kodun ne yaptığı ya da tersine mühendislikle bir yazılımın incelenmesi assembly okumayı gerektirir. Üçüncüsü performanstır. Derleyicinin kendiliğinden üretemediği özel komutlar, örneğin SIMD ya da şifreleme komutları, ve zamanı kritik döngüler hâlâ elle yazılır.

3.1.3.2Kaynak koddan çalışan programa

Programcının yazdığı metin dosyasına kaynak kod (source code) denir. Kaynak koddan çalışan bir programa giden yolda üç araç görev alır. Şekil 3.1.8 bu araç zincirini gösterir.

prog.asmkaynak assembler prog.objnesne kodu bağlayıcı prog.exeçalıştırılabilir + kütüphaneler (.lib/.obj) MASM, NASM, TASM, GASLINK, ld yükleyici → bellek
Şekil 3.1.8: Kaynak dosya assembler ile nesne dosyasına, bağlayıcı ile çalıştırılabilir dosyaya dönüşür; yükleyici bu dosyayı belleğe yerleştirir.
Assembler
Kaynak dosyayı (prog.asm) okur ve her satırı makine koduna çevirir. Çıktısı bir nesne dosyasıdır (object file, prog.obj ya da prog.o). Nesne dosyası makine kodunu içerir, ama henüz tamamlanmamıştır. Başka dosyalarda tanımlı fonksiyonların adresleri boş bırakılmış, bu boşluklar bir listeye yazılmıştır. MASM, NASM, TASM ve GNU as bu işi yapan araçlardır.
Bağlayıcı
Bağlayıcı (linker) bir ya da daha çok nesne dosyasını ve kütüphaneleri (.lib, .obj) birleştirir. Boş bırakılmış adresleri doldurur ve işletim sisteminin anlayacağı biçimde bir çalıştırılabilir dosya (executable file, prog.exe) üretir. Microsoft'un LINK'i ve GNU'nun ld'si yaygın bağlayıcılardır.
Yükleyici
Yükleyici (loader) işletim sisteminin bir parçasıdır. Çalıştırılabilir dosyayı diskten belleğe kopyalar, kesim yazmaçlarını ve yığını hazırlar, gerekirse adresleri programın yüklendiği yere göre düzeltir ve denetimi programın ilk komutuna verir.

Assembler'ın bir başka çıktısı daha vardır: liste (listing) çıktısı. Liste, her kaynak satırının yanına o satırın adresini ve ürettiği makine kodunu yazar. Ekrana “Merhaba!” yazan küçük bir DOS programının listesi şöyledir:

Adres   Makine kodu   Kaynak
0100    B4 09         mov ah, 09h
0102    BA 0C 01      mov dx, offset mesaj
0105    CD 21         int 21h
0107    B8 00 4C      mov ax, 4C00h
010A    CD 21         int 21h
010C    4D 65 72 ...  mesaj db 'Merhaba!$'

Liste satır satır okunabilir. mov ah, 09h iki bayttır: B4 “AH'ye sabit yükle”, 09 sabittir. mov dx, offset mesaj üç bayttır: BA “DX'e 16 bitlik sabit yükle” anlamına gelir ve sabit, mesaj etiketinin adresi olan 010Ch'dir; little-endian olarak 0C 01 yazılır. int 21h iki bayttır: CD kesme komutu, 21 kesme numarasıdır. mov ax, 4C00h AX'e 4C00h yükler, yani AH = 4Ch ve AL = 00h olur. Son satırdaki db (define byte) direktifi komut değil, veridir: “Merhaba!$” karakterlerinin ASCII kodları bellekte art arda durur. Liste çıktısı hem assembler'ın ne ürettiğini denetlemek hem de bir hata ayıklayıcıda adresleri kaynak satırlarla eşleştirmek için kullanılır. emu8086 bu adımların hepsini tek tuşla yapar ve COM ya da EXE dosyasını doğrudan üretir.

3.1.3.3Assembler'ın iki geçişi

Yukarıdaki listede ilginç bir durum vardır. mov dx, offset mesaj satırı ikinci satırdadır, ama mesaj etiketi en sonda tanımlanır. Assembler bu satıra geldiğinde mesaj'ın adresini henüz bilmez. Bir etikete tanımından önce başvurulmasına ileri başvuru (forward reference) denir. Assembler'lar bu sorunu kaynak kodu iki kez okuyarak çözer.

Tanım 3.1.3

İki geçişli assembler, kaynak kodu iki kez baştan sona okur. Birinci geçişte her satırın kaç bayt tuttuğunu hesaplayıp konum sayacını (location counter, LC) ilerletir. Her etiketi de o anki LC değeriyle sembol tablosuna (symbol table) yazar. İkinci geçişte sembol tablosundaki adresleri kullanarak makine kodunu üretir.

Konum sayacı, sıradaki baytın bellekte hangi adrese yerleşeceğini tutar. Kaynakta $ işaretiyle okunabilir. Birinci geçiş için önemli bir koşul vardır: her komutun uzunluğu, işlenenin değeri bilinmeden hesaplanabilmelidir. mov dx, offset mesaj komutunun 3 bayt olduğu, mesaj'ın değeri ne olursa olsun bellidir. İzlenen program şudur:

org 100h
mov ah, 09h
mov dx, offset mesaj
int 21h
mov ax, 4C00h
int 21h
mesaj db 'Merhaba!$'
Tablo 3.1.5: İki geçişli assembler'ın izlemesi.
GeçişSatırUzunlukLC (satırdan sonra)Sembol tablosuÜretilen baytlar
1org 100h00100hmesaj = ?—
1mov ah, 09h20102hmesaj = ?—
1mov dx, offset mesaj30105hmesaj = ?—
1int 21h, mov ax, 4C00h, int 21h2 + 3 + 2010Chmesaj = ?—
1mesaj db 'Merhaba!$'90115hmesaj = 010Ch—
2mov ah, 09h20102hmesaj = 010ChB4 09
2mov dx, offset mesaj30105hmesaj = 010ChBA 0C 01
2üç satır7010Chmesaj = 010ChCD 21 B8 00 4C CD 21
2mesaj db …90115hmesaj = 010Ch4D 65 72 68 61 62 61 21 24

Birinci geçiş yalnız sayar. org 100h LC'yi 0100h yapar; direktif olduğu için bayt üretmez. mov ah, 09h 2 bayt tutar ve LC 0102h olur. Üçüncü satırda mesaj henüz tanımlanmamıştır, ama komutun 3 bayt olduğu bilinir ve LC 0105h'ye ilerler. Sonraki üç komut 2, 3 ve 2 bayttır; LC 010Ch'ye gelir. Son satırda mesaj etiketi görülür ve o anki LC ile sembol tablosuna girer: mesaj = 010Ch. Dizgi 9 karakterdir (“Merhaba!” sekiz harf ve sondaki $), LC 0115h olur.

İkinci geçiş LC'yi yeniden 0100h'den başlatır ve bu kez baytları üretir. mov ah, 09h için B4 09 yazılır. offset mesaj artık sembol tablosundan okunur: 010Ch, little-endian olarak 0C 01. Kalan komutlar CD 21, B8 00 4C ve CD 21'dir. En sonda dizginin ASCII kodları yazılır: M = 4Dh, e = 65h, r = 72h, h = 68h, a = 61h, b = 62h, a = 61h, ! = 21h, $ = 24h. Çıktı, 0115h − 0100h = 15h = 21 baytlık bir COM dosyasıdır. Çalıştırılınca ekrana “Merhaba!” yazar. Şekil 3.1.9 iki geçişin sonundaki durumu gösterir.

1. GEÇİŞkonum sayacı (LC)0115hsembol tablosumesaj010Chüretilen baytlar(1. geçişte bayt üretilmez)satır 72. GEÇİŞkonum sayacı (LC)010Chsembol tablosumesaj010Chüretilen baytlarB409BA0C01CD21B8004CCD214D6572686162612124satır 7
Şekil 3.1.9: Birinci geçişin sonunda sembol tablosu dolmuş, ama hiç bayt üretilmemiştir (üstte). İkinci geçişin sonunda 21 baytın hepsi üretilmiştir (altta).

İki geçişli yapı, makine dilindeki adres kayması sorununu da kendiliğinden çözer. Programcı araya bir komut eklerse assembler bir sonraki çevirmede bütün etiketlerin adresini yeniden hesaplar ve atlama komutlarına yeni adresleri yazar.

Sık yapılan hata

Direktifleri komut sanmak. org, db, equ ve offset işlemcinin komutları değildir; assembler'a verilen bilgilerdir. org 100h bayt üretmez, yalnız konum sayacını ayarlar. db komut değil veri üretir. İşlemci bir db satırının baytlarına komut gibi ulaşırsa onları da komut diye çözmeye çalışır; bu yüzden veri kodun arkasına konur.

3.1.3.4Assembler'lar ve söz dizimleri

Aynı x86 işlemcisi için birden çok assembler vardır ve hepsi aynı kuralları kullanmaz. Bir assembly satırının nasıl yazılacağını belirleyen kurallara söz dizimi (syntax) denir. x86 için iki ana söz dizimi kullanılır: Intel ve AT&T. Tablo 3.1.6 yaygın assembler'ları özetler.

Tablo 3.1.6: Yaygın x86 assembler'ları.
AssemblerSöz dizimiKullanım
MASMIntelMicrosoft, Windows
TASMIntelBorland, DOS
NASMIntelaçık kaynak, çok platformlu
GAS (as)AT&TGNU/Linux, gcc'nin kullandığı assembler
emu8086Intel (MASM benzeri)8086 öykünücüsü; bu dersin aracı

İki söz dizimindeki en önemli fark işlenenlerin sırasıdır. Intel söz diziminde önce hedef, sonra kaynak yazılır. AT&T söz diziminde sıra terstir: önce kaynak, sonra hedef. Aynı iki komut iki söz diziminde şöyle görünür:

; Intel:  hedef, kaynak
mov eax, 5
add eax, [ebx+4]

# AT&T:  kaynak, hedef
movl $5, %eax
addl 4(%ebx), %eax

AT&T söz diziminde yazmaç adlarının önüne %, sabitlerin önüne $ konur. İşlemin boyutu komutun sonundaki harfle belirtilir: b 8, w 16, l 32 bit demektir. Bellek işleneni köşeli parantez yerine uzaklık(taban) biçiminde yazılır: 4(%ebx), Intel'deki [ebx+4] ile aynı adrestir. Açıklama işareti de farklıdır: Intel söz dizimli assembler'lar ;, GNU as ise x86'da # kullanır. Her iki yazım da işlemciye aynı baytları verir. Fark yalnız insanın okuduğu metindedir.

Sık yapılan hata

Bir derleyicinin ürettiği AT&T kodunu Intel kuralıyla okumak. subl %ebx, %eax komutu EBX'ten EAX'i çıkarmaz; EAX ← EAX − EBX yapar, yani Intel'deki sub eax, ebx'tir. gcc ve clang varsayılan olarak AT&T söz dizimi üretir; -masm=intel seçeneği çıktıyı Intel söz dizimine çevirir.

3.1.4Farklı ortamlarda “Merhaba”

Ekrana kısa bir metin yazan program, bir dilin ya da ortamın en küçük tam örneğidir. Bu kesimde aynı iş altı ortamda yapılır: 32 bitlik Linux, DOS, Windows, Motorola 68000 işlemcili Amiga, ARM ve PowerPC. Programların hepsi aynı üç adımı izler: metni bellekte tanımla, işletim sisteminden “şu adresteki şu kadar baytı ekrana yaz” hizmetini iste, programı sonlandır. Farklar, bu üç adımın her ortamda nasıl söylendiğindedir.

3.1.4.1Linux, x86 (32 bit)

İlk program 32 bitlik x86 işlemcide, Linux işletim sistemi altında çalışır ve NASM ile çevrilir. 8086'nın 16 bitlik AX, BX, CX, DX yazmaçları 80386'dan itibaren 32 bite genişletilmiştir; 32 bitlik adları EAX, EBX, ECX, EDX'tir. AX, EAX'in alt yarısıdır.

section .data
hello  db 'Merhaba, Linux!', 10
len    equ $ - hello     ; = 16 bayt
section .text
global _start
_start:
  mov eax, 4     ; sys_write
  mov ebx, 1     ; stdout
  mov ecx, hello ; dizginin adresi
  mov edx, len   ; bayt sayısı
  int 0x80       ; çekirdeği çağır
  mov eax, 1     ; sys_exit
  xor ebx, ebx   ; çıkış kodu 0
  int 0x80

Program iki bölümden oluşur. section .data verinin, section .text kodun bulunduğu bölümü açar. hello etiketi metnin başlangıç adresini gösterir; db metnin baytlarını ve sonuna 10'u, yani satır sonu karakterini koyar. len equ $ - hello satırı bellekte yer kaplamaz. equ, bir ada sabit bir değer verir; $ o andaki konum sayacıdır. Konum sayacından metnin başlangıç adresi çıkarılınca metnin uzunluğu bulunur. “Merhaba, Linux!” 15 bayttır; satır sonuyla 16 bayt olur. Uzunluğu elle saymak yerine assembler'a hesaplatmak, metin değiştiğinde hatayı önler.

global _start, _start etiketini bağlayıcıya görünür kılar. Linux'ta bağlayıcı programın giriş noktasını bu adla arar. Kodun kendisi iki sistem çağrısıdır. Tablo 3.1.7 her satırdan sonra yazmaçları verir.

Tablo 3.1.7: Linux “Merhaba” programının izlemesi.
SatırEAXEBXECXEDXEkran
mov eax, 400000004????????????????????????
mov ebx, 10000000400000001????????????????
mov ecx, hello0000000400000001hello (adres)????????
mov edx, len0000000400000001hello (adres)00000010
int 0x800000001000000001hello (adres)00000010Merhaba, Linux!
mov eax, 10000000100000001hello (adres)00000010Merhaba, Linux!
xor ebx, ebx0000000100000000hello (adres)00000010Merhaba, Linux!
int 0x80süreç 0 çıkış koduyla sonlanır; çağrı geri dönmez

Birinci çağrının parametreleri yazmaçlara konur. EAX = 4, Linux'un 32 bitlik çağrı tablosunda sys_write hizmetinin numarasıdır. EBX = 1, yazılacak dosya tanıtıcısıdır; 1 numaralı tanıtıcı standart çıktıdır, yani ekran. ECX metnin adresini alır, içeriğini değil. EDX yazılacak bayt sayısıdır: 16, onaltılık 10h. int 0x80 komutu denetimi çekirdeğe verir. Çekirdek metni ekrana yazar ve dönüşte EAX'e yazılan bayt sayısını koyar; EAX bu yüzden 00000010h olur.

İkinci çağrı programı sonlandırır. EAX = 1, sys_exit'tir. EBX çıkış kodunu taşır ve 0 başarı demektir. xor ebx, ebx bir sayıyı kendisiyle dışlayıcı VEYA işlemine sokar. Her bit kendisiyle aynı olduğundan sonuç sıfırdır. Bu yol mov ebx, 0'dan daha kısa bir makine kodu üretir (2 bayta karşı 5 bayt). XOR bayrakları da etkiler: sonuç sıfır olduğu için ZF = 1, mantıksal işlem olduğu için CF = 0 ve OF = 0 olur. Son int 0x80 süreci sonlandırır ve geri dönmez.

Sık yapılan hata

Uzunluğu elle yazıp sonra metni değiştirmek. len 15 yazılsaydı satır sonu yazılmaz ve sonraki çıktı aynı satıra eklenirdi. Metin kısaltılıp uzunluk 16 bırakılsaydı, metnin arkasındaki bellek baytları da ekrana basılırdı. $ - hello bu hataların ikisini de önler. Türkçe karakterlerde ayrıca dikkat gerekir: UTF-8'de “ü” gibi harfler 2 bayt tutar; “Merhaba, Dünya!” 15 karakter ama 16 bayttır.

3.1.4.2Sistem çağrısı

Linux programı ekrana kendisi yazmadı; çekirdekten yazmasını istedi. Modern işletim sistemlerinde uygulama programları donanıma doğrudan erişemez. İşlemcinin koruma düzeneği iki çalışma modu tanımlar. Kullanıcı modunda çalışan bir program giriş/çıkış komutlarını ve bazı denetim komutlarını kullanamaz. Çekirdek modu ise ayrıcalıklıdır ve yalnız işletim sistemine açıktır. Uygulama bir donanım hizmetine ihtiyaç duyduğunda kontrollü bir kapıdan çekirdeğe geçer.

Tanım 3.1.4

Sistem çağrısı (system call), bir programın işletim sisteminden hizmet istediği özel çağrıdır. Program hizmet numarasını ve parametreleri belirli yazmaçlara koyar, ardından yazılım kesmesi ya da özel bir komutla denetimi çekirdeğe verir. Çekirdek hizmeti yapar, sonucu bir yazmaca koyar ve denetimi programa geri verir.

Kullanıcı programıEAX=4 EBX=1ECX=adres EDX=16 Çekirdek (kernel)EAX'e göre hizmettablosundan seçer Sürücü · donanımterminal / ekranint 0x80EAX = sonuçkullanıcı moduçekirdek modu (ayrıcalıklı)
Şekil 3.1.10: Sistem çağrısının yolu. Kullanıcı programı parametreleri yazmaçlara koyar ve int 0x80 ile çekirdeğe geçer. Çekirdek EAX'teki numaraya göre hizmeti seçer, sürücü üzerinden ekrana yazar ve sonucu EAX'te döndürür.

Şekil 3.1.10 bu yolu gösterir. int 0x80 bir yazılım kesmesidir. İşlemci bayrakları ve dönüş adresini kaydeder, kesme tablosunun 80h numaralı girişinde yazan adrese atlar ve aynı anda çekirdek moduna geçer. Çekirdekteki giriş yordamı EAX'e bakar ve hizmet tablosundan ilgili fonksiyonu çağırır. Hizmet bitince özel bir dönüş komutu hem kayıtlı adrese döner hem de kullanıcı moduna geri geçer.

8086'da koruma düzeneği ve kullanıcı/çekirdek modu ayrımı yoktur; her program her şeye erişebilir. Yine de kesme düzeneği aynıdır. 8086 INT n komutunu yürütürken bayrak yazmacını, CS'yi ve IP'yi yığına iter, IF ve TF bayraklarını sıfırlar. Yeni CS:IP'yi belleğin en başındaki kesme vektör tablosundan okur. Her giriş 4 bayttır; INT 21h için giriş 21h × 4 = 84h adresindedir. DOS, kendi hizmet yordamının adresini bu girişe yazar. Tablo 3.1.8 farklı ortamlardaki çağrı yollarını karşılaştırır.

Tablo 3.1.8: Ortamlara göre sistem çağrısı.
OrtamÇağrıHizmet numarasıParametreler
Linux 32 bitint 0x80EAX (4 = write, 1 = exit)EBX, ECX, EDX
Linux 64 bitsyscallRAX (1 = write, 60 = exit)RDI, RSI, RDX
MS-DOSint 21hAH (09h = yaz, 4Ch = çık)DX, AL …
BIOSint 10hAH (0Eh = karakter yaz)AL, BH …

Tablo, numaraların ortama özgü olduğunu açıkça gösterir. “Yaz” hizmeti 32 bitlik Linux'ta 4, 64 bitlik Linux'ta 1'dir. Aynı işletim sisteminin iki sürümü bile farklı numara ve farklı yazmaç kullanır. BIOS hizmetleri işletim sisteminden de alttadır. Bilgisayarın kalıcı belleğindeki bu yordamlar, işletim sistemi yüklenmeden önce de çalışır.

3.1.4.3DOS, 8086 (COM programı)

Üçüncü program bu dersin ortamındadır: emu8086 ile çevrilen, DOS altında çalışan bir COM programı.

org 100h          ; COM programı
mov ah, 09h       ; DOS 09h: dizgi yaz
lea dx, mesaj     ; DS:DX = adres
int 21h           ; DOS hizmetini çağır
mov ax, 4C00h     ; DOS 4Ch: sonlandır
int 21h
mesaj db 'Merhaba, DOS!$'

org 100h, COM programının kesimin 100h adresine yükleneceğini bildirir. DOS, programı yüklerken kesimin ilk 256 baytına (0000h–00FFh) program kesim öneki (PSP, program segment prefix) adlı bir yapı koyar. Program bu yapının hemen arkasından başlar. COM programında dört kesim yazmacının hepsi aynı kesimi gösterir: CS = DS = ES = SS. Kod, veri ve yığın aynı 64 KB içindedir.

mov ah, 09h, DOS'un 09h numaralı hizmetini seçer: “DS:DX adresinden başlayan ve $ ile biten dizgiyi ekrana yaz”. lea dx, mesaj, mesaj etiketinin bağıl konumunu DX'e yükler (load effective address). DS zaten programın kesimini gösterdiği için DS:DX dizginin tam adresidir. int 21h DOS'u çağırır. DOS karakterleri $ görene kadar ekrana basar; $'ın kendisi yazılmaz. mov ax, 4C00h AH'ye 4Ch'yi, yani “programı sonlandır” hizmetini, AL'ye 00h çıkış kodunu yükler. Son int 21h denetimi DOS'a geri verir.

Tablo 3.1.9: DOS “Merhaba” programının izlemesi.
SatırMakine koduIP (sonra)AHALDXEkran
org 100h—0100h????????
mov ah, 09hB4 090102h09h??????
lea dx, mesajBA 0C 010105h09h??010Ch
int 21hCD 210107h09h??010ChMerhaba, DOS!
mov ax, 4C00hB8 00 4C010Ah4Ch00h010ChMerhaba, DOS!
int 21hCD 21010Ch4Ch00h010Chprogram biter

Tablo 3.1.9'daki adresler komut uzunluklarından gelir. lea dx, mesaj üç bayttır: emu8086 doğrudan adresli LEA'yı mov dx, offset mesaj ile aynı biçimde kodlar, yani BA işlem kodu ve 0C 01 adresi (MASM aynı komutu 8D 16 ile başlayan dört baytlık gerçek LEA olarak kodlar). Kod 0100h–010Bh aralığında 2 + 3 + 2 + 3 + 2 = 12 bayt tutar; bu yüzden mesaj 010Ch'dedir. “Merhaba, DOS!” 13 karakterdir ve $ ile birlikte 14 bayt yer kaplar.

İkinci int 21h satırında IP'nin 010Ch'yi göstermesi dikkat çekicidir: bu adres mesaj'ın başıdır. Program sonlandığı için işlemci bu adrese hiç ulaşmaz. Veri kodun arkasına konduğu için işlemci veriyi komut sanıp çalıştırmaz. Veri kodun önüne konsaydı, program 0100h'den başlarken “M” harfinin kodu olan 4Dh'yi bir komut olarak çözerdi. 4Dh, 8086'da DEC BP komutudur. Arkadan gelen harfler de rastgele komutlara dönüşür ve program öngörülemez biçimde davranırdı.

Sık yapılan hata

09h hizmetinde dizginin sonundaki $'ı unutmak. DOS, dizginin nerede bittiğini başka bir yoldan bilemez. $ yoksa bellekte ilk 24h baytına rastlayana kadar kodun ve verinin arkasındaki ne varsa ekrana basar. Ekranda anlamsız karakterler, bip sesleri ve imleç sıçramaları görülür.

3.1.4.4Windows, Win32 API

Windows programları sistem çağrısını doğrudan yapmaz. İşletim sisteminin hizmetleri DLL (dynamic-link library, devingen bağlantılı kütüphane) dosyalarındaki fonksiyonlar olarak sunulur. Program bu fonksiyonları sıradan bir alt program gibi call komutuyla çağırır. Aşağıdaki program ekranda bir ileti kutusu açar ve sonra süreci sonlandırır.

extern MessageBoxA    ; user32.dll
extern ExitProcess    ; kernel32.dll

section .data
mesaj db 'Merhaba, Windows!', 0

section .text
global _start
_start:
    push 0          ; uType = MB_OK
    push mesaj      ; lpCaption (başlık)
    push mesaj      ; lpText (metin)
    push 0          ; hWnd = NULL
    call MessageBoxA
    push 0          ; uExitCode = 0
    call ExitProcess

extern satırları, MessageBoxA ve ExitProcess adlarının başka bir yerde tanımlı olduğunu söyler. Assembler bu adların adresini bilmez; nesne dosyasına “burayı bağlayıcı doldursun” notu düşer. Bağlayıcı, user32.dll ve kernel32.dll kütüphanelerinin içe aktarma bilgisini kullanarak çağrıları bu fonksiyonlara yönlendirir. Dizgi bu kez $ ile değil 0 ile biter. Windows API'si C diliyle tasarlanmıştır ve C dizgileri sıfır baytıyla sonlanır.

Parametreler yazmaçlarla değil yığınla geçirilir. C'de çağrı MessageBoxA(hWnd, lpText, lpCaption, uType) biçimindedir. Assembly'de parametreler sağdan sola itilir: önce uType, en son hWnd. Böylece ilk parametre yığının tepesine, dönüş adresinin hemen üstüne gelir ve fonksiyon onu parametre sayısından bağımsız olarak hep aynı yerde bulur. Bu programda başlık ve metin için aynı dizgi kullanılmıştır; iki push mesaj aynı adresi iter. Tablo 3.1.10 yığının durumunu izler; ESP'nin başlangıç değeri olarak 0012FF80h örnek alınmıştır.

Tablo 3.1.10: Windows programında yığının izlemesi.
KomutESP (sonra)Yazılan adres ← değerEAX
başlangıç0012FF80h—????????
push 0 (uType)0012FF7Ch0012FF7Ch ← 0 (MB_OK)????????
push mesaj (lpCaption)0012FF78h0012FF78h ← &mesaj????????
push mesaj (lpText)0012FF74h0012FF74h ← &mesaj????????
push 0 (hWnd)0012FF70h0012FF70h ← 0 (NULL)????????
call MessageBoxA0012FF6Ch0012FF6Ch ← dönüş adresi????????
fonksiyon ret 16 ile döner0012FF80h—00000001h (IDOK)
push 0 (uExitCode)0012FF7Ch0012FF7Ch ← 000000001h
call ExitProcesssüreç 0 çıkış koduyla sonlanır; çağrı geri dönmez

Yığın düşük adreslere doğru büyür. 32 bitlik kipte her push ESP'yi 4 azaltır ve değeri ESP'nin yeni gösterdiği adrese yazar. Dört parametre 16 bayt tutar. call da dönüş adresini iter; fonksiyona girildiği anda ESP = 0012FF80h − 20 = 0012FF6Ch'dir. Şekil 3.1.11 bu anı gösterir.

yüksek adres ↑0012FF80h0012FF7Ch0uType = MB_OK0012FF78h&mesajlpCaption0012FF74h&mesajlpText0012FF70h0hWnd = NULL0012FF6Chdönüş adr.call'dan sonraki komutdüşük adres ↓ (yığın bu yöne büyür)ESP→
Şekil 3.1.11: call MessageBoxA yürütüldükten hemen sonra yığın. En üstte dönüş adresi, onun üstünde ilk parametre hWnd vardır.

Kullanıcı ileti kutusundaki Tamam düğmesine basınca fonksiyon döner. Dönüş değeri EAX'e konur: 1, yani IDOK, “Tamam'a basıldı” demektir. Windows API fonksiyonları stdcall çağrı kuralını kullanır. Bu kuralda parametreleri yığından çağrılan fonksiyon temizler. MessageBoxA, ret 16 komutuyla döner: önce dönüş adresini alır (ESP + 4), sonra 16 bayt parametreyi atar (ESP + 16). ESP yeniden 0012FF80h olur. C'nin kendi fonksiyonlarında kullanılan cdecl kuralında ise bu temizliği çağıran yapar.

Bağlarken bir ayrıntı daha karşılaşılabilir. Bazı 32 bitlik araç zincirlerinde stdcall fonksiyonlarının adları süslenmiş biçimde aranır: _MessageBoxA@16. Baştaki alt çizgi C adlandırma geleneğinden, sondaki 16 parametrelerin bayt sayısından gelir. 8086 düzeyinde aynı yığın düzeneği Yığın, CALL ve RET simülatöründe adım adım izlenebilir; tek fark 16 bitlik kipte her itmenin 2 bayt tutmasıdır.

3.1.4.5Motorola 68000, Amiga

Amiga bilgisayarları Motorola 68000 işlemcisini kullanır. 68000, 8086 ile aynı dönemin (1979) işlemcisidir, ama mimarisi farklıdır. Sekiz veri yazmacı (d0–d7) ve sekiz adres yazmacı (a0–a7) vardır; hepsi 32 bittir. Bellek kesimlere bölünmez, adresler doğrusaldır.

    move.l  4.w,a6         ; a6 = ExecBase
    lea     dosname(pc),a1 ; kütüphane adı
    moveq   #0,d0          ; herhangi bir sürüm
    jsr     -552(a6)       ; OpenLibrary()
    tst.l   d0
    beq.s   cik            ; açılamadı → çık
    move.l  d0,a6          ; a6 = DOSBase
    jsr     -60(a6)        ; Output() → d0
    move.l  d0,d1          ; dosya tanıtıcısı
    move.l  #mesaj,d2      ; dizginin adresi
    moveq   #16,d3         ; bayt sayısı
    jsr     -48(a6)        ; Write(d1,d2,d3)
    move.l  a6,a1
    move.l  4.w,a6
    jsr     -414(a6)       ; CloseLibrary()
cik moveq   #0,d0          ; dönüş kodu 0
    rts
dosname dc.b 'dos.library',0
mesaj   dc.b 'Merhaba, Amiga!',10

AmigaOS'ta sistem hizmetleri kütüphaneler hâlinde sunulur ve her kütüphanenin bir taban adresi vardır. Fonksiyonlar bu tabana göre negatif uzaklıklarda duran bir atlama tablosundan çağrılır. Bellekteki 4 numaralı adreste, bütün sistemin çekirdek kütüphanesi olan exec.library'nin taban adresi bulunur. İlk satır bu adresi a6'ya yükler. lea dosname(pc),a1 kütüphane adının adresini a1'e koyar. moveq #0,d0 “herhangi bir sürüm olur” demektir. jsr -552(a6), a6'nın 552 bayt altındaki girişe atlar; bu giriş OpenLibrary'dir.

Dönüşte d0 açılan dos.library'nin tabanını taşır. tst.l d0 ve beq.s cik, kütüphane açılamadıysa (d0 = 0) programdan çıkar. Açıldıysa taban a6'ya alınır. jsr -60(a6), Output() fonksiyonudur ve standart çıktının tanıtıcısını d0'da döndürür. Write(d1, d2, d3) çağrısının parametreleri yazmaçlarla verilir: tanıtıcı d1'e, metnin adresi d2'ye, uzunluk 16 d3'e konur. “Merhaba, Amiga!” 15 bayttır; satır sonuyla 16 eder. jsr -48(a6) yazmayı yapar. Son olarak kütüphane CloseLibrary ile kapatılır (-414 uzaklığı) ve d0 = 0 dönüş koduyla rts çalıştırılır.

68000 söz diziminde işlenen sırası AT&T gibidir: önce kaynak, sonra hedef. Boyut, komutun sonundaki .b, .w ya da .l sonekiyle belirtilir. Sabitler # ile yazılır. Kütüphane önce açılır, kullanılır, sonra kapatılır. İşletim sisteminin kaynakları bu düzende yönetilir.

3.1.4.6ARM

ARM işlemcileri bugün akıllı telefonlarda, gömülü sistemlerde ve birçok dizüstü bilgisayarda kullanılır. ARM bir RISC (reduced instruction set computer, indirgenmiş komut kümeli bilgisayar) mimarisidir: komutlar az sayıda, basit ve sabit uzunluktadır.

    .data
mesaj:  .ascii "Merhaba, ARM!\n"
uzun = . - mesaj       @ 14 bayt

    .text
    .global _start
_start:
    mov  r7, #4        @ sys_write
    mov  r0, #1        @ stdout
    ldr  r1, =mesaj    @ dizginin adresi
    mov  r2, #uzun     @ bayt sayısı
    svc  #0            @ çekirdeği çağır
    mov  r7, #1        @ sys_exit
    mov  r0, #0        @ çıkış kodu
    svc  #0

Program GNU as ile çevrilir. GNU as'ta veri db ile değil .ascii direktifiyle tanımlanır; \n satır sonudur. uzun = . - mesaj satırındaki nokta konum sayacıdır, NASM'deki $'ın karşılığıdır. “Merhaba, ARM!” 13 bayttır; satır sonuyla 14 eder. Açıklama işareti ARM için GNU as'ta @'dir.

32 bitlik ARM Linux'ta hizmet numarası r7'ye, parametreler r0, r1, r2'ye konur. Yazma hizmeti burada da 4, çıkış 1'dir; numaralar 32 bitlik x86 Linux ile aynı olsa da yazmaçlar farklıdır. ldr r1, =mesaj özel bir yazımdır. ARM komutları 32 bit olduğundan 32 bitlik bir adres tek bir komutun içine sığmaz. Assembler adresi kodun yakınındaki bir sabit havuzuna yazar ve ldr komutunu oradan okuyacak biçimde üretir. svc #0 (eski adı swi) denetimi çekirdeğe verir. ARM'da bellekle yalnız ldr (oku) ve str (yaz) komutları çalışır. Toplama gibi işlemler yalnız yazmaçlar üzerinde yapılır. 8086'daki add ax, [0202h] gibi bellekten doğrudan toplayan bir komut ARM'da yoktur.

3.1.4.7PowerPC

PowerPC, IBM, Motorola ve Apple'ın ortak geliştirdiği bir RISC mimarisidir. Eski Macintosh bilgisayarlarında, oyun konsollarında ve otomotiv denetleyicilerinde kullanılmıştır.

    .data
mesaj:  .ascii "Merhaba, PowerPC!\n"
uzun = . - mesaj        # 18 bayt

    .text
    .globl _start
_start:
    li   0, 4           # r0 = sys_write
    li   3, 1           # r3 = stdout
    lis  4, mesaj@ha    # r4 = adres (üst)
    addi 4, 4, mesaj@l  #    + alt yarı
    li   5, uzun        # r5 = bayt sayısı
    sc                  # çekirdeği çağır
    li   0, 1           # r0 = sys_exit
    li   3, 0           # r3 = çıkış kodu
    sc

PowerPC'de 32 genel amaçlı yazmaç vardır. GNU as'ta bu yazmaçlar düz sayılarla yazılır: li 3, 1 komutundaki 3, r3 yazmacıdır. li (load immediate) yazmaca sabit yükler. Hizmet numarası r0'a, parametreler r3, r4, r5'e konur; çekirdeğe geçiş sc komutuyla yapılır. Açıklama işareti #'dir. “Merhaba, PowerPC!” 17 bayttır; satır sonuyla 18 eder.

Adres yükleme burada da iki adımdır. PowerPC komutları da 32 bittir ve sabit alanı 16 bit tutar. 32 bitlik adres iki 16 bitlik parçaya bölünür. lis 4, mesaj@ha üst yarıyı yazmacın üst 16 bitine yükler; addi 4, 4, mesaj@l alt yarıyı ekler. @ha, “ayarlanmış üst yarı” demektir. addi alt yarıyı işaretli bir sayı olarak eklediği için, alt yarının en yüksek biti 1 ise üst yarı önceden 1 artırılır. Bu ayrıntı assembler'a bırakılmıştır.

3.1.4.8Altı ortamın karşılaştırması

Tablo 3.1.11 altı programı yan yana koyar.

Tablo 3.1.11: Aynı “Merhaba”, altı ortam.
Ortamİşlemciİşletim sistemine geçişHizmet no.ParametrelerDizgi sonu
Linuxx86 (32)int 0x80EAXEBX, ECX, EDXuzunluk verilir
DOS8086int 21hAHDX (adres)'$'
Windowsx86 (32)call ile DLLfonksiyon adıyığın0
AmigaOS68000jsr -48(a6)kütüphane uzaklığıd1, d2, d3uzunluk verilir
LinuxARMsvc #0r7r0, r1, r2uzunluk verilir
LinuxPowerPCscr0r3, r4, r5uzunluk verilir

Tablodan çıkan sonuç şudur: assembly kodu iki şeye birden bağlıdır. Birincisi işlemcidir: komut kümesi, yazmaç adları, adres yükleme yöntemi ve söz dizimi işlemciyle değişir. Açıklama işareti bile değişir: x86'da ;, ARM için GNU as'ta @, PowerPC'de #. İkincisi işletim sistemidir: aynı x86 işlemcisinde Linux, DOS ve Windows bambaşka çağrı yolları, numaralar ve dizgi kuralları ister. Bir programı yeni bir ortama taşımak, assembly düzeyinde onu yeniden yazmak demektir. Yüksek seviyeli dillerin en büyük kazancı bu bağımlılığı derleyiciye ve yorumlayıcıya devretmesidir. C'deki printf("Merhaba\n") satırı altı ortamın hepsinde aynıdır; farkı, her ortamın C kütüphanesi ve derleyicisi karşılar.

3.1.5Yüksek seviyeli diller

3.1.5.1Derlenen, yorumlanan, hibrit

Yüksek seviyeli bir dilde yazılmış program da sonunda işlemcinin makine diliyle çalışmak zorundadır. Diller, bu çeviriyi kimin ve ne zaman yaptığına göre ayrılır.

Tanım 3.1.5

Derleyici (compiler), yüksek seviyeli kaynak kodu bütün olarak inceleyip program çalışmadan önce makine koduna çeviren programdır. Yorumlayıcı (interpreter), kaynak kodu çalışma anında ifade ifade okuyup işleyen programdır; ayrı bir çalıştırılabilir dosya üretmez. Tam zamanında derleme (JIT, just-in-time compilation), programın parçalarını çalışma sırasında, ihtiyaç duyuldukça makine koduna çevirmektir.

Derlenen dillerde kaynak kod önceden makine diline çevrilir. Derleyici bütün kaynağı gördüğü için tip ve söz dizimi hatalarının çoğu derleme sırasında yakalanır. Çalışma anında çeviri yükü olmadığından program hızlı çalışır. Derleme iki aşamalıdır: derleyici her kaynak dosyayı bir nesne dosyasına çevirir, bağlayıcı da nesne dosyalarını kütüphanelerle birleştirir. Çıktı belirli bir işlemciye ve belirli bir işletim sistemine özgüdür. Windows için derlenmiş bir x86 programı Linux'ta ya da bir ARM işlemcide çalışmaz.

Yorumlanan dillerde kaynak kodun kendisi dağıtılır ve çalışma anında yorumlayıcıda işlenir. Derleme adımı olmadığı için bir değişiklik hemen denenebilir; etkileşimli bir kabukta satır satır çalışmak mümkündür. Kaynak kod platformdan bağımsızdır: yorumlayıcısı olan her bilgisayarda aynı dosya çalışır. Bedeli iki yönlüdür. Bazı hatalar ancak ilgili satır çalışınca ortaya çıkar. Ayrıca her ifadenin çalışırken çözümlenmesi zaman alır ve program genellikle daha yavaştır.

derlenenkaynakderleyicimakine koduCPUyorumlanankaynakyorumlayıcı (çalışırken)CPUhibritkaynakderleyiciara kodVM + JITCPU
Şekil 3.1.12: Üç çalışma modeli. Derlenen dilde kaynak önceden makine koduna çevrilir. Yorumlanan dilde yorumlayıcı kaynağı çalışırken işler. Hibrit modelde kaynak ara koda derlenir; sanal makine bu ara kodu çalışırken JIT ile makine koduna çevirir.

İki modelin arasında bir de hibrit model vardır (Şekil 3.1.12). Java ve C# programları önce işlemciden bağımsız bir ara koda derlenir. Java'da bu koda bytecode, C#'ta IL (intermediate language) denir. Ara kodu bir sanal makine çalıştırır: Java için JVM, C# için CLR. Sanal makine ara kodu önce yorumlar; sık çalışan kısımları JIT ile gerçek makine koduna çevirip saklar. Böylece hem taşınabilirlik hem de yüksek hız elde edilir. Bugün Python ve JavaScript motorları da içeride bytecode ve JIT kullanır; sınırlar keskin değildir.

3.1.5.2Derleme süreci

C dilinde yazılmış bir programın çalıştırılabilir dosyaya dönüşmesi beş aşamada olur. Örnek olarak bir sayının tek mi çift mi olduğunu yazan tekcift.c programı alınsın. gcc derleyicisi bütün aşamaları tek komutla çalıştırır, ama her aşamada durmak için bir seçenek de sunar.

  1. Önişlemci: #include satırlarının yerine ilgili başlık dosyalarının içeriğini koyar, #define ile tanımlanmış adları açar. Çıktı yine C kodudur: tekcift.i (gcc -E). 15 satırlık program, stdio.h eklenince binlerce satıra çıkar; bu not hazırlanırken kullanılan clang tabanlı derleyicide 1.543 satır oldu.
  2. Derleyici: C kodunu söz dizimi ve tip denetiminden geçirir, sonra assembly diline çevirir: tekcift.s (gcc -S). Hataların çoğu bu aşamada bildirilir.
  3. Assembler: assembly kodunu makine koduna çevirip nesne dosyası üretir: tekcift.o (gcc -c). Bu dosyada printf ve scanf çağrılarının adresleri henüz boştur.
  4. Bağlayıcı: nesne dosyasını C kütüphanesindeki printf, scanf gibi kodlarla birleştirir ve çalıştırılabilir dosyayı üretir: Linux'ta tekcift, Windows'ta tekcift.exe.
  5. Yükleyici: program çalıştırıldığında işletim sistemi dosyayı belleğe yükler, yığını ve başlangıç ortamını hazırlar ve sonunda main fonksiyonuna atlar.

Tek komutla bütün iş gcc tekcift.c -o tekcift ile yapılır. Derleyicinin ürettiği ara assembly kodunu Intel söz dizimiyle görmek için gcc -S -masm=intel tekcift.c kullanılır. Bu çıktıyı okumak, yüksek seviyeli bir satırın hangi komutlara dönüştüğünü görmenin en doğrudan yoludur. Örneğin tek/çift programının döngüsü, x86-64 için iyileştirmeyle (-O2) derlendiğinde i % 2 == 0 karşılaştırması bir bölme komutuna değil, test bl, 1 komutuna dönüşür. Derleyici bölme yerine en düşük biti sınar, çünkü bölme komutu çok daha yavaştır. Aynı hile 8086 assembly ile elle de yazılabilir: test cl, 1.

Sık yapılan hata

Derleme hatasıyla bağlama hatasını karıştırmak. printf'in adını yanlış yazmak derleyicide yalnız bir uyarıya yol açabilir. Asıl hata bağlayıcıda “tanımsız başvuru” (undefined reference) olarak çıkar, çünkü o adı taşıyan bir fonksiyon hiçbir kütüphanede bulunamaz. Hata iletisinin hangi araçtan geldiğine bakmak, sorunun hangi aşamada olduğunu gösterir.

3.1.5.3Kod izleme: derleyicinin ürettiği döngü

Derleyicinin bir döngüyü nasıl çevirdiğini görmek için küçük bir C parçası alalım:

toplam = 0;
i = 10;
while (i > 0) {
    toplam += i;
    i -= 3;
}

Bu parçanın 8086 için üretilebilecek bir karşılığı şöyledir:

org 100h
mov bx, 0         ; toplam = 0
mov cx, 10        ; i = 10
kosul: cmp cx, 0  ; i > 0 ?
jle bitti
add bx, cx        ; toplam += i
sub cx, 3         ; i -= 3
jmp kosul
bitti: mov toplam, bx
ret
toplam dw ?

Derleyici değişkenleri yazmaçlara yerleştirmiştir: toplam BX'te, i CX'te durur. Döngünün sonunda toplam, bellekteki toplam değişkenine yazılır. while koşulu döngünün başında sınanır. Burada önemli bir dönüşüm vardır. C'deki koşul “i > 0 ise gövdeye gir” der. Assembly'de ise koşul tersine çevrilir: “i ≤ 0 ise döngüden çık”. cmp cx, 0 komutu CX − 0 çıkarmasını yapar ama sonucu saklamaz; yalnız bayrakları ayarlar. jle bitti (jump if less or equal), işaretli karşılaştırmada küçük ya da eşitse atlar. Bu koşul ZF = 1 ya da SF ≠ OF olduğunda doğrudur. Gövdeden sonra jmp kosul koşulsuz olarak başa döner.

Tablo 3.1.12: while döngüsünün 8086 karşılığının izlemesi.
AdımSatırlarBX (toplam)CX (i)ZFSFOFKarar
1mov bx, 0, mov cx, 10010–––MOV bayrakları değiştirmez
2cmp cx, 0, jle01000010 > 0, atlamaz
3gövde, jmp107000koşula dön
4cmp, jle107000atlamaz
5gövde, jmp174000koşula dön
6cmp, jle174000atlamaz
7gövde, jmp211000koşula dön
8cmp, jle211000atlamaz
9gövde, jmp22−2 (FFFEh)010koşula dön
10cmp, jle22−2 (FFFEh)010SF ≠ OF, atlar
11mov toplam, bx22−2 (FFFEh)010toplam = 22

Tablo 3.1.12'deki bayraklar gövdenin son komutundan, yani sub cx, 3'ten gelir; jmp bayrakları değiştirmez. Döngü dört tur döner ve i sırasıyla 10, 7, 4, 1 değerlerini alır. Toplam 10 + 7 + 4 + 1 = 22'dir. Dördüncü turun sonunda sub cx, 3 1 − 3 = −2 üretir. 16 bitlik ikiye tümleyen gösterimde −2, FFFEh'dir; en yüksek biti 1 olduğu için SF = 1 olur. Bu çıkarma işaretli olarak da doğrudur, yani taşma yoktur ve OF = 0'dır. Onuncu adımda cmp cx, 0 aynı bayrakları yeniden üretir. SF = 1 ve OF = 0, SF ≠ OF demektir; işaretli olarak CX < 0'dır ve jle atlar. Şekil 3.1.13 bu anı C kaynağıyla yan yana gösterir.

C kaynağıtoplam = 0;i = 10;while (i > 0) { toplam += i; i -= 3;}toplam (BX)22i (CX)-2cmp: i − 0 → ZF=0 SF=1 OF=0jle atlar → bitti10 + 7 + 4 + 1 = 22
Şekil 3.1.13: i = −2 olduğunda cmp cx, 0 SF = 1, OF = 0 üretir ve jle bitti atlar. Döngüden çıkılmıştır; toplam 22'dir.

Bu örnekte koşulun işaretli bir atlamayla sınanması önemlidir. i bir int'tir ve negatif olabilir. Derleyici işaretsiz bir atlama, örneğin jbe seçseydi, FFFEh değerini 65534 diye okur ve 0'dan büyük bulurdu; döngü yanlış yerde devam ederdi. C'deki değişken tipi, assembly'de hangi koşullu atlamanın seçileceğini belirler. Tek bir C döngüsü üç komut türüne dönüşmüştür: karşılaştırma, koşullu atlama ve koşulsuz atlama. Aynı C parçası bilgisayarda derlenip çalıştırıldığında da toplam 22 çıkar ve ara değerler (10, 7), (17, 4), (21, 1), (22, −2) aynıdır.

Sık yapılan hata

Döngü koşulunu assembly'ye çevirirken tersine çevirmeyi unutmak. C'deki while (i > 0) doğrudan jg gövde diye yazılırsa, gövdeye atlanmayan durumda akış gövdenin hemen altındaki komuta düşer. Doğru kalıp, koşul yanlışken döngünün sonuna atlayan tersine çevrilmiş bir atlama ve gövde sonunda başa dönen bir jmp'tir.

3.1.6Derlenen diller

Derlenen diller, kaynak kodu çalışmadan önce bütünüyle inceleyen bir derleyiciyle çalışır. Derlenmiş kod doğrudan işlemcide çalıştığı için hızlıdır ve tip ile söz dizimi hataları derleme aşamasında yakalanır. Her dil farklı bir ihtiyaçtan doğmuştur:

C ve C++
Sistem programlaması, işletim sistemleri, gömülü yazılım ve yüksek performanslı uygulamalar. Mikrodenetleyici yazılımının büyük çoğunluğu C ile yazılır.
Rust
Güvenli ve hızlı sistem programlaması; bellek güvenliği derleyicide denetlenir.
Fortran
Bilimsel ve sayısal hesap; ilk yaygın yüksek seviyeli dil (1957).
Go
Sunucu ve ağ yazılımı; hızlı derleme ve eşzamanlılık.
Ada
Havacılık, savunma, demiryolu gibi güvenliğin kritik olduğu sistemler.
Java ve C#
Hibrit: ara koda derlenir, sanal makinede JIT ile çalışır.

3.1.6.1Ortak görev ve C

Diller arasındaki farkları adil biçimde görmek için hepsine aynı görev verilir. Program kullanıcıdan bir sayı okur ve 1'den o sayıya kadar her sayının tek mi çift mi olduğunu yazar. Görev küçüktür, ama bir programın temel parçalarının hepsini içerir: girdi, tip dönüşümü, sayaçlı döngü, kalan işlemi, koşul ve çıktı. C sürümü şöyledir:

#include <stdio.h>

int main() {
    int number;
    printf("Bir sayi girin: ");
    scanf("%d", &number);
    for (int i = 1; i <= number; i++) {
        if (i % 2 == 0) {
            printf("%d sayisi bir cift sayidir.\n", i);
        } else {
            printf("%d sayisi bir tek sayidir.\n", i);
        }
    }
    return 0;
}

#include <stdio.h>, standart giriş/çıkış fonksiyonlarının bildirimlerini ekler. Program main fonksiyonundan başlar. int number; bir tamsayı değişkeni ayırır; değeri henüz belirsizdir. printf istemi yazar. scanf("%d", &number) klavyeden bir ondalık tamsayı okur. scanf'e değişkenin kendisi değil adresi verilir (&number). Fonksiyon okuduğu değeri o adrese yazar. Bu, assembly'de DOS'a dizginin içeriğini değil adresini vermekle aynı düşüncedir.

for (int i = 1; i <= number; i++) üç parçalıdır: başlangıç (i = 1), her turdan önce sınanan koşul (i <= number) ve her turdan sonra yapılan adım (i++). i % 2, i'nin 2'ye bölümünden kalandır; kalan 0 ise sayı çifttir. printf'teki %d yerine i'nin değeri yazılır, \n satır sonudur. return 0 işletim sistemine başarılı çıkış kodu döndürür. Tablo 3.1.13, girdi 3 iken programın izlemesidir.

Tablo 3.1.13: C programının izlemesi (girdi 3).
AdımSatırnumberii % 2Eklenen çıktı
1printf?––Bir sayi girin:
2scanf3––(kullanıcı 3 yazar)
3for: 1 ≤ 331–
4if → else3111 sayisi bir tek sayidir.
5i++; 2 ≤ 332–
6if doğru3202 sayisi bir cift sayidir.
7i++; 3 ≤ 333–
8if → else3313 sayisi bir tek sayidir.
9i++; 4 ≤ 3 yanlış34–döngü biter
10return 03––çıkış kodu 0

Döngü değişkeni i, for satırında tanımlandığı için yalnız döngünün içinde geçerlidir; döngü bittikten sonra kullanılamaz. Program bu not hazırlanırken derlenip 3 ve 5 girdileriyle çalıştırılmıştır; çıktı tablodakiyle aynıdır. Metinlerde Türkçe karakter kullanılmamasının nedeni de pratiktir: bazı uçbirimler UTF-8 karakterleri doğru göstermez. Bu dizgilerin her karakteri tek bayttır.

3.1.6.2C++

#include <iostream>

int main() {
    int number;
    std::cout << "Bir sayi girin: ";
    std::cin >> number;
    for (int i = 1; i <= number; i++) {
        if (i % 2 == 0) {
            std::cout << i << " sayisi bir cift sayidir."
                      << std::endl;
        } else {
            std::cout << i << " sayisi bir tek sayidir."
                      << std::endl;
        }
    }
    return 0;
}

C++ (1985, Bjarne Stroustrup), C'nin üst kümesi olarak tasarlanmıştır; geçerli C kodunun çoğu C++'ta da geçerlidir. Sınıflar, şablonlar ve istisnalar gibi yapılar ekler. Giriş ve çıkış için printf/scanf yerine akışlar kullanılır. std::cout << yazar, std::cin >> okur. << işleci art arda kullanılabilir: önce i, sonra metin, sonra satır sonu akışa gönderilir. Okumada adres işareti gerekmez; C++'ın başvuru (reference) düzeneği bunu kendisi halleder.

std::endl satır sonu ekler ve ayrıca çıktı tamponunu boşaltır. Tamponu her satırda boşaltmak çok satırlı çıktılarda yavaştır; bu durumda '\n' daha hızlıdır. Derleme g++ tekcift.cpp -o tekcift komutuyla yapılır. Çıktı C sürümüyle birebir aynıdır.

3.1.6.3C# ve Java

using System;

class Program {
  static void Main() {
    Console.Write("Bir sayi girin: ");
    int number = int.Parse(Console.ReadLine());
    for (int i = 1; i <= number; i++) {
      if (i % 2 == 0) {
        Console.WriteLine($"{i} sayisi bir cift sayidir.");
      } else {
        Console.WriteLine($"{i} sayisi bir tek sayidir.");
      }
    }
  }
}

C# (2000, Microsoft), .NET platformunun dilidir. Kaynak kod IL ara diline derlenir; .NET'in sanal makinesi CLR (common language runtime), çalışırken bu kodu JIT ile makine koduna çevirir. Program bir sınıfın içindeki Main metoduyla başlar. Console.ReadLine() bir satır metin okur, int.Parse bunu tamsayıya çevirir. Girdi sayı değilse int.Parse istisna fırlatır ve program durur; int.TryParse bu durumda yanlış döndürdüğü için daha güvenlidir. $"{i} …" yazımı, dizginin içine bir değişkenin değerini yerleştirir (string interpolation). Program dotnet run ile çalıştırılır.

import java.util.Scanner;

public class Main {
  public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);
    System.out.print("Bir sayi girin: ");
    int number = scanner.nextInt();
    for (int i = 1; i <= number; i++) {
      if (i % 2 == 0) {
        System.out.println(i + " sayisi bir cift sayidir.");
      } else {
        System.out.println(i + " sayisi bir tek sayidir.");
      }
    }
  }
}

Java (1995, Sun Microsystems) da hibrit bir dildir. javac Main.java komutu, işlemciden bağımsız bytecode içeren Main.class dosyasını üretir. java Main komutu JVM'yi başlatır; JVM bytecode'u önce yorumlar, sık çalışan kısımları JIT ile derler. Aynı .class dosyası JVM'si olan her platformda çalışır. Java'nın “bir kez yaz, her yerde çalıştır” sloganı buradan gelir.

Java'da her şey bir sınıfın içindedir ve public sınıfın adı dosya adıyla aynı olmak zorundadır: Main sınıfı Main.java dosyasında durur. Girdi Scanner sınıfıyla okunur; nextInt() sıradaki tamsayıyı döndürür. + işleci bir sayıyı dizgiyle birleştirirken sayıyı metne çevirir. Program bu not hazırlanırken JDK 21 ile derlenip çalıştırılmış ve C ile aynı çıktıyı vermiştir.

3.1.6.4Go ve Rust

package main

import "fmt"

func main() {
    var number int
    fmt.Print("Bir sayi girin: ")
    fmt.Scan(&number)
    for i := 1; i <= number; i++ {
        if i%2 == 0 {
            fmt.Printf("%d sayisi bir cift sayidir.\n", i)
        } else {
            fmt.Printf("%d sayisi bir tek sayidir.\n", i)
        }
    }
}

Go (2009, Google), doğrudan makine koduna derlenir ve bütün bağımlılıklarını içeren tek, bağımsız bir çalıştırılabilir dosya üretir. Belleği bir çöp toplayıcı (garbage collector) yönetir; artık kullanılmayan bellek kendiliğinden geri alınır. Eşzamanlı işler için çok hafif iş parçacıkları olan goroutine'ler vardır. Söz dizimi C'ye benzer, ama koşul ve döngülerde parantez kullanılmaz. i := 1 yazımı değişkeni tanımlar ve tipini sağdaki değerden çıkarır. fmt.Scan(&number) C'deki gibi adres alır. Program go run tekcift.go ile çalıştırılır.

use std::io;

fn main() {
    let mut input = String::new();
    println!("Bir sayi girin:");
    io::stdin().read_line(&mut input)
        .expect("okuma hatasi");
    let number: i32 = input.trim().parse()
        .expect("cevirme hatasi");
    for i in 1..=number {
        if i % 2 == 0 {
            println!("{} sayisi bir cift sayidir.", i);
        } else {
            println!("{} sayisi bir tek sayidir.", i);
        }
    }
}

Rust (2015, Mozilla), çöp toplayıcı kullanmadan bellek güvenliği sağlar. Her değerin tek bir sahibi (owner) vardır; sahiplik ve ödünç alma kuralları derleme sırasında denetlenir. Boşaltılmış belleğe erişmek ya da aynı veriyi iki yerden aynı anda değiştirmek gibi hatalar program çalışmadan önce yakalanır. Değişkenler varsayılan olarak değiştirilemez; input değişkeni okunan satırla dolacağı için mut ile tanımlanmıştır.

Girdi read_line ile bir dizgiye okunur. trim() satır sonunu atar, parse() sayıya çevirir. Bu işlemler başarısız olabilir; Rust bu olasılığı görmezden gelmeye izin vermez. .expect("…"), hata olursa iletiyi yazıp programı durdurur. 1..=number üst sınırı dahil eden bir aralıktır. Program rustc tekcift.rs ya da cargo run ile derlenir.

3.1.6.5Fortran ve Ada

program Main
    implicit none
    integer :: number, i
    write(*,*) 'Bir sayi girin:'
    read(*,*) number
    do i = 1, number
        if (mod(i, 2) == 0) then
            write(*,*) i, ' sayisi bir cift sayidir.'
        else
            write(*,*) i, ' sayisi bir tek sayidir.'
        end if
    end do
end program Main

Fortran (1957, IBM, John Backus), ilk yaygın yüksek seviyeli dildir. Adı FORmula TRANslation sözcüklerinden gelir ve formülleri doğrudan kod olarak yazmayı hedefler. Hava tahmini, akışkanlar dinamiği ve fizik benzetimlerinde bugün de yaygındır; bu alanlardaki büyük sayısal kütüphanelerin çoğu Fortran ile yazılmıştır.

implicit none, bütün değişkenlerin açıkça tanımlanmasını zorunlu kılar. Eski Fortran'da adı i–n harfleriyle başlayan değişkenler kendiliğinden tamsayı sayılırdı; bu kural yazım hatalarını gizlediği için kapatılır. do i = 1, number döngüsü üst sınırı dahil eder. Kalan işlemi bir işleçle değil, mod(i, 2) fonksiyonuyla yapılır. write(*,*) serbest biçimde yazar; sayıların önünde boşluk görülebilir. Derleme gfortran tekcift.f90 -o tekcift komutuyla yapılır.

with Ada.Text_IO;         use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;

procedure Main is
   Number : Integer;
begin
   Put ("Bir sayi girin: ");
   Get (Number);
   for I in 1 .. Number loop
      Put (I, Width => 0);
      if I mod 2 = 0 then
         Put_Line (" sayisi bir cift sayidir.");
      else
         Put_Line (" sayisi bir tek sayidir.");
      end if;
   end loop;
end Main;

Ada (1983, ABD Savunma Bakanlığı), güvenliğin kritik olduğu büyük sistemler için tasarlanmıştır. Tip denetimi çok güçlüdür; örneğin farklı tipteki tamsayılar açık dönüşüm olmadan karıştırılamaz. Havacılık, savunma ve demiryolu yazılımlarında kullanılır. with satırları kullanılan paketleri, use satırları bu paketlerin adlarını kısaltmadan kullanmayı belirtir. Tamsayı okumak ve yazmak için Ada.Integer_Text_IO paketi gerekir. Put (I, Width => 0) sayıyı önünde boşluk bırakmadan yazar; Put_Line metnin sonuna satır sonu ekler. I mod 2 kalanı verir. Derleme gnatmake main.adb ile yapılır.

Go, Rust, Fortran ve Ada derleyicileri bu notun hazırlandığı bilgisayarda kurulu değildi. Bu dört program çalıştırılmadı; her dilin belgelerine göre satır satır denetlendi.

3.1.7Yorumlanan diller

3.1.7.1Yorumlayıcı nasıl çalışır

Yorumlanan bir dilde ayrı bir derleme adımı yoktur. Programcı kaynak dosyayı doğrudan yorumlayıcıya verir: python tekcift.py. Yorumlayıcı da işlemcide çalışan sıradan bir programdır; çoğunlukla C ile yazılmıştır. Görevi, kaynak koddaki ifadeleri sırayla okuyup her birinin istediği işi yapmaktır. Bu yaklaşım hızlı geliştirme ve deneme sağlar. Python ve Ruby gibi dillerin etkileşimli kabuğunda (REPL, read–eval–print loop) bir satır yazılır, sonucu hemen görülür.

tekcift.py yorumlayıcı (CPython) sıradaki ifadeyi oku çözümle (bytecode'a çevir) çalıştır
Şekil 3.1.14: Yorumlayıcı döngüsü. CPython sıradaki ifadeyi okur, bytecode'a çevirerek çözümler, çalıştırır ve bir sonraki ifadeye geçer.

Şekil 3.1.14'teki döngü, işlemcinin getir-çöz-yürüt döngüsünün yazılımdaki karşılığıdır. Fark, her adımın donanım yerine bir programla yapılmasıdır. Bir toplama işlemi işlemcide tek bir komut tutarken yorumlayıcıda onlarca, bazen yüzlerce komut tutar. Yorumlayıcı önce işlenenlerin tipine bakmalı, sonra uygun toplama yordamını seçmeli, sonucu yeni bir nesne olarak oluşturmalıdır. Yorumlanan dillerin yavaşlığı buradan gelir. Gerçek yorumlayıcılar kaynak kodu her seferinde metin olarak çözmez; önce bytecode'a çevirir ve bytecode'u yorumlar. CPython, çevirdiği bytecode'u .pyc dosyalarında saklayarak bir sonraki çalıştırmada çeviriyi de atlar.

Yaygın yorumlanan dillerin her biri belirli bir alanda öne çıkar. Python genel amaçlıdır; veri bilimi, yapay zekâ ve otomasyon için çok kullanılır. JavaScript web'in dilidir; tarayıcıda ve Node.js ile sunucuda çalışır. Ruby kullanım kolaylığı ve esnekliğiyle, özellikle Ruby on Rails web çatısıyla tanınır.

3.1.7.2Python

# Kullanıcıdan girdi alma
number = int(input("Bir sayi girin: "))

# 1'den number'a kadar sayıları kontrol et
for i in range(1, number + 1):
    if i % 2 == 0:
        print(f"{i} sayisi bir cift sayidir.")
    else:
        print(f"{i} sayisi bir tek sayidir.")

Python (1991, Guido van Rossum) okunabilirliği öne koyar. Bloklar süslü parantezle değil girintiyle belirlenir. for satırının altındaki girintili satırlar döngünün gövdesidir; girinti bitince gövde de biter. Girinti burada bir yazım alışkanlığı değil, dilin kuralıdır. Yanlış girinti ya bir söz dizimi hatası verir ya da programın anlamını değiştirir.

input her zaman metin döndürür. Kullanıcı 3 yazsa bile dönen değer "3" dizgisidir; int(...) onu tamsayıya çevirir. range(1, number + 1) 1'den başlayan bir sayı dizisi üretir, ama üst sınırı dahil etmez. Bu yüzden number'a 1 eklenmiştir. f"{i} sayisi …" biçimli dizgidir (f-string): süslü parantez içindeki ifadenin değeri metne yerleştirilir. Program python tekcift.py ile çalıştırılır.

Python dinamik tiplidir: bir değişkenin tipi çalışırken, ona atanan değere göre belirlenir. number önceden tanımlanmaz; atama onu yaratır. C sürümünde 15 satır tutan iş burada 6 satır koddur. Yüksek seviyeli dillerin ifade gücü budur. Bedeli hızdır: CPython aynı döngüyü derlenmiş C'den onlarca kat yavaş çalıştırır, çünkü her i % 2 işleminde tipleri denetler ve yeni tamsayı nesneleri oluşturur.

3.1.7.3JavaScript

// Kullanıcıdan girdi alma
let number = Number(prompt("Bir sayi girin:"));

// 1'den number'a kadar sayıları kontrol et
for (let i = 1; i <= number; i++) {
    if (i % 2 === 0) {
        console.log(i + " sayisi bir cift sayidir.");
    } else {
        console.log(i + " sayisi bir tek sayidir.");
    }
}

JavaScript (1995, Brendan Eich) tarayıcıların dilidir; Node.js ile sunucuda da çalışır. V8 gibi modern motorlar kodu önce bytecode'a çevirip yorumlar, sık çalışan fonksiyonları JIT ile makine koduna derler. Bu yüzden JavaScript bugün “yorumlanan” ile “hibrit” arasında bir yerdedir.

prompt tarayıcıda bir giriş kutusu açar ve kullanıcının yazdığını metin olarak döndürür. Number(...) bu metni sayıya çevirir. Çevirme yapılmasaydı i <= number karşılaştırması yine çalışırdı, çünkü JavaScript sayıyla metni karşılaştırırken metni kendiliğinden sayıya çevirir. Ama number + 1 gibi bir ifade “3” ile 1'i yan yana yazıp “31” üretirdi. Açık çevirme bu tür sürprizleri önler. === işleci tip dönüştürmeden eşitliği sınar: 0 === "0" yanlıştır, 0 == "0" doğrudur. console.log çıktıyı tarayıcının geliştirici konsoluna ya da uçbirime yazar. Node.js'te prompt fonksiyonu bulunmadığından, bu not hazırlanırken program girdiyi veren küçük bir prompt tanımıyla Node.js'te çalıştırılmış ve C ile aynı satırları yazmıştır.

3.1.7.4Ruby, PHP ve Perl

# Kullanıcıdan girdi alma
print "Bir sayi girin: "
number = gets.chomp.to_i

# 1'den number'a kadar sayıları kontrol et
for i in 1..number do
  if i % 2 == 0
    puts "#{i} sayisi bir cift sayidir."
  else
    puts "#{i} sayisi bir tek sayidir."
  end
end

Ruby (1995, Yukihiro Matsumoto) tamamen nesne yönelimlidir: sayılar dahil her şey bir nesnedir. gets.chomp.to_i art arda üç metot çağrısıdır. gets bir satır okur, chomp sonundaki satır sonunu atar, to_i tamsayıya çevirir. 1..number üst sınırı dahil eden bir aralıktır. Bloklar end ile kapanır. "#{i} …", Python'daki biçimli dizginin karşılığıdır: #{…} içindeki ifadenin değeri metne yerleşir. puts yazdığı metnin sonuna satır sonu ekler. Program ruby tekcift.rb ile çalıştırılır.

<?php
// Kullanıcıdan girdi alma
echo "Bir sayi girin: ";
$number = (int) trim(fgets(STDIN));

// 1'den number'a kadar sayıları kontrol et
for ($i = 1; $i <= $number; $i++) {
    if ($i % 2 == 0) {
        echo $i . " sayisi bir cift sayidir.\n";
    } else {
        echo $i . " sayisi bir tek sayidir.\n";
    }
}
?>

PHP (1995, Rasmus Lerdorf) sunucu tarafında çalışan bir web dilidir. Kod <?php ile ?> arasına yazılır ve HTML sayfalarının içine gömülebilir. Değişken adları $ ile başlar. . işleci dizgileri birleştirir; $i . " sayisi …" önce sayıyı metne çevirir, sonra birleştirir. Komut satırında fgets(STDIN) bir satır okur, trim satır sonunu atar, (int) tamsayıya çevirir. Program php tekcift.php ile çalıştırılır.

use strict;
use warnings;

# Kullanıcıdan girdi alma
print "Bir sayi girin: ";
my $number = <STDIN>;
chomp $number;

# 1'den number'a kadar sayıları kontrol et
for my $i (1..$number) {
    if ($i % 2 == 0) {
        print "$i sayisi bir cift sayidir.\n";
    } else {
        print "$i sayisi bir tek sayidir.\n";
    }
}

Perl (1987, Larry Wall) metin işleme ve sistem yönetimi için tasarlanmıştır ve güçlü düzenli ifadeleriyle tanınır. <STDIN> standart girdiden bir satır okur; satır sonu da okunan metne dahildir ve chomp onu atar. Çift tırnaklı dizgide $i doğrudan değeriyle değiştirilir. use strict; ve use warnings; tanımlanmamış değişken kullanımı gibi yaygın hataları yakalar. my değişkeni tanımlar. Program perl tekcift.pl ile çalıştırılır.

Bu üç dilden yalnız Perl bu not hazırlanırken çalıştırılabildi; çıktısı C ile aynıdır. Ruby ve PHP yorumlayıcıları kurulu değildi, iki program dillerin belgelerine göre denetlendi.

3.1.7.5MATLAB

% Kullanıcıdan girdi alma
number = input('Bir sayi girin: ');

% 1'den number'a kadar sayıları kontrol et
for i = 1:number
    if mod(i, 2) == 0
        fprintf('%d sayisi bir cift sayidir.\n', i);
    else
        fprintf('%d sayisi bir tek sayidir.\n', i);
    end
end

MATLAB (1984, MathWorks), adını MATrix LABoratory sözcüklerinden alır ve matris tabanlı sayısal hesap için tasarlanmıştır. Mühendislikte, özellikle sinyal işleme ve denetim sistemlerinde çok yaygındır. input kullanıcının yazdığını bir ifade olarak değerlendirir; 3 yazılınca sayı olarak 3 döner. 1:number, 1, 2, …, number değerlerinden oluşan bir satır vektörü üretir ve for bu vektörün öğelerini sırayla dolaşır. Kalan mod(i, 2) ile bulunur. Satır sonundaki ; sonucun ekrana basılmasını engeller. Açıklamalar % ile başlar. MATLAB ticari bir üründür; ücretsiz ve büyük ölçüde uyumlu bir seçenek GNU Octave'dır. MATLAB'in yeni sürümleri de içeride JIT kullanır.

3.1.7.6On dört dilin karşılaştırması

Tablo 3.1.14, aynı programın on dört dildeki yazılışını üç noktada karşılaştırır: döngü başlığı, kalan işlemi ve yazdırma.

Tablo 3.1.14: Aynı program, on dört dil.
DilModelDöngü başlığıKalanYazdırma
Cderlenenfor (int i = 1; i <= n; i++)i % 2printf
C++derlenenfor (int i = 1; i <= n; i++)i % 2std::cout <<
C#IL + JITfor (int i = 1; i <= n; i++)i % 2Console.WriteLine
Javabytecode + JITfor (int i = 1; i <= n; i++)i % 2System.out.println
Goderlenenfor i := 1; i <= n; i++i%2fmt.Printf
Rustderlenenfor i in 1..=ni % 2println!
Fortranderlenendo i = 1, nmod(i, 2)write(*,*)
Adaderlenenfor I in 1 .. N loopI mod 2Put_Line
Pythonyorumlananfor i in range(1, n + 1):i % 2print
JavaScriptyorumlanan/JITfor (let i = 1; i <= n; i++)i % 2console.log
Rubyyorumlananfor i in 1..n doi % 2puts
PHPyorumlananfor ($i = 1; $i <= $n; $i++)$i % 2echo
Perlyorumlananfor my $i (1..$n)$i % 2print
MATLAByorumlanan/JITfor i = 1:nmod(i, 2)fprintf

Tabloda iki aile açıkça görülür. C'den türeyen diller (C++, C#, Java, JavaScript, PHP, Go) üç parçalı for başlığını ve % işlecini korur. Öteki diller döngüyü bir aralık ya da vektör üzerinde yürür: Rust'ta 1..=n, Python'da range, Ruby'de 1..n, Perl'de (1..$n), MATLAB'de 1:n, Fortran ve Ada'da sınır çiftiyle. Aralık yazımında en sık hata kaynağı üst sınırın dahil olup olmadığıdır. Python'un range'i üst sınırı dışarıda bırakır; Rust'ta 1..n dışarıda, 1..=n içeride bırakır. Ruby'de ise 1..n üst sınırı dahil eder, 1...n etmez. Kalan için Fortran, Ada ve MATLAB bir işleç yerine mod kullanır.

Sık yapılan hata

Bir dildeki aralık kuralını başka bir dile taşımak. C'deki for (i = 1; i <= n; i++) döngüsü Python'a for i in range(1, n): diye çevrilirse son değer (n) atlanır. Program söz dizimi olarak doğrudur ve hata vermez; eksik satır ancak çıktı incelenince fark edilir.

3.1.8Derlenen mi, yorumlanan mı?

3.1.8.1Ölçütler

Üç çalışma modeli altı ölçüt üzerinden karşılaştırılabilir (Tablo 3.1.15).

Tablo 3.1.15: Derlenen, yorumlanan ve hibrit modellerin karşılaştırması.
ÖlçütDerlenenYorumlananHibrit (bytecode + JIT)
Çeviri zamanıçalışmadan önce, bir kezher çalıştırmadaönce ara koda, sonra çalışırken
Hızen yükseken düşükyüksek (ısındıktan sonra)
Dağıtılanmakine kodukaynak kodara kod (.class, .dll)
Taşınabilirlikher platform için yeniden derleniryorumlayıcı olan her yerdesanal makine olan her yerde
Hata yakalamaçoğu derlemedeçoğu çalışırkentip hataları derlemede
ÖrnekC, C++, Go, Rust, Fortran, AdaPython, Ruby, PHP, Perl, MATLABJava, C#, JavaScript (V8)

“Isındıktan sonra” ifadesi JIT'in bir özelliğini anlatır. Program başladığında sanal makine kodu yorumlar ve hangi kısımların sık çalıştığını sayar. Sık çalışan bir döngü ya da fonksiyon belirli bir eşiği aşınca makine koduna derlenir. Bu yüzden hibrit bir program ilk saniyelerde yavaş, sonra derlenmiş koda yakın hızda çalışır. JIT derleyicisi bazı durumlarda önceden derleyen bir derleyiciden de iyi kod üretebilir, çünkü programın gerçekte hangi değerlerle çalıştığını görür.

Tanım 3.1.6

“Derlenen” ya da “yorumlanan” olmak bir dilin değil, o dilin gerçeklemesinin (implementation) özelliğidir. Aynı dil için hem derleyici hem yorumlayıcı yazılabilir.

C genellikle derlenir, ama C yorumlayıcıları da vardır. Python genellikle CPython ile yorumlanır, ama Cython Python benzeri kodu C'ye çevirip derler, PyPy ise JIT ile çalıştırır. “Python yavaş bir dildir” demek yerine “CPython yorumlayıcısı yavaştır” demek daha doğrudur. Dilin kendisi bir tanımdır; hızı, onu çalıştıran araç belirler.

3.1.8.2Ara kod ve yığın makinesi

Ara kod, hiçbir gerçek işlemciye ait olmayan, hayalî bir işlemcinin makine dilidir. JVM ve CLR gibi sanal makineler bu hayalî işlemciyi yığın makinesi (stack machine) olarak tanımlar. Yığın makinesinde komutların işlenen alanı yoktur. İşlenenler bir işlenen yığınında durur. Komut, üstteki öğeleri yığından alır, işlemi yapar ve sonucu yığına geri koyar. Yazmaç sayısı işlemciden işlemciye değiştiği için yazmaçsız bir tasarım ara kodu taşınabilir kılar. Bu kodu gerçek işlemcinin yazmaçlarına eşlemek JIT'in işidir.

x = (a + b) * c ifadesi için derleyicinin ürettiği ara kod şöyledir:

// x = (a + b) * c;
iload a      // a'yı yığına it
iload b      // b'yi yığına it
iadd         // üst iki öğeyi topla
iload c
imul         // üst iki öğeyi çarp
istore x     // yığından x'e yaz
return

Yerel değişkenler a = 6, b = 7, c = 3 olsun. Tablo 3.1.16 işlenen yığınını alttan üste doğru yazar.

Tablo 3.1.16: Yığın makinesinde x = (a + b) * c izlemesi (a = 6, b = 7, c = 3).
Komutİşlemİşlenen yığını (alt → üst)x
iload aa'yı it[6]?
iload bb'yi it[6, 7]?
iadd7 ve 6'yı çek, 13'ü it[13]?
iload cc'yi it[13, 3]?
imul3 ve 13'ü çek, 39'u it[39]?
istore xüstü çek, x'e yaz[ ]39
returndön[ ]39

iadd komutunda işlenen adı yoktur; komut hangi değerleri toplayacağını yığının durumundan bilir. Komutların başındaki i, işlemin tamsayı (integer) üzerinde olduğunu belirtir. Parantezli ifade, ara kodda işlemlerin sırasına dönüşmüştür: toplama çarpmadan önce yapılır, çünkü önce o yazılmıştır. Ara kodun bu sırası, ifadenin sonek gösterimi olan a b + c * ile aynıdır.

işlenen yığını13← üstyerel değişkenlera6b7c3x?
Şekil 3.1.15: iadd'den sonra yığın makinesi. Üstteki iki öğe (6 ve 7) çekilmiş, toplamları 13 yığına itilmiştir; yerel değişkenler değişmemiştir.

Gerçek Java bytecode'u da aynı yapıdadır. Yalnız yerel değişkenlere adla değil, sıra numarasıyla başvurulur. Aşağıdaki Java metodu JDK 21 ile derlenip javap -c ile açıldığında şu kod görülür:

static int hesapla(int a, int b, int c) {
    int x = (a + b) * c;
    return x;
}

 0: iload_0      // a (0 numaralı yerel)
 1: iload_1      // b
 2: iadd
 3: iload_2      // c
 4: imul
 5: istore_3     // x (3 numaralı yerel)
 6: iload_3
 7: ireturn

Soldaki sayılar bytecode içindeki bayt konumlarıdır; her komut burada tek bayttır. hesapla(6, 7, 3) çağrısı 39 döndürür. CPython'un ürettiği bytecode da aynı biçimdedir: x = (a + b) * c satırı LOAD_NAME a, LOAD_NAME b, BINARY_OP +, LOAD_NAME c, BINARY_OP *, STORE_NAME x komutlarına çevrilir. İki sanal makine de yığın makinesidir.

Bu ara kod hiçbir işlemciye bağlı değildir. Sanal makine her komutu ya yorumlar ya da JIT ile 8086, x86-64 ya da ARM gibi gerçek bir işlemcinin makine koduna çevirir. 8086'da bu ifade, işlenen yığını yerine yazmaçlarla birkaç komut tutar: mov ax, a, add ax, b, imul c, mov x, ax. Yığın makinesindeki “yığına it” ve “yığından çek” işlemleri, gerçek işlemcide yazmaçlara yükleme ve yazmaçtan yazmaya dönüşür.

3.1.9Aynı görev 8086 assembly ile

3.1.9.1Program ve bellek yerleşimi

Tek/çift görevi son olarak 8086 assembly ile yazılır. Program emu8086'da bir COM dosyası olarak çevrilir. Görevi kısa tutmak için iki basitleştirme yapılmıştır. n = 3 sabit olarak koda yazılmıştır; klavyeden okunmaz. Sayılar tek basamaklıdır; bu yüzden bir sayıyı ekrana yazmak tek bir ASCII karakteri yazmak demektir.

org 100h
       mov  cx, 1       ; i = 1
dongu: cmp  cx, 3       ; i ile n karşılaştır
       ja   bitti       ; i > n ise bitir
       mov  dl, cl
       add  dl, '0'     ; rakam → ASCII
       mov  ah, 02h
       int  21h         ; DL'yi yaz
       test cl, 1       ; en düşük bit
       jnz  tek         ; 1 ise tek
       lea  dx, cift_m
       jmp  yaz
tek:   lea  dx, tek_m
yaz:   mov  ah, 09h
       int  21h         ; dizgiyi yaz
       inc  cx          ; i++
       jmp  dongu
bitti: ret
tek_m  db ' sayisi bir tek sayidir.',0Dh,0Ah,'$'
cift_m db ' sayisi bir cift sayidir.',0Dh,0Ah,'$'

CX yazmacı döngü sayacıdır ve C'deki i'nin yerini tutar. dongu etiketindeki cmp cx, 3 ve ja bitti, C'deki i <= n koşulunun tersine çevrilmiş hâlidir: i > n ise döngüden çık. ja (jump if above) işaretsiz karşılaştırmada “büyükse” atlar; CF = 0 ve ZF = 0 olduğunda atlama yapılır. Sayaç hiçbir zaman negatif olmadığı için işaretsiz atlama burada doğrudur.

Gövdenin ilk dört satırı sayıyı yazar. mov dl, cl sayacın alt baytını DL'ye kopyalar. add dl, '0' ona '0' karakterinin ASCII kodunu, yani 30h'yi ekler. ASCII tablosunda rakamlar 30h'den 39h'ye sıralıdır; 1 + 30h = 31h, '1' karakteridir. mov ah, 02h ve int 21h, DOS'un “DL'deki karakteri yaz” hizmetidir.

Sonraki iki satır C'deki i % 2 == 0 sınamasının karşılığıdır. 2'ye bölümden kalan, sayının en düşük bitidir. test cl, 1, CL ile 1'in bit bit VE işlemini yapar, sonucu saklamaz, yalnız bayrakları ayarlar. En düşük bit 1 ise sonuç 1 olur ve ZF = 0'dır; bit 0 ise sonuç 0 olur ve ZF = 1'dir. jnz tek (jump if not zero) ZF = 0 iken atlar. Bölme komutu (div) 8086'da 80 saat çevriminden fazla sürebilir; test ise birkaç çevrimdir. Derleyiciler de aynı yolu seçer.

Çift sayı için akış atlamadan devam eder: lea dx, cift_m DX'e çift iletisinin adresini yükler ve jmp yaz ortak yazdırma koduna gider. Tek sayıda tek etiketine atlanır ve DX'e tek iletisinin adresi yüklenir. İki yol yaz etiketinde birleşir: 09h hizmeti DX'teki adresten başlayıp $'a kadar yazar. İletilerin sonundaki 0Dh, 0Ah satır başı ve satır sonu karakterleridir; DOS'ta yeni satıra geçmek için ikisi birlikte gerekir. inc cx sayacı artırır, jmp dongu koşula döner. ret, COM programını DOS'a döndürür.

Program belleğe şöyle yerleşir. Komutların uzunlukları ve adresleri Tablo 3.1.17'de verilmiştir; kod 40 bayttır ve iletiler kodun hemen arkasından başlar.

Tablo 3.1.17: 8086 tek/çift programının bellek yerleşimi.
AdresBaytSatır
0100h3mov cx, 1
0103h3dongu: cmp cx, 3
0106h2ja bitti
0108h2mov dl, cl
010Ah3add dl, '0'
010Dh2 + 2mov ah, 02h, int 21h
0111h3test cl, 1
0114h2jnz tek
0116h3lea dx, cift_m
0119h2jmp yaz
011Bh3tek: lea dx, tek_m
011Eh2 + 2yaz: mov ah, 09h, int 21h
0122h1inc cx
0123h2jmp dongu
0125h1bitti: ret
0126h27tek_m db … (24 karakter + 0Dh + 0Ah + '$')
0141h28cift_m db … (25 karakter + 0Dh + 0Ah + '$')

Koşullu ve koşulsuz atlamaların hepsi iki bayttır. İlk bayt işlem kodu, ikinci bayt işaretli bir uzaklıktır: hedef, bir sonraki komutun adresine bu uzaklık eklenerek bulunur. Örneğin ja bitti 0106h'dedir; sonraki komut 0108h'de başlar ve hedef 0125h'dir. Uzaklık 0125h − 0108h = 1Dh olur ve komut 77 1D diye kodlanır. Bu bağıl adresleme sayesinde program belleğin neresine yüklenirse yüklensin atlamalar doğru çalışır.

3.1.9.2Kod izleme

Tablo 3.1.18 programın tamamını izler. Bayraklar, satır grubunun son bayrak etkileyen komutundan sonraki değerlerdir. MOV, LEA, INT 21h'nin dönüşü ve atlama komutları bayrakları değiştirmez.

Tablo 3.1.18: 8086 tek/çift programının izlemesi (n = 3).
AdımSatırlarCXDL / DXZFCFOlay
1mov cx, 10001h–––i = 1
2cmp cx, 3, ja0001h–011 − 3 ödünç ister; atlamaz
3mov dl, cl … int 21h0001hDL = 31h00“1” yazılır
4test cl, 1, jnz0001hDL = 31h001 VE 1 = 1; atlar (tek)
5lea dx, tek_m … int 21h0001hDX = 0126h00“ sayisi bir tek sayidir.”
6inc cx, jmp0002h0126h00i = 2
7cmp cx, 3, ja0002h0126h01atlamaz
8mov dl, cl … int 21h0002hDL = 32h00“2” yazılır
9test cl, 1, jnz0002hDL = 32h102 VE 1 = 0; atlamaz (çift)
10lea dx, cift_m, jmp yaz0002hDX = 0141h10ortak yazdırmaya git
11mov ah, 09h, int 21h0002hDX = 0141h10“ sayisi bir cift sayidir.”
12inc cx, jmp0003h0141h00i = 3
13cmp cx, 3, ja0003h0141h103 − 3 = 0; ZF = 1, atlamaz
14mov dl, cl … jnz0003hDL = 33h00“3” yazılır; tek
15lea dx, tek_m … int 21h0003hDX = 0126h00“ sayisi bir tek sayidir.”
16inc cx, jmp0004h0126h00i = 4
17cmp cx, 3, ja0004h0126h004 − 3 = 1; atlar
18bitti: ret0004h0126h00DOS'a dönülür

İkinci adımda cmp cx, 3, 1 − 3 çıkarmasını yapar. İşaretsiz olarak 1, 3'ten küçüktür; çıkarma ödünç ister ve CF = 1 olur. ja yalnız CF = 0 ve ZF = 0 iken atlar, bu yüzden atlamaz. Üçüncü adımda add dl, '0' 01h + 30h = 31h üretir; elde yoktur, CF = 0 olur. Dördüncü adımda test ZF = 0 bırakır ve jnz atlar. test komutu CF ve OF'yi her zaman sıfırlar.

Altıncı adımdaki inc cx bir ayrıntı taşır: INC komutu ZF, SF ve OF'yi günceller ama CF'yi değiştirmez. Burada CF zaten 0'dır, ama döngü sayaçlarında bu özellik önemlidir; bir önceki toplamanın eldesi INC'ten geçerek korunabilir. On üçüncü adımda 3 − 3 = 0 olur: ZF = 1, CF = 0. ja'nın koşulu ZF = 0'ı da istediği için atlama olmaz; eşitlik “büyük” sayılmaz. On yedinci adımda 4 − 3 = 1 olur: ZF = 0, CF = 0 ve ja bitti atlar. Ekranda sonunda şu üç satır vardır:

1 sayisi bir tek sayidir.
2 sayisi bir cift sayidir.
3 sayisi bir tek sayidir.

Çıktı, C programının girdi 3 ile verdiği çıktının istem satırı dışında aynısıdır.

3.1.9.3C ile karşılaştırma

C'deki for ve if yapıları 8086'da yoktur. Hepsi karşılaştırma + koşullu atlama çiftleriyle kurulur. Tablo 3.1.19 bu eşlemeyi satır satır verir.

Tablo 3.1.19: C yapılarının 8086 karşılıkları.
C8086
int i = 1mov cx, 1 (değişken yerine yazmaç)
i <= numbercmp cx, 3 + ja bitti (tersine çevrilmiş koşul)
i++inc cx + jmp dongu
i % 2 == 0test cl, 1 + jnz tek (en düşük bit)
if … elseiki dal: jmp yaz ile birleşen iki lea
printf("%d …", i)add dl, '0' + 02h hizmeti, ardından 09h hizmeti
return 0ret

C sürümü 15 satırdır ve her sayıyı yazabilir. 8086 sürümü 20 satırdır, ama yalnız tek basamaklı sayıları yazar ve n'yi okumaz. Çok basamaklı bir sayıyı yazmak için sayıyı art arda 10'a bölüp kalanları rakama çeviren bir döngü, klavyeden okumak için de rakamları birleştiren bir döngü gerekir. C'de printf ve scanf'in yaptığı iş budur. Yüksek seviyeli dilin bir satırı, assembly'de çoğu zaman bir yordam tutar. Karşılığında assembly programcısı her baytı ve her saat çevrimini denetler: 8086 programı 95 bayttır ve hiçbir kütüphaneye ihtiyaç duymaz.

3.1.10Sık yapılan hatalar

Sık yapılan hata

Adres ile içeriği karıştırmak. Bir dizgiyi ekrana yazdıran her hizmet dizginin adresini ister: DOS'ta DX, Linux'ta ECX, ARM'da r1, Windows'ta yığına itilen değer. Assembly'de mov dx, mesaj yazımı bazı assembler'larda adresi değil, o adresteki sözcüğü yükler. Adres için lea dx, mesaj ya da mov dx, offset mesaj kullanılır. C'deki scanf("%d", &number) çağrısında &'ın unutulması aynı hatanın yüksek seviyedeki biçimidir.

Sık yapılan hata

Dizgi sonu kuralını ortama göre seçmemek. DOS'un 09h hizmeti $, Windows API'si ve C kütüphanesi 0 baytı bekler; Linux'un write çağrısı ise sonlandırıcıya bakmaz, uzunluğu parametre olarak alır. Linux programındaki dizginin sonuna $ konursa bu karakter ekrana basılır; DOS programında 0 ile biten bir dizgi ise $ bulunana kadar taşar.

Sık yapılan hata

Veriyi kodun önüne koymak. COM programı ilk baytından çalışmaya başlar. org 100h'ın hemen arkasına bir db satırı konursa işlemci dizginin baytlarını komut olarak çözer. Veri ya kodun arkasına konur ya da kodun başında bir jmp ile verinin üstünden atlanır.

Sık yapılan hata

İşaretli ve işaretsiz atlamaları karıştırmak. Aynı cmp komutundan sonra ja/jb işaretsiz, jg/jl işaretli karşılaştırma yapar. Negatif olabilen bir sayaçla ja kullanılırsa −1 (FFFFh) en büyük işaretsiz sayı olarak görülür ve döngü yanlış dalda devam eder.

Sık yapılan hata

“Yorumlanan dilde hata yoktur” sanmak. Yorumlanan bir dilde program çalışmaya başlar, ama hiç çalışmayan bir daldaki yazım ya da tip hatası ancak o dal çalıştığında ortaya çıkar. Test sırasında her dalı en az bir kez çalıştıran girdiler seçilmelidir.

Sık yapılan hata

Çağrı kuralına uymamak. stdcall bir fonksiyonu çağırdıktan sonra çağıranın da add esp, 16 ile yığını temizlemesi, yığını iki kez temizlemek demektir. ESP fazladan 16 bayt yukarı kayar ve sonraki ret yanlış dönüş adresini alır. Hangi kuralın geçerli olduğu fonksiyonun belgesinden okunmalıdır.

3.1.11Alıştırmalar ve çözümleri

Sunumdaki alıştırmalar burada ara adımlarıyla çözülmüştür. İlk üç alıştırma Tanım 3.1.2'deki örnek komut biçimini kullanır: 4 bit işlem kodu, 2 bit hedef, 2 bit kaynak; HALT = 0000, ADD = 0001, SUB = 0010, AND = 0011, OR = 0100; R0 = 00, R1 = 01, R2 = 10, R3 = 11.

1. SUB R3, R0 ve OR R0, R2 komutlarının ikili ve onaltılık kodları nedir?

SUB'un kodu 0010'dır. Hedef R3 = 11, kaynak R0 = 00. Alanlar sırayla yazılınca 0010 1100 elde edilir. Dört bitlik gruplar onaltılık basamaklara çevrilir: 0010 = 2, 1100 = C. Sonuç 2Ch'dir. OR'un kodu 0100'dür; hedef R0 = 00, kaynak R2 = 10. Komut 0100 0010 = 42h'dir.

2. 0010 0111 hangi komuttur? R1 = 9, R3 = 4 ise sonuç nedir?

İlk dört bit 0010 = SUB. Kalan dört bit 01 11'dir: hedef R1, kaynak R3. Komut SUB R1, R3'tür ve R1 ← R1 − R3 yapar. R1 = 9 − 4 = 5 olur; R3 = 4 olarak kalır. İkili sağlama: 1001 − 0100 = 0101.

3. Bu tasarımda kaç farklı işlem kodu tanımlanabilir? Yazmaç sayısını 8'e çıkarmak için komut biçimi nasıl değişmeli?

İşlem kodu alanı 4 bittir: 24 = 16 işlem kodu. Sekiz yazmaçtan birini seçmek için log2 8 = 3 bit gerekir. Hedef ve kaynak alanları 3'er bite çıkar ve komut 4 + 3 + 3 = 10 bit olur. Komut 8 bitte tutulmak istenirse yazmaç alanları 6 bit kaplar ve işlem koduna 2 bit kalır. 2 bitle yalnız 22 = 4 işlem tanımlanabilir; tasarımdaki beş işlem sığmaz. Bu, komut uzunluğu, yazmaç sayısı ve komut sayısı arasındaki temel ödünleşimdir.

4. DOS programında dizgi sonundaki '$' unutulursa ne olur?

09h hizmeti DS:DX'ten başlayarak her baytı ekrana yazar ve 24h değerli bir bayt görünce durur. Sonlandırıcı yoksa dizginin arkasındaki bellek baytları da yazılır. Bu baytlar programın kendi kodu, başka veriler ya da rastgele değerler olabilir. Aralarındaki 07h baytı bip sesi, 0Dh ve 0Ah imleç hareketi üretir. Yazma, bellekte ilk $ karakterine rastlanana kadar sürer.

5. Linux örneğinde len elle 15 yazılsaydı ekrana ne çıkardı?

sys_write tam olarak EDX kadar bayt yazar. Dizginin ilk 15 baytı “Merhaba, Linux!” metnidir; 16. bayt satır sonudur. EDX = 15 ise metin yazılır ama satır sonu yazılmaz. Uçbirimin istem satırı metnin hemen arkasına, aynı satıra gelir. Dönüşte EAX = 15 olur.

6. Windows örneğinde parametreler neden ters sırada itilir? call'dan hemen sonra ESP kaçtır?

C çağrı kurallarında ilk parametre yığının tepesinde, dönüş adresinin hemen üstünde bulunmalıdır. Yığın aşağı doğru büyüdüğü için en son itilen değer en üstte kalır; bu yüzden parametreler sondan başa doğru itilir. Fonksiyon ilk parametreyi her zaman [ESP + 4]'te, ikinciyi [ESP + 8]'de bulur. Dört push ve call'ın ittiği dönüş adresi 5 × 4 = 20 bayt tutar. ESP = 0012FF80h − 14h = 0012FF6Ch olur.

7. Java neden “derlenen” ya da “yorumlanan” diye tek kelimeyle sınıflanamaz?

Java kaynak kodu javac ile derlenir, ama çıktı makine kodu değil bytecode'dur. Bu bytecode JVM'de önce yorumlanır; sık çalışan kısımları çalışma sırasında JIT ile makine koduna derlenir. Program hem önceden derlenir hem de çalışırken yorumlanır ve yeniden derlenir. Bu yüzden Java hibrit modelin örneğidir. Üstelik “derlenen/yorumlanan” ayrımı dilin değil gerçeklemenin özelliğidir: Java'yı doğrudan makine koduna önceden derleyen araçlar da vardır.

3.1.12Özet

  • Yazılım donanımı yönetir. İşletim sistemi temel yazılımdır; uygulamalar donanıma onun sistem çağrıları üzerinden ulaşır. Programlama dilleri yazılım geliştirmek için kullanılır.
  • Diller üç seviyededir: makine dili, assembly dili ve yüksek seviyeli diller. Seviye yükseldikçe okunabilirlik ve taşınabilirlik artar, donanım üzerindeki doğrudan denetim azalır.
  • Makine dili işlemcinin tek anladığı dildir: işlem kodu ve işlenen alanlarından oluşan ikili komutlar. Donanıma tamamen bağımlıdır; 8086'da B0 05, ARM'da E3A00005 aynı işi yapar. 8086 çok baytlı sayıları düşük bayt önce saklar.
  • Bir komut biçiminde k bitlik bir alan 2k seçenekten birini kodlar. Komut çözmek, bitleri alanlarına ayırıp her alanı tablodan okumaktır.
  • Assembly dili makine komutlarını anımsatıcılarla yazar. Assembler kaynak kodu nesne dosyasına, bağlayıcı nesne dosyalarını ve kütüphaneleri çalıştırılabilir dosyaya çevirir; yükleyici programı belleğe yerleştirir. İki geçişli assembler birinci geçişte sembol tablosunu doldurur, ikinci geçişte baytları üretir.
  • Assembly kodu hem işlemciye hem işletim sistemine bağlıdır: int 0x80, int 21h, DLL çağrısı, jsr, svc, sc. Hizmet numaraları, parametre yazmaçları ve dizgi sonu kuralları ortama göre değişir.
  • Derlenen diller (C, C++, Go, Rust, Fortran, Ada) önceden makine koduna çevrilir: hızlıdır ama çıktı platforma özgüdür. Yorumlanan diller (Python, Ruby, PHP, Perl, MATLAB) çalışırken işlenir: hızlı geliştirme ve taşınabilir kaynak sağlar, daha yavaş çalışır. Java ve C# ara kod + JIT ile ikisinin arasındadır; JavaScript motorları da JIT kullanır.
  • Ara kod, yazmaç kullanmayan bir yığın makinesinin dilidir; işlenenler yığından alınır, sonuç yığına konur.
  • Yüksek seviyedeki while, for ve if yapıları assembly'de karşılaştırma + koşullu atlama olarak gerçeklenir; koşul çoğunlukla tersine çevrilir. Değişkenin işaretli ya da işaretsiz olması, hangi atlama komutunun seçileceğini belirler.

3.1.13Kavram listesi

Terimlerin İngilizce karşılıkları ve öteki bölümlerdeki kullanımları terim sözlüğünde bulunur.

3.1.14Kendini Dene

  1. Örnek komut biçiminde 0, 1, 2 adreslerinde sırasıyla 13h, 2Ch ve 00h baytları duruyor. Başlangıçta R0 = 2, R1 = 0, R2 = 5, R3 = 9'dur. HALT'ta yazmaçlar kaçtır?

    Cevap

    13h = 0001 00 11: ADD R0, R3 → R0 = 2 + 9 = 11. 2Ch = 0010 11 00: SUB R3, R0 → R3 = 9 − 11 = −2; 8 bitlik yazmaçta ikiye tümleyen olarak 1111 1110 = FEh. 00h: HALT. Sonuç R0 = 11, R1 = 0, R2 = 5, R3 = FEh (−2). İkinci komut R0'ın birinci komuttan sonraki değerini kullanır.

  2. 8086'da mov ax, [0300h] komutu A1 00 03 diye kodlanır. 0300h adresinde 34, 0301h adresinde 12 baytları varsa AX kaç olur? Komut 0301h adresinden bir sözcük okusaydı veri yolu açısından ne değişirdi?

    Cevap

    Düşük bayt önce saklandığı için AX = 1234h olur. 0301h tek bir adrestir. 8086'nın 16 bitlik veri yolu bir çevrimde yalnız çift adresten başlayan bir sözcüğü okuyabilir. Tek adresteki sözcük iki ayrı bayt okumasıyla, yani iki veri yolu çevrimiyle gelir. Sonuç aynıdır, ama erişim en az 4 saat çevrimi uzar.

  3. İki geçişli bir assembler'da birinci geçiş neden komutların baytlarını üretmez? Etiketlerin hepsi kullanılmadan önce tanımlansaydı tek geçiş yeterli olur muydu?

    Cevap

    Birinci geçişte ileri başvuruların adresi henüz bilinmez; offset mesaj gibi bir işlenen kodlanamaz. Bu geçiş yalnız uzunlukları toplayıp sembol tablosunu doldurur. Bütün etiketler kullanılmadan önce tanımlansaydı her adres gerektiği anda bilinirdi ve tek geçiş yeterdi. Ancak atlama komutlarının çoğu ileriye, örneğin döngü sonuna gider; bu kısıt pratikte kullanılamaz.

  4. Bir DOS COM programında mesaj db 'Selam$' satırı 0110h adresinde başlıyor. lea dx, mesaj komutunun makine kodu ne olur? 09h hizmeti kaç karakter yazar?

    Cevap

    emu8086 doğrudan adresli lea dx, … komutunu mov dx, offset … gibi kodlar: BA ve little-endian adres, yani BA 10 01 (3 bayt). MASM'da aynı komut 8D 16 10 01 olurdu. 09h hizmeti “Selam” sözcüğünün 5 karakterini yazar; $ yazılmaz.

  5. 32 bitlik Linux'ta mov eax, 1 / mov ebx, 7 / int 0x80 dizisi ne yapar? Aynı iş 64 bitlik Linux'ta hangi yazmaçlarla ve hangi komutla yapılır?

    Cevap

    EAX = 1 sys_exit'tir; süreç 7 çıkış koduyla sonlanır. 64 bitlik Linux'ta çıkış hizmetinin numarası 60'tır ve RAX'e, çıkış kodu RDI'ya konur; çekirdeğe syscall komutuyla geçilir.

  6. stdcall kuralıyla iki parametreli bir fonksiyon çağrılmadan önce ESP = 0012FF40h'dir. Fonksiyona girildiği anda ESP kaçtır ve [ESP + 8] neyi tutar? Fonksiyon hangi komutla dönmelidir?

    Cevap

    İki push ve dönüş adresi 12 bayttır: ESP = 0012FF40h − 0Ch = 0012FF34h. [ESP] dönüş adresi, [ESP + 4] ilk parametre, [ESP + 8] ikinci parametredir. Fonksiyon parametrelerin 8 baytını kendisi atmak için ret 8 ile döner; dönüşte ESP yeniden 0012FF40h olur.

  7. Yığın makinesinde a * (b − c) ifadesinin ara kodu nasıl olur? a = 4, b = 10, c = 3 için yığının en yüksek derinliği kaç olur?

    Cevap

    iload a, iload b, iload c, isub, imul, istore x. Yığın sırasıyla [4], [4, 10], [4, 10, 3], [4, 7], [28], [ ] olur. En yüksek derinlik 3'tür; sonuç x = 28.

  8. 8086 tek/çift programında cmp cx, 3 yerine cmp cx, 5 yazılıyor ve ja yerine yanlışlıkla jae kullanılıyor. Program hangi sayıları yazar?

    Cevap

    jae CF = 0 iken, yani CX ≥ 5 olduğunda atlar. CX = 1, 2, 3, 4 için CF = 1'dir ve gövde çalışır; CX = 5'te 5 − 5 = 0, CF = 0 olur ve döngüden çıkılır. Program 1'den 4'e kadar yazar; 5 eksik kalır. Doğrusu ja'dır: eşitlikte de gövde çalışmalıdır.

  9. Bir Python programı for i in range(1, n): ile yazıldığında n = 4 için hangi değerleri dolaşır? Aynı döngü Ruby'de 1..n ve 1...n ile yazılırsa ne olur?

    Cevap

    Python 1, 2, 3 değerlerini dolaşır; üst sınır dahil değildir. Ruby'de 1..4 1, 2, 3, 4'ü, 1...4 ise 1, 2, 3'ü dolaşır. İki nokta üst sınırı içerir, üç nokta dışarıda bırakır.

3.1.15İleri okuma

  • K. R. Irvine, Assembly Language for x86 Processors (7. baskı), Bölüm 1–3: dil seviyeleri, assembler ve bağlayıcı, liste çıktısı ve x86'da ilk programlar; bu notun assembly kısımlarına en yakın kaynaktır.
  • B. B. Brey, The Intel Microprocessors (8. baskı), Bölüm 1 ve 4: makine dili, 8086 komutlarının kodlanması, ModR/M baytı ve COM/EXE programlarının yapısı.
  • A. S. Tanenbaum ve H. Bos, Modern Operating Systems (3. baskı), Bölüm 1.6: sistem çağrılarının işleyişi, kullanıcı ve çekirdek modları.
  • J. R. Levine, Linkers and Loaders, Morgan Kaufmann: nesne dosyaları, sembol çözümleme, yer değiştirme ve yükleme sürecini ayrıntılı anlatır.

3.1.16Kaynaklar

  1. A. S. Tanenbaum, H. Bos, Modern Operating Systems, 3. baskı, Pearson.
  2. B. B. Brey, The Intel Microprocessors, 8. baskı, Pearson.
  3. K. R. Irvine, Assembly Language for x86 Processors, 7. baskı, Pearson.
  4. NASM belgeleri (nasm.us) ve GNU Binutils as kılavuzu.
  5. Ralf Brown's Interrupt List (DOS INT 21h hizmetleri); Microsoft Win32 API belgeleri (MessageBoxA, ExitProcess).
  6. emu8086 belgeleri ve 8086 komut kümesi başvurusu.
  7. J. R. Levine, Linkers and Loaders, Morgan Kaufmann, 1999.

İlgili sayfalar: Bölüm 3.1 sunumu · Çalışma soruları · Terim sözlüğü · © Sercan Külcü. Tüm hakları saklıdır.