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 02gibi 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;
forveifyapı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.
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.
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 yazBurada 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], AXHer 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.
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 ; C3org 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.
| Adım | Evre | Komut baytları | IP | AX | 0204h–0205h (x) |
|---|---|---|---|---|---|
| 1 | getir | A1 00 02 | 0100h → 0103h | ???? | ?? ?? |
| 2 | yürüt | 0200h'den sözcük oku: 15 00 | 0103h | 0015h | ?? ?? |
| 3 | getir | 03 06 02 02 | 0103h → 0107h | 0015h | ?? ?? |
| 4 | yürüt | 0202h'den 27 00; 15h + 27h | 0107h | 003Ch | ?? ?? |
| 5 | getir | A3 04 02 | 0107h → 010Ah | 003Ch | ?? ?? |
| 6 | yürüt | AX'i 0204h'ye yaz | 010Ah | 003Ch | 3C 00 |
| 7 | getir, yürüt | C3 (RET) | 010Ah → dönüş | 003Ch | 3C 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.
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.
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.
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.
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.
| İşlem kodu | Anımsatıcı | Anlamı |
|---|---|---|
0000 | HALT | programı durdur |
0001 | ADD | hedef ← hedef + kaynak |
0010 | SUB | hedef ← hedef − kaynak |
0011 | AND | hedef ← hedef VE kaynak (bit bit) |
0100 | OR | hedef ← 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.
| Makine kodu | Onaltılık | İşlem | Hedef | Kaynak | Anlamı |
|---|---|---|---|---|---|
0001 0001 | 11h | ADD | R0 | R1 | R0 ← R0 + R1 |
0011 1011 | 3Bh | AND | R2 | R3 | R2 ← R2 VE R3 |
0100 0110 | 46h | OR | R1 | R2 | R1 ← R1 VEYA R2 |
0000 0000 | 00h | HALT | — | — | 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.
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 ; HALTBaş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.
| Adım | Komut | PC | R0 | R1 | R2 | R3 |
|---|---|---|---|---|---|---|
| başlangıç | — | 0 | 0000 0101 (5) | 0000 0011 (3) | 0000 1100 (12) | 0000 1010 (10) |
| 1 | ADD R0, R1 | 1 | 0000 1000 (8) | 0000 0011 (3) | 0000 1100 (12) | 0000 1010 (10) |
| 2 | AND R2, R3 | 2 | 0000 1000 (8) | 0000 0011 (3) | 0000 1000 (8) | 0000 1010 (10) |
| 3 | OR R1, R2 | 3 | 0000 1000 (8) | 0000 1011 (11) | 0000 1000 (8) | 0000 1010 (10) |
| 4 | HALT | 3 | 0000 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.
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.
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, 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.
- Assembler
- Kaynak dosyayı (
prog.asm) okur ve her satırı makine koduna çevirir. Çıktısı bir nesne dosyasıdır (object file,prog.objya daprog.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.
İ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!$'| Geçiş | Satır | Uzunluk | LC (satırdan sonra) | Sembol tablosu | Üretilen baytlar |
|---|---|---|---|---|---|
| 1 | org 100h | 0 | 0100h | mesaj = ? | — |
| 1 | mov ah, 09h | 2 | 0102h | mesaj = ? | — |
| 1 | mov dx, offset mesaj | 3 | 0105h | mesaj = ? | — |
| 1 | int 21h, mov ax, 4C00h, int 21h | 2 + 3 + 2 | 010Ch | mesaj = ? | — |
| 1 | mesaj db 'Merhaba!$' | 9 | 0115h | mesaj = 010Ch | — |
| 2 | mov ah, 09h | 2 | 0102h | mesaj = 010Ch | B4 09 |
| 2 | mov dx, offset mesaj | 3 | 0105h | mesaj = 010Ch | BA 0C 01 |
| 2 | üç satır | 7 | 010Ch | mesaj = 010Ch | CD 21 B8 00 4C CD 21 |
| 2 | mesaj db … | 9 | 0115h | mesaj = 010Ch | 4D 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.
İ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.
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.
| Assembler | Söz dizimi | Kullanım |
|---|---|---|
| MASM | Intel | Microsoft, Windows |
| TASM | Intel | Borland, DOS |
| NASM | Intel | açık kaynak, çok platformlu |
| GAS (as) | AT&T | GNU/Linux, gcc'nin kullandığı assembler |
| emu8086 | Intel (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), %eaxAT&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.
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 0x80Program 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.
| Satır | EAX | EBX | ECX | EDX | Ekran |
|---|---|---|---|---|---|
mov eax, 4 | 00000004 | ???????? | ???????? | ???????? | |
mov ebx, 1 | 00000004 | 00000001 | ???????? | ???????? | |
mov ecx, hello | 00000004 | 00000001 | hello (adres) | ???????? | |
mov edx, len | 00000004 | 00000001 | hello (adres) | 00000010 | |
int 0x80 | 00000010 | 00000001 | hello (adres) | 00000010 | Merhaba, Linux! |
mov eax, 1 | 00000001 | 00000001 | hello (adres) | 00000010 | Merhaba, Linux! |
xor ebx, ebx | 00000001 | 00000000 | hello (adres) | 00000010 | Merhaba, Linux! |
int 0x80 | sü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.
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.
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.
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.
| Ortam | Çağrı | Hizmet numarası | Parametreler |
|---|---|---|---|
| Linux 32 bit | int 0x80 | EAX (4 = write, 1 = exit) | EBX, ECX, EDX |
| Linux 64 bit | syscall | RAX (1 = write, 60 = exit) | RDI, RSI, RDX |
| MS-DOS | int 21h | AH (09h = yaz, 4Ch = çık) | DX, AL … |
| BIOS | int 10h | AH (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.
| Satır | Makine kodu | IP (sonra) | AH | AL | DX | Ekran |
|---|---|---|---|---|---|---|
org 100h | — | 0100h | ?? | ?? | ???? | |
mov ah, 09h | B4 09 | 0102h | 09h | ?? | ???? | |
lea dx, mesaj | BA 0C 01 | 0105h | 09h | ?? | 010Ch | |
int 21h | CD 21 | 0107h | 09h | ?? | 010Ch | Merhaba, DOS! |
mov ax, 4C00h | B8 00 4C | 010Ah | 4Ch | 00h | 010Ch | Merhaba, DOS! |
int 21h | CD 21 | 010Ch | 4Ch | 00h | 010Ch | program 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ı.
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 ExitProcessextern 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.
| Komut | ESP (sonra) | Yazılan adres ← değer | EAX |
|---|---|---|---|
| başlangıç | 0012FF80h | — | ???????? |
push 0 (uType) | 0012FF7Ch | 0012FF7Ch ← 0 (MB_OK) | ???????? |
push mesaj (lpCaption) | 0012FF78h | 0012FF78h ← &mesaj | ???????? |
push mesaj (lpText) | 0012FF74h | 0012FF74h ← &mesaj | ???????? |
push 0 (hWnd) | 0012FF70h | 0012FF70h ← 0 (NULL) | ???????? |
call MessageBoxA | 0012FF6Ch | 0012FF6Ch ← dönüş adresi | ???????? |
fonksiyon ret 16 ile döner | 0012FF80h | — | 00000001h (IDOK) |
push 0 (uExitCode) | 0012FF7Ch | 0012FF7Ch ← 0 | 00000001h |
call ExitProcess | sü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.
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!',10AmigaOS'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 #0Program 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
scPowerPC'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.
| Ortam | İşlemci | İşletim sistemine geçiş | Hizmet no. | Parametreler | Dizgi sonu |
|---|---|---|---|---|---|
| Linux | x86 (32) | int 0x80 | EAX | EBX, ECX, EDX | uzunluk verilir |
| DOS | 8086 | int 21h | AH | DX (adres) | '$' |
| Windows | x86 (32) | call ile DLL | fonksiyon adı | yığın | 0 |
| AmigaOS | 68000 | jsr -48(a6) | kütüphane uzaklığı | d1, d2, d3 | uzunluk verilir |
| Linux | ARM | svc #0 | r7 | r0, r1, r2 | uzunluk verilir |
| Linux | PowerPC | sc | r0 | r3, r4, r5 | uzunluk 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.
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.
İ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.
- Önişlemci:
#includesatırlarının yerine ilgili başlık dosyalarının içeriğini koyar,#defineile tanımlanmış adları açar. Çıktı yine C kodudur:tekcift.i(gcc -E). 15 satırlık program,stdio.heklenince binlerce satıra çıkar; bu not hazırlanırken kullanılan clang tabanlı derleyicide 1.543 satır oldu. - 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. - Assembler: assembly kodunu makine koduna çevirip nesne dosyası üretir:
tekcift.o(gcc -c). Bu dosyadaprintfvescanfçağrılarının adresleri henüz boştur. - Bağlayıcı: nesne dosyasını C kütüphanesindeki
printf,scanfgibi kodlarla birleştirir ve çalıştırılabilir dosyayı üretir: Linux'tatekcift, Windows'tatekcift.exe. - 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
mainfonksiyonuna 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.
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.
| Adım | Satırlar | BX (toplam) | CX (i) | ZF | SF | OF | Karar |
|---|---|---|---|---|---|---|---|
| 1 | mov bx, 0, mov cx, 10 | 0 | 10 | – | – | – | MOV bayrakları değiştirmez |
| 2 | cmp cx, 0, jle | 0 | 10 | 0 | 0 | 0 | 10 > 0, atlamaz |
| 3 | gövde, jmp | 10 | 7 | 0 | 0 | 0 | koşula dön |
| 4 | cmp, jle | 10 | 7 | 0 | 0 | 0 | atlamaz |
| 5 | gövde, jmp | 17 | 4 | 0 | 0 | 0 | koşula dön |
| 6 | cmp, jle | 17 | 4 | 0 | 0 | 0 | atlamaz |
| 7 | gövde, jmp | 21 | 1 | 0 | 0 | 0 | koşula dön |
| 8 | cmp, jle | 21 | 1 | 0 | 0 | 0 | atlamaz |
| 9 | gövde, jmp | 22 | −2 (FFFEh) | 0 | 1 | 0 | koşula dön |
| 10 | cmp, jle | 22 | −2 (FFFEh) | 0 | 1 | 0 | SF ≠ OF, atlar |
| 11 | mov toplam, bx | 22 | −2 (FFFEh) | 0 | 1 | 0 | toplam = 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.
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.
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.
| Adım | Satır | number | i | i % 2 | Eklenen çıktı |
|---|---|---|---|---|---|
| 1 | printf | ? | – | – | Bir sayi girin: |
| 2 | scanf | 3 | – | – | (kullanıcı 3 yazar) |
| 3 | for: 1 ≤ 3 | 3 | 1 | – | |
| 4 | if → else | 3 | 1 | 1 | 1 sayisi bir tek sayidir. |
| 5 | i++; 2 ≤ 3 | 3 | 2 | – | |
| 6 | if doğru | 3 | 2 | 0 | 2 sayisi bir cift sayidir. |
| 7 | i++; 3 ≤ 3 | 3 | 3 | – | |
| 8 | if → else | 3 | 3 | 1 | 3 sayisi bir tek sayidir. |
| 9 | i++; 4 ≤ 3 yanlış | 3 | 4 | – | döngü biter |
| 10 | return 0 | 3 | – | – | çı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 MainFortran (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.
Ş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
endRuby (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
endMATLAB (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.
| Dil | Model | Döngü başlığı | Kalan | Yazdırma |
|---|---|---|---|---|
| C | derlenen | for (int i = 1; i <= n; i++) | i % 2 | printf |
| C++ | derlenen | for (int i = 1; i <= n; i++) | i % 2 | std::cout << |
| C# | IL + JIT | for (int i = 1; i <= n; i++) | i % 2 | Console.WriteLine |
| Java | bytecode + JIT | for (int i = 1; i <= n; i++) | i % 2 | System.out.println |
| Go | derlenen | for i := 1; i <= n; i++ | i%2 | fmt.Printf |
| Rust | derlenen | for i in 1..=n | i % 2 | println! |
| Fortran | derlenen | do i = 1, n | mod(i, 2) | write(*,*) |
| Ada | derlenen | for I in 1 .. N loop | I mod 2 | Put_Line |
| Python | yorumlanan | for i in range(1, n + 1): | i % 2 | print |
| JavaScript | yorumlanan/JIT | for (let i = 1; i <= n; i++) | i % 2 | console.log |
| Ruby | yorumlanan | for i in 1..n do | i % 2 | puts |
| PHP | yorumlanan | for ($i = 1; $i <= $n; $i++) | $i % 2 | echo |
| Perl | yorumlanan | for my $i (1..$n) | $i % 2 | print |
| MATLAB | yorumlanan/JIT | for i = 1:n | mod(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.
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).
| Ölçüt | Derlenen | Yorumlanan | Hibrit (bytecode + JIT) |
|---|---|---|---|
| Çeviri zamanı | çalışmadan önce, bir kez | her çalıştırmada | önce ara koda, sonra çalışırken |
| Hız | en yüksek | en düşük | yüksek (ısındıktan sonra) |
| Dağıtılan | makine kodu | kaynak kod | ara kod (.class, .dll) |
| Taşınabilirlik | her platform için yeniden derlenir | yorumlayıcı olan her yerde | sanal makine olan her yerde |
| Hata yakalama | çoğu derlemede | çoğu çalışırken | tip hataları derlemede |
| Örnek | C, C++, Go, Rust, Fortran, Ada | Python, Ruby, PHP, Perl, MATLAB | Java, 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.
“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
returnYerel değişkenler a = 6, b = 7, c = 3 olsun. Tablo 3.1.16 işlenen yığınını alttan üste doğru yazar.
| Komut | İşlem | İşlenen yığını (alt → üst) | x |
|---|---|---|---|
iload a | a'yı it | [6] | ? |
iload b | b'yi it | [6, 7] | ? |
iadd | 7 ve 6'yı çek, 13'ü it | [13] | ? |
iload c | c'yi it | [13, 3] | ? |
imul | 3 ve 13'ü çek, 39'u it | [39] | ? |
istore x | üstü çek, x'e yaz | [ ] | 39 |
return | dö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.
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: ireturnSoldaki 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.
| Adres | Bayt | Satır |
|---|---|---|
| 0100h | 3 | mov cx, 1 |
| 0103h | 3 | dongu: cmp cx, 3 |
| 0106h | 2 | ja bitti |
| 0108h | 2 | mov dl, cl |
| 010Ah | 3 | add dl, '0' |
| 010Dh | 2 + 2 | mov ah, 02h, int 21h |
| 0111h | 3 | test cl, 1 |
| 0114h | 2 | jnz tek |
| 0116h | 3 | lea dx, cift_m |
| 0119h | 2 | jmp yaz |
| 011Bh | 3 | tek: lea dx, tek_m |
| 011Eh | 2 + 2 | yaz: mov ah, 09h, int 21h |
| 0122h | 1 | inc cx |
| 0123h | 2 | jmp dongu |
| 0125h | 1 | bitti: ret |
| 0126h | 27 | tek_m db … (24 karakter + 0Dh + 0Ah + '$') |
| 0141h | 28 | cift_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.
| Adım | Satırlar | CX | DL / DX | ZF | CF | Olay |
|---|---|---|---|---|---|---|
| 1 | mov cx, 1 | 0001h | – | – | – | i = 1 |
| 2 | cmp cx, 3, ja | 0001h | – | 0 | 1 | 1 − 3 ödünç ister; atlamaz |
| 3 | mov dl, cl … int 21h | 0001h | DL = 31h | 0 | 0 | “1” yazılır |
| 4 | test cl, 1, jnz | 0001h | DL = 31h | 0 | 0 | 1 VE 1 = 1; atlar (tek) |
| 5 | lea dx, tek_m … int 21h | 0001h | DX = 0126h | 0 | 0 | “ sayisi bir tek sayidir.” |
| 6 | inc cx, jmp | 0002h | 0126h | 0 | 0 | i = 2 |
| 7 | cmp cx, 3, ja | 0002h | 0126h | 0 | 1 | atlamaz |
| 8 | mov dl, cl … int 21h | 0002h | DL = 32h | 0 | 0 | “2” yazılır |
| 9 | test cl, 1, jnz | 0002h | DL = 32h | 1 | 0 | 2 VE 1 = 0; atlamaz (çift) |
| 10 | lea dx, cift_m, jmp yaz | 0002h | DX = 0141h | 1 | 0 | ortak yazdırmaya git |
| 11 | mov ah, 09h, int 21h | 0002h | DX = 0141h | 1 | 0 | “ sayisi bir cift sayidir.” |
| 12 | inc cx, jmp | 0003h | 0141h | 0 | 0 | i = 3 |
| 13 | cmp cx, 3, ja | 0003h | 0141h | 1 | 0 | 3 − 3 = 0; ZF = 1, atlamaz |
| 14 | mov dl, cl … jnz | 0003h | DL = 33h | 0 | 0 | “3” yazılır; tek |
| 15 | lea dx, tek_m … int 21h | 0003h | DX = 0126h | 0 | 0 | “ sayisi bir tek sayidir.” |
| 16 | inc cx, jmp | 0004h | 0126h | 0 | 0 | i = 4 |
| 17 | cmp cx, 3, ja | 0004h | 0126h | 0 | 0 | 4 − 3 = 1; atlar |
| 18 | bitti: ret | 0004h | 0126h | 0 | 0 | DOS'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.
| C | 8086 |
|---|---|
int i = 1 | mov cx, 1 (değişken yerine yazmaç) |
i <= number | cmp cx, 3 + ja bitti (tersine çevrilmiş koşul) |
i++ | inc cx + jmp dongu |
i % 2 == 0 | test cl, 1 + jnz tek (en düşük bit) |
if … else | iki dal: jmp yaz ile birleşen iki lea |
printf("%d …", i) | add dl, '0' + 02h hizmeti, ardından 09h hizmeti |
return 0 | ret |
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
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.
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.
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.
İş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.
“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.
Ç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'daE3A00005aynı 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,forveifyapı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.
- yazılım software
- işletim sistemi operating system
- yüksek / düşük seviyeli dil high-level / low-level language
- makine dili machine language
- makine kodu machine code
- assembly (assembly dili) assembly language
- anımsatıcı mnemonic
- komut kümesi instruction set
- komut biçimi instruction format
- işlem kodu opcode
- işlenen operand
- kodlayıcı / çözücü encoder / decoder
- kaynak kod source code
- assembler assembler
- nesne dosyası object file
- bağlayıcı linker
- yükleyici loader
- çalıştırılabilir dosya executable file
- liste (listing) çıktısı listing file
- direktif directive
- konum sayacı ($) location counter
- sembol tablosu symbol table
- söz dizimi (Intel / AT&T) syntax
- sistem çağrısı system call
- little-endian (düşük bayt önce) little-endian
- dizgi string
- çağrı kuralı calling convention
- derleyici compiler
- yorumlayıcı interpreter
- ara kod bytecode / intermediate language
- sanal makine virtual machine
- tam zamanında derleme just-in-time (JIT) compilation
- yığın makinesi stack machine
- gerçekleme implementation
3.1.14Kendini Dene
Ö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.
8086'da
mov ax, [0300h]komutuA1 00 03diye kodlanır. 0300h adresinde34, 0301h adresinde12baytları 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.
İ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 mesajgibi 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.Bir DOS COM programında
mesaj db 'Selam$'satırı 0110h adresinde başlıyor.lea dx, mesajkomutunun makine kodu ne olur? 09h hizmeti kaç karakter yazar?Cevap
emu8086 doğrudan adresli
lea dx, …komutunumov dx, offset …gibi kodlar:BAve little-endian adres, yaniBA 10 01(3 bayt). MASM'da aynı komut8D 16 10 01olurdu. 09h hizmeti “Selam” sözcüğünün 5 karakterini yazar;$yazılmaz.32 bitlik Linux'ta
mov eax, 1/mov ebx, 7/int 0x80dizisi 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ğesyscallkomutuyla geçilir.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
pushve 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çinret 8ile döner; dönüşte ESP yeniden 0012FF40h olur.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.8086 tek/çift programında
cmp cx, 3yerinecmp cx, 5yazılıyor vejayerine yanlışlıklajaekullanılıyor. Program hangi sayıları yazar?Cevap
jaeCF = 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ğrusuja'dır: eşitlikte de gövde çalışmalıdır.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'de1..nve1...nile yazılırsa ne olur?Cevap
Python 1, 2, 3 değerlerini dolaşır; üst sınır dahil değildir. Ruby'de
1..41, 2, 3, 4'ü,1...4ise 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
- A. S. Tanenbaum, H. Bos, Modern Operating Systems, 3. baskı, Pearson.
- B. B. Brey, The Intel Microprocessors, 8. baskı, Pearson.
- K. R. Irvine, Assembly Language for x86 Processors, 7. baskı, Pearson.
- NASM belgeleri (nasm.us) ve GNU Binutils as kılavuzu.
- Ralf Brown's Interrupt List (DOS INT 21h hizmetleri); Microsoft Win32 API belgeleri (
MessageBoxA,ExitProcess). - emu8086 belgeleri ve 8086 komut kümesi başvurusu.
- 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.