Assembly etiketine sahip kayıtlar gösteriliyor. Tüm kayıtları göster
Assembly etiketine sahip kayıtlar gösteriliyor. Tüm kayıtları göster

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.

Ö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;
 

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.



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.

Fonksiyona yazmaç (register) yoluyla parametre aktarımı

05:07 ,
Eğer tüm işlemlerini assembly dili ile kendiniz yazacaksanız bu durumda genel amaçlı tüm register'ları fonksiyonlarınıza parametre aktarma için kullanabilirsiniz. Aşağıdaki örnekte eax, ebx, ecx register'ları bu amaçlarla kullanılmıştır.


AddProc fonksiyonu işini yaparken parametrelerin nerede olduğunu bildiği için o yazmaçlardan değerleri alarak işlemini gerçekleştirmiş ve ret komutu ile geri dönülmüştür. Compile işlemi nasm ile link işlemi gcc ile gerçekleştirilmiş ve kod çalıştırıldığında sorunsuz bir şekilde çalıştığı görülecektir. Tabii ki bu şekilde kod yazma ne portable ne de başkası tarafından kullanılabilir bir kod örneğidir. ABI kavramıda zaten bu tür durumlar için ortak bir standart oluşturmak için belirlenmiştir.


Bir Program CPU Tarafından Nasıl Çalıştırılma Durumuna Getirilir?

04:05 ,

Bir program yazılıp derlendikten sonra nasıl çalışma aşamasına getirilmektedir? Eğer çalıştırma ortamı bir işletim sisteminin yüklü olduğu bir ortam değilse bizim programı bir biçimde reset vektöründen itibaren o ortamın belleğine (Ram) yüklememiz gerekir. Tipik olarak mikrodenetleyici (microcontroller) ile çalışmalar bu biçimde yürütülmektedir. Bir mikrodenetleyicinin içerisinde bir CPU'nun yanı sıra bir RAM ve EEPROM bellekte bulunmaktadır. Bu EEPROM belleğe mikrodenetleyicinin program belleği denilmektedir. Bu ortamlarda programcı programını başka ortamlarda yazıp derler ve "programlayıcı" denilen bir devre yoluyla mikrodenetleyicinin içerisine yerleştirir. Mikrodenetleyici dünyasının dışında genellikle bir işletim sisteminin üzerinde çalıyor durumdayızdır. (Gelişmiş mikrodenetleyicilerde işletim sistemi yüklenerek kullanılabilmektedir.)

Biz bir işletim sisteminin üzerinde çalışıyorsak programı yüklemek ve onu CPU'ya işletecek hade vermek tamamen artık işletim sisteminin bir görevidir. İşletim sistemlerinin programı yükleyip çalışır hale getiren kısmına kavramsal olarak yükleyici (loader) denilmektedir. İşletim sistemi üzerinde çalışılan bir ortamda tipik olarak program bir editörle (ya da IDE ile) yazılır. Derleyici ile derlenerek amaç dosya (object file) oluşturulur. Bu amaç dosya da bağlama (link) işlemine sokularak çalıştırılabilen (executable) dosya elde edilir.

Amaç dosyaların (object file) ve çalıştırılabilen dosyaların (executable file) içerisinde ne vardır?

Çalıştırılabilen bir dosyanın içerisinde en azından derlenmiş ve ikilik sisteme dönüştürülmüş kodlar ve bu kodların kullandığı data'lar bulunmak zorundadır. Tabii bunların dışında çalıştırılabilen dosyaların içerisinde dosyanın işletim sistemi tarafından yüklenebilmesi için gereken başka bir takım metadata bilgiler de bulunmak zorundadır. Örneğin yükleyici programı RAM'e yükledikten sonra IP (ya da PC) yazmacının ilk değerini nasıl verecektir. Bilindiği gibi C'de programın akışı main gibi bir fonksiyondan başlar ya da biz dışarıdan link edilirken nerden başlayacağını ayarlayabiliriz. İşte program akışının başlatılacağı adrese "enrty point" denilmektedir. Bu adres çalıştırılabilen dosyanın içerisinde belli bir yerde bulunur. Yükleyici de programı yükledikten sonra IP (ya da PC) yazmacına bu değeri atar. Sonra da program kendi kendine çalışmaya devam eder. Tabii çalıştırılabilen dosyaların içerisinde "entry point" dışında daha gerekli olan pek çok yükleme bilgisi de bulunmaktadır. O halde  çalıştırılabilen bir dosya kabaca şu biçimde çalıştırılma durumuna getirilmektedir:

  1. Yükleyici çalıştırılabilen dosyanın başlık kısımlarına bakarak onun içindeki kod ve data bilgilerinin nerelerde olduğunu anlar. Çalıştırılabilen dosyanın kod ve data bölümlerini bellekte uygun bir yere yükler.
  2. Yükleyici çalıştırılabilen dosyanın başlık kısımlarından programın “entry point” adresini belirler ve CPU'nun IP (ya da PC) yazmacına kodun başlangıç adresini yükler. Sonra da CPU'yu serbest bırakır.
  3. CPU IP (ya da PC) adresinde gösterilen yerden itibaren talimatları işletmeye başlar.



Döndürme (Rotate) Komutları

02:18 ,

Döndürme işlemi için C/C++ dillerinde özel bir operatör bulundurulmamıştır. Döndürme işlemi ötelemeye benzemektedir. Ancak ötelemede kaybedilen bit döndürme işleminde diğer tarafı beslemede kullanılır. Örneğin döndürme işlemlerini özetle aşağıdaki resimde olduğu gibi gösterilebiliriz:


Döndürme işlemi bir sayının belli kısımlarının yer değiştirilmesi için kullanılabilmektedir. Ayrıca sayıyı ötelemek yerine döndürdüğümüzde biz onu yeniden ters döndürerek eski haline de getirebiliriz. Diğer özel bazı durumlarda da döndürme işleminden faydalanılmaktadır. Intel işlemcilerinde 4 tane döndürme (rotate) komutu vardır. Komutların ikisi carry’li döndürme için diğer ikisi de carry’siz döndürme için kullanılır:

Carry’li döndürmede sanki CF bayrağı sayının en yüksek anlamlı ekstra biti gibi davranmaktadır. Dolayısıyla döndürmeye o da dahil edilir. Örneğin sağa bir kez carry’li döndürmeyi yukarıdaki ilk resimde görüleceği üzere RCR ve ROR komutlarında gösterildiği gibi carry flag'de ötelemeye dahil edilmiştir. Rotate komutlarının biçimleri de tamamen öteleme komutları gibidir. Yani bir kez döndürme için ayrı bir makine komutu vardır. Birden fazla kez döndürme sabit bir değerle ya da CL yazmacıyla yapılabilmektedir. Komut uzunlukları da yine öteleme komutlarında olduğu gibidir. Örneğin bazı geçerli döndürme komutları şöyledir:

ror eax, 6
rcl eax, 9
rcr eax, cl

CF bayrağı her zaman (carry’li döndürme ve carry’siz döndürme durumlarında da geçerlidir.) son döndürmede kaybedilen biti tutar (ötelemede olduğu gibi). Ancak döndürme döndürülecek değerin bit uzunluğunun bir eksiğinden fazla olursa bu bayrak tanımsız durumda olur. OF bayrağı yine yalnızca bir kez öteleme söz konusu olduğunda etkilenir. SF, ZF, AF, PF bayrakları ise normal biçimde etkilenmektedir.

API ve ABI Kavramları Üzerine

Yazılımların tüm mimarilerde çalışması ve geriye doğru uyumlu (backward compatibility) olması en çok istenen özelliklerdir. Geliştirilen yazılımın belirli bir dağıtım veya mimariye bağımlı olmayıp, taşınabilir olması büyük kolaylık sağlayacaktır. Sistem seviyesinden bakıldığında taşınabilirlikle ilgili 2 farklı özellik bulunması gerekir. Bunlar;
  • Application Programming Interface (API)
  • Application Binary Interface (ABI)
Şimdi sırasıyla bu kavramları inceleyelim;

ABI ( Application Binary Interface )

ABI, yazılım bileşenleri arasında belirli bir mimari için amaç dosyalar (object file) arasında arayüz  (interface) tanımlamaktadır. ABI demek ayrı ayrı derlenmiş modüllerin bir arada çalışabileceklerine dair alt seviyeli ve detaylı teknik kurallar bütünüdür. ABI uygulama bileşenleri arasında makine kodu seviyesinde uyumluluğu sağlar. Bu uyum korunduğu müddetçe aralarında etkileşim bulunan yazılım bileşenlerinin arka planı değişse de yeniden derlenmeye ihtiyaç duymaksızın eskisi gibi kulanmaya devam ederler.

  • Fonksiyonların nasıl çağrılacağı (calling convention), 
  • Parametrelerin nasıl geçirileceği, 
  • Yazmaçların kullanım şekilleri,
  • Sistem çağrılarının gerçekleştirilme biçimi, 
  • Amaç dosyaların bağlanması (linklenmesi), 
  • Amaç (object dosya) formatı gibi konular ABI kavramı içerisinde değerlendirilebilir. 
Yazılım geliştirme sürecinde ABI kavramı çok karşımıza çıkmaz. Kullanılan geliştirme araçları hedef platform için belirlenen ABI kurallarına uygun kod üretirler. Alt seviye işler yapanlar için bu genelleme doğru değildir. Sistem düzeyinde kod geliştirmek isteyenlerin üzerinde çalıştığı ve diğer çalışmasını istediği sistemlerin ABI standartlarını bilmesi gerekir.


API ( Application Program Interface )

API, uygulamaların kaynak kod seviyesinde birbirleriyle iletişim kurabilmelerine imkan sağlayan, önceden kararlaştırılmış arayüzler (interface) olarak tanımlanabilir. Genellikle her bir API, daha karmaşık ve alt seviye detaylar içeren bir sürecin, çeşitli arayüzlerle (fonksiyon çağrıları gibi) soyutlanmasını sağlar. Bu şekildeki bir soyutlama üzerinden kullanılan API'yi hizmet olarak veren yazılım bileşenleri güncellense ve alt tarafta yapılan işlemlerle ilgili yöntemler değiştirilmiş bile olsa, API seviyesinde aynı arayüz sağlandığı müddetçe bu API'yi kullanan uygulamalar için bir değişiklik yapılmasına gerek olmayacaktır.




Kısaca bir yazılımın, kullandığı kütüphanelerin,servislerin sonraki versiyonlarında herhangi bir değişikliğe gitmek zorunda kalmadan problemsiz çalışabilmesini sağlayan bir arayüz tanımlanması API kavramının ürünüdür.

32 Bit Windows Sistemler için "Hello World" Assembly Programı


Tamamen sembolik makine diliyle yazılmış ekrana "Hello World" yazısını çıkartan temel assembly  programı Windows'ta şöyle yazılabilir:


  • Aşağıdaki resimde görüleceği gibi ilk adım olan yazılan kodu 1 numaralı komutta olduğu gibi nasm ile derlenebilir:


Buradan ürün olarak HelloWorld.obj dosyası elde edilecektir. Komuttaki –f win32 seçeneği amaç kodun 32 bit Windows sistemleri için COFF formatında olmasını sağlar.

  • Yukarıdaki resimde görüleceği gibi 2 numaralı komutta olduğu gibi Microsoft’un link.exe programı ile link edilmelidir.

Komuttaki /entry:start seçeneği programın başlangıç noktasını belirlemek için kullanılır. HelloWorld.asm programının başlangıç noktası _start etiketinin bulunduğu yerdedir. Windows uygulamaları “GUI” ve “Console” olmak üzere ikiye ayrılmaktadır. Console uygulamalarında işletim sistemi programı yüklendiğinde bir console ekranını kendisi oluşturmaktadır. Console uygulaması için /subsystem:console seçeneği kullanılmalıdır. Link edilecek dosya HelloWorld.obj dosyasıdır. "Hello World" programında kernel32.dll içerisindeki çeşitli API fonksiyonları (sistem fonksiyonları) kullanılmıştır. Bu nedenle link aşamasına "kernel32.lib" isimli import kütüphanesinin dahil edilmesi gerekmektedir. Bu programda üç API fonksiyonu çağrılmıştır. Önce GetStdHandle API fonksiyonuyla (kodun içerisinde 1 numaralı kısım) console ekranının handle değeri elde edilmiş, sonra WriteFile API fonksiyonu (kodun içerisinde 2 numaralı kısım) ile oraya yazma yapılmıştır. Prosesin sonlanması için ExitProcess API fonksiyonuyla (kodun içerisinde 3 numaralı kısım) gerçekleştirilmiştir. C’nin standart exit fonksiyonu da zaten ExitProcess API fonksiyonu çağırır. Ve link işleminden sonra üretilen exe dosya çalıştırıldığında aşağıdaki gibi başarıyla çalıştığı görülebilir.





Assembly ile Sistem Fonksiyonu Çağırma [Linux]

06:50 , , , ,


Assembly programı içinden Linux sistem fonksiyonları çağrılırken uyulması gereken standart işlemler vardır. Linux System Call Table 'da görülebileceği gibi hangi register'ları set etmemiz gerektiği, parametreleri hangi register aracılığıyla göndermemiz gerektiği bilinmesi gerekmektedir. 80h kesmesinden sonra hangi fonksiyonun çağrılacağı bu registerlardaki değerler üzerinden belirlenmektedir.


Aşağıdaki örnekte ekrana "Hello world" yazan programı yazacağız:


Yukarıda görüldüğü gibi ;

  • eax'e hangi sistem fonksiyonu yazılacaksa onun numarası 
  • ebx'e birinci parametre olan file descripter değeri -stdout için 1
  • ecx'e yazılacak string'in adresi
  • edx'e ise  kaç karakter yazılacak ise onun uzunluğu girilmelidir.



Gerekli çağırma parametreleri girildikten sonra 80h kesmesiyle kernel mod'a geçiş yapar ve sistem fonksiyonları çağrılır.



Yukarıdaki kod derlenip,link edilip çalıştırıldığında ekrana Hello World yazısı çıkacaktır. Ekrana yazma işini sys_write sistem fonksiyonu ile stdout'a yazarak gerçekleştirilmiştir.

Assembly içinden C Fonksiyonu Çağırma [Linux]

03:23 , , , ,


Linux'te assembly içinden C fonksiyonu çağrılacak ise yapılması gereken temel işlemler aşağıdaki örnekte anlatıldığı gibidir:

  • Çağrılacak fonksiyon .text alanında extern bildirimi yapılması gerekmektedir. 
  • Fonksiyon çağrılmadan önce ise parametreler soldan sağa stack'e push edilmesi gerekmektedir.



sample.asm isimli dosya içine yukarıdaki gibi kod yazıldıktan sonra :
  • nasm -f elf32 sample.asm ile object dosya oluşturulur
  • gcc -m32 -o sample sample.o ardından gcc ile çalıştırılabilir dosya oluşturulur.



  • gcc -m32 veya -m64 flaglerini kullandığımızda hata veriyorsa gcc için kurulum yapılması gerekmektedir.




  • apt-get install gcc-multilib kurulumu ile 32 - 64 bit derleme işlemlerini gerçekleştirebiliriz.




sample isimli çalıştırılabilir dosyamız oluşturulmuş oldu. İleride gcc'yi de devre dışı bırakacağız ve doğrudan linker ile elf dosyamızı oluşturacağız.

Assembly' de yazılıp derlenen bir fonksiyonu Windows'ta çağırma

Windows sistemlerde assembly' de yazdığımız bir fonksiyon C içerisinden aşağıdaki gibi çağrılır;


Derleme işlemi yapıldıktan sonra oluşan .obj dosyasını Visual Studio'da açılan C projesine dahil edelim.


 Aşağıdaki gibi örnek bir kod derleme&link işleminin ardından çalıştırıldığında hatasız bir şekilde çalıştığı gözükecektir.


Kısaca fonksiyonun ne yaptığını anlatırsak:
  1. Parametreler stack'e sağdan sola doğru aktarılmış yani fonksiyonla gönderilen iki parametre stack'in üstüne sırasıyla konulmuş.
  2. EBP değeri stack'e konulmuş /*geri dönüş yapıldığında akışın sorunsuz devam etmesi için*/ 
  3. ESP değeri EBP değerine atanmış
  4. EDX'e sağdan ilk parametrenin adres değeri atanmış
  5. EDX'in içinde bir adres bulunmakta bu adresin gösterdiği değer EAX'e atanmış. ( Yani ilk parametrenin değeri EAX'e atanmış )
  6. EAX içindeki değer EBP-4'e konulmuş. ( birinci parametre'nin gösterdiği adresteki değer artık burada )
  7. İkinci parametrenin adresi ECX'e atanmış
  8. İkinci parametrenin değeri EAX'e atanmış
  9. İkinci parametre' nin değeri EDX (edx'in içinde 1. parametrenin adresi var) aracılığıyla 1. parametrenin gösterdiği yere atanmış. (ilk değişim gerçekleşti)
  10. Stack'te saklanan birinci parametre'nin değeri de [ EBP-4 ]  EAX yazmacına atandı. 
  11. ECX yazmacında ikinci parametrenin adresi olduğuna göre şimdi de EAX (birinci parametrenin değeri bunun içindeydi) yazmacındaki değer ikinci parametrenin gösterdiği yere aktarılmış oldu.
  12. EBP'nin değeri de ESP'ye aktarıldı. 
  13. Ardından stack'te tutulan eski ebp değeri de pop ile geri alınmış oldu.  /*geri dönüş yaptığında akışın sorunsuz devam etmesi için*/


Not1: Yukarıdaki işlemlerden anlayacağınız üzere stack'i düzenleme işi çağrılan (callee) fonksiyon tarafından gerçekleştirilmiştir. Fonksiyon çağrılarında stack düzenlemesi önemli bir konudur ve ileride daha detaylı anlatılacaktır.

Not2: Bu swap işlemi daha basit şekilde yazılabilmektedir. Bu yazıda en kısa biçimde kod yazılmaya çalışılmamıştır. 

32 Bit Linux Sistemler için "Hello World" Assembly Programı


Linux sistemlerinde assembly diliyle "Hello World" programının kodu aşağıdaki gibidir.


1 numaralı kod parçası ekrana yazı yazan,
2 numaralı kod parçası ise programdan çıkış işleminin yapıldığı kısımdır.

Linux’taki merhaba dünya programı Windows’takine göre daha sadedir. Bunun nedeni Linux’ta sistem fonksiyonlarının dinamik kütüphaneden değil kesme (interrupt) yoluyla çağrılıyor olmasıdır. Bu nedenle ekrana yazı yazdırmak için "sys_write", programı sonlandırmak için de "sys_exit" isimli sistem fonksiyonu çağrılmıştır. Sistem fonksiyonları çağrılmadan önce onların parametreleri yazmaçlara yerleştirilmektedir. Her sistem fonksiyonunun bir numarası vardır. Çağrılacak sistem fonksiyonunun numaraları 32 bit sistemde EAX yazmacına yerleştirilir. Sonra sırasıyla EBX, ECX, EDX yazmaçlarına da fonksiyonun parametreleri yerleştirilmektedir. Örneğin void sys_exit(int exitcode) fonksiyonu şöyle çağrılmıştır; Yukarıdaki iki numaralı kod parçasında görüleceği üzere eax yazmacına 1 atanması sys_exit fonksiyonunu temsil etmekte, ebx yazmacına 0 atanması ise exitcode parametresine 0 değerinin gönderildiğini ve int 80h kesmesi ile de fonksiyonun çağrıldığını (call) temsil eder. Şimdilik bu kadar bilinmesi yeterlidir. Linux’taki sistem fonksiyonlarının çağrılma biçimi daha sonra detaylı biçimde anlatılacaktır.


1. kısım derleme işlemidir. Derleme işlemi için nasm programı kullanılmıştır.
2. kısım link işlemidir. Linker ise GNU projesi kapsamında geliştirilmiş olan Linux’un temel linker programı "ld" isimli programdır. ld programı kullanılırken –o seçeneği ile çalıştırılabilen dosyaya isim verilmiştir. Eğer link sırasında çalıştırılabilen dosyaya isim verilmezse default olarak a.out ismi kullanılır. Ayrıca Linux sistemlerinde "ld" programı ile link işlemi yapılırken "entry point" verilmediğine dikkat ediniz. "ld" linker’ı default olarak _start adresini "entry point" olarak almaktadır. Link işlemi bittikten sonra yukarıda görüleceği üzere programımız başarıyla çalışmış ve ekrana Hello World yazısı yazdırılmıştır.