Korumalı Mod Nedir?

Linux Boot Sequence

Korumalı mod proseslerin bir arada çalıştığı çok prosesli sistemlerde sistem güvenliğini artırmak için düşünülmüştür. Intel’in koruma mekanizmasında dört öncelik derecesi vardır. Ancak işletim sistemleri genellikle yalnızca iki dereceyi kullanmaktadır: 0 ve 3. Intel sisteminde düşük numara daha yüksek, yüksek numara ise daha düşük öncelik belirtmektedir.

  • 0 önceliğine "çekirdek modu (kernel mode)" önceliği, 
  • 3 önceliğine de "kullanıcı modu (user mode)" önceliği denilmektedir. 

32 bit Windows ve Linux gibi sistemler düz model (flat model) kullanmaktadır. Bu modelde tüm segment yazmaçlarının gösterdiği betimleyicilerin taban adresleri 0 ve limit değerleri de 4 GB’dir. Düz modelde segment tabanlı bir koruma uygulanmamaktadır. Yani bu modelde bir prosesin CS yazmacı dışındaki segment yazmaçlarının değerlerini değiştirmesi için bir gerekçe yoktur. Zaten düz model uygulayan sistemlerde genellikle tüm kullanıcı mod programları için tek bir kod ve data/stack betimleyicisi kullanılmaktadır. Bu betimleyicilerle de terorik olarak belleğin her yerine erişilebilmektedir. Korumalı modda o anda çalışmakta olan kodun öncelik derecesi CS yazmacının düşük anlamlı iki bitiyle belirlenmektedir. Bu bitlere CPL (Current Privilege Level) denir. Intel’de özel makine komutlarını ancak CPL değeri 0 olan kodlar kullanabilirler. Böylece sıradan prosesler CPL = 3 değeriyle çalıştıkları için bu makine komutlarını kullanamamaktadır. Ayrıca CPL değeri sayfalama mekanizması aktifken bellekte sayfalara erişirken de kontrol işlemlerine sokulmaktadır. Şöyle ki: Her sayfanın iki öncelik derecesi vardır. Önceliklerden birine "kullanıcı (user)", diğerine ise  "yönetici (supervisor)" önceliği denir. CPL değeri 1, 2 ve 3 olan kodlar ancak kullanıcı önceliğindeki sayfalara erişebilirler. CPL değeri 0 olan kodlar ise tüm sayfalara erişebilmektedir. İşletim sisteminin çekirdek kodları "yönetici (supervisor)" önceliğindeki sayfalarda tutulur. Böylece bu alanlara yalnızca işletim sisteminin kodları erişebilmektedir. 



Korumalı modda programcı DS, ES, SS, FS ve GS segment yazmaçlarındaki değerleri MOV komutlarıyla değiştirmek isterse bazı kontroller uygulanmaktadır. Özet olarak programcı CPL değerinden daha yüksek önceliğe sahip bir betimleyiciyi gösteren selektörü bu segment yazmaçlarına yükleyememektedir. Zaten yukarıda da ifade ettiğimiz gibi Windows, Linux gibi sistemler "düz bellek modeli (flat model)"  kullanmaktadır. Düz bellek modelinde de DS, ES, SS, FS ve GS segment yazmaçlarını yüklemek istemenin pratikte bir amacı yoktur. Korumalı modda çalışmakta olan kodun önceliğinin yükseltilmesi uzak CALL ve JMP işlemleriyle yapılamamaktadır. Bunu yapmanın tek yolu "kapı (gate)" denilen özel bir mekanizmayı kullanmaktır. Düşük öncelikli kodlar kapı ile belirtilen adresteki kodları yüksek öncelikle çalıştırabilmektedir.

İşletim Sisteminin Sistem Fonksiyonları ve Kapılar

Korumalı modda çalışan Windows, Linux ve MacOS X gibi işletim sistemlerinde sıradan proseslerin kodları CPL = 3 önceliğinde çalışmaktadır. Bu kodlar işletim sisteminin yüksek öncelikle çalışması gereken sistem fonksiyonlarını kapılar yoluyla çağırırlar.



Böylece işletim sisteminin sistem fonksiyonları çalışırken kodun önceliği CPL = 0'a yükseltilmiş olur. Bu sürece "prosesin kullanıcı modundan çekirdek moduna geçmesi (user mode to kernel mode transition)" denilmektedir. Yani bu sistemlerde bizim programlarımız aslında sürekli olarak CPL = 3 ile kullanıcı modunda çalışmamaktadır. Sistem fonksiyonları ya da aygıt sürücülerdeki kodlar çağrıldığında programımızın öncelik seviyesi geçici olarak CPL = 0'a yükseltilmektedir. İşte kapılar Intel işlemcilerindeki bu geçişi sağlayan mekanizmalardır. Linux, BSD ve MacOS X sistemlerinde sistem fonksiyonları geleneksel olarak 80h kesmesi yoluyla çağrılmaktadır. (Yeni sistemler 64 bit Intel işlemcilerindeki SYSENTER ve SYSEXIT makine komutlarını da bu amaçla kullanabiliyorlar.) Bu 80h kesmesi bir tuzak kapısını tetikler. Bu kapı da kodun önceliğini CPL = 0’a çekerek kodun işletim sisteminin belirlediği bir noktaya aktarılmasını sağlar. İşte o noktada çağrılan sistem fonksiyonunun numarasına göre akış ilgili sistem fonksiyonun koduna aktarılmaktadır. Örneğin Linux sistemlerinde sistem fonksiyonu 80h kesmesi ile çağrılmadan önce onun numarası EAX yazmacına yerleştirilir. Böylece akış çekirdek moduna geçtiğinde buradaki kod EAX yazmacının değerine bakarak akışı uygun yere aktarır. Bu süreci aşağıdaki kodla temsil edebiliriz:

SYS_ENTER:     // kapıya girildiğinde akışın aktarıldığı yer. Artık kod için CPL = 0'dır
switch (eax) {
case 1:
sys_exit();
break;
case 2:
sys_fork();
break;
case 3:
sys_read();
break;
...
}

Tabii bu sözde kodu (pseudo code) yalnızca kafamızda bir fikir oluşsun diye verdik. Aslında Linux'ta uygun sistem fonksiyonuna dallanma işlemi EAX yazmacı switch içerisine sokularak değil bir diziye index yapılarak bir "look up" tablosu yoluyla gerçekleştirilmektedir. Yani bu sistemlerde sistem fonksiyonlarının adresleri bir dizide tutulmaktadır. Sistem fonksiyonlarının numarası da (EAX yazmacı içerisindeki değer) bu diziye indeks yapılarak dolaylı CALL işlemi ile çağrılmaktadır. Linux sistemleriyle BSD ve MacOS X arasında sistem fonksiyonlarının çağrılması arasında küçük bir farklılık vardır. Linux’ta sistem fonksiyonlarının parametreleri yazmaçlarla aktarılırken BSD ve MacOS X sistemlerinde (C’deki gibi) stack yoluyla aktarım yapılmaktadır. (Ayrıca Linux sistemlerindeki sistem fonksiyonlarının numaralarının ve parametrik yapılarının BSD ve MacOS X sistemleriyle bire bir aynı olduğunu da düşünmemelisiniz.) Windows sistemlerinde ise çekirdek moduna geçiş genel olarak 2EH kesmesiyle yapılmaktadır. Fakat genel mekanizma Linux, BSD ve Mac OS X sistemlerine oldukça benzemektedir.

32 Bit Intel İşlemcilerinde Komut Kalıpları

06:58
İşlemcilerdeki yazmaç ve bellek kullanım kalıplarına adresleme modları (addressing mode) denilmektedir. Intel’in 32 bit işlemcilerinde komut kalıplarını aşağıda maddeler halinde tek tek ele alacağız. Intel sisteminde komutlar çoğunlukla iki operandlıdır. Ayrıca örneklerimizde pek çok assembly derleyicisinin kabul ettiği gibi komutların hedef operandları sol taraftaki operandla belirtilmektedir. Aşağıda bu syntax'a (Intel Syntax) uygun bir kod parçası görülmektedir.

Intel Syntax
Aşağıda ise bunun tam tersi (AT&T Syntax) hedef operand sağ taraftaki operandlı bir kod parçası görülmektedir.

AT&T Syntax

Sembolik makine dillerinde genel olarak komutlarda belirtilen işlemlerin kaç bit düzeyinde yapılacağı eğer komutta yazmaç varsa yazmacın uzunluğuyla tespit edilir. Ancak komutta yazmaç yoksa işlemlerin kaç bit düzeyinde yapılacağı onların yanına getirilen qword, dword, word, byte gibi anahtar sözcüklerle belirlenmektedir. Tabii bu anahtar sözcükler assembly derleyicisinden derleyicisine farklılık gösterebilmektedir. 32 Bit Intel İşlemcilerinin komut kalıpları özet olarak şöyledir:
  1. Yazmaçlarla + sabitler 
  2. Yazmaçlarla + yazmaçlar 
  3. Yazmaçlarla + bellekteki değerler
  4. Bellekteki değerlerle + sabitler
Bellek operandı genellikle sembolik makine dillerinde köşeli parantezlerle gösterilmektedir. Bellek operandı herhangi bir biçimde oluşturulamaz. Köşeli parantezlerin içerisine nelerin yazılabileceği önceden belirlenmiştir. Şimdi komut kalıplarını daha ayrıntılı olarak tek tek ele alalım:

1) Yazmaçlarla sabitler işleme sokulabilirler. Örneğin:

ADD EAX, 100
ADD AH, 20
MOV EAX, 512

Sembolik makine dillerinde doğrudan yazılan sayılara kavram olarak "Immediate, Constant" ya da "literal" kelimeleri de kullanılmaktadır.

2) Yazmaçlarla yazmaçlar işleme sokulabilirler. Ancak yazmaç - uzunluk uyumunun sağlanmış olması gerekir. Örneğin:

ADD EAX, EBX
SUB AX, CX
XOR AH, BL

Örneğin aşağıdaki komutlar yazmaç uzunlukları aynı olmadığı için geçersizdir:
ADD EAX, BX
XOR BX, AL

3) Yazmaçlarla köşeli parantez içerisindeki sabit değerler işleme sokulabilirler (Burada sabit değerler hafıza alanında bir adres belirtmektedir). Genel olarak bütün işlemciler bellekteki değerlere onların adreslerini alarak erişirler. Sembolik makine dillerinin çoğunda bir bellek adresindeki değerler köşeli parantezlerle belirtilmektedir. Bu durumda o yazmaçtaki değer ile köşeli parantez içerisindeki bellek adresinden başlayan değer işleme sokulmuş olur. İşlemin kaç bit düzeyinde yapılacağı yazmacın uzunluğuna bağlıdır. Örneğin:

ADD EAX, [1FC14A]

Burada EAX yazmacındaki değer ile belleğin 1FC14A adresinden başlayan 32 bitlik değer toplanmıştır. Sonuç EAX yazmacındaki değer bozularak oraya aktarılmaktadır. Örneğin:

MOV BX, [1FC14A]

Burada BX’e 1FC14A adresinden başlayan 16 bit değer aktarılmaktadır. Örneğin:

ADD AH, [1FC14A]

Burada AH içerisindeki değerle 1FC14A adresinden başlayan 8 bitlik değer toplanmış, sonuç yine AH’ya atanmıştır. Örneğin:

ADD [1FC14A], EAX

Burada EAX yazmacı ile bellekte 1FC14A adresinden başlayan 32 bit değer işleme sokulmuştur. Fakat sonuç belleğin yine 1FC14A adresinden itibaren 32 biti etkileyecek biçimde aktarılmaktadır. Kalıptaki köşeli parantezin işlevine dikkat ediniz. Köşeli parantezler olmasaydı komut tamamen asssembly derleyicisi tarafından farklı yorumlanırdı. Örneğin:

ADD EAX, 1FC14A

Bu komutta EAX yazmacındaki değer doğrudan 1FC14A sabiti ile (immediate) ile toplanmıştır. Sonuç EAX’te bulunacaktır. Fakat örneğin:
ADD EAX, [1FC14A]
Burada EAX yazmacındaki değer ile 1FC14A adresinden başlayan 32 bit değer toplanmıştır.

Not : RISC işlemcilerinde yazmaç ile bellek bölgesinin işleme sokulamamaktadır. Yazmaç-bellek işlemleri tipik olarak CISC tarzı eski işlemcilerde karşılaşılan komut kalıplarıdır.

Normal olarak köşeli parantez içerisindeki sabit adresler 32 bit korumalı modda 32 bit uzunluğundadır. Ancak köşeli parantez içerisindeki bellek adresi 16 bitten de oluşabilir. Ancak komutlardaki 16 bit adresler 32 bit korumalı modda 0x67 öneki gerektirmektedir. Bu nedenle böyle komutlarla 32 bit programlamada pek karşılaşmayız.

4) Köşeli parantez içerisindeki adresler ile sabitler işleme sokulabilirler. Bu durumda sembolik makine dili derleyicileri komutta yazmaç olmadığı için işlemin kaç bit üzerinden yapılacağını ek anahtar sözcükler yardımıyla işleme sokarlar. Örneğin:
ADD dword [1FC14A], 100
Burada belleğin 1FC14A adresinden başlayan 32 bitlik değer 100 ile toplanmıştır, sonuç yine 1FC14A adresinden başlayarak oraya aktarılmıştır. Komuttaki dword işlemin 32 bit (double word = 4 byte ( 8 bit ) = 32 bit) olduğunu assembly derleyicisine anlatmak için gerekmektedir. Bellekteki değerlerle sabitler işleme sokulurken köşeli parantez içerisindeki bellek adresi sonraki maddelerde olduğu gibi yazmaçlarla da oluşturulabilmektedir.

Not : RISC işlemcilerinde bellekteki değerlerle sabitler işlemlere sokulamamaktadır. 

5) Bir yazmaçla köşeli parantez içerisindeki bir yazmaç işleme sokulabilir. Bu durumda bellekte o yazmacın içerisindeki değerle belirtilen adresteki bilgi işleme sokulur. Örneğin:

MOV EAX, 1FC10A
MOV EBX, [EAX]

Burada EAX yazmacının içerisinde 1FC10A değeri vardır. O halde bellekteki 1FC10A adresinden başlayan 32 bit değer EBX yazmacına aktarılmıştır. Örneğin:

MOV EAX, 1FC10A
MOV EBX, [EAX]
ADD EAX, 4
MOV EBX, [EAX]
ADD EAX, 4
MOV EBX, [EAX]
...

Bu adresleme modunun neden kullanıldığı yukarıdaki örnek kodla anlaşılabilir. Biz bu sayede örneğin bir yazmaca bir dizinin adresini atayıp sonra o yazmacı köşeli parantez içerisinde kullanarak dizinin tüm elemanlarına erişebiliriz. Benzer biçimde gösterici işlemleri de derleyici tarafından bu komut kalıbıyla yapılmaktadır. Örneğin:

int *pi = (int *) 0x1FC41A;
*pi = 20;
Bu işlem aşağıdaki gibi makine komutlarına dönüştürülebilir (örneğimizdeki pi, pi göstericisinin adresini belirtiyor olsun):

MOV dword [pi], 1FC41A
MOV EAX, [pi]
MOV dword [EAX], 20

Anahtar Notlar: Sembolik makine dillerinde (ve tabii doğal makine dillerinde) değişkenlerin isimleri yoktur. Dolayısıyla biz onlara isimleriyle erişmeyiz. Biz ancak onlara onların adresleriyle erişebiliriz. Yani yüksek seviyeli dillerdeki değişken isimleri aslında programcılar tarafından uydurulmuş isimlerdir. Kaynak program derlendikten sonra artık çalıştırılabilen program bir değişken ismi içermez. Yüksek seviyeli dillerin derleyicileri belli adreslerden başlayan bilgileri bize belli bir isimle sunmaktadır. Tabii yazmaçlar bir adres belirtmezler. Onlar CPU içerisinde yeri belli olan küçük bellek bölgeleridir. Makine komutları bile yazmaçları bilmektedir. İşin aslı yazmaçların da makine komutlarında isimleri yoktur. Yazmaçlar ikilik sisteme dönüştürülmüş makine komutlarında numaralarla temsil edilirler.

Köşeli parantez içerisindeki yazmaçlar 16 bitlik yazmaçlar olabilir. Ancak 8 bitlik yazmaçlar olamaz. Örneğin:

ADD EAX, [BX]

komutu geçerlidir. Ancak:

ADD EAX, [BL]

komutu geçersizdir. Köşeli parantez içerisine 16 bit yazmaç yerleştirmek 32 bit programlamada çok nadir görülebilecek bir durumdur. Çünkü 16 bit yazmaç ile ancak belleğin tepesindeki 64K’ya erişebiliriz.

Not: 32 bit korumalı modda köşeli parantez içerisine 16 bit yazmaç yerleştrmek için komutun başında 0x67 önekinin bulunması gerektiğini anımsayınız. Bu önek komutu 1 byte büyütmektedir.

6) Bir yazmaçla "köşeli parantez içerisindeki bir yazmaç + 8 bit sabit" ya da "köşeli parantez içerisindeki yazmaç + 32 bit sabit" işleme sokulabilir. Bu gösterimi Intel [ base + disp8 ] ve [ base + disp32 ] ile belirtmektedir. Buradaki disp "displacement" sözcüğünden gelmektedir. Örneğin:

ADD EAX, [EBX + 1F] ; yazmaç + 8 bit
ADD EAX, [EBX + 1001FC01] ; yazmaç + 32 bit

Burada köşeli parantezin içindeki ifadeden bir bellek adresi elde edilmektedir. Bu bellek adresi oradaki yazmacın içerisindeki değerle o sabit değerin (displacement) toplamıyla oluşturulmaktadır. Örneğin:

MOV EBX, 1FC010
MOV EAX, [EBX + 1C]

Burada EBX değerinin içerisindeki değerle 1C değeri toplanarak bellek adresi elde edilmiştir. Yani işlemci 1FC010 + 1C = 1FC02C adresinden başlayan 32 bit değeri EAX yazmacına yerleştirir.

Köşeli parantez içerisinde 16 bit yazmaç kullanılırsa sabit değer (displacement) 8 bit ya da 16 bit olabilmektedir. (Fakat bu biçimdeki komutların önüne 0x67 öneki getirildiğini anımsayınız. Bu da makine komutunu bir byte büyüteceğini anımsayınız!) Örneğin:

MOV EAX, [BX + 1FC0]

Burada BX yazmacının içerisindeki değerle 16 bitlik 1FC0 değeri toplanarak bellek adresini oluşturmaktadır.

7) Bir yazmaçla "köşeli parantez içerisindeki iki yazmaç toplamı" işleme sokulabilir. Örneğin:

ADD EAX, [EBX + ECX]

Burada EAX yazmacının içerisindeki değer bellekte EBX ve ECX yazmaçlarının içerisindeki değerlerin toplamıyla belirtilen adresteki 32 bit değer ile toplanmaktadır. Sonuç EAX yazmacına aktarılmaktadır. Örneğin bir dizinin başlangış adresi EBX’te olsun. ECX’te de 0 değerinin bulunduğunu varsayalım:

MOV EAX, [EBX + ECX]
ADD ECX, 4
...
MOV EAX, [EBX + ECX]
ADD ECX, 4
...
MOV EAX, [EBX + ECX]
ADD ECX, 4

Not: Yine 32 bit korumalı modda bellek operandı ve bit düzeyini belirten yazmaçlar 16 bit olabilir. Bu durumda 0x66 ve 0x67 önekleri gerekecektir.

8) Bir yazmaçla köşeli parantez içerisindeki yazmaç + yazmaç + 8 bit sabit ya da yazmaç + yazmaç + 32 bit sabit işleme sokulabilir. Örneğin:

MOV EAX, [EBX + ECX + 1C] ; yazmaç + yazmaç + 8 bit sabit

Burada EAX yazmacına EBX, ECX yazmacının içindeki değerlerin toplamıyla 1C değerinin toplanması sonucunda elde edilen bellek adresindeki 32 bit değer aktarılmaktadır. Örneğin:

MOV EAX, [EBX + ECX + 001A121C] ; yazmaç + yazmaç + 32 bit sabit

9) Yukarıdaki 7’inci ve 8’inci kalıplarda toplama işlemine sokulan yazmaçlardan yalnızca bir tanesi iki ile, 4 ile ya da 8 ile çarpılabilir. Örneğin:

MOV EAX, [EBX + ECX * 4]

ya da örneğin:

MOV EAX, [EBX + ECX * 4 + 1C]

ya da örneğin:

MOV EAX, [EBX + ECX * 4 + 001a121C]

Burada ECX’in içerisindeki değer 4 ile çarpılıp toplama işlemine sokulmuştur. Yukarıdaki kalıplarda köşeli parantezler içerisinde hangi yazmaçların bulunabileceği konusunda da bazı kısıtlar vardır. Genel olarak EIP ve EFLAGS yazmaçlarının dışındaki 32 bit yazmaçların hepsi köşeli parantez içerisinde kullanılabilir. Fakat köşeli parantez içerisinde hem 32 bit yazmaçlar hem de 16 bit yazmaçlar toplama işlemine sokulamazlar. Ayrıca köşeli parantez içerisinde 16 bit yazmaçların kullanımı konusunda bazı ayrıntılar vardır.

Bit Düzeyinde İşlemler Yapan Makine Komutları

07:10 ,


Sembolik makine dillerinde bit düzeyinde işlemlere sıkça ihtiyaç duyulmaktadır. Bu nedenle bit düzeyinde işlem yapan komutları bilmek gerekmektedir. Şimdi temel bit işlemlerini yapan makine komutları sırasıyla görelim.

AND ve OR Komutları


AND ve OR komutları iki tamsayı değerin karşılıklı bitlerini AND ve OR işlemlerine sokmaktadır. Örneğin:

AND eax, ebx
OR eax, [ebx + ecx]
AND eax, 1

Komut sonucunda her zaman OF ve CF bayrakları reset’lenir.
AF bayrağının durumu tanımsızdır (undefined).
İşlemden SF, ZF, PF bayrakları etkilenir.

AND işleminin operand'ı etkilemeyen yalnızca bayrakları etkileyen TEST isimli bir komutta bulunmaktadır. AND ile TEST arasındaki ilişki SUB ile CMP arasındaki ilişkiye benzetilebilir. TEST işlemi AND işlemi yapar fakat bu işlemden yalnızca bayraklar etkilenir. Pekiyi TEST işleminin ne faydası vardır? Bir değeri kendisiyle AND işlemine soktuğumuzda onunla aynı değeri elde ederiz. Fakat bu işlemden bayraklar etkileneceği için artık koşullu jump işlemi yapılabilir. Örneğin EAX’teki değer negatifse jump etmek isteyelim. Henüz bir işlem yapmadığımıza göre bayraklara bakamayız. Bayrakları etkileyecek bir işleme ihtiyacımız vardır. Bu AND işlemi ya da TEST işlemi olabilir:

TEST EAX, EAX
JS EXIT

Bu işlemle EAX’teki değer negatif ise jump yapılmaktadır. Benzer biçimde:

TEST EAX, EAX
JNZ EXIT

Burada da EAX’teki değer sıfır değilse jump yapılmıştır.


XOR Komutu


XOR komutu iki değerin karşılıklı bitlerini exclusive or işlemine sokar. Exclusive or işlemi iki bit aynıyse 0, farklıysa 1 değerini veren bir işlemdir. Exclusive or işlemi geri dönüşümlüdür. Bu nedenle şifreleme gibi işlemlerde tercih edilir. Aynı değeri kendisiyle exclusive or işlemine sokarsak sıfır elde ederiz. Bu nedenle assembly programcıları bir yazmacı sıfırlamak için bu komutu sık kullanmaktadır:

XOR EAX, EAX

Aynı işlem MOV ile yapılırsa komut uzamaktadır:

MOV EAX, 0

Eskiden XOR işlemi SUB işleminden daha hızlıydı. Ancak artık bunlar aynı hızdadır:

SUB EAX, EAX

Komut sonucunda her zaman OF ve CF bayrakları reset’lenir.
AF bayrağının durumu tanımsızdır (undefined).
İşlemden SF, ZF, PF bayrakları etkilenir.

NOT Komutu

Tek operandlı bir komuttur. Girilen sayının değilini alır. Aşağıdaki örnekte olduğu gibi;

param equ                         0101 0011
NOT param -> Operand1:    1010 1100
çalışır.

PE ve ELF Çalıştırılabilen Dosyalarındaki Bölüm (Section) Kavramı

06:59 , ,

PE (Portable Executable) | ELF (Executable and Linkable Format) 



Bugün için en çok kullanılan çalıştırılabilir (executable) dosya formatları PE ve ELF’tir. PE formatını Microsoft 32 bit Windows sistemleri ilk çıktığında tasarlamıştır.  Microsoft daha önce 16 bit Windows 3.X sistemlerinde NE (New Executable) denilen bir format kullanmıştır. Microsoft’un DOS’ta kullandığı çalıştırılabilen dosya formatı da MZ (by Mark Zbikowski = aynı zamanda exe'yi tasarlayan kişi) formatıydı. 


UNIX/Linux dünyasında da pek çok çalıştırılabilir dosya formatı denenmiştir. "a.out" isimli format uzun süre pek çok UNIX türevi sistemde kullanılmıştır. Linux'te başlangıçta bu formatı kullanıyordu. Artık UNIX türevi sistemlerin büyük kısmı ELF formatını birincil çalıştırılabilen format olarak desteklemektedir. Ayrıca bir işletim sistemi birden fazla çalıştırılabilen dosya formatını destekliyor olabilir. Örneğin Linux sistemleri ELF formatının yanı sıra hala klasik "a.out" formatını da desteklemektedir. Windows’un pek çok versiyonu NE ve MZ formatlarını da desteklemiştir. PE ve ELF formatlarının 32 bitlik ve 64 bitlik birbirine çok benzeyen biçimleri de vardır. Böylece bazen bu formatlar PE32, PE64, ELF32, ELF64 isimleriyle de belirtilmektedir. PE ve ELF formatları genel tasarım olarak aslında birbirlerine benzemektedir. Her iki formatta da önemli bilgilerin dosyanın neresinde bulunduğunu gösteren bir başlık (header) kısmı vardır. Her iki format da bölümlerden (sections) oluşmaktadır. Bu formatlara sahip bir program çalıştırılmak istendiğinde işletim sistemi çalıştırılabilen dosyayı açar formattaki bölümleri inceler ve bölümleri belleğe (RAM’e) yükler.



İşletim sisteminin çalıştırılabilen dosyayı okuyarak çalıştırmak üzere belleğe yükleyen kısmına kavramsal olarak yükleyici (loader) denilmektedir. Bölümler aynı özelliklere sahip ardışıl sayfalardan (page) oluşmaktadır. Bölümlerin birer isimleri vardır. ELF ve PE formatında geleneksel olarak bölümler başında "." olacak biçimde isimlendirilmektedir. Tabii aslında böyle bir zorunluluk yoktur. İşletim sistemi bölümler için bellekte yer ayırıp içini çalıştırılabilen dosyadan ilgili alanları okuyarak yüklemektedir.

PE ve ELF Formatlarındaki Çok Karşılaşılan Bölümler



PE ve ELF formatlarında en çok karşılaşılan bölümler şunlardır:

.text Bölümü: 

Bir programın bütün makine kodları (yani kaynak kodları) bu bölümde bulunur. Yani yukarıdaki C programında programdaki main, foo, bar ve tar fonksiyonlarının kodları .text bölümüne yerleştirilmektedir.

.data Bölümü:

Bu bölümde ilk değer verilmiş global değişkenler ve static yerel değişkenler tutulmaktadır. Yani örneğin yukarıdaki C programında g_a ve count değişkenleri derleyici tarafından tipik olarak ".data" bölümünde tutulacaktır. Derleyiciler ilk değer verilmiş global değişkenleri ilk değerleriyle birlikte çalıştırılabilen dosyanın ".data" bölümüne yerleştirirler. İşletim sisteminin yükleyicisi de onları bu bölümden alıp blok olarak fiziksel belleğe yüklemektedir. Bu nedenle ".data" bölümündeki değişkenlerin çalıştırılabilen dosyada yer kaplamaktadır. Ancak bazı çalıştırılabilen dosya formatları bölümler içerisinde hangi ilk değerden ne miktarda olduğunu tutma yeteneğine sahiptir (örneğin Windows’un PE formatı böyledir). Böylece aşağıdaki gibi global bir dizi bu sistemlerdeki çalıştırılabilen dosyalarda çok fazla yer kaplamayabilir:

int g_x[1000000] = {1, 2, 3};

Ancak ELF gibi bazı formatların bu yeteneği yoktur. Dolayısıyla bu formatlarda yukarıdaki dizinin hepsi ".data" bölümünde ilk değerleriyle bulunacak, dolayısıyla bu da çalıştırılabilen dosyanın uzunluğunu büyütecektir.

.rdata | .rodata Bölümleri: 

PE formatındaki ".rdata", ELF formatındaki ".rodata" bölümleri global ve static read-only verileri tutmak için düşünülmüştür. String ifadeleri genellikle bu sistemlerdeki derleyiciler tarafından bu bölümlerde saklanmaktadır. Örneğin yukarıdaki C programında g_b, g_name ve ival değişkenleri derleyici tarafından tipik olarak ".r(o)data" bölümünde tutulacaktır. Windows ve Linux’un yükleyicileri bu bölümlerdeki bilgileri "read-only" sayfalara yüklerler. Dolayısıyla programın çalışma zamanı sırasında buradaki değerler değiştirilmek istenirse exception (page fault) oluşur.

.bss Bölümü: 

Bu bölümde ilk değer verilmemiş global değişkenler (g_c, g_d) ve ilk değer verilmemiş static değişkenler tutulmaktadır. Bunlara ilk değer verilmediği için bunların çalıştırılabilen dosyalarda boşuna yer kaplamasına gerek de yoktur. PE ve ELF formatlarında bu bölümün yalnızca uzunluğu çalıştırılabilen dosya içerisinde tutulur. İşletim sisteminin yükleyicisi bu uzunluğa bakarak ".bss" bölümünü bellekte (RAM) tahsis eder ve orayı sıfırlar. (C ve C++’ta ilk değer verilmemiş global ve static yerel nesnelerini içerisinde 0 değeri bulunmaktadır. || bss alanının sıfırlanması henüz akış main fonksiyonuna girmeden derleyicilerin başlangıç kodları (startup codes) tarafından da yapılabilmektedir.)


Öteleme (Shift) Komutları

07:11 ,


C/C++ dillerinde "<<" ve ">>" operatörleri aslında işlemcinin sola ve sağa öteleme komutlarına karşılık gelmektedir. Öteleme işlemleri  Intel işlemcilerinde SAL, SAR, SHL ve SHR makine komutları ile yapılmaktadır.

  • SAL (Shift Arithmetic Left) ve SAR (Shift Arithmetic Right) komutlarına aritmetik öteleme komutları
  • SHL (Shift Logical Left), SHR (Shift Logical Right) komutlarına da mantıksal öteleme komutları denilmektedir.

SAL ve SHL komutları arasında farklılık yoktur. SAR ile SHR komutları arasında ise küçük bir farklılık vardır. Diğer yüksek seviyeli dillerden de bilindiği gibi sola bir kez ötelemede bütün bitler bir sola kaydırılır ve sağdan sıfır ile besleme yapılır. Sağa bir kez ötelemede ise bütün bitler bir sağa kaydırılır ancak en soldan 0 ile mi 1 ile mi besleme yapılacağı SAR ve SHR komutlarında değişmektedir. SAR komutunda besleme işaret bitiyle, SHR komutunda ise her zaman 0 ile yapılmaktadır. SAL ve SHL komutları arasında bunun dışında hiçbir fark yoktur. Mantıksal bütünlük oluşturmak için sanki iki farklı komut varmış gibi isimlendirme yapılmıştır. (Yani aslında SAL ve SHL iki ayrı makine komutu değil aynı komutun iki farklı ismidir.) 1' den fazla kez ötelemede aynı işlemler birden fazla yapılmaktadır. Öteleme komutlarında ötelenecek operand yazmaç ya da bellek olabilir. Bir kez öteleme için 2 byte’lık bir komut versiyonu (opcode) bulundurulmuştur. Birden fazla öteleme yapılmak isteniyorsa öteleme sayısı ya sabit olarak verilmek zorundadır ya da CL yazmacına yerleştirilmek zorundadır. Öteleme miktarının sabit olarak verilmesi durumunda ise komut uzunluğu 3 byte olur. Eğer komut uzunluğu CL yazmacına yerleştirilirse bu durumda komut uzunluğu yine 2 byte’tır. Örneğin bazı geçerli öteleme komutları şöyle olabilir:

sal  eax, 1      ; komut uzunluğu 2 byte
shl  dword [ebx], 5    ; komut uzunluğu 3 byte 
sar  byte [ebx + ecx], cl   ; komut uzunluğu 2 byte
Aşağıdaki komutlar ise geçersizdir:
sal eax, bl ; geçersiz!
sal ebx, ecx ; geçersiz!

Komutların bayrakları etkilemesi şöyle olmaktadır: Her zaman kaybedilen bit CF bayrağına yerleştirilir. Yani örneğin biz sola bir kez öteleme yaptığımızda en soldaki bit CF bayrağına yerleşecektir. Sağa bir kez öteleme yaptığımızda da en sağdaki bir CF bayrağına yerleşir. Birden fazla öteleme yapıldığında son ötelemede kaybedilen bit CF’de kalır. SHL ve SHR komutlarında öteleme sayısı ötelenmek istenen değerin bit uzunluğunun bir eksiğini aşıyorsa bu durumda CF bayrağı tanımsız durumdadır. OF bayrağı yalnızca 1 kez ötelemede etkili olur. Birden fazla kez ötelemede OF de tanımsız durumdadır. Bir kez ötelemede OF bayrağı bize sayıda işaretli taşma olup olmadığını bildirmektedir. Ayrıca SF, ZF ve PF bayrakları normal biçimde işlemden etkilenirler. Sıfır kere öteleme geçerlidir. Ancak bu durumda işlemden bayraklar etkilenmez.

Sola öteleme bilindiği gibi iki ile çarpma anlamına, sağa öteleme ise iki ile bölme anlamına gelmektedir. İşaretli sayıların sağa ötelenmesi için SAR komutu işaretsiz sayıların sağa ötelenmesi için SHR komutu kullanılmaktadır. İşaretli ya da işaretsiz sola öteleme için aslında yukarıda da belirtildiği gibi tek bir komut vardır. Bu komuta SAL ve SHL isimleri verilmiştir.

Mantıksal sağa ötelemede en soldan beslemenin 0 ile artimetik sağa ötelemede ise 1 ile yapıldığını anımsayınız. Örneğin AL yazmacında aşağıdaki değerin bulunduğunu düşünelim:

AL: 1011 0111

Şimdi AL yazmacındaki değeri SAR AL, 1 ile aritmetik olarak bir kez sağa öteleyelim. AL’deki değer şu hale gelecektir.

AL: 1101 1011

Oysa AL’ye SHR AL, 1 komutuyla mantıksal sağa öteleme uygulasaydık AL’deki değer şu hale gelecekti:

AL: 0101 1011

Bildiğiniz gibi sağa öteleme sayıyı tamsayısal olarak (yani nokta oluşmayacak biçimde) ikiye bölme anlamına gelir. Fakat aritmetik sağa ötelemede eğer ötelenecek değer negatifse sonuç azalacak biçimde (yani eksi sonsuza doğru) tamsayı olarak elde edileceğine dikkat ediniz. Yani örneğin biz -3 değerini bir kez sağa aritmetik ötelemek isteyelim:

1111 1101 -3

Sağa bir kez artimektik ötelendiğinde sayının -2 olduğuna dikkat ediniz:

1111 1110 -2

Örnek çalışmalar;
 

CISC ve RISC Mimarileri


Mikroişlemci tasarımında iki önemli mimari vardır. Bunlar;

  • CISC ( Complex Instruction Set Computing )
  • RISC ( Reduced Instruction Set Computing )

İlk mikroişlemciler CISC mimarisine uygun tasarlanmışlardır. Belli bir mikroişlemci teknik kısıtlamaların izin verdiği ölçüde her iki mimariden de özellikleri barındırabilir. Örneğin Intel’in son dönem işlemcileri pek çok RISC özelliğini de barındırmaktadır. Bu nedenle CISC ve RISC mimarilerini var yok biçiminde değil bir özellik barındırma biçiminde düşünmek daha uygundur.
CISC mimarisinde daha fazla makine komutu RISC mimarisinde daha az makine komutu bulunma eğilimindedir. RISC mimarisinde daha az komut daha hızlı ve etkin çalışacak biçimde mantık devreleriyle oluşturulmuştur. Dolayısıyla komutların hemen hepsi tek bir mantık devresiyle doğrudan çalıştırılır. Halbuki CISC mimarisinde mikrokod programlamayla karmaşık komutlar daha yalın parçalara ayrılarak çalıştırılır.

CISC mimarisinde makine komutları farklı uzunluklarda olma eğilimindedir. Halbuki RISC mimarisinde tüm makine komutları aynı uzunluktadır. CISC mimarisinde çok kullanılam makine kamutları az byte’la az kullanılan makine komutları çok byte’la ifade edilmeye çalışılmıştır. İlk zamanlar bunun iyi bir teknik olduğu sanılmışsa da daha sonraları bazı problemler göze çarpmıştır. Komutlar farklı uzunluklarda olursa genel olarak komutu alıp yorumlama (fetch ve decode) işlemi yavaş yapılmaktadır. Ayrıca komut seviyesinde pipeline mekanizması komutlar eşit uzunluktaysa daha etkin yapılabilmektedir.

RISC mimarisinde çok sayıda yazmaç (register) bulunma eğilimindedir. Halbuki CISC mimarisinde daha az sayıda yazmaç vardır. Ayrıca RISC mimarisinde her yazmaçla her şey yapılabilmektedir. Halbuki CISC mimarisinde bazı işlemler ancak bazı özel yazmaçlarla yapılabilmektedir.

RISC mimarisinde belleğe erişen makine komutları ile işlem yapan makine komutları ayrı tutulmuştur. Bu nedenle RISC işlemcilerine Load/Store işlemcileri de denir.


RISC mimarisinde toplama, çıkartma, çarpma gibi tüm işlemler operandlarını yazmaçlardan alacak biçimde tasarlanmıştır. Halbuki CISC mimarisinde komutların bir operandı yazmaç iken diğer operandı bellek adresi olabilmektedir.
RISC mimarisinde Load/Store komutları dışındaki komutlar üç operandlı olma eğilimindedir. Halbuki CISC mimarisinde neredeyse tüm komutlar iki operandlıdır. RISC’te her iki operandın yazmaç olması zorunludur. Toplamda RISC’teki tasarımın daha faydalı ve etkin olduğu ispatlanmıştır. Bu nedenle artık yeni işlemcilerin hepsi RISC mimarisinde tasarlanmaktadır.
RISC işlemcileri pipeline işleminin etkinliğiyle ünlüdür. Pipeline işlemci'nin bir komutu yaparken diğerleri üzerinde de bazı hazırlık işlemlerini yapabilmesi anlamına gelir. Şüphesiz Intel gibi CISC işlemcileri de pipeline mekanizmasına sahiptir. Ancak RISC tasarımı pipeline mekanizmasının daha etkin yapılabilmesine yol açmaktadır.



Sembolik Makine Dilinde Yazılmış Fonksiyonların C ve C++’tan Çağrılması

02:03 ,

Bir programın tamamının sembolik makine dilinde yazılması çok özel durumlar dışında tercih edilen bir yol değildir. Bunun nedenleri şöyle sıralanabilir:

  • Sembolik makine dilleri taşınabilir (portable) değildir. Makine komutları işlemciden işlemciye değişebildiği gibi genel sentaks da aynı işlemci söz konusu olduğunda bile derleyiciden derleyiciye değişebilmektedir.
  • Sembolik makine dilleri alçak seviyeli olduğu için bu dillerde programcıların hata yapması daha olasıdır.
  • Sembolik makine dillerinde kod yazım hızı daha düşüktür. Bu da üretkenliği düşürmektedir.
  • Sembolik makine dillerinde programda hata ayıklama daha zordur.

Bu nedenlerden dolayı pratikte kodun büyük bölümünün C, C++ ( bizim için bu diller yüksek seviye :) gibi yüksek seviyeli ve taşınabilir dillerde, yalnızca kritik yerlerin sembolik makine dilinde yazılması daha yaygın kullanım biçimidir.(Örneğin Linux çekirdeğinin yalnızca %3 kadarlık kısmı sembolik makine dilinde yazılmıştır.) Bunlar göz önüne alındığında sembolik makine dilinde yazılan kodların C ve C++ gibi dillerden çağrılmasının önemi anlaşılabilir. Bu yazıda bu işlemin nasıl yapıldığı detaylı bir şekilde anlatılmaya çalışılacaktır.

Sembolik makine dilinde yazılmış bir fonksiyonun C ya da C++’tan çağrılabilmesi için önce onun sembolik makine dili derleyicisiyle derlenmesi gerekir. Bu derleme işleminden bir amaç dosya (object file) elde edilir. (Amaç dosya uzantılarının Windos’ta ".obj" biçiminde, UNIX/Linux sistemlerinde ".o" biçimindedir.) Sonra bu fonksiyonu çağıran C ya da C++ kodu da bu dillerin derleyicileri ile derlenir. Bu işlemden de bir amaç dosya (object file) elde edilir. Nihayet bu amaç dosyalar birlikte link işlemine sokularak çalıştırılabilir (executable) dosya olurulur. Örneğin "run.c" isimli C programından "utility.s" isimli sembolik makine dilinde yazılmış olan fonksiyonlar çağrılmak istensin. Yapılacak işlemleri aşağıdaki şekille özetleyebiliriz:


Yukarıda özetlediğimiz adınmlardan sorunsuz geçilebilmesi için şu noktalara dikkat edilmesi gerekir:

  • Sembolik makine dilinde fonksiyonu yazarken onun için NASM’de "global" bildirimi yapmak gerekir.
  • Derleyiciler global fonksiyonların ve değişkenlerin isimlerini amaç dosyaya yazarken değiştirebilmektedir. Bu duruma "isim dekorasyonu (name decoration)" denilmektedir. Ancak burada şunu ifade etmek istiyoruz: İsim dekorasyonu çağırma biçimine göre, derleyiciye göre ve hatta dile göre değişebilmektedir. Örneğin Windows’ta Microsoft derleyicileri "cdecl" çağırma biçiminde global sembollerin (fonksiyon ve değişkenlerin) başına bir alt tire (underscore) eklemektedir. Halbuki Linux’ta gcc derleyicileri bu isimleri hiç değiştirmeden amaç koda yazmaktadır. (Dolayısıyla Windows’ta bizim de sembolik makine dilinde yazdığımız fonksiyonların başına alt tire eklememiz gerekir. Aksi takdirde linker iki modüldeki isimleri eşleyemez.)
  • C ya da C++’ta belirlenen çağırma biçimine sembolik makine dilinde kod yazarken uyulmalıdır. (Yani örneğin C’de "cdecl" çağırma biçimine sahip olarak çağırdığımız add fonksiyonunu sembolik makine dilinde yazarken parametrelerin sağdan sola stack’e atılacağını bilerek fonksiyonu yazmalıyız ve geri dönüş değerini de EAX yazmacında bırakmalıyız.)
  • Çağrılan fonksiyonun hangi yazmaçları saklaması gerektiği bilinmelidir ve fonksiyonu yazarken bu kurala uyulmalıdır. Örneğin cdecl ve stdcall çağırma biçimlerinde çağrılan fonksiyon EAX, ECD ve EDX yazmaçlarını bozma hakkına sahiptir. Fakat diğerlerinin değerlerini değiştirecekse önce onları stack’te saklamalı fonksiyon sonlanmadan önce geri almalıdır.
  • Semboik makine dilindeki kodlarla C ya da C++’taki kodların aynı bölüm (section) içerisinde bulunması gerekir.Örneğin C ve C++ derleyicileri Windows ve Linux sistemlerinde kodu ".text" isimli bölüme yerleştirmektedir. O halde bizim de fonksiyonları ".text" isimli bölümde yazmamız gerekir. Aynı isimli bölümler linker tarafından birleştirilmektedir. (Yani birleştirme işlemi sonucunda çalıştırılabilen dosyada tek bir ".text" bölümü bulunacaktır)
  • C ya da C++ kaynak programında sembolik makine dilinden çağrılan fonksiyonun prototip bildirimi/çağırma biçimi aynı olacak biçimde yapılmalıdır.


Koşullu Jump Komutları

07:20 , ,
Jump komutları sembolik makine dillerinin mutlaka bilinmesi gereken komutlarındandır. Bazı işlemci ailelerinde bu komutlara branch (dallanma) komutları denilmektedir. Jump komutları olmadan yüksek seviyeli dillerdeki if, switch, while for gibi deyimler gerçekleştirilemez. Jump komutları 
  • koşulsuz (unconditional
  • koşullu (conditional) olmak üzere ikiye ayrılmaktadır. 
Koşulsuz jump komutları C’deki goto deyimi gibidir. Koşulsuz olarak EIP yazmacını belli bir değere çeker. Koşulsuz jump komutlarının doğrudan (direct) ve dolaylı (indirect) biçimleri de vardır. Koşulsuz jump komutları pek çok işlemcide olduğu gibi göreli uzaklık değerini operand olarak alır. Doğrudan koşulsuz jump komutlarının son byte’larından sonraki ilk byte göreli uzaklık için sıfır orijini belirtir. Negatif uzaklık "yukarıya", pozitif uzaklık "aşağıya" jump yapılacağı anlamına gelmektedir.

Koşullu Jump Komutları

Koşullu jump komutları bayraklara bakarak jump işlemi yapmaktadır. Intel’deki bütün koşullu jump komutları doğrudandır ve "göreli uzunluk" değerini operand olarak alırlar. Bayrakların uygun karşılaştırma sonuçlarını içermesi SUB ya da CMP komutlarıyla sağlanmaktadır. Bu nedenle önce bayrakların karşılaştırma için set ya da reset edilmesi gerekir. Yani koşullu jump komutları tipik olarak SUB ya da CMP komutlarından sonra uygulanmaktadır. Zaten onların isimlendirmeleri de SUB ya da CMP komutlarından sonra kullanılacağı fikriyle yapılmıştır. Intel’de çok fazla koşullu jump komutu varmış gibi görülse de aslında bazı komutlar diğerleriyle aynı işlemi yaparlar. Yani bu komutlar aynı makine kodunun farklı isimleridirler. Örneğin JA ile JNBE aslında aynı komutlardır. Bunlar tek bir komutun iki farklı isimleridir desek yanlış olmaz. Koşullu jump komutlarının isimlendirilmeleri SUB ya da CMP komutlarının birinci operandına göre yapılmıştır. Örneğin:

cmp eax, ebx
jb REPEAT

Burada jb (jump below) eax’teki değer ebx’teki değerden küçükse anlamına gelmektedir. İsimlendirmede A (Above) ve B (Below) işaretsiz tamsayılar için G (Greater) ve L (Less) de işaretli sayılar için kullanılmaktadır. Eşitlik E (Equal) ya da Z (Zero flag set) ile ifade edilebilmektedir. Koşullu jump komutlarında eğer koşul sağlanmışsa jump işlemi yapılır. Eğer koşul sağlanmamışsa sonraki komuttan devam edilir. Aşağıda tüm jump komutlarının listesi verilmiştir:





Sembolik makine dilinde döngüler ve if deyimleri koşullu ve koşulsuz jump komutlarının kullanılmasıyla gerçekleştirilir. Örneğin aşağıdaki gibi bir C kodunun sembolik makine dili karşılığını yazmak isteyelim:

int g_x = 0;
/* ... */
while (g_x < 10) {
printf(“AhmetUlucay\n”);
++g_x;
}

Böyle bir while döngüsü aşağıdaki gibi oluşturulabilir:


Döngüdeki en önemli nokta şurasıdır:

cmp dword [g_x], 10
jge EXIT

Burada g_x ile 10 değeri karşılaştırılmıştır. Eğer g_x 10'dan büyükse ya da 10'a eşitse while koşulu sağlanmadığı için döngüden çıkılmıştır. Eğer bu koşul sağlanmıyorsa akış aşağıdan devam eder. Orada da mesaj ekrana yazdırılmıştır. Dönünün devamının sağlanması için yukarıya jump yapıldığına dikkat ediniz:

; ...
inc dword [g_x]
jmp REPEAT

Şimdi de aşağıdaki while döngüsünü sembolik makine dilinde yazmaya çalışalım:

unsigned g_x = 10;
while (g_x != 0) {
/* ... */
--g_x;
}

Kodun eşdeğer assembly karşılığı şöyle olabilir:

REPEAT:
cmp dword [g_x], 0
je EXIT
;  ...
dec dword [g_x]
jmp REPEAT
EXIT:
; ...

Tabii bu döngüyü şöyle de oluşturabilirdik:

cmp dword [g_x], 0
je EXIT
REPEAT:
; ...
dec dword [g_x]
jnz REPEAT
EXIT:
; ...

Burada dec komutuyla g_x değeri sıfıra düştüğünde ZF bayrağı set edileceğine dikkat ediniz. do-while döngüleri de benzer biçimde yapılabilir. Örneğin:

unsigned g_x = 10;
do {
/* ... */
--g_x;
} while (g_x != 0);

İşlemi sembolik makine dilinde şöyle yapılabilir:

REPEAT:
; ...
dec dword [g_x]
jnz REPEAT

for döngüleri de benzer biçimde sembolik makine dilinde oluşturulabilir. (Örneklerimizde stack görmediğimiz için hep global değişkenleri kullanıyoruz). Örneğin:

int g_i;
/* ... */
for (g_i = 0; g_i < 10; ++g_i) {
/* ... */
}

Bu işlemin sembolik makine dili karşılığı şöyle oluşturulabilir:

MOV dword [g_i], 0
@2:
cmp dword [g_i], 10
jge @1
; ...
inc dword[g_i]
jmp @2
@1:
; ...

Notlar: jmp işlemlerinde etiket kullanırken isim uydurmak zordur. Sembolik makine dilinde @ karakteri geçerli bir isimlendirme karakteridir. Pek çok C derleyicisi programın sembolik makine dili karşılığını oluştururken bu biçimdeki fabrikasyon etiketleri kullanmaktadır.

if deyimleri de yine koşullu ve koşulsuz jump komutlarıyla gerçekleştirilir. Örneğin yalnızca doğruysa kısmı olan bir if deyimi düşünelim:

if (g_i > 100) {
/* ... */
}
/* ... */

Bu işlem aşağıdaki gibi sembolik sembolik makine dilinde ifade edilebilir:

cmp dword [g_i], 100
jle @1
; doğruysa kısmı
@1:
; if deyimin dışı

Şimdi de else kısmı olan bir if deyimini sembolik makine dili ile ifade etmeye çalışalım:

if (g_i > 10) {
/* ... */
}
else {
/* ... */
}

Bu işlemin eşdeğer sembolik makine dili karşılığı şöyle olabilir:

cmp dword [g_i], 10
jle @1
; if’in doğruysa kısmı
jmp @2
@1:
; if’in yanlışsa kısmı
@2:

Şimdi de else-if örneği üzerinde duralım:

int g_a;
if (g_a > 0) {
/* .... */
}
else if (g_a < 0) {
/* ... */
}
else {
/* ... */
}

Bu işlemin sembolik makine dili karşılığı şöyle oluşturulabilir:

cmp dword [g_a], 0
jle @1
; g_a > büyükse sıfır
jmp @3
@1:
cmp dword [g_a], 0
jge @2
; g_a < 0
jmp @3
@2:
; g_a == 0
@3:

x86 işlemci mimarisinde bayrak (flag) yapısı

13:34 ,


     Dünya genelinde hala en yaygın işlemci mimarisi olan x86 işlemci ailesinin bayrak (flag) yapısını öğrenmemiz assembly dilinde kod yazmak isteyenler için birazda zorunlu bir konu başlığıdır. En basitinden bir for döngüsünü yazabilmek için flag değerine başvurmamız gerekmektedir. Bayrak (flag) yazmaçları CPU'nun çalışmasını (real/protected mod gibi) belirlediği gibi çalışma sırasındaki durumlarını da öğrenmemizi sağlayan yazmaç değerleridir. İşlem sonucu 0 ise şunu yap şuraya dallan gibi işlemleri gerçekleştirebilmemiz için öğrenmemiz gerekmektedir. Şimdi sırası ile yazmaçları ve ne işe yaradıklarına kısaca bakalım;

Direction flag (DF)

Bir tane kontrol bayrağı (Control Flag) bulunmaktadır. Bu bayrak Direction flag (DF) bayrağıdır. String komutları denilen bir grup makine komutu bu bayrağa bakarak işlemin yönüne karar vermektedir (sağdan sola mı yoksa soldan sağa doğru mu). STD ( set direction flag ) ile set (yani 1) ve CLD (clear direction flag) ile reset (yani 0) işlemi yapılır. 

Durum bayrakları (Status Flag)

Aşağıda görüleceği üzere altı tane durum bayrağı (Status Flag) bulunmaktadır. Bunlar;

  • Carry flag (CF)
  • Parity flag (PF) 
  • Auxiliary carry flag (AF) 
  • Zero flag (ZF)
  • Sign flag (SF)
  • Trap flag (TF)
  • Interrupt flag (IF)
  • Overflow flag (OF)



Carry flag (CF)

İşaretsiz tamsayılar üzerinde işlemler yapılırken işlem sonucunda taşma ya da borç oluşursa bu bayrak set edilmektedir.

MOV         AH, 3F
MOV         AL, F4
ADD         AH, AL

Burada 3F ile F4 toplandığında sonuç 8 bite sığmamaktadır. Bu nedenle taşmadan dolayı carry flag set edilmiştir. İşaretsiz sayıların çıkartılması durumunda borç oluşursa da bu bayrak set edilmektedir. CF bayrağı işaretsiz düzeydeki çıkartma işleminde borç oluşuyorsa set edilir, borç oluşmuyorsa reset edilir.

MOV         AH, 3F
MOV         AL, F4
SUB         AH, AL

Yukarıda 8 bitlik çıkarma işlemi yapılmıştır. İşaretsiz düzeyde AH’taki değer AL’deki değerden (işaretsiz olarak) daha küçük olduğu için borç oluşur. Dolayısıyla işlem sonucunda CF bayrağı set edilecektir.

Parity flag (PF) 

Bir işlemin sonucundaki değerde 1 olan bitlerin sayısı çift ise PF bayrağı set (1) edilmektedir, Tek ise reset (0) edilmektedir. PF bayrağı “parity” denilen hata kontrol (error check) mekanizması için düşünülmüştür.

Auxiliary carry flag (AF) 

Bu bayrak 3’üncü bit'ten 4’üncü bit'e taşma oluşmuşsa set edilir, taşma yoksa reset edilir. Yani bu bayrak düşük anlamlı 4 bitteki taşmaya bakmaktadır.

Zero flag (ZF)

Son yapılan işlemin sonucu sıfır ise bu bayrak set edilir, sıfır değilse reset edilir. Örneğin:

MOV         EAX, 1
DEC         EAX

Buradaki DEC makine komutu EAX yazmacının içerisindeki değeri 1 eksiltir. EAX yazmacındaki değer 1 olduğuna göre DEC komutundan sonra işlem sonucu sıfır olduğu için ZF bayrağı set edilmektedir.

Sign flag (SF)

Bu bayrak işlem sonucunda elde edilen değerin en soldaki bitini (işaret bitini) tutar. Başka bir deyişle işlem sonucunda elde edilen değerin işaret bit'i (en soldaki biti) 0 ise bu bayrak reset edilir, 1 ise set edilir. Örneğin bu bayrak sayesinde biz son yapılan çıkarma veya işaretli toplama işlemden elde edilen değerin negatif olup olmadığını anlayabiliriz. Ayrıca CMP işleminden sonra kullanıldığında sayının büyük ya da küçük olup olmadığı kontrol edilebilir.

Trap flag (TF)

Trap flag debuggerlar için en önemli bayraktır. Set edilmişse (yani 1 değerinde ise) işlemci her bir makine komutundan sonra Single Step diye bilinen 1 numaralı kesmeyi çağırır. Set edildiğinde EI (Enable Interrupt); reset edildiğinde DI (Disable Interrupt) değerindedir. 

Interrupt flag (IF)

INTR girişini kontrol eder. Böylece IRQ kesmelerini kapatır ya da açar. 1 ise aktif 0 ise pasif durumdadır. Bu durumda iken gelen kesmelere cevap verilmez. Pasif durumdayken NMI (Non Maskable Interrupt) haricindeki bütün kesmeler iptal edilir. Reset etmek için CLI, set etmek için ise STI komutları kullanılır. Reset işleminden sonra DI (Disabled Interrupts), set edince EI (Enabled Interrupts) değerini alır. 

Overflow flag (OF)

Bu bayrak sıklıkla CF bayrağı ile karıştırılmaktadır. OF işaretli tamsayılarda bir taşma ya da borç oluştuğunda SET edilmektedir. Eğer bir işleme sokulan sayıların en soldaki bitleri (yani işaret biti) aynı ise fakat işlem sonucunda elde edilen değerin en soldaki biti bunlardan farklı ise OF set edilir aynıysa reset edilir. (İşleme sokulan sayıların işaret bitleri farklı ise OF bayrağının her durumda reset edildiğine dikkat ediniz.) Örneğin:

mov eax, 0xFFFFFFF0 ;       1111 1111  1111  1111 1111  1111 1111 0000
mov ebx, 0x000000FF ;       0000 0000 0000 0000 0000 0000 1111 1111
add  eax, ebx                 ; (1) 0000 0000 0000 0000 0000 0000 1110 1111

Yukarıdaki add işleminde OF bayrağı reset edilecektir. Çünkü sayıların işaret bitleri farklıdır. Ancak CF bayrağının set edileceğine dikkat ediniz. Örneğimizdeki 0xF0000000 sayısı işaretli olarak 10’luk sistemde -16’dır. 0x000000FF ise işaretli olarak 10’luk sistemde +255’tir. Toplama işleminin sonucunda 10’luk sistemde 239 sayısı elde edilecektir. Toplama işleminde işaretli düzeyde bir taşma olmadığına dikkat ediniz. 

Fakat örneğin:

mov eax, 0xFFFFFFF0 ;     1111 1111 1111 1111 1111 1111 1111 0000
mov ebx, 0x80000000 ;     1000 0000 0000 0000 0000 0000 0000 0000
add eax, ebx                 ;     (1) 0111 1111 1111 1111 1111 1111 1111 0000

Buradaki add komutunda sayıların işaret bitleri (yani en soldaki bitleri) 1’dir (yani aynıdır). Fakat işlem sonucunda elde edilen değerin işaret biti 0’dır. Bu durumda OF set edilecektir. (Ayrıca CF bayrağının da set edileceğine dikkat ediniz.) Pekiyi OF bayrağının programcı için anlamı nedir? OF bayrağı sayıların işaretli (signed) olduğu fikriyle işleme sokulması durumunda işlem sonucunda işaretli taşma ya da borç oluştuğunda set edilmektedir. OF bayrağı ile CF bayrağının birbirlerine benzediğine dikkat ediniz. CF bayrağı sayıların işaretsiz kabul edildiği durumda taşma ya da borç oluştuğunda set edilirken OF bayrağı sayıların işaretli kabul edildiği durumda taşma ya da borç olduğunda set edilmektedir.



Harvard & Von Neumann Mimarileri

Harvard mimarisi, veri ve komutların Merkezi İşlem Birimine ( MİB veya CPU ) giden kanallarının ayrılması ile oluşturulmuş bilgisayar tasarımıdır. Von Neumann mimarisi, veri ve komutları tek bir yığın (depolama) biriminde bulunduran bilgisayar tasarımıdır.



Von Neumann mimarisi

Verilerin ve program kodlarının aynı hafıza birimi üzerinde bulunduran tasarımdır. Von Neumann mimarisinin temel tasarımı aşağıdaki gibidir:


"von Neumann mimarisi" isim olarak John von Neumann'ın 1945 tarihli makalesine dayanır. Bellek ile Merkezi işlem biriminin (MİB) ayrılması von Neumann dar geçidi olarak bilinen soruna yol açmıştır. Bu sorun MİB ile bellek arası veri taşıma hızının, bellek miktarına göre çok düşük olmasından kaynaklanmaktadır. Bu nedenle, CPU zamanın büyük çoğunluğunu bellekten istenilen verinin gelmesini beklemekle geçirir. Son yıllarda CPU'ların hızları ile bellek erişim hızlarının arasındaki farkın açılması ile bu sorun daha da büyümüştür. Sorunu hafifletmek adına cache memory ve branch prediction geliştirilmiştir. von Neumann mimarisinin dar geçit sorunu dışında, en olumsuz yanı ise hatalı yazılımların (buffer overflow gibi) kendilerine, işletim sistemine ve hatta diğer yazılımlara zarar verebilme olasılığıdır.



Harvard mimarisi

Harvard mimarisi, ismini ilk kez bu mimariyi kullanan bilgisayar Harvard Mark I'den almıştır. Bu mimariyi kullanan makinalar, veriler ile komutlar arasında herhangi bir köprü bulundurmazlar. Veri adresi ile program (komut) adresinin adresleme boyutları farklıdır. Harvard mimarisinin temel tasarımı aşağıdaki gibidir:


Günümüz bilgisayarlarında tam anlamıyla kullanıldığı söylenilemez. Yine de Von Neumann mimarisi ve Harvard mimarisinden ortak özellikler günümüz teknolojisinde kullanılmaktadır.


Von Neumann vs Harvard


  • Von Neumann mimarisinde, işlemcinin doğası gereği ya komutlarla ya da verilerle uğraşmaktadır. Çünkü ikisi de aynı belleği paylaşmaktadır. İkisinin aynı anda olması durumu söz konusu değildir. 
  • Harvard mimarisini kullanan bir bilgisayarda ise komut ve veriler ayrı tutulduğu için, işlemci aynı esnada hem komutları değerlendirip hem verileri işleyebilir. Bir önbelleğe de gerek yoktur. Bu Harvard mimarisine bir avantaj sağlasa da; Von Neumann mimarisinde komutlar verilerle bir tutulduğundan, program kendi kendine değişim gösterebilir. 
  • Harvard mimarisinde komutlar ile veriler arasında bir kanal yoktur bu yüzden kodların içine veri gömülmüş programlar çalıştırılırken veya kendi kendine değişim gösterilecek programlar için Von Neumann mimarisi temel alınır.
  • Bellek adresleri açısından Harvard mimarisi iki ayrı adres kullandığından; boş komut adresi boş veri adresinden de farklı olacaktır. Von Neumann mimarisinde ise ikiside aynı adresi paylaşır.

Değiştirilmiş Harvard mimarisi

Değiştirilmiş Harvard mimarisi, Harvard mimarisindeki veri/komut bağlantısının eksikliğini gidermesi amacıyla yapılan tasarımsal düzenlemelere verilen isimdir. İşlemci halen veri ve komut erişimine de sahip olsa da aralarında bağlantılar mevcuttur. Bu düzenlemelerden en önemlisi aynı bellek adresi tarafından desteklenen iki ayrı önbellek kullanmasıdır. Biri komutları, biri verileri tutar. Önbellekte işlem yapılırken Harvard mimarisinin, asıl bellekte işlem yapılırken Von Neumann mimarisinin uygulandığını söyleyebiliriz. Günümüzde kullanılan yeni bilgisayar teknolojilerinde buna benzer mimariler sıkça kullanılmaktadır fakat isimlendirilmesinde ne Von Neumann ne de Harvard demek doğru değildir. Ayrıca komutları, okunabilir verilermiş gibi göstermek de değiştirilmiş Harvard mimarisine örnek gösterilebilir. 


Matematik İşlemci Nedir?


İlk üretilen mikroişlemciler (örneğin 8 bitlik işlemciler ve 16 bitlik işlemciler) yalnızca tamsayı işlemleri yapabiliyordu. Çünkü gerçek sayılarla işlemler için büyük mantık devreleri gerekiyordu. O zamanın teknolojileri buna fazlaca müsait değildi. O yıllarda matematiksel işlemler aslında arka planda tamsayı işlemleriyle, yani bir kod çalıştırılarak (başka bir deyişle fonksiyon çağrılarak) yapılıyordu. Örneğin 80’li yıllarında başında 8086 için zamanlarında aşağıdaki gibi bir kod yazmış olalım:

double a = 3.4, b = 67.8, c;
c = a + b;

İşte C derleyicileri bu tür işlemleri kendi kütüphane fonksiyonlarınu kullanarak adeta aşağıdaki gibi yapıyorlardı:

double a = 3.4, b = 67.8, c;
c = fadd(a, b);

Tabii gerçek sayı işlemlerinin tamsayı aritmetiğiyle emülasyon yoluyla yapılması yavaşlığa yol açıyordu. İşte Intel işlemleri hızlandırmak için 8087 isminde, 8086 işlemcisi ile bağlanarak koordineli çalışacak bir matematik işlemci (math coprocessor) de tasarladı. 8087 matematik işlemcisi gerçek sayı işlemlerini donanım yoluyla, yani elektrik devreleriyle yapıyordu. Böylece bu eski günlerde noktalı sayılarla bir işlem yaptığımızda eğer sistemimizde matematik işlemci yoksa bu işlemler emülasyon yoluyla, eğer sistemimizde matematik işlemci varsa matematik işlemcinin devreleriyle yapılmaktaydı. Intel 286’yı çıkartınca matematik işlemcisini de 80287 ismiyle güncelledi. Sonra 80386 çıktığında matematik işlemci 8037 oldu. İşte nihayet 80486 DX modeliyle birlikte artık Intel matematik işlemciyi de ana işlemciyla aynı entegre devre içerisine yerleştirmeye başladı. Bugünkü kullandığımız Intel işlemcilerinde yine matematik işlemci ayrı bir birim olarak vardır fakat bunlar aynı entegre devrenin içerisindedir. Intel’in matematik işlemci sistemi şöyle çalışmaktadır: Ana işlemci (yani tamsayı işlemcisi) komutu çektiğinde (fetch) onun başındaki byte’a bakar. O byte özel bir değerdeyse (bu tür byte’lara Intel terminolojisinde "prefix" denilmektedir). O komutun gerçek sayı işlemi yapan makine komutu olduğunu anlar. Komutu matematik işlemciye verir. Onu artık yardımcı işlemci çalıştırır. Intel bu konuda zaman içerisinde bazı optimizasyonlar yaptıysa da temel çalışma biçimi hala böyledir. Artık günümüzde tasarlanan yeni işlemciler kendi içlerinde gerçek sayı işlem birimini de içeriyorlar. Yani yeni tasarımlarda artık ayrı bir matematik işlemci diye kavram yoktur. Tasarımcı zaten işlemciyi gerçek sayı işlemlerini de yapacak biçimde tek parça olarak tasarlamaktadır. 

Notlar1: Bugün için hala küçük mikrodenetleyicilerin ve mikroişlemcilerin matematik işlemci modülleri yoktur. (Örneğin Microchip’in PIC16  mikrodenetleyicilerinin matematik işlemci birimleri yoktur. Bu mikrodenetleyicilerde noktalı sayılarla işlemler yine emülasyon yoluyla yapılmakatadır.)

Notlar2: Noktalı sayılarla işlemler özellikle IEEE 754 gibi kayan noktalı (floating point) formatlar zahmetlidir. Bu nedenle küçük mikrodenetleyicilerde programcılar sabit noktalı (fixed point) formatları tercih etmektedir. Çünkü sabit noktalı formatlarla işlemler tamsayılarla çok kolay emüle ederek halledebilmektedir.

Notlar3: Bugün modern kapasiteli mikroişlemcilerin hemen hepsi IEEE 754 kayan noktalı formatı kullanmaktadır. Kayan noktalı formatlar daha dinamik olduğu için daha verimlidir. Bu formatlarda sayı nokta yokmuş gibi ikilik sistemde saklanır. Sonra noktanın yeri sayı içerisinde bazı bitlerde tutulmaktadır.

Koşulsuz Jump Komutları

07:20

Jump komutları sembolik makine dillerinin mutlaka bilinmesi gereken komutlarındandır. Bazı işlemci ailelerinde bu komutlara branch (dallanma) komutları denilmektedir. Jump komutları olmadan yüksek seviyeli dillerdeki if, switch, while for gibi deyimler gerçekleştirilemez. Jump komutları koşulsuz (unconditional) ve koşullu (conditional) olmak üzere ikiye ayrılmaktadır. Koşulsuz jump komutları C’deki goto deyimi gibidir. Koşulsuz olarak EIP yazmacını belli bir değere çeker. Koşulsuz jump komutlarının doğrudan (direct) ve dolaylı (indirect) biçimleri de vardır. Koşulsuz jump komutları pek çok işlemcide olduğu gibi göreli uzaklık değerini operand olarak alır. Doğrudan koşulsuz jump komutlarının son byte’larından sonraki ilk byte göreli uzaklık için sıfır orijini belirtir. Negatif uzaklık "yukarıya", pozitif uzaklık "aşağıya" jump yapılacağı anlamına gelmektedir.

Koşulsuz doğrudan jump komutları operand olarak "göreli uzaklık" miktarını almaktadır. Göreli uzaklıkların sembolik makine dili programcısı tarafından hesaplanması çok zordur. Assembly derleyicileri etiket (label) yöntemi ile bu yükü bizim üzerimizden almaktadır. Sembolik makine dillerinde JMP komutlarının yanına bir etiket ismi verilmektedir. Sembolik makine dili derleyicileri de jmp komutunun sonundan o etiketin bulunduğu uzaklığı hesaplayarak makine komutu oluşturur.

Pekiyi jump komutları operand olarak neden mutlak adres yerine göreli uzunluk almaktadır? Çünkü bu sayede biz kodu bellekte başka yere yüklesek bile o jump komutları yine aynı yere atlamayı sağlayacaktır. Koşulsuz jump komutlarının 8 bit, 16 bit ve 32 bit göreli uzunluk alan biçimleri de vardır. Tabii programcı özel bir belirleme yapmakdıktan sonra derleyici zıplama miktarını hesaplayarak en uygun jump komutunu üretir. Intel sisteminde 8 bit göreli uzunluk alarak yapılan jump işlemlerine "short jump", 16 bit ve 32 bit göreli uzunluk alarak yapılan jump işlemlerine de "near jump" denilmektedir. Pek çok sembolik makine dili derleyicisinde short ya da near anahtar sözcüğü ile bunu isterse programcı belirleyebilmektedir. Örneğin:

. . .
jmp near NEXT ; ...
NEXT:
    call _ExitProcess@4
. . .

Eğer "short" ya da "near" anahtar sözcüklerinin hiçbiri kullanılmamışsa default durumda derleyici en uygun jump komutunu hesaplamaktadır. 32 bit sistemde 16 bit göreli uzunluk için komutta 0x66 ön eki gerekmektedir. Intel’deki koşulsuz jump komutlarının yazmaç ve bellek operandı alan biçimleri de vardır. Yazmaç operandı alan biçimi mutlak jump işlemi yapar. Yani yazmacın içerisindeki değer göreli uzunluk değil, bizzat jump edilecek yerin adrestir. Örneğin:
mov EAX, 0x123469
jmp EAX
Burada koşulsuz olarak 0x123469 adresine jump yapılmaktadır. Koşulsuz jump komutlarının bellek operandı alan biçimleri de vardır. Bu durumda önce o bellek bölgesindeki 32 bit değer çekilir. Oraya mutlak jump uygulanır. Örneğin:
jmp [EAX]
Burada EAX yazmacının içerisinde bulunan adresten 32 bit adres bilgisi çekilerek o adrese dallanma yapılmaktadır. Intel mimarisinde jmp komutlarının operandları yazmaç ya da bellek ise böyle jump’lara “indirect jump” denilmektedir. Örneğin:

[BITS 32]
SECTION .data
   ptrJmp dd EXIT
SECTION .text
global _start
extern _ExitProcess@4
_start:
jmp [ptrJmp]
EXIT:
xor eax, eax
push eax
   call _ExitProcess@4
Burada EXIT etiketinin adresi ptrJmp adresindeki bellek bölgesine yazılmıştır:

SECTION .data
jmpPoint dd EXIT
Sonra oraya aşağıdaki kod parçasında görüleceği üzere dolaylı jump işlemi indirect jump yapılmıştır:
jmp [jmpPoint]

 Bir sonraki yazımız dallanma konusunda daha önemli bir parçayı oluşturan koşullu dallanma konusunda olacaktır.