Professional Documents
Culture Documents
X86 NEDİR ?
X86 Intel’in ilk mikroişlemcilerinden olan 8086 ile ilgili programlama kurallarını ifade
eden bir tanımlamadır. Intel’in önemli özelliklerinden biri olan "geriye dönük yazılım
uyumluluğu" böyle bir tanımlamanın oluşmasını sağlamış.
Şöyle ki; 8086 mikroişlemcisi olan bir bilgisayar sisteminde hazırladığınız herhangi
bir assembly programı X86 uyumlu tüm bilgisayarlarda çalışır. 80286, 386, 486 SX
veya DX, Pentium, Pentium III, AMD nin 286, 386 işlemcilerinde veya Nexgen,
Cyrix’in aynı tür işlemcilerinde ve diğerlerinde bu program çalışacaktır. Bu demek
oluyor ki yukarıda saydığımız tüm mikroişlemciler ve uyumlu olan diğerlerinin ortak
bir yönü var. Bu ortak yönler ortak program kodların olması ve mikroişlemcilerinin
temel mimarileri birbirinin aynı olmasıdır.
Yeni bir mikroişlemci üretildiğinde, eski mikroişlemcilere uyumlu olması büyük bir
avantajdır. Öyle olmasaydı bugün dünyada %90 oranında PC, X86 uyumlu işlemci
kullanıyor olmazdı. Bu yüzden mikroişlemci pazarının bu büyük dilimini birkaç firma
paylaşıyor, aslan payını Intel, daha sonra AMD geliyor, Cyrix ve Nexgen gibi diğer
mikroişlemci üreten firmaların isimlerini pek duymuyoruz.
X86 tabanlı sistemlerin mimarisi birbirine benzer. Birde X86 tabanlı olmayan
sistemler vardır. Apple firmasının iMAC bilgisayarları, yada SUN Microsystems
firmasının sistemleri X86 tabanlı mikroişlemciler kullanmadıkları için bu tür
bilgisayarda çalışan programlar X86 tabanlı sistemlerde çalışmazlar.
Son zamanlarda mikroişlemcilerin hızları GHz’ler (giga hertz - ciga herz diye
okunur) mertebesine çıktığından assembly dilinin en büyük özelliği olan hızlılığı artık
pek popüler değildir. Kullanıcıya, 2GHz hızındaki bir CPU da hemen hemen her
uygulama aynı hızda çalışıyor gibi görünür. Buna rağmen C gibi yüksek düzey
programlama dilleri ile hazırlanan büyük projelerde işlemciyi çok fazla meşgul
edecek olan kod bölümleri assembly rutinleri çağrılarak yapılmaktadır.
NEDEN ASSEMBLY?
X86 Assembly dilini öğrenmek kolaydır ama bu dilde proje hazırlamak insanı çileden
çıkartabilir. Bu yüzden günümüzde X86 assembly dili yerine, daha çok endüstriyel
alanda kullanılan microcontroller chip’lerin assembly dili kullanılmaktadır. Bir
programcı assembly dilinde büyük uygulamalar hazırlamaz (genellikle). Bunun
nedeni üst düzey programlama dilleri olan delphi, C, C++, Pascal’a göre daha
dikkat gerektirir ve kod yazımı daha zordur. Programcı assembly dilinde proje
hazırlayacaksa kullandığı sistemin mikroişlemcisini ve hafıza haritasının yanında,
sistemin donanımında iyi bilmesi gerekir. Her şeyden önemlisi zamandır ki sadece
assembly dili ile PC uygulamaları geliştirmek iğne ile kuyu açmaya benzer.
Assembly alt düzey bir programlama dilidir yalnız alt düzey kelimesini yanlış
anlamayın, bu kelime bu dilin işlemciye ve hafızaya olan yakınlığını belirtir, üst
düzey diller ise daha çok programcıya yani insana yakındır.
Bu yüzden assembly dilinin özel kullanım alanları vardır. Kısaca bunları söyle
sıralayabiliriz.
1- Başlat menüsünden çalıştır’ı tıklayıp cmd programını çalıştırın. Şayet Windows 98 kullanıyorsanız
başlat menüsünden MSDOS komut istemini çalıştırmanız gerekiyor. Hala DOS işletim sistemini
kullanıyorsanız bilgisayarınızın power butonuna basmanız yeterlidir :) yani halihazırda komut isteminde
olan bir trex siniz.
2- Komut istemindeyken cd\ yazın ve root dizinine geçin burada kendinize md asm yazarak asm
adında bir klasör oluşturun ve cd asm komutu ile klasörün içine girin. Tabi ki klasör oluşturmak için bu
kadar eziyet çekmenize gerek yok, amacımız biraz nostalji. Maksat projemiz bir klasörde oluşsun.
3- Debug yazıp enter tuşuna basın. Resimdeki görüldüğü gibi debug programı çalıştırıldığında ekranın
solunda bir - simgesi görülür ve bu programla harikalar yapabilirsiniz :)
6- Klavyeden Enter tuşuna basın ve ekran görüntüsü 3. adımdaki gibi olunca g tuşuna basıp ardından
son olarak enter tuşuna tekrar basın.
Yukarıdaki ekran çıktısında görüldüğü gibi konsol ekranına MERHABA ASSEMBLY yazdırdık ve debug
programından çıkıp komut istemine geri döndük. Hala hevesiniz kırılmadıysa :) neler yaptığımızı tek-
tek açıklayalım.
Debug programı Microsoft’un tüm işletim sistemlerinde bulunan, hafıza ve CPU’nun içindekileri
görmemizi, düzenlememizi sağlayan bir programdır. Çok ilkel bir program olmasına rağmen beynimizin
derinliklerine PC’nin yapısını kazıyacak olan ve bir assembly programcısının bilgisayara ne şekilde
bakması gerektiği konusunda yardımcı olacak yegane programlardandır. En fazla 2 makale sonra
Debug programını kullanmayı bırakıp daha jan-jan lı debuger programları kullanacağız, şimdilik böyle
idare edelim.
Biraz önce Debug programının kendisi içinde kullanacağımız e (enter) ve g (go) komutlarını kullandık.
e komutu hafızaya kod girişi yapmamızı sağlar ve g komutu ise CPU’ya şu sihirli kelimeyi söylememizi
sağlar; "Ey CPU yazdığım bu kodları sırasıyla çalıştır bakayım". CPU programcının sadık kölesi
olduğundan aynen denileni yapar.
E komutu ile hafızaya giriş yaptığımızı söyledik, komut isteminin en solunda 152F:0100 gibi bazı
rakamlar gördünüz, buradaki 152F sizin bilgisayarınızda farklı olabilir, ama 0100 aynıdır. işte : simgesi
ile ikiye ayrılmış bu 8 rakam hafızanın adresidir. Kodlar hafızaya yazılır, silinir, değiştirilir fakat bu
süreçte değişmeyen tek şey hafıza adresidir. Adresler her zaman bizim onları doldurmamız veya
erişmemiz için hali hazırda beklerler, bunu evinizin adresine benzetebilirsiniz. Eve anne gelir baba gelir
arkadaş gelir bazen tatilde boş kalır yani evin içindekiler değişkendir ama ama ev adresi her zaman
sabit kalır, ta ki ev yıkılana kadar :)
E komutu ile bu örnekte hafızanın 152F:0100 adresi ile 152F:011B adresleri arasını makine kodları ile
doldurduk. Bunu yaparken kod yazacağımız adreste hali hazırda hangi kod’un olduğunu görme gibi bir
lükse de sahiptik, tesadüfen hepsi 00’dı. Makine kodları CPU’nun anlayacağı yegane kodlardır ve 2’lik
(binary) yada 16’lık (hexadecimal) gösterimle ifade edilirler. Zamanla binary gösterim biraz fazla yer
kapladığından hexadecimal gösterim benimsenmiştir. Daha sonra hexadecimal gösterimin anlaşılması
zor olduğundan sembolik kodlar ile yazılan Assembly programlama dili geliştirilmiştir. İnsan oğlu
rahatına pek düşkün olduğundan :) daha sonra, kod yazması ve anlaşılması daha kolay olan B
Programlama dilini geliştirmiş bu da yetmemiş C dilini geliştirmiştir. (Bu sıralamada Assembly
programlama dilide A oluyor tabi ki) Programlama dilleri ABC diye devam derken C harfini çok seven
programcılar buna ++ ve # gibi aksesuarlar takarak değişik varyasyonlar denemişler ve çokta başarılı
olmuşlardır :) Neyse lafı fazla uzatmadan ve "ey programcı geçmişini bilmeyen geleceğinden bir
haberdardır" diyerek konumuz geri dönelim.
X86 PC’lerin hafızası byte adreslenebilir diye bir programcı ata sözü vardır. Bu söz her adrese en fazla
1 byte’lık kod yazabileceğinizi anlatır. Bizde burada öyle yaptık zaten, toplam 28 hafıza konumuna
(memory location) kod yazdık ve göreceksiniz biraz sonra programı derleyip dosya haline getirince
dosya boyutu 28 byte olacak.
Debug’ın g komutu ise meşhur fetch-decode-execute sürecini yani hafızadan al getir - kodunu çöz -
icra et sürecini başlatır. İşte biz buna halk dilinde program çalışıyor diyoruz. Programı mikroişlemci
(CPU) çalıştırır ve programcı tarafından kısmen kullanılabilir. Bu süreç programcıları fazla ilgilendirmez,
daha çok donanımla alakalıdır, bu yüzden ayrıntıya girmiyorum. Ama çok istek gelirse bu şarkıyıda
sizler için söyleyebilirim :)
BU KODLARI ÇOK SEVDİYSENİZ GELİN BUNLARI SAKLAMAK İÇİN BİR PROGRAM DOSYASI
OLUŞTURALIM
Bu işlemi yapmak için öncelikle programda ne kadar kod kullandığımızı bilmemiz yani programın byte
cinsinden uzunluğunu bilmemiz gerekir. Kodları kendi elimizle teker teker girdiğimizden uzunluğunun
28 byte olduğunu biliyoruz. Emin olmak için 5. adımdaki şekilden faydalanınız. Dosya oluşturma işlemi
için ilk önce CPU’nun CX kaydedicisine (CX ne yahu, kaydedicide ne demek kardeşim dediğinizi duyar
gibiyim merak etmeyin bu terimler ileride hiç ağzınızdan düşmeyecek) 28’in hex. karşılığı olan 1C’yi
yazacağız. Daha sonra debug’ın N komutu ile dosyaya isim vereceğiz ve son olarak W komutu ile
harddiskimize kayıt yapacağız. Son olarak debug’tan çıkıp komut istemine geri döneceğiz ve
oluşturduğumuz programı çalıştıracağız. Lütfen aşağıdaki şekli inceleyiniz. (Yalnız komut istemi
penceresini kapattıysanız işiniz yaş 1.adıma geri dönmeniz gerekiyor çünkü kodlar hafızadan silinmiş
olabilir)
Meşhur DOS komutlarından olan "dir" ile tekrar bir nostalji yapıp dosyamızı görelim :)
Görüldüğü üzere 28 byte’lık merhaba.com adlı dosyamız oluşturuldu ve çalıştırılmaya hazır, o zaman
hemen çalıştıralım.
Programımız konsol ekranına MERHABA ASSEMBLY yazdı ve işletim sistemi tarafından sonlandırıldı.
Programımızı bir dosya haline getirdik, Microsoft’un işletim sistemlerinde 2 tür program dosyası
mevcuttur ve bunlar dosya ismini takip eden EXE ve COM uzantıları (file extensions) ile tanınırlar. Bir
burada COM olanından oluşturduk, endişelenmeye gerek yok EXE uzantılı dosya’da oluşturacağız ve
COM ile EXE arasında ne gibi farklar var onları da göreceğiz fakat bunlar bizim için henüz çok ileri
konular, sabretmek lazım :)
Bu programı bir sonraki makalemize kadar silmeyin, çünkü bir sonraki makalemizde bu dosyadan
faydalanıp sembolik kodlardan oluşan assembly diline geçiş yapacağız ve eksik kalan noktaları
tamamlayacağız. Siz bu arada boş durmayıp bu kodlar üzerinde oynamalar yapabilirsiniz özellikle 12.
koddan itibaren son koda kadar olanları değiştirin (son kod hariç) çünkü bunlar MERHABA ASSEMBLY
yazısının ASCII kod karşılığıdır. Bunun için ASCII kod tablosundan faydalanabilirsiniz. Başka bir
programlama dili biliyorsanız aynı yazıyı yazan programı o programa dili ile yazıp dosya boyutunu
buradaki ile karşılaştırınız. Ayrıca Debug programının diğer komutlarını görmek için - işaretinin
görüldüğü modda ? yazıp enter tuşuna basınız.
Geçen makalemizde debug programını tanımış ve makine kodları ile küçük bir program
hazırlamıştık. Sonra bu programı yine debug ile bir dosya haline getirmiştik. Şimdi bu
dosya ile biraz daha yakından ilgilenelim. Yine komut istemine geçiş yapıp debug
programını şekildeki gibi çalıştıralım.
Ekranın solunda bir - işareti göreceksiniz ve başka hiçbir şey olmamış görünecek ama
aslında merhaba.com adlı program dosyasını oluşturan makine kodlarını çoktan hafızaya
yüklediniz bile. Makine kodlarını görmeden önce sizlere küçük bir tavsiye vermek
istiyorum. Assembly programlama dilinde ayrıntılar çok önemlidir ve her bir ayrıntı kendi
çapında bir araştırma konusu olabilir, örneğin burada yaptığınız program yükleme işlemini
windows kullanıcıları, programın simgesine mouse ile tıklayarak yapıyorlar ve hatta bu
şekilde program hafızaya yüklendikten sonra birde işletim sistemi tarafından çalıştırılıyor.
Şimdi aklınıza şu soru gelmeli; program simgesinin üzerine gelip mouse ile tık-tık
yapıldığı andan itibaren, programın yüklenip çalıştırılmasına kadar sistemde neler ve nasıl
yapılıyor? Öyle değil mi? Nelerin yapıldığını geçen makalemizi takip ettiyseniz tahmin
edebilirsiniz ama nasıl yapıldığına gelince o iş biraz sistem programcılığına giriyor yani
bizim için biraz erken. Neyse biz konumuza geri dönelim, program şu anda hafızada ve
0100h offset adresinden itibaren yüklenmiş durumda. Hemen kodları görelim o zaman.
Debug�ın d komutu "dump" anlamına gelip programlama aleminde kodları ekrana yada
kağıda dökmek gibi bir anlamı vardır. Bizde burada kodları ekranda gösterdik. Neden
0100h adresinden itibaren hafızaya yerleşti? sorusunun yanıtı program dosyasının .com
uzantılı olmasından kaynaklanır ve geçen makalemizde açıkladığımız gibi sonraki
makalelerde ele alınacak bir konudur. Burada önemli olan husus sizin 0100 offset adresi
ile 010B offset adresleri arasında kalan makine kodlarından bazılarının komut kodları
bazılarının ise data (veri) kodları olduğunu bilmenizdir. Tabi ki ekrana yazdırılacak olan
MERHABA ASSEMBLY burada data sınıfına giriyor peki komutlar nerede?
Şekil 3 - Datalarımız...
Kırmızı ile işaretli olan kısımdan öncekiler (dikkat bu kısımdan sonraki kodlar bizi
ilgilendirmiyor) yani 0100h ile 010Ah adresleri arasında kalan kısım naçizane komut
kodlarımızdır. Tabi ki bu şekli ile bize pek bir şey çağrıştırmıyorlar şayet bunlar bizlerin
anlayabileceği dile biraz yakın olsaydı anlardık.
Assembly dilindeki MOV AH,09�un makine dilindeki karşılığının B409 olduğu görülüyor ve
assembly programcıları "B409" ile gösterilen makine dilindeki bu ifadeyi iki kısıma
ayırırlar. Bunlar opcode ve operand alanlarıdır.
Opcode Operand
B4 09
Tablo 1 : Opcode ve Operand
Burda 3 byte�lık bir komut satırı, opcode 1 byte�lık operand 2 byte�lık. Ama şimdi
kafamızda 2 tane soru işareti var, birincisi neden MOV komutunun makine dilinde 2 tane
farklı opcode�u var? (yani B4 ve BA�dan bahsediyoruz ve aslında MOV komutunun
makine kod karşılığı 2 den de fazladır) ve ikinci soru neden MOV DX,010B için operand
kısmı makine dilinde 0B01 olarak ters bir biçimdedir?
Opcode Operand
BA 0B 01
Tablo 2 : Opcode ve Operand
Bu sorların cevabı X86 uyumlu mikroişlemci mimarisini anlayarak bulabilirsiniz. İlk olarak
B4 veya BA MOV�un karşılığı gibi görünse de öyle değildir, B4= MOV AH�ın BA=MOV
DX�in opcode karşılığıdır. Yani ikisi de farklı assembly ifadeleridir ve farklı
opcode�larının olması son derece normaldir. İkinci sorunun cevabı ise x86 ailesindeki
işlemcilerin hafızaya erişme şekillerinin ters sıralı olmasından kaynaklanır. Tabi ki birde
düz olarak byte�ları yerleştirme olayı var, bunlar kitaplarda little endian byte ordering ve
big endian byte ordering olarak geçer. Aslında byte düzenleri çok çok ileri seviyede
önemli konulardır ayrıca opcode�ları gösterirken seçilen bu hexadecimal değerlerde (B4
ve BA gibi) rastgele seçilmiş değerler değildir. Bu tür konuları öğrencilik yıllarımdayken
araştırmıştım, gerçekten epey zevkli konular, genellikle yüksek lisans ve doktora
sınıflarında instruction anatomy (komut anatomisi) başlığı altında incelenen konulardır.
Programcılık açısından ise sadece byte düzeni önemlidir. Bu yüzden burada sadece bun
iki kavramı açıklayacağım.
Little Endian Byte Ordering: Hafızda yüklenecek olan byte�lar düşük değerlikli kısmından
itibaren yazılır. Örneğin 1234 gibi iki byte�lık bir veriyi hafızaya 0100 offset adresinden
itibaren yazdığınızı düşünelim, bu işlem bitip hafızaya baktığınızda göreceğiniz şey
0100=34, 0101=12 olacaktır.
Burada debug�ın "a" (assembly) komutunu vererek assembly dilinde program yazma
moduna geçtik ve dw talimatı ile hafızaya 1234 değerini girdik ve hafızaya, önce bu
sayının düşük değerlikli byte�ı sonrada yüksek değerlikli byte�ı yazıldı.
Big Endian Byte Ordering: Hafızaya yüklenecek byte�lar en yüksek değerlikli kısmından
itibaren yazılır aynen bizim kağıt üstüne sayıları yazdığımız gibi. Sizlere burada bir tanede
big endian örneği vermek isterdim fakat şu anda elimde bir Apple PowerPC olmasına
rağmen MAC OSX işletim sistemi için debugger yok :( Aşağıdaki tablo hangi sistemlerin
hangi byte düzenini kullandığını gösteriyor.
Byte
İşlemci Türü İşletim Sistemi
Düzeni
little-
Digital Alpha AXP Tru64 UNIX
endian
little-
Windows
endian
little-
Solaris x86
endian
SunOS big-endian
Sun SPARC
Solaris big-endian
CD21 INT 21
B44C MOV AH,4C
CD21 INT 21
INT 21�de bir byte�lık opcode�u ve bir byte�lık operandı olan başka bir komut satırı.
Bu arada MOV AH,4C nin opcode�u ile yukarıda açıkladığımız MOV AH,09�un
opcode�larının aynı olduğuna dikkatinizi çekerim.
Buraya kadar ;
2- Makine kodları ile assembly kodları arasındaki bağıntıyı gördük, Borland gibi bir
derleyici (compiler) yazan bir firma kurmak isterseniz, bu alanda en az bir doktora tezi
vermeniz gerekir. Yani for (int i=0 ; i<100 ; i++) gibi bir ifadenin derleyici ile makine
diline dönüşmesi zannettiğinizden daha karışıktır.
Özetle buraya kadar derin konuların giriş kısımları anlatıldı. Şimdi sıra assembly kodlarına
bir hacker gibi değil de bir programmer gözüyle bakmaya geldi.
Yazdığımız programda iki tane iş yapılıyor. Birincisi ekrana bir dizi byte�ın ASCII
görünümünü yazdırmak, ikincisi bilgisayarı tekrar kullanıcının ellerine bırakmak yani
işletim sistemine geri dönüş.
Şekil 6 - Programımızın parçaları
Daha sonra AAP bilgisayarının başına gider yukarıdaki şekildeki programın ilk üç satırını
yazar ve çalıştırır, tabi ki ekranda MERHABA ASSEMBLY görünür ama komut istemindeki
penceresini kilitlenir, çünkü ekrana yazdırma işleminden sonra programı
sonlandırmamıştır. Bunun üzerine programı sonlandırmak için tekrar interrupt servislerini
araştırmaya koyulur ve interrupt 21�in 4C fonksiyonunu keşfeder. MOV AH,4C ve INT 21
satırlarını da programına ekledikten sonra programı kusursuz çalışır.
Yukarıdaki diyalog ta kaydediciler (register) diye bir kavram geçti, bunları şimdilik
mikroişlemci içindeki hafıza konumlarına (memory location) benzeyen yapılar olarak
düşünün. Kaydedicilerin listesini görmek için debug programını kullanabilirsiniz.
X86 kaydedicilerinin AX, BX, CX, DX, SP, BP... diye adları vardır ve bunların hepsinin
değişik işlevleri mevcuttur. Program yazarken elbette bunları kullanacağız ama şimdilik
üzümünü yiyin bağını sormayın, çünkü bağın yerini söylersem ve sizde gider o bağa
bakarsanız iştahınız kaçabilir. Kafalar fazla karışmadan yeni bir uygulama yapalım.
10 defa Merhaba Assembly Yazdırıyoruz
Tabi böyle bir işi yaptırmanın değişik yolları mevcut ( bir .bat uzantılı script dosyası
hazırlayıp ilk programımızı 10 defa çalıştırmak bunlara dahil değil tabi :) ) Şimdi
yazacağımız programda ilk programımızın ilk üç satırını 10 defa çalıştırıp sonra programı
sonlandıracağız. C dilindeki for döngüsü gibi. Aşağıdaki işlemleri takip edin.
Programı yukarıdaki gibi hafızaya girdikten sonra bir önceki makalemizden faydalanıp bu
programı dosya haline getirin, byte�ları saymada sizler için pratik olur. Burada CX
kaydedicisine (0A)16=(10)10 değerini sayaç olarak yüklüyoruz. LOOP komutu CX�in
değeri kadar operandı ile belirtilen adrese dallanır, burada dallanılacak adres 0103 offset
adresidir ve bu işlem sayesinde ekrana 10 defa MERHABA ASSEMBLY yazılır. Daha sonra
programımız fonksiyon:4C interrupt 21 ile sonlanıyor. Buradaki string�e dikkat, debug
ile assembly modunda hafızaya bir dizi karakter girmek için DB (Define Byte) talimatını
kullanıyoruz. Talimatlar (daha sonra ayrıntılı bir şekilde göreceğiz) derleyiciye verilir yani
opcode olarak dönüştürülmezler, aynı C�deki int, double, string veri tipleri gibi. DB�den
sonra şayet byte�ları karakter olarak girmek isterseniz " " arasına yazmalısınız. Bu işlemi
karakterlerin hexadecimal kodlarını yazarak ta yapabilirsiniz bu sefer her bir karakterden
sonra "," koymanız gerekir. Burada her iki teknikte kullanılmıştır. ASCII kod tablosuna
bakacak olursanız 0A=LF yani Line Feed (bir satır aşağı) 0D=CR yani Carriage Return
(Kursör satır başına) olduğunu görürsünüz, zaten CR ve LF kontrol karakterleri
olduğundan dolayı standart ASCII kod tablosunda A,C,P,Z gibi normal karakter olarak
karşılıkları yoktur mecburen hex. kod karşılıklarını kullanmak zorunda kaldık. INT 21
fonksiyon 9 ile yazdıracağınız karakter dizilerinin sonunda mutlaka 24=$ bulunmalı yoksa
bu interrupt servisi hafızada 24 değerini bulana kadar ekrana yazma işlemine devam
eder.
Son olarak debug ile bu programı adım adım çalıştırabilirsiniz, bunun için program yazma
işlemi bitince P (Proceed) komutunu kullanın T (Trace) komutu ile programınız içinden
çağrılan interrupt servislerine de girebilirsiniz (ama çıkabilirmisiniz orasını bilemem)
Debug�tan çıkmadan bir daha programınızı çalıştırmak için "r ip" komutunu kullanarak ip
kaydedicisini 0100 yapın, böylece programın başlangıç adresini doğru ayarlamış
olursunuz.
Ama o kişi X86 PC Assembly dili, Power PC Assembly dili veya 8051 Assembly dili
biliyorum deseydi o zaman iş değişirdi. X86 PC Assembly dili, Power PC Assembly dili gibi
bir sınıflandırma bile tam olarak her şeyi ifade etmez. Örneğin X86 PC Assembly dili
donanım platformunu tanımlar ve bu donanımı çalıştıracak işletim sistemi Windows,
Linux, Solaris olabilir, her bir işletim sisteminin hafızayı ele alış şekli başka olduğundan
programcılar işletim sisteminin hafıza organizasyonunu göz önüne almak
mecburiyetindedirler. Ayrıca kodlarınızı derlerken kullandığınız program olan
Assembler’da işe ayrı bir sınırlama koyar. Örneğin donanım X86 bir PC, işletim sistemi
Windows olsun, bu durumda assembler olarak Netwide kullanırsanız farklı Turbo
assembler kullanırsanız farklı talimatlarla program kodu yazmak zorunda kalırsınız ve bu
kodlar adı geçen assembler programları tarafından makine koduna çevrilirler. Sonuç
olarak assembly dilinin programcılık açısından en büyük dezavatajı budur. Fakat
assembly’yi hızlı ve özelliklede sağlam bir dil yapan unsurlarda bu dezavantajın
sonucudur.
Bunca donanım ve işletim sistemi içinde acaba hangisi en çok kullanılıyor? Intel’in
işlemcileri ve Microsoft’un işletim sistemleri tabi ki. İşte bizde Intel uyumlu ve
Microsoft’un MSDOS ve Windows işletim sistemlerine uygun bir assembly dili
üzerine makalelerimizi yazıyoruz. Yani, "Bu programa dilini zor mudur?" sorusuna,
programa dilini bırak adı bile zor diyebilirsiniz. Bu yüzden assembly denildikten sonra
genelde "hangi assembly?" kelimesi de konuşulur. Konuların içinde boğulup kalmamak
için biz bu programlama dilinin neresindeyiz ve bundan sonra hangi rotayı izlememiz
gerekiyor gibi soruları sanırım yukarıdaki paragrafta açıklamış olduk.
Hangi ASSEMBLER?
X86 uyumlu (eski tabiri ile IBM uyumlu) PC’ler için en popüler assembler’lar TASM, MASM
ve NASM dır.
Turbo
TASM Borland
Assembler
Microsoft
MASM Macro Microsoft
Assembler
Netwide
NASM LGPL
Assembler
Tablo 1 : En Popüler Assemblerler
X86 ailesi için daha başka assemblerlarda mevcut fakat en çok kullanılanlar yukarıda
saydıklarımızdır. MASM ve TASM .asm uzantılı bir dosyanın içindeki assembly ifadelerini
ve direktiflerini okur ve bunları makine kodlarına dönüştürür. NASM ise TASM ile MASM’ın
bu yaptığına ek olarak Linux ve Unix ortamlarında da çalışır. TASM ve MASM kardeş
assemblerlardır. Yazım kuralları hemen hemen aynıdır. Peki hangi assemblerı seçmeli?
Aslında bu yapacağınız projeye bağlı tabiki, örneğin Visual Studio IDE’si ile C++
uygulamaları geliştiriyorsanız MASM kullanmanız daha akıllıca olur, en azından MSDN’de
desteği mevcut. Biz makalelerimizde hem TASM hemde MASM’ı kullanacağız.
Artık Programlarımızı Assembly Dilinde Yazıyoruz!
Sadece Assembly dili için geliştirilmiş Visual Studio.NET gibi çok kullanışlı IDE’ler yoktur.
Eskiden edit.com (DOS zamanından kalma) programını kullanırdık ve bu program hala
windows’un tüm versiyonlarında mevcuttur.
Yine Windows’un klasik programlarından notepad (not defteri) assembly dili için başka bir
seçenektir. Ama dosyalarınızı ANSI olarak kaydetmeniz gerekiyor.
Şekil 4 - Her derde deva Notepad ile programlarınızı ANSI olarak kaydetmeniz gerekiyor.
Peki bu yazıyı yazan editörün editör seçimi nedir? :) Ben sizlere GVIM’i tavsiye ediyorum.
GVIM hemen hemen tüm popüler assembler söz dizimini (syntax) desteklediği gibi tüm
popüler programlama dillerinin de söz dizimini destekleyen süper bir editör. TASM, MASM
ve GVIM’ı bu makalenin sonundaki download kısmından edebilirsiniz.
Şekil 5 - Gvim ile assembly kodlarını yazmak daha kolay, programı çalıştırdıktan sanra
yazma moduna geçmek için klavyeden i tuşuna basmanız gerekiyor.
Editör programlarını kısaca tanıttık dan sonra artık gvim ile örnek bir assembly program
yazabiliriz.
Nihayet Assembly
Şimdi geçen makalemizde yazdığımız programı tekrar yazacağız ama bu sefer kaynak
kodlarımız .asm uzantılı bir dosyada olacak. GVIM’i çalıştırdıktan sonra klavyeden i
tuşuna basıp aşağıdaki kodları yazınız yada kaynak kod dosyasını buradan download
ediniz.
.MODEL SMALL
.STACK 32
.DATA
.CODE
ANA PROC
ANA ENDP
END ANA
Assembly dili yazım kuralları ile program bu şekilde yazılıyor, nasıl daha önce
yazdıklarımızdan biraz farklı değil mi? Tüm bu farklılıklar sadece assembler için.
Assembler bu dosyayı satır satır okuyacak ve makine koduna çevrilecek kısımları algılayıp
bunları 1 ve 0’lara çevirecek. Vakit kaybetmeden bu kodları program dosyası haline
getirelim. Bunun için ilk önce assembler programına sahip olmamız gerekiyor. Assembler
programlarından dilediğinizi buradan download ettikten sonra aşağıdaki adımları sırayla
takip ediniz.
Dikkat ederseniz her iki assemblerda aynı kaynak kodunu derledi ve sonunda .exe
uzantılı bir program dosyası oluşturdu bu assemblerları değişik parametrelerle kullanıp
.com türünden dosyalarda oluşturabiliriz. Aslında ben bu kaynak kodunu TASM için
yazmıştım ama önceden de dediğim gibi iki assemblerın da çok fazla ortak yanı vardır
fakat bu aynı oldukları anlamına gelmez ve her zamanda aynı kaynak kodunu
derlemeyebilirler. Buradaki basit bir program olduğundan sorun çıkmadı ve biz uzun bir
süre böyle basit programcıklar yazacağız. Bu yüzden şu anda derleme aşamasında hangi
assemblerı kullandığınızın pek bir önemi yok.
Burada download ettiğiniz assembler ve linker programları TASM ve MASM paketinin
içinden alınmış dosyalardır. Daha sonra tüm paketi vereceğim şimdilik bu kadarı yeterli.
Şimdi kaynak kodlarımızı bir daha gözden geçirelim ve dikkatimizi rakamların sonundaki
h harfine odaklayalım. h harfi burada rakamın veya sayının hexadecimal (16lık sayı
sistemi) olduğunu gösterir. Biz debug ile program yazarken bunu belirtmemiştik çünkü
debug her yazılan sayıyı hex. olarak (daha doğrusu binary olarak) kabul eder. Bu
makalemize sayı sistemlerine kısaca inceleyerek son vereceğiz. Kaynak kodumuzu satır
satır anlatmamı bekliyor olabilirsiniz ama bu işlemci kaydedicileri ve x86 hafıza yapısını
öğrenmeden bu iş biraz zor.
Bilgisayar Aritmetiği
Genelde programcılık derslerinin ilk ve sıkıcı konularındandır sayı sistemleri, bunu nedeni;
bu sayı sistemlerinin ne için kullanıldığının tam kavranamamasından kaynaklanır. Aslında
işlemci sayılardan falan anlamaz, sadece elektrik akımın var yada yok olması temeline
göre çalışırlar. Hani duymuşsunuzdur pentium işlemcinin içinde bilmem kaç milyon tane
transistör var diye işte elektrik akımları bu transistörler aracılığı ile kontrol edilir.
Transistör yarı iletken bir elektronik devre elemanıdır ve her modern elektronik cihazın
içinde bulunur. Birçok kullanım alanı olan transistörler bilgisayarlarda anahtarlama
elemanı olarak kullanılırlar.
Şekil 12 - 1’ler ve 0’lar anahtarın açık yada kapalı olmasını temsil ederler
Aşağıdaki tablo 1’den 15’e kadar decimal (onluk tabandaki) sayıların hex. ve binary
karşılıklarını veriyor. Rakamlara bakacak olursanız assemblerin bunları nasıl ayırt edeceği
problemini anlarsınız. Bütün rakamlar benzer sembolleri kullanıyorlar bu yüzden
assembly dilinde program yazarken sayı binary ise sonuna b hexadecimal ise h ve
decimal ise d (yada hiçbirşey) getirilir. Aşağıdaki tabloda binary kısım 8 haneli
gösterilmiştir çünkü x86 hafızası byte adreslenebilir yapıdadır yani hafızada en küçük
rakamları depolamak için bile 8 hane (digit) kullanılır. Hexadecimal değerler de binary
değerlerin sanki sıkıştırılmış halidir. Herhangi bir hex. değerin 1 basamağı 4 bit’e denk
gelir (nibble).
Decimal Binary Hexadecimal
0000
0 00
0000
0000
1 01
0001
0000
2 02
0010
0000
3 03
0011
0000
4 04
0100
0000
5 05
0101
0000
6 06
0110
0000
7 07
0111
0000
8 08
1000
0000
9 09
1001
0000
10 0A
1010
0000
11 0B
1011
0000
12 0C
1100
0000
13 0D
1101
0000
14 0E
1110
0000
15 0F
1111
Assembler
tasm.zip Download
ve Linker
Assembler
masm.zip Download
ve Linker
Kaydediciler ve Hafıza
Mikroişlemci bir çok karmaşık birimden oluşur, neyse ki programcılar donanımsal olarak
sistemleri fazla yakından bilmek zorunda değillerdir. Bir assembly programcısı sadece
mikroişlemci içindeki birkaç kaydediciyle ilgilenir. Bunun dışındaki birimler genelde
okullarda bilgilendirmek amacıyla verilir, örneğin CPU içinde bir ALU biriminin olduğunu
bir çok programcı bilir ama bu birimin çalışması ile ilgilenmezler. Bir assembly
programcısı keskin bakışlarını mikroişlemcinin içinden çekip :) sistemin tamamına
baktığında bile, donanımların ve donanımları birbirine bağlayan birimlerin çalışması ile
ilgili fazla birşey göremez yada görmesine gerek yoktur. Programcılık ve elektroniğin sınır
noktası budur aslında. Assembly programcıları kaydediciler, hafıza veya sistemi oluşturan
birimlerde dolaşan elektron akımlarını 1�ler ve sıfırlar olarak görürler ve genelde bu 1 ve
0�lardan enaz 8 tanesini yan yana gördüklerinde bir şeyler anlamaya veya yorumlamaya
başlarlar.
Sistemi içindeki birimleri birbirine bağlayan yollar, mikroişlemcinin içinde de devam eder.
BUS dediğimiz bu yapılar birçok iletkenden oluşur. Kontrol yolunda R/W (oku/yaz) gibi
kontrol sinyalleri, veri yolundan işlenecek veriler gidip gelir. Adres yolu iste tek yönlüdür
ve CPU�dan diğer birimlere doğru sinyal akışı olur, örneğin hafızaya bir veri
yerleştirilmeden önce adres yolundan verinin yerleştirileceği hafıza adresinin bilgisi
gönderilir. Bunu daha iyi anlamak için aşağıdaki animasyonu izleyebilirsiniz yada
animasyonu offline izlemek için download edebilirsiniz.
En temel 3 birimden biri olan ALU aritmetik ve mantıksal işlemleri yapan bir elektronik
devredir. BIU ise komut kodlarının icrası ile ilgilenir yani emirleri icra eden birim olarak
basitçe tanımlanabilir. Kontrol ünitesi ise sistemdeki birimleri harekete geçirmek için
kontrol sinyalleri üretir. Bu üç birime programcının doğrudan müdahalesi söz konusu
değildir. Programcı sadece CPU içindeki kaydedicilere erişebilir.
Kaydedicileri tam olarak anlayabilmeniz için onları kullanmanız gerekir, yani burada ne
kadar anlatsak boş bu yüzden kısaca yukarıdaki şekilde gördüklerinizi açıklayıp hemen bir
uygulama yapalım.
Accumulator (EAX, AX, AH, AL): En sık kullanacağınız kaydedicidir. Çok genel kullanım
alanına sahiptir, daha önceki makalelerimizde yazdığımız kodlara bakarsanız çok değişik
amaçlarla kullanıldığını görebilirsiniz. Bu kaydedici birçok giriş/çıkış işleminde ve aritmetik
işlemlerde (çarpma, bölme ve taşıma gibi) kullanılır. Ayrıca bazı komutlar işlenmeden
önce accumulator�den parametre alır veya bu kaydediciye işlemin sonucunu kaydeder.
Base (EBX, BX, BH, BL): Accumulator gibi genel amaçlı ve hafıza erişiminde indexleri
göstermede kullanılır. Bir başka kullanım alanıda hesaplamalardır.
Counter (ECX, CX, CH, CL): Özel amaçlar ve hesaplamalarda kullanılacağı gibi
genellikle bu kaydediciyi sayıcı olarak kullanılır, daha önce loop komutunun CX
kaydedicisini otomatik olarak değiştirdiğini söylemiştik.
Data (EDX, DX, DH, DL): Bazı giriş/çıkış komutlarında bu kaydedicinin kullanılması
gerekir, ayrıca çarpma ve bölme işlemlerinde accumulator ile birlikte büyük sonuçları bu
kaydediciden okuruz.
Segment Kaydedicileri
Code Segment Kaydedicisi (CS): DOS işletim sisteminde programları oluşturan kodlar
code segment�e yüklenir. CS kaydedicisi ise IP kaydedicisi ile birlikte programın çalışma
sürecinde, programın oluşturan kodların adreslerini gösterirler.
Data Segment Kaydedicisi (DS): .exe türündeki bir programda kullanılacak olan
veriler data segment denilen hafıza bölümünde tutulur. DS kaydedicisi ise bu bölgedeki
verilerin konumlarını gösterir.
Stack Segment Kaydedicisi (SS): Tüm programlar stack segment denilen bir hafıza
alanını geçici depolama alanı olarak kullanmak zorundadırlar (örneğin dallanma
işlemlerinde). SS kaydedicisi ise SP kaydedicisi ile birlikte bu verilerin adreslerini referans
eder.
Extra Segment Kaydedicisi (ES): Bazı string işlemlerinde DI kaydedicisi ile birlikte
karakterlerin bulunduğu hafıza adreslerini tutarlar.
Özel kaydediciler
FLAG ve EFLAG kaydedicileri: Flag kaydedicisi 16 Eflag kaydedicisi ise 32 bitten oluşur.
Bildiğiniz gibi mikroişlemci matematiksel işlem yapar, bu kaydedicilerde her işlemden
sonra o işleme özel sonuçları gösterirler. İşlemci durum kaydedicisi olarakta bilinen bu
kaydediciler sonucun sıfır, pozitif veya negatif olduğunu veya işlemin sonucunda elde
üretilip üretilmediği gibi birçok önemli veriyi bitsel olarak programcıya bildirirler.
Index Kaydedicileri
HAFIZA
Şimdi hafızanın fiziksel ve mantıksal yapısını anlatacağım, fiziksel yapısı ile programcı
fazla ilgilenmez hafızanın mantıksal yapısı ise tüm programcılara hemen hemen aynı
görünür. Çok karışık bir konu olmasına karşın basit bir anlatımla bu konuyu kavramanızı
umuyorum.
Yukarıdaki gibi bir hafıza yapısı, 16 bitlik bir veri yolu bulunan işlemciler için uygundur,
örneğin 8086 işlemcisi için. Şayet veri yolu 32 bitlik ise daha fazla bank gerekir, özetle
data bus�ın (veri yolu) genişliği kullanılacak olan hafıza chip�ini belirler. Hafıza denilince
boyutu 8 bit olan bir dizi hücre ve bu hücrelerin numarası olan adresler akla gelir. Hafıza
chip�leri veri, adres ve kontrol yollarına bağlıdır. Hafızada bulunan veriler hexadecimal
formatta gösterilirler, bu binary formata en uygun sayı sistemidir.
x86 hafızası ne kadar byte olarak adreslense de programcı hafızadan tek bir komut ile 8,
16 veya 32 bitlik bir değer işleyebilir. Böyle bir durumda hafızanın fiziksel yapısını bilmek
işe yarayabilir. Örneğin 0. adresten itibaren 16 bitlik (2 byte veya word) bir değerin
işlemci kaydedicilerine herhangi bir işlem için çekileceğini varsayalım. X86 ailesi
işlemciler, hafızadan verileri düşük değerlikli kısmından itibaren okurlar (little endian). Bu
örnekte hafızaya erişim çift numaralı adresten yapıldığı için ABFFh verisi data bus�a
doğru bir şekilde yerleşecektir ve en hızlı hafıza erişimi bu şekilde yapılır. Birde tek
numaralı bir adresten örneğin 7. adresten okuma yapıldığını varsayalım. Bu durumda
okunması gereken değer 4676h olmasına rağmen data bus�a 7646h verisi yerleştirilir
gibi görünüyor değil mi? Hal böyle olunca işlemci önce Odd Bank�tan daha sonra Even
Bank�tan okuma yapar ve bu byte�ları sıraya koyar, bu tür bir hafıza erişimi daha uzun
zaman alır. Özetle çift numaralı adreslerden 1 byte�tan fazla bir değere erişilirse buna
sıralı erişim (Aligned Memory Access), tek numaralı adreslerden okuma yapılırsa buna
sırasız hafıza erişimi (Unaligned Memory Access) denir. Sıralı hafıza erişimi sırasızdan 2
kat daha hızlı olduğundan programlamada buna dikkat edilmelidir. Bu tür ayarlamalar
assembly dilinde direktiflerle yapılır.
Bir mikroişlemcinin ne kadarlık bir hafızaya erişebileceği adres yolu ile doğru orantılıdır.
Adres yolu 16 bit olan bir mikroişlemci 216 yani 65536 adet hafıza konumuna erişebilir.
8086 işlemcisi 20 bitlik bir adres yoluna sahiptir ve adresleyebileceği alan 220=1048576
yani 1 MB. a denk gelir. 80286 işlemcisinin adres yolu 24 bitlik�ti ve 224= 16777216
hücreyi, başka bir deyişle 16MB. lık bir hafıza alanını adresleyebilirdir. Günümüzde
kullanılan mikroişlemcilerin adres yolu çoğunlukla 32 bitliktir, buda 4 Milyar küsür hafıza
konumuna denk gelir. Yavaş yavaş 64 bit devri başlıyor, AMD 64 bitlik işlemcilerini çoktan
piyasaya sürdü 264 adet hafıza konumu geleceğin programlarını çalıştırmak için epey
yeterli. Her ne kadar mikroişlemci böyle büyük hafıza alanlarını desteklese de böyle bir
alanı kullanabilmemiz hafızayı nasıl organize edileceğine bağlıdır. Hafıza organizasyonuna
yapılan bu yaklaşıma hafızanın mantıksal yapısı diyebiliriz.
Hafızanın mantıksal yapısı çalışma moduna göre değişiklik gösterir ve çalışma modu
işletim sistemleri tarafından belirlenir. X86 uyumlu tüm PC�ler Real Mod denilen çalışma
modunda açılırlar, daha sonra işletim sistemine göre hafıza farklı bir şekil alabilir. Örneğin
Linux yada Windows işletim sistemi yükleniyorsa hafıza Protected Mod (korumalı mod)
denilen yapıya bürünür. DOS işletim sistemi yükleniyorsa (neydi o günler :)) hafıza real
mod�da kalır. Birde korumalı mod altında DOS programlarını çalıştırmak ve korumalı
mod ile real mod programları arasında geçiş yapmak için windows işletim sisteminin
Virtual Protected Mod�u vardır. Tüm bu modlar tek başına bir makale konusu
olduğundan ve şu an için biraz ağır konular olduğundan fazla ayrıntıya girmiyorum. Şimdi
sadece en basit ve programlarımızda kullanacağımız mod olan Real Mode Adressing yeni
gerçek modda hafıza�yı inceleyeceğiz.
Real mod�da hafıza 64Kb�lık segmentlerden oluşur ve bu segment içindeki her byte�a
erişmek için offset adresleri kullanılır. Bu durumda hafızanın tamamı bir kitap, segmentler
sayfalar ve adreslenebilen her byte ise satır olara düşünülebilir. Protected mod çok daha
karmaşıktır. 80286 ve sonrası işlemciler protected mod�a geçiş yapabilir. Sayfalama
(paging), sanal bellek (virtual memory) ve multitasking (çok görevlilik) korumalı modun
getirdiği büyük avantajlardır.
045F:0032 adresi için, segment adresi 4 bit sola kaydırılır veya başka bir deyişle adresin
sonuna 4 adet 0 getirilir (0000)2=(0)16 böylece segment adresi 045F0 olur, sonra buna
offset adresi eklenir ve gerçek adres bulunur. 045F0h+0032h=04622h gerçek hafıza
adresidir.
Gerçek modda bilgisayarınız Pentium 4 bile olsa hafızanın sadece 1MB. lık alanını
adresleyebilirsiniz, ayrıca sadece kaydedicilerin 16 bitlik kısımlarını kullanabilirsiniz. Bu
son model ferrari�nizi otobanda 1. viteste sürmeye benzer.
Şimdi�de hafıza ve kaydedicileri daha iyi anlamak için örnek program hazırlayalım.
TITLE Kaydediciler ve Hafıza (regmem.asm)
;########################################
;# Bu program hafızadaki 2 adet byte�ı toplar ve sonucu #
;# tekrar hafızaya kaydeder. #
;# Son Güncelleme: 05/03/05 #
;# Yazan --> Eren ERENER #
;########################################
.MODEL SMALL
.STACK 32
.DATA
Sayi1 DB 5
Sayi2 DB 6
Sonuc DB ?
.CODE
ANA PROC
ANA ENDP
END ANA
Bir programın çalışma süreci genel olarak şöyledir, program önce hafızaya yüklenir ve
mikroişlemci ilk komuttan itibaren programı atır-satır icra etmeye başlar. Bu program
hafızaya yüklenirken hafızada mantıksal olarak 3 tane segment oluşturacak 1.si 32
byte�lık stacak segment bölgesi 2.�si 3 byte�lık data segment bölgesi ve 3.sü
kodlarımın kapladığı alan kadar code segment bölgesi. İşlemci kaydedicileride programın
yüklenme sürecinde ilk değerlerini alırlar. Örneğin CS kaydedicisi code segment
bölgesinin segment adresini ve IP kaydediciside bu bölgede bulunan kodların ilkinin offset
adresinin değerini alır. Bu olayları en güzel uygulama yaparak anlayabiliriz, bunun için
sizlere bir video hazırladım. Şimdi kendinize bir kahve hazırlayın ve keyifle bu videoyu
izleyin. Kafanıza takılan bir soru olursa bana mail atabilirsiniz, bu arada olumlu-olumsuz
eleştirilerinizi bekliyorum. Bir sonraki makalede görüşmek üzere hoşça kalın.
x86 Adresleme Modları ve Turbo Debugger
ADRESLEME MODLARI
Bildiğiniz gibi programları oluşturan kodlar ve veriler hafızaya yüklendikten sonra işlemci
tarafından satır-satır icra edilirler. Ayrıca CPU tüm giriş çıkış işlemlerini de hafızaya
erişerek yapar. Bazen hafızadan doğrudan bir kod ya da veri alır, işler. Bazen hafızaya bir
veri gönderdiğinizde birde bakmışsınız bu bir yazıcıdan belge olarak çıkmış vs. İşte
bilgisayarın donanım ve yazılım düzeyinde yaptığı bunca çeşitli iş için CPU hafızaya
değişik yollardan erişme ihtiyacı duyar. Sizlerde programlarınızı yazarken CPU�nun
hafızaya nasıl erişeceğini yazdığınız kodlarla belirtmek zorundasınız. Assembly dilinin ilk
basamağı olan adresleme modları da bu konuları kapsıyor.
Her mikroişlemci üreticisi bir mikro işlemci piyasaya sürdüğünde, komut setini ve
adresleme modlarınıda yayınlar. Programcılar da bu belgelere göre programlarını
yazarlar. Intel 8086 işlemcisini piyasaya sürdüğünde oldukça kullanışlı bir dizi adresleme
modu sağladı. Intel�in şu ana kadar ürettiği işlemcilerde bu adresleme modlarını
kullanabilirsiniz. Daha sonraları bir devrim niteliğinde olan 80386 işlemcisi ile ek
adresleme modlarıda geldi. Bu özel adresleme modları sadece 386 ve sonrası işlemcilerde
kullanılabilir. Tabi ki bu ek adresleme modları ile assembly programlama dili daha esnek
bir yapıya bürünmüştü. Bizde bu makalemizde adresleme modlarını 386 öncesi ve sonrası
olarak ikiye ayıracağız.
Şu ana kadar yaptığımız programlarda çok fazla komut örneği görmediniz ama
makalelerimizi takip ettiyseniz MOV komutunu yakından tanımanız lazım. MOV komutu
assembly programlama dilinde en çok kullanılan komutların başında gelir. Çünkü bir
program çalışırken genelde hafızaya yazar yada okur. MOV komutu da bu iş için biçilmiş
kaftandır ve bu kadar çok kullanıldığından dolayı bir çok adresleme modunu destekler,
yani bu komut ile hafızaya çok değişik yollardan erişebilirsiniz. Bu yüzden bu makalede
adresleme modlarını anlatırken MOV komutunu örnek alacağım.
1- Segment kaydedicileri arasında bir transfer işlemi ancak genel amaçlı bir kaydedici
vasıtasıyla yapılabilir.
mov ax,cs
mov ds,ax
Herhangi bir genel amaçlı veya indeks kaydedicisine doğrudan bir değer yükleye bilirsiniz.
Yüklenecek olan veri kod segmentten alınacağından bu tür kullanımları şahsen ben pek
tavsiye etmem. İyi bir program organizasyonu için, veriler hafızanın ayrı bir bölümünde
(mesela data segmentte) değişkenler veya sabitler olarak belirtilmelidir.
Aslında kaynak kod hazırlanırken genelde bu şekilde bir kod yazımı yapılmaz. Değişkenler
ve sabitler sembolik kelimelerle ifade edildiğinden buna gerek yoktur. Ne var ki debugger
programlarında da sembolik değişken isimlerini değil adresleri görürüz. Aşağıdaki kod
parçasını inceleyin;
.data
sayi1 db 5
.code
mov ax, @data
mov ds, ax
Adının kaydedici olduğuna aldanmayın. Burada operand olarak kullanılan kaydedici köşeli
parantez içine alınır ve bu andan itibaren bir offset adresi oluverir.
mov al, [bx] ; hafızadan AL�ye 1 byte taşınır. Alınacak verinin offset adresi
BX�in değeridir.
Kullanım şekli;
Not : disp kısaltması İngilizcede displacement kelimesinin karşılığıdır. Buradaki anlamı ise
referans alınan ofset adresidir. Komut setlerinde adresleme modları açıklanırken disp
veya mem kısaltması ile çok sık karşılaşacağınızdan, komutları yazarken İngilizce ifadeler
kullanmak durumunda kaldım.
Mesela BX=2000h olsun, mov dl, 20h[bx] şeklinde bir komut kullanıldığında, DS:2020h
adresindeki 1 byte�lık değer dl�ye kopyalanacaktır. Aynı şekilde BP=3030h olduğunu
varsayalım, mov dh,1020h[bp] gibi bir komut ile, SS:4050h adresindeki 1 byte�lık değer
dh�a kopyalanır.
mov al,[bx][si]
gibi bir komut işlenince AL�ye kopyalanacak veri DS:0860 adresinden alınır.
Aynı şekilde;
BP=1598h DI=1004 ve mov ax,[bp+di] gibi bir komut işleniyorsa; AX, SS:259Ch ve
SS:259Dh adreslerindeki veri ile yüklenir.
f- Taban İndeksli artı direkt adresleme (Based Indexed Plus Displacement Addressing)
Bu adresleme modu taban indeksli adreslemeye 8 yada 16 bitlik sabit bir değerin
eklenmiş halidir.
TASM ve MASM assembler�ları indeksli, taban indeksli, ve taban indeksli artı direkt
adresleme için değişik yazım şekillerini desteklerler;
Yukarıda yazılan bu 3 adresleme modundaki operandlar aynı işi yaparlar. MASM ve TASM
"[ ]" sembollerine "+" operatörü gibi davranır. ( disp[bx][si] = disp[bx+si] örneğinde
olduğu gibi )
8086 Adresleme Hafıza Adresleme Modlarını Hatırlamak İçin Kolay Bir Yol:
8086 işlemcisi için toplam 17 adet adresleme modu mevcuttur. Bunlar disp, [bx], [bp],
[si], [di], disp[bx], disp[bp], disp[si], disp[di], [bx][si], [bx][di], [bp][si], [bp][di],
disp[bx][si], disp [bx][di], disp[bp][si], and disp[bp][di] adresleme modlarıdır. Aşağıdaki
şekil, bu 17 adresleme modunu ezberlemeniz yerine kolayca hatırlamanıza yardımcı
olacaktır.
Şekil 1 - 8086 adresleme modları için yardımcı şekil.
Bu şekil ile 17 adresleme modunu kolayca görebilirsiniz. Her sütundaki elemanları teker-
teker seçebilirsiniz veya herhangi bir sütunu geçip diğer iki sütunun birleşimi ile doğru bir
adresleme modu yakalayabilirsiniz.
Örnekler;
disp[bx], disp[bp]
disp[si], disp[di]
Son olarak tüm sütunların birleşimi ile aşağıdaki geçerli adresleme modlarını kolayca
çıkartabilirsiniz.
Not:Hafıza adresleme modlarında şayet hesaplanan etkin adres 0FFFFh değerinden büyük
olursa, CPU bu hesaplama sonucu oluşan taşmayı göz ardı eder ve FFFFh�e ekleneni 0
dan itibaren ekler. Örneğin BX=10h olsun ve mov al,0FFFFh[bx] komutu işlensin. Bu
durunda AL kaydedicisine ds:1000Fh adresindeki veri değil ds:000Fh adresindeki veri
yüklenir. (FFFFh+1h=0000, FFFFh+2h=0001,�.. FFFFh+16h=000Fh)
Sonuç:
8086 adresleme modlarını 2 bölümde inceledik, ilk olarak kaydedici adresleme modlarını
anlatmaya çalıştım. Kaydedici adresleme de segment kaydedicilerini kendi amaçları
dışında kullanmanız tavsiye edilmez, bu iş için yeterince genel amaçlı ve indeks
kaydedicisi var zaten.
Karışık olan kısım ise hafıza adresleme modlarıdır. Hafıza adresleme modlarında belirtilen
operand daima bir offset adresini işaret eder, buna etkin adres hesaplama da denir.
Hafıza adresleme modlarını unutmamanız için sizlere kolay bir yol göstermeye çalıştım.
Adresleme modlarının isimlerinden ziyade kullanım formatları önemlidir. Her komut her
adresleme modunu desteklemez. Hangi komutun hangi adresleme modunda kullanılacağı
komut setlerinden faydalanılarak bulunur.
�8086 instruction set� veya �x86 instruction set� anahtar kelimelerini internetten
aratacak olursanız karşınıza yığınla komut seti gelecektir. Genelde komut seti kılavuzları
aşağıdaki tablo gibidirler.
Size
Operands Clocks
Bytes
286 386 486
reg, reg 2 2 1 2
mem, reg 3 2 1 2-4
reg, mem 5 4 1 2-4
mem,
3 2 1 3-6
imm
reg, imm 2 2 1 2-3
segreg,
2 2 3 2
reg16
segreg,
5 5 9 2-4
mem16
... ... ... ... ...
... ... ... ... ...
... ... ... ... ...
Tablo 1 - MOV komutu için örnek tablo
Bu tabloda birinci sütun MOV komutu ile kullanılacak operandın hangi adresleme
modlarını desteklediğini gösteriyor. Örneğin mem,imm (displacement,immediate) acil
adresleme yi desteklediğini gösterir. reg,reg (register,register) kaydedici adreslemenin
yapılabileceğini, segreg,reg16 (segment register, register 16 bit ) bir segment
kaydedicisine 16 bitlik bir kaydediciden kopyalama işleminin yapılabileceğini gösterir.
Son olarak x86 uyumlu hiçbir işlemci mem,mem şeklinde yani hafızadan hafızaya
doğrudan bir adresleme modunu desteklemez, aslında bu güne kadar bunu destekleyen
hiç bir işlemci görmedim, zaten böyle bir adresleme modunun desteklenmesi durumunda
o sistem için yapılan programların çökme olasılığı çok büyüktür.
kullanım formatı;
16 bitlik Gerçek moda (Real Mode) 80386 ve üstü işlemcilerde programlama yapmanız
offset adreslerinin 32 bit olacağı anlamına gelmez. Segmentlerin her zaman gerçek moda
64Kb. olduğunu unutmamak gerekir, bu yüzden offset adresleri gerçek modda 0...0FFFFh
arasını gösterir. Yani 64Kb.�ı geçecek ofset adreslerini gerçek moda değil ancak
korumalı moda (Protected mode) kullanabilirsiniz. Fakat gerçek moda 32 bitlik
kaydedicileri hesaplamalarda kullanmanızda bir engel yoktur. 80386 kaydedici dolaylı
adreslemede kullanabileceğiniz tüm geçerli adresleme formatı aşağıdaki gibidir.
80386 İndeksli adresleme modu 32 bitlik bir kaydedici ile sabit bir değerin birleşiminden
meydana gelir. Taban indeksli adresleme modu iki adet 32 bitlik kaydedicinin
birleşiminden ve taban indeksli artı direkt adresleme modu ise bir sabit değer ile iki tane
32 bitlik kaydedicinin birleşiminden meydana gelir. Hiçbir zaman unutmamalısınız ki bu
adresleme modları ne kadar 32 bitlik olsa da, şayet 16 bitlik gerçek moda kullanılırlarsa
erişebilecekleri ofset adresleri 16 bit�i geçemez.
80386 taban indeksli hafıza adresleme modlarında taban adres ve indeks adresi diye iki
tane terim kullanılır. Taban adresini ilk operand iken indeks adresini son operand
gösterir. Bu kural kaydedici ismi ne olursa olsun geçerlidir. Fakat 8086 indeksli veya
taban indeksli hafıza adresleme modlarında, taban adresini sadece BX veya sabit bir
değer gösterebilirken, indeks adreslerinide SI, DI, BP ve BX gösterebiliyordu. 80386 ve
sonrası işlemcilerde bulunan bu esneklik şüphesiz ki programcıların işini kolaylaştırır.
Aşağıda 80386 indeksli hafıza adresleme modları için birkaç örnek görülüyor;
mov al, disp[eax] ;İndeksli adresleme
mov al, [ebx+disp] ;modları.
mov al, [ecx][disp]
mov al, disp[edx]
mov al, disp[esi]
mov al, disp[edi]
mov al, disp[ebp] ;SS in ofsetleri için
mov al, disp[esp] ;için.
Aşağıdaki örneklerde ise 80386 taban indeksli adresleme modları için çeşitli örnekler
görünüyor. Bu örneklerde ilk kaydedici taban adresini gösterirken, ikinci kaydedici ise
indeks adresini gösterir. Tabi ki sonuçta bu iki kaydedicinin içindeki değerler toplanarak
etkin olan hafıza adresini işaret edecekler. Kaydedicilerin 32 bitlik olduğunu düşünecek
olursanız 4GB�lık hafızanın istediğiniz bir konumuna erişebilineceğini de görebilirsiniz
(tabi ki korumalı modda). Burada taban adresi olarak esp ve ebp kullanıldığında SS�deki
bir ofset adresini göstereceğini unutmayın. Bununla birlikte indeks adreslerini gösteren
kaydedicilerin türü verinin hangi segmentten alınacağına etkisi olmaz.
Yukarıdaki adresleme modlarına sabit bir değer eklerseniz 80386 taban indeksli artı
direkt adresleme yapmış olursunuz.
80386 indeksli adresleme modlarında sadece tek bir kısıtlama söz konusudur; esp
kaydedicisini indeks kaydedicisi olarak kullanamazsınız ama esp nin taban kaydedicisi
olarak kullanılmasının bir kısıtlaması yoktur.
c- 80386 Ölçekli İndeksli Adresleme Modu (80386 Scaled Indexed Addressing Modes)
Kullanımı;
disp[index*n]
[base][index*n]
veya
disp[base][index*n]
"base" ve "index" 80386�nın herhangi bir genel amaçlı kaydedicisi olabilir. "n" ise 1, 2, 4
ve 8 değerlerini alabilir.
Sonuç:
80386 adresleme modları 8086 adresleme modlarına nazaran programcıya daha çok
olanak sağladığından daha çok tercih edilirler. Benim şahsi görüşüm X86 uyumlu PC�ler
gerçek gücünü 80386 işlemcisi ile birlikte gelen bu olanaklardan almışlardır. Zaten CPU
tarihinde 80386 bir devrin kapanıp diğer bir devrin başladığı nokta olarak kabul edilir. Ne
var ki 8086 adresleme modları 80386 adresleme modlarına nazaran daha hızlı çalışırlar.
Bu yüzden söz konusu olan hız ise (mikro saniyelerden bahsediyoruz ki bunlar bazen
birleşip dakikalar oluyor) adresleme modu seçilirken çok dikkat edilmesi gerekir.
80386 taban indeksli ve taban indeksli artı direkt adresleme modları, ölçeği 1 olan (n=1)
80386 Ölçekli İndeksli Adresleme Modu olarak düşünülebilir ve bu hafıza adresleme
modlarında gösterilen ilk kaydedici taban ikinci kaydedici indeks adresini gösterir. Taban
adresini gösteren kaydedici ebp veya esp ise bu adresleme SS�deki bir ofset adresine
diğer kaydediciler taban adresi olarak seçildiğinde DS deki bir ofset adreslenmiş olunur.
İlk yazılan kaydedici ölçekli olarak gösterilirse ("*n") bu bir taban adresi olmaktan çıkar
bir indeks adresi olur. Ayrıca operanda segment kaydedicisini de göstererek adresleme
modunu zorlayabilirsiniz.
Şimdi sıra uygulamada. Bu uygulama ile hem adresleme modlarını hem de Turbo
Debugger programının kullanımını açıklamaya çalışacağım.
.MODEL SMALL
.STACK 32
.DATA
.CODE
ANA PROC
;----------------------------------------
;8086 Hafıza Adresleme Modlarına Örnekler
;----------------------------------------
;----------------------------------------
;80386 Hafıza Adresleme Modlarına Örnekler
;----------------------------------------
ANA ENDP
END ANA
Yukarıdaki program sadece 8086 ve 80386 adresleme modlarına örnek olması için
hazırlanmıştır. Hafızadaki veriler nasıl erişilir sorusuna yanıt arayanların bu programı
dikkatle ve satır-satır incelemesini tavsiye ederim.
Sıra bu programı derledikten sonra Turbo Debugger ile açıp hafızada ve işlemcide hangi
olayların olduğunu incelemeye geldi. Turbo Debugger programının 16 ve 32 bitlik hafızayı
incelemek için iki ayrı versiyonu var şu anda gerçek modda çalışan programlar
hazırladığımızdan dolayı 16 bitlik sürümünü kullanacağız. Programı buradan
indirebilirsiniz. İndireceğiniz bu dosya Turbo Debugger, Turbo Assembler�ı, Turbo Linker
paketini içermekte. Ayrıca bu pakete, içinizde belki hala DOS veya Windows 95 kullanan
vardır diye bir mouse programı ekledim. Şayet durum böyleyse önce konsola "mouse"
yazdıktan sonra Turbo Debugger�ı çalıştırın.
Turbo Debugger Microsoft�un debug�ı ile aynı işi yapar fakat daha kullanışlı bir kullanıcı
ara yüzüne sahiptir. Turbo Debugger ile program dosyalarınızı açtıktan sonra bir çok
işlem yapabilirsiniz.
Not: Turbo Debugger�ı pencere modunda çalıştırırsanız GDI (Graphics Device Interface)
kaynaklarını sömürürcesine kullandığından bilgisayarınız yavaşlayabilir. Bu yüzden tam
ekran modunda başlatmanızı öneririm. Programı pencere modunda başlattıktan sonra
konsol penceresinin ayarlarından tam ekran yapmanızın da bir faydası olmaz. Bu yüzden
önce komut istemini açın, sonra tam ekran yapın ve Turbo Debugger�i çalıştırın.
İlk olarak konsola "td" yazarak programı çalıştırın. Turbo Debugger�ın ekranına
ulaşacaksınız.
Şekil 2 - Turbo Debugger ekranı.
Turbo Debugger açıldıktan sonra "File" menüsünden derlediğiniz program dosyasını açıp
kodlarını inceleyebilirsiniz. Program dosyanızı açtıktan sonra "Program has no symbol
table" diye bir mesaj gelebilir. Bu mesaj penceresinde "OK" e tıklayıp çalışmaya
başlayabilirsiniz.
Şayet bu pencerede yazı yerine hex kodları görürseniz, pencerenin üzerine sağ tıklayıp
açılan menüden "Display As" ı seçin.
Verilerinizi görmek için 2. pencereye sağ tıklayıp "goto" yu seçin. Açılan pencereye
görmek istediğiniz adresi yazın.
Yalnız .exe tipindeki program dosyalarında verilerinizi görebilmek için mov ax,@data ve
mov ds,ax satırlarının işlenmiş olması gerekir.
Mesela yukarıdaki şekilde mov al, [BX] komutu işleniyor ve kaydedici dolaylı hafıza
adresleme yapılıyor. Burada AL kaydedicisine kopyalanacak olan veriyi "ds:0000 = 05"
şeklinde görebilirsiniz.
32 bitlik kaydedicileri görmek içinse kaydedicileri gösteren kısıma (4. kısım) sağ tıklayıp
"Registers 32 bit" i seçebilirsiniz.
Turbo Debugger�ın daha bir çok özelliği var ama şu anda bizlere bu kadarı yeter. Şimdi
sizler Turbo Debugger ile bu makaledeki programı inceleyerek pratik yapın, ayrıca bu
güne kadar yazdığımız programları da Turbo Debugger ile inceleyerek programı
kullanmasını daha iyi kavrayın.
Bir sonraki makalemizde veri tipleri ve x86 komutlarını inceleyeceğiz. Daha sonra
prosedür ve makrolarıda gördükten sonra nihayet kendi programlarınızı yazabilir seviyeye
geleceksiniz. Genelde sizlerden makalelerin daha sık aralıklarla yayınlanması ile ilgili
mailler alıyorum. Fakat bir makale hazırlamak en az 1 haftamı alıyor ve üniversitedeki
derslerden ve işlerden dolayı çok az vaktim oluyor. Bu yüzden makalelerimin yayınlanma
aralığı birden 20 güne sıçrayabiliyor. Hal böyleyken sizlerin arayı soğutmamak için başka
kaynaklardan faydalanmasını tavsiye ediyorum. Bu şekilde çalışan bazı arkadaşlar
internetten kaynak kod bulup, derleyip takıldıkları yerlerde bana mail atıyorlar, bunlara
cevap vermesi daha az zamanımı alıyor ve sizler içinde çok faydalı olacağından eminim.
Şayet İngilizce okuduğunuzu anlıyorsanız sizlere Art of Assembly�yi tavsiye ederim.
İnternetten okuyabileceğiniz bedava bir kitap. Mesela bu makaleyi yazarken bu
kaynaktan çok yararlandım ve sizlere de tavsiye ederim.
Evet yine geldi ayrılık vakti. Sizlere bol assembly�li günler dileyerek huzurlarınızdan
ayrılıyor ve bir sonraki makalede görüşmek dileğiyle esen kalın diyorum. (TV programı
sunucusu olabilirdim aslında :))
X86 Assembly Dilinde Değişken Bildirimi – 1
Daha önce üst seviye programlama dilleri ile uğraştıysanız değişkenler hakkında bilginiz
muhakkak vardır. x86 assembly dilinde de programcılar programlarında kullanacakları
bazı verileri, değişken olarak tanımlarlar. Aslında bunlar byte yada bytelardan oluşan
dizilerdir. Hafızadaki bu veriler, programınızdaki kullanım şekline göre değişik haller
alabilir.
Örneğin 2 sayıyı toplayan program yazmayı düşünelim. Bunu 2 değişik şekilde yapabiliriz.
Birincisi acil adresleme modu ile;
MOV AL,5
ADD AL,4
.DATA
sayi1 DB 5
sayi2 DB 4
toplam DB ?
.
.
.
.CODE
.
.
MOV AL,sayi1
ADD AL,sayi2
MOV toplam,AL
Yukarıdaki program parçasında 3 adet bir byte�lık değişken tanımlanmıştır. Bunlar sayi1,
sayi2 ve toplam değişkenleridir. Aslında bu yazdığımız satırlar assembler�a bir demeç
şeklindedir. Assembler programımızı derlerken, hafızada bizim kullanmamız için 3
byte�lık alan ayıracak ve bu alanlardan birine 5 diğerine 4 değerlerini yerleştirecektir.
Toplam değişkeninin değeri program çalıştığında önemli olacağından dolayı, onun değeri
henüz atanmamış fakat şundan da emin olun ki o adreste muhakkak bir değer mevcuttur.
Gördüğünüz gibi değişkenlerini isimleri, türleri ve değerleri mevcut. Değişken ismi (sayi1
gibi) aslında bir offset adresidir ve kullanımı seçimliktir. Şayet değişken tanımlarken isim
kullanmazsanız assembler yine hafızada değişkenin türüne göre yer ayırır. Fakat böyle bir
durumda değişkeni programınızın içinde kullanmak için adresini (ds:0000 gibi) bilmeniz
gerekir. Çok sayıda değişken tanımlarken bu adresleri hesaplamak güç olacağından dolayı
değişkenlere isim vermemek pek tavsiye edilmez. Değişkenin türü ise boyutu ile alaklıdır,
örneğin DB ile değişkenimizin 1 byte�lık olacağını belirtiyoruz. Son olarak değişkenin
değeri tabi ki bir rakam yada sayı olmak durumunda. Şayet başlangıçta değer önemli
değilse ? operatörünü kullanabilirsiniz.
Kaynak kodda değişkenleri bu şekilde yazarak assemblera hafızada yer tahsis etmesini
söylemiş, bir başka deyişle değişken deklare etmiş oluyoruz.
İşaret Karmaşası
1 byte�lık bir hafıza alanına ne sığar? Toplam 256 adet farklı değer 1 byte ile
tanımlanabilir. Bunlar onluk düzende 0-255 arası sayılar veya -128�den 127 ye kadar
olan işaretli sayılar olabilir. Kafanız karışmasın bu değerleri aslında binary olarak
düşünmek lazım, çünkü 0...255 ve -128...127 arası sayılar toplam 512 adet ediyor.
Aslında, bunlar sadece 1�lerin ve 0�ların kombinasyonlarıdır. Yani 1 byte 0000 00002 ile
1111 11112 arasında değişen kombinasyonlardan oluşur.
Günlük hayatımızda işaretli bir sayıyı tanımlamak ne kadarda kolay değil mi? Başına -
işaretini getirdiğimiz zaman işlem tamamdır. Fakat bilgisayarlarda 1 ve 0 dan başka
kullanacağınız bir değer yok. İşaretli veya işaretsiz sayılar, harfler, metinler, resimler,
mp3�ler ve Tomb Raider :) tamamen 1 ve 0�lardan oluşur. İşte bu 1 ve 0�ları program
içinde yorumlamak önemlidir, ancak o zaman anlam kazanırlar.
Aslında işlemci (CPU) sadece matematiksel, mantıksal ve veri taşıma işlemleri yapar. Bu
bağlamda bizim için alt seviyede önemli olan 1 ve 0�lardır. O zaman işaretli ve işaretsiz
sayılar neye göre belirlenecek? Boyutlar ve işlemci durum kaydedicisi (flag register)
burada imdadımıza yetişiyor. Örneğin 1111 1111 değeri onluk düzende 255�mi yoksa -
1�mi.
-1�dir, çünkü 1 ile toplandığında 1 byte�lık alanda 0000 00002 değeri elde edilir ve elde
biti oluşur.
Şekil 1 - 1 byte�lık bir değer 1 byte�lık başka bir değerle toplanınca sonuç 1 byte�lık olmak zorundadır.
1 byte�lık bir değer 1 byte�lık başka bir değerle toplanınca sonuç bir byte�lık olmak
zorundadır, çünkü ayrılan fiziksel alan bu kadardır. Bu işlemde artan 1 bitlik değer ilkokul
matematiğinden bildiğimiz elde�dir ve bu elde işlemci durum kaydedicisinin C (carry)
bitinde saklanır. Daha önce işlemci durum kaydedicisinin bitsel olarak önemli olduğundan
bahsetmiştim.
BASİT DEĞİŞKENLER
Byte Değişkenleri
i db 0
j byte 255 ;MASM için alternatif direktif
k sbyte -1 ;MASM için alternatif direktif
DB direktifi ile;
Word Değişkenler
-32768...65535 arasındaki tam sayları DW direktifi ile deklare edebiliriz. MASM için word
ve sword direktifleri de aynı işi görür. Word değişkenler hafızada 16 bit yani 2 byte�lık
alan kaplarlar.
sayi1 dw ?
sayi2 dw ?
isayi1 dw ?
sayi_0 dw 0
sayi_eksi dw -1
sayi_enbuyuk dw 65535
pointer dw sayi1
sayi1 dd ?
sayi2 dd ?
isayi1 dd ?
sayi_0 dd 0
sayi_eksi dd -1
sayi_enbuyuk dd 4000000000
pointer dd sayi2
DD direktifi ile;
6, 8 ve 10 Byte�lık Değişkenler
DF: 80386 ve üstü işlemcilerde, 32 bitlik korumalı modda, 48 bitlik pointer (indeksleme
amacıyla, offset adreslerini göstermek için kullanılan bir işaretçi) olarak kullanılır. Bu
direktif ile 6 byte�lık değişkenlerin tanımlanması mümkün olsa da 80386 ve sonrası
işlemcilerde pointer olarak kullanılır.
DQ: 64 bitlik kayar noktalı sayları 64 bitlik tamsayıları deklare etmek için kullanılır. Daha
çok kayar noktalı sayıları deklare etmek için kullanılır, çünkü x86 ailesinin henüz 64 bitlik
genel amaçlı bir kaydedicisi yoktur. Yani bu direktif ile 64 bitlik bir tam sayı deklare
ettiğinizde, bu sayı ile doğrudan işlem yapamazsınız çünkü en büyük kaydedici 32
bitliktir.
DT: Daha hassas (80 bitlik) kayar noktalı sayılar ve 10 byte�lık BCD değerleri deklare
etmek için kullanılır.
Yukarıdaki direktiflerle ilgili ayrıntılı bilgileri kayar noktalı sayılarla işlemler başlığı altında
ilerleyen makalelerimizde vereceğim.
Pointer işaretçi anlamına gelip adresleme modlarında gördüğümüz indeksli adresleme ile
alakalıdır. x86 uyumlu işlemciler iki tür pointer�ı desteklerler. Bunlar near (yakın) ve far
(uzak) pointer�lardır. Buradaki yakın ve uzak kavramı referans alınan adresin segmenti
(64Kb.) aşmaması yada aşması durumunu ifade eder.
Near pointer�ler 16 bitlik�tir ve bir segmentin offsetlerine erişmek için kullanılırlar. p bir
değişken olsun, p=1000h için aşağıdaki kodları inceleyelim.
5555:3333 adresine erişip, buraya FFh değerini kopyalamaya çalışacağız. Bu işlemi yapan
program aşağıdaki gibidir. Kaynak kodları buradan download edebilirsiniz.
ÇOKLU DEĞİŞKENLER
Bu tür değişkenler birden fazla parçadan oluşur. Tek boyutlu ve çok boyutlu diziler
(arrays) ve yapılar (structures) bu tür değişkenlerdir. Bu makalede sadece tek boyutlu
dizileri anlatmaya çalışacağım, çok boyutlu diziler ve yapıları başka bir makalede güzel
örneklerle, amacına uygun bir şekilde anlatacağım.
Diziler
Dizilerlerde diğer değişkenler gibi hafızaya yerleştirilmiş verilerden oluşur. Ard-arda gelen
byte, word veya double word değişkenler dizileri oluşturabilir. Daha önceki
makalelerimizde "Merhaba Assembly" yazısını ekrana yazdırmıştık, işte bu karakterlerde
hafızada dizi olarak saklanır. Şu anda yazdığım karakterlerde hafızada bir dizi veri olarak
saklanacak.
Dizi kavramının yanında getirdiği bazı terimler vardır, bunlar indeks, taban adresi, dizi
elemanı ve eleman tipi�dir. Burada dikkat edilmesi gereken en önemli nokta diziyi
oluşturacak elemanların aynı türden olma koşuludur. Diğer terimleride isterseniz
aşağıdaki şekille açıklamaya çalışalım.
Dizi DB 100 DUP (?) ;byte türünden 100 elemanlı bir dizi
Dizi DW 100 DUP (?) ;word türünden olduğundan dolayı hafızada 200 byte�lık
alan kaplar.
Dizi DB 32, 45, 67, 3F, 0, 7, 11 ;7 tane byte türünden elemandan oluşan bir dizi.
Yazi DB �Merhaba Assembly� ;16 elemanlı bir karakter dizisi, hafızada ascii
kod karşılıkları saklanır.
Yukarıdaki dizinin 2. elemanına erişmek demek o elemanın adresini bulmak ile aynı
anlama gelir. Bunun için formülümüz vardı;
yada;
Tabi ki bu işlemi bir program ile yapmak lazım. Aynı işi yapan program parçası aşağıda
verilmiştir.
mov bx, 2 ; indeks değeri BX�e yükleniyor,
add bx, bx ; 2*bx işlemi yapılıyor,
mov ax, WordDizi [bx] ; AX�e dizinin 2. elemanı (1234h) yükleniyor.
Dizi elemanlarına erişmek için çok değişik teknikler mevcut, fakat bunları ilerleyen
makalelerimizde açıklayacağım. Bazen assembly programcıları dizilerini deklare ederken,
dizinin ilk elemanı olarak indeks değerini koyarlar. Tabi ki bu durumun doğruluğu
görecelidir. Örneğin byte türünden bir dizi için bu teknikle en fazla 255 elemandan oluşan
bir dizi deklare edebilirsiniz. Daha fazla eleman deklare etmek için indeks değerinin
boyutunu arttırmak gerekir ama bu durumda da dizi elemanlarının aynı türden olması
şartını göz ardı etmemek gerekir.
*
**
***
****
Alın size ödev, bir daha ki makaleye kadar bu programı yazmaya çalışın, böylece hem
adresleme modlarını hem de tek boyutlu dizileri kavramış olursunuz. Sakın zor demeyin
elin oğlu dizilerden matrix türetip, sonra ona felsefi bir boyut ekleyip filim yapıyor :)
Diyelim ki klavyeden girilen iki adet değişken var bunlar eşit ise ekrana "eşit" eşit değilse
"eşit değil" yazdırmak istiyoruz. Bunu üst seviye programlama dillerinde basit bir şekilde
if-else mantığıyla yapabilirsiniz. Peki mikroişlemci bu değerlerin eşit olup olmadığını nasıl
gösterecek bize. Cevabı basit bu değerlerin farkını alarak, sonuç sıfırsa girilen değerler
eşittir aksi taktirde eşit değildir. İyi ama assembly programcısı bu sonuç sıfır değerini
nereden okuyacak?
Bir işlemin sonucunun sıfır, negatif, pozitif veya elde ürettiği veya işlemin
gerçekleştirilmesi için borç alındığı gibi durumları gösteren kaydediciye işlemci durum
kaydedicisi diyoruz. Turbo debugger ile işlemci durum kaydedicisinin içeriğini
gözlemleyebilirsiniz.
Şekilde kırmızı çerçeve içine alınmış kısım işlemci durum kaydedicisini bitsel olarak
gösteriyor. Diğer kaydedicilerin aksine işlemci durum kaydedicisi programcı için bitsel
olarak anlamlıdır. Ve bu bitlerin her birinin bir adı vardır. Örneğin C biti Carry yani taşıma
biti veya Z biti Zero yani sıfır biti’dir. Terminolojide genellikle bu bitler "flag" yani bayrak
olarak adlandırılır. Aslında assembly programcıları işlemci durum kaydedicisi yerine
bayrak kaydedicisi terimini daha çok kullanırlar. Örneğin Z için Zero Flag yani sıfır
bayrağı denir.
8086, 80286, 80386, 80486 ve Pentium işlemcilerin bayrak kaydedicileri biraz farklıdır,
daha doğrusu işlemciler geliştikçe ek bayraklar eklenmiştir, mesela 80286’dan itibaren
işletim sistemlerinin yazımı için gerekli olan ayrıcalıklı mod bayrağı eklenmiştir.
Yukarıdaki şekilde 8086’nın 8 bitlik bayrak kaydedicisini görmektesiniz, aşağıdaki şekilde
ise 8086’dan 80486’ya kadar olan işlemcilerin sahip olduğu bayrakları görebilirsiniz.
Şekil 2 - x86 işlemci durum (bayrak) kaydedicisi.
Bayrak kaydedicisindeki her bir bitin 1 olma durumuna SET, 0 olma durumuna da RESET
denir. İşlemcinin ürettiği sonuçlar hakkında programcıyı uyaran bayrak kaydedicisindeki
durumlar görecelidir. Örneğin carry (taşıma) bayrağı 1 olduğunda bazen işlemin elde
ürettiğini bazen de borç alındığını ifade edebilir. Bazen de iki bayrağın durumu birlikte
değerlendirilerek bazı sonuçlara varılır. Örneğin;
Bu örneği biraz daha açıklayacağım fakat daha önce matematik dersinde gördüğümüz
bazı olayları hatırlatmak istiyorum. Hani bir fonksiyonun çözümü için sorularda "her x
gerçel sayısı için" gibi ifadeler geçer ya, işte bu tür kabullenmeler assembly dilinde çok
önemlidir. Çünkü 1 ve 0’ları ancak bu şekilde yorumlayabilirsiniz, yani baştan evrensel
kümenizi kafanızda tanımlamanız gerekir. Bu örnekte de FFFE=65534 değerini gösterirse
diye başlıyoruz işe, nitekim FFFE işaretli sayı olarak kabul edilseydi -2 değerini
gösterecekti. Ama burada işaretli değil sadece pozitif tamsayılarla işlem yapıldığı
varsayılıyor. Bu işlemin sonucu AX kaydedicisinde 0001 olarak görülür. İşlemci sayıların
işaretli veya işaretsiz olduğuna bakmadan FFFE ye 3 ekler ve FFFE değeri önce FFFF
sonra 0000 ve daha sonrada 0001 olur. Tabi ki 65534 + 3, işleminin sonucu 1’e eşit
değildir. İşlemin doğru sonucu olan 65537 değeri 2 byte’lık AX kaydedicisinin içine
sığmadığından bu sonuç üretilmiştir. Bu toplama işlemi sürecinde sayı FFFF’den 0000’a
dönerken C=1 olur, bunu elde olarak düşünebilirsiniz (tabi ki pozitif tamsayılar kümesi
baz alınarak işlem yapılıyorsa) Aslında 0001 sonucunun başına bu elde getirildiğinde yani
sonuç C biti ile birlikte okunursa 10001 olur ve bu gerçektende 65537 değerine eşittir.
Fakat programcı doğal olarak AX kaydedicisini içindeki değeri yani 0001’i sonuç olarak
alacaktır. Burada sonuç anlamlı aralık olan 0...65535 arasında olduğundan O=0 olmuştur.
Özetle sonuç anlamlı aralıkta yanlış bir değerdir, bu durumu programcı iki bayrağa birden
bakarak anlayabilir.
Yukarıdaki gibi bir durumda programcı işlem yapıldıktan sonra bazı özel komutlarla C ve
O (overflow-taşma) bitlerinide kontrol eder ve bu bitler sırasıyla 1 ve 0 ise bir boyut
taşması olmuş demektir. Bu durumda programcı kullanıcı ekranına "sonuç çok büyük" gibi
bir mesaj gönderebilir. Hesap makinelerinin belli bir değere kadar işlem yapabildiğini
hatırlayın.
Bu örnek için en başta FFFE işaretli bir saydır deseydik herhangi bir taşma söz konusu
olmayacaktı. İşlemci yine aynı sonucu üretecek fakat bu sefer programcı bunu -2+3
olarak değerlendireceğinden sonuç doğal olarak 1 ve AX’te 0001 değeri doğru sonuç
olacaktı. Yine C=1 ve O=0 olsa da programcı için bu durum bir taşmayı
göstermeyecektir.
İşte assembly programlama dili matematiğin bu basit gibi görünen temellerine bağlıdır.
Hafızadaki 1 ve 0’ları nasıl yorumlarsanız öyle şekil alırlar. Bazen bu 1 ve 0’lar bir resim,
bazen bir mp3 bazen de bir kelime oluveririler. Hani matrix filminde herkes kodlara
bakıyor ama sadece Neo olayı idrak edebiliyordu ya, işte o misal :)
8086 için bayrak kaydedicisi 16 bitliktir ve bu 16 bit’te sadece 9 adet bayrak mevcuttur.
386 ve sonrası işlemcilerde ise bayrak kaydedicisi 32 bitliktir ve bu 32 bit’in her biri bir
bayrağı temsil etmez, aralarda hiçbir şeyi ifade etmeyen boş bit’ler mevcuttur. Şu anda
gerçek mod programları yani 8086 için programlar yazdığımızdan bu 9 adet bayrağın ne
anlama geldiklerini incelememiz yeterlidir.
Overflow Flag (OF) (Taşma Bayrağı): İşaret taşması olduğunda olduğunda set (1)
olur, mesela 1 byte’lık işaretli sayılarda 100+50=150’dir, fakat sonuç -128...127
aralığında değildir.
Direction Flag (DF) (Yön bayrağı): String işlemlerini yapan komutlar için kullanılır. 1
olduğunda index kaydedicisi otomatik olarak azalır. 0 ise index kaydedicisi otomatik
olarak artar. (mesela bir string hafızanın bir bölgesinden başka bir bölgeye kopyalanırken
işlemin stringin sonundanmı yoksa başındanmı yapılacağını D flag ile belirleyebilirsiniz)
Single-step Flag or Trace (TF) (Tek Adım veya İzleme Bayrağı): Genelde debugger
programları için kullanılır, 1 olduğunda bir anda sadece 1 komut işlenir.
Sign Flag (SF) (İşaret Bayrağı): MSB biti (en soldaki bit) 1 olduğunda bu bayrakta 1
olur.
Zero Flag (ZF) (Sıfır Bayrağı): Sonuç sıfır olduğunda 1 olur.
Auxiliary carry Flag (AF) (Ara Elde Bayrağı): AL kaydedicisinin 0-3 arasındaki
bitlerinde yapılan işlemde elde veya borç üretilirse 1 olur.
Parity Flag (PF) (Eşlik Bayrağı): 1 olduğunda sonucun düşük değerlikli baytındaki
(mesela 127Fh gibi word boyutunda bir sonuç üretilmişse bunun 7F kısmı için) 1’lerin
sayısının çift olduğunu gösterir.
Carry Flag (CF) (Taşıma Bayrağı): 1 olduğunda elde üretildiğini veya borç alındığını
belirtir.
3- Programcılar bazı işlemleri anlayabilmeleri için bazen birden fazla bayrağın birlikte
ürettikleri durumları göz önüne almak zorundadırlar.
Uzun bir süre bu bayraklardan OF, SF, ZF, CF olanları bizim için önemli olacak.
Bayrakların geri kalan kısımları ve bayrakları etkilen komutları sonraki makalelerimizde
yeri geldikçe açıklamaya çalışacağım. İsterseniz şimdi bayrak kaydedicisi ve bayraklarla
ilgili kafanızda daha belirgin bir şeklin oluşabilmesi için makalemizin başında
bahsettiğimiz örneği hayata geçirelim.
JMP
Bu komut ile hedef adrese dallanma gerçekleştirilir.
HedefAdres
JE
Bu komut ile hedef adrese Z bayrağı 1 ise dallanma gerçekleştirilir.
HedefAdres
Yukarıdaki programın kaynak kodlarını buradan download edin ve TASM ile derleyin. Bu
programı çalıştırdığınızda sizden iki adet değer girmenizi isteyecek, değerleri girerken
enter tuşuna basmanıza gerek yok. Şayet girilen değerler eşitse ekranda "Girilen
Degerler Esittir" değilse "Girilen Degerler Esit Degildir" mesajını göreceksiniz.
Değerlerin eşit olup olmama durumunu belirleyen işlemci durum kaydedicisinin Z biti (Z
bayrağı) dir. Bu bayrağın durumu karşılaştırma komutundan sonra girilen değerlere göre
1 yada 0 olur. Şayet girilen değerler eşit ise, eşit olan bu değerler karşılaştırılır ve sonuç
sıfır elde edilir, bu durumda Z=1 olur. JE komutu ise ancak Z=1 olduğunda, operandında
belirtilen adrese dallanma işlemini geçekleştirir. Aksi durumda işlemci klasik yolunu izler
ve bir sonraki komutu işler.
Şekil 6 - Z=1 olduğunda 0024h offset adresine dallanılacak.
Klavyeden farklı değerler girildiğinde ise, karşılaştırma işleminin sonucunda z=0 olur. Bu
durumda dallanma gerçekleşmez ve işlemci normal seyrini sürdürerek bir sonraki komutu
işler.
Şekil 7 - Z=0 olduğunda dallanma gerçekleşmeyecek, program sonraki komutu takip edecektir.
Bayrakların durumları bayrakları etkileyen bir komut işlendiğinde değişir, her komut
bayrakları etkilemez. Burada CMP komutu 6 adet bayrağı etkiliyor. Hangi komutun hangi
bayrakları etkilediğine Intel’in komut setinden bakabilirsiniz. Komut setinden CMP
komutunu bulursanız bu 6 bayrağın neler olduğunu görebilirsiniz.
Şekil 8 - CMP ile AF, CF, OF, PF, SF, ZF bayrakları etkilenir.
İsterseniz Şekil7 deki etkilenen tüm bayrakları ve neden dolayı 1 yada 0 olduklarını
inceleyelim. Burada yapılan işlem "CMP BL, AL" dir ve BL=33, AL=34’dür. BL-AL yani
33h-34h işlemini yapan CMP komutu aşağıdaki nedenlerden dolayı bu bayrakları
etkilemiştir.
1- 33-34 işlemi için borç gerektiğinden (Küçük sayı büyük sayıdan çıkartılıyor) CF=1,
5- Sonuçta elde edilen -1 yani FFh, başka bir deyişle 11111111b değerindeki 1’lerin sayısı
sekiz adet yani çift olduğundan PF=1,
borç
6- Binary olarak çıkartma işleminde daha 2. bitte 0’dan 1 alındığından
çıkarılırken AF=1
olmuştur.
Gördüğünüz gibi program akış kontrolünde bayraklar hayati önem taşır. İşlemci durum
kaydedicisinde bulunan bu bayraklar şu anda aklınızın alamayacağı bir çok işlemde
bizlere yardımcı olacak.
Bu konuyu daha iyi anlamak için Turbo Debugger’da programın her komutunu adım-adım
çalıştırın ve komut setinden faydalanarak, çalıştırdığınız komutun etkilediği bayrak varsa
bunun nedenini araştırın.
80x86 KOMUT SETİ (Bölüm 1)
X86 tabanlı mikroişlemcilerin icra ettiği makine kodları sabit olmasına rağmen,
programlama dillerinin komut ve ifadeleri farklı olabilir. Assembly programlama dilininde
diğer programlama dillerinde olduğu gibi bir dizi komutu vardır. Bu komutlar genelde
mnemonik’ler (nivmonik diye okunur) şeklindedir. Örneğin LEA mnemoniği Load Effective
Adres kelimelerinin kısaltılmış şeklidir. Bu makalemizde x86 Assembly programlama
dilinin komutlarını anlatmaya çalışacağım ve bu makalede açıklanan komutları
öğrendiğinizde kendi başınıza program yazabilir hale geleceksiniz. Aslında 80386 ve
sonrası mikroişlemciler için daha birçok komut mevcuttur ve bu komutlar assembly
dilinde program yazma işini kolaylaştırır. Bu komutları ilerleyen makalelerimizde
açıklamaya çalışacağım.
2) Dönüştürme komutları
3) Aritmetic komutlar
add, inc sub, dec, cmp, neg, mul, imul, div, idiv
in, out
8) Diğer komutlar
MOV komutu
MOV komutu assembly dilinde çok kullanılan bir komuttur. Yukarıdaki kullanım şekilleri
için İngilizce ifadeler kullanılmıştır, çünkü internetten erişebileceğiniz komut seti
referanslarında hep bunlar karşınıza çıkacak. Tüm komutlar için geçerli olan bu İngilizce
ifadelerin Türkçe karşılıkları aşağıdaki gibidir.
mem16 : memory 16 bit à 16 bitlik hafıza alanı (dw direktifi ile tanımlanan veriler)
MOV komutunu kullanırken yapamayacağınız iki şey vardır, bunlardan birincisi “mem,
mem” tipinde bir kullanımdır. Yani hafızanın bir konumunda diğer bir konumuna
doğrudan taşıma yapamazsınız. Bu işlemi yapmak için taşınacak veri önce mikroişlemci
kaydedicilerinden birine getirilmelidir.
Yukarıdaki gibi bir komut satırı yazarsanız, derleyiciniz hata mesajı verir. Böyle bir işlemi
yapmak için genel amaçlı bir kaydediciyi kullanmanız gerekir.
Segment kaydedicilerine bir değer yükleyebilmek için genellikle genel amaçlı kaydedicileri
kullanılır. Ayrıca segment kaydedicilerine ancak 16 bitlik boyutunda değerler
yüklenebileceğinden genel amaçlı kaydedicilerin 8 bitlik kısımlar değil 16 bitlik kısımları
kullanılabilir.
MOV AX, toplam ; burada toplam değişkeninin boyutu kesinlikle word tipinde
yani iki byte uzunluğunda olmalıdır.
Şayet acil adresleme kullanarak bir veri taşıyorsanız işlemci operandın boyutunu
kaydediciye uyarlar.
MOV AX, 15h ; Bu komut işlenince AX’in içinde 0015h değerini görürsünüz.
Dikkat edilemesi gereken diğer bir husus ise hafıza operandlarıdır. Örneğin MOV [BX], 5
gibi bir komut ile hafızaya neyin yükleneceği belli değildir (Burada BX kaydedicisine değil
hafızaya taşıma yapıldığına dikkat edin) MOV [BX], 5 gibi bir komutla acaba hafızaya byte
boyutunda bir 5 değerimi (05) yoksa word boyutunda bir 5 değerimi (0005) yüklenecek?
Bunu kodlarınızda belirtmeniz gerekir. Derleyici bu komut satırına hata verir. Doğru
kullanım aşağıdaki gibi olmalıdır.
mov byte ptr [bx], 5 satırını açıklayalım. Burada BX=0000 olduğunu varsayalım, böyle bir
durumda ds:0000 adresine bir taşıma işlemi gerçekleşeceltir. Ama bu adresten itibaren
05’mi yoksa 0005’mi yoksa 00000005’mi taşınacaktır? İşte bunu ptr operatörü belirler.
mov byte ptr [bx], 5 komut satırı için ds:0000 adresine 1 byte’lık bir veri yani 05 taşınır.
Şayet operatör byte ptr değilde word ptr olsaydı o zaman ds:0000 ve ds:0001
adreslerine dırasıyla 05 ve 00 değerleri taşınacaktı.
XCHG komutu
Bu komutlar 32 bitlik bir hafıza bölgesindeki değeri bir segment kaydedicisine ve bir
genel amaçlı kaydediciye bir defada yükler. Kullanım formatı aşağıdaki gibidir;
Reg16 genel amaçlı herhangi bir kaydedici olabilir mem32 ise double word boyutunda bir
veri olmalıdır, bunu “dd” direktifi ile oluşturabilirsiniz. Daha önce bu komutlardan biri olan
LES komutu için “X86 Assembly Dilinde Değişken Bildirimi -1” adlı makalede çok güzel bir
örnek vermiştim. Erişmek istediğimiz adresin segment ve ofset bölümlerini birleştirerek
bir değişken oluşturuyor sonrada bunu program çalışırken istediğimiz gibi kullanıyorduk.
LEA Komutu
LEA (Load Effective Address – Etkin Adresi Yükle) sadece offset adreslerini hedef operandına yükleyen bir
pointer gibi düşünebilirsiniz. Genel kullanım formatı
şeklindedir.
lea reg16, mem
lea reg32, mem (*)
Daha önceki makalelerimizde ekrana bir karakter dizisini yazdırmıştık, bunun için kaynak
kodumuzda ekrana yazdılıralacak olan veriyi aşağıdaki gibi tanımlamıştık;
80x86 push ve pop komutları Stack Memory (Yığın hafıza bölgesi) ile ilgili işlemlerde
kullanılır. Yğın hafıza bölgesini sizler .exe tipi program hazırlarken .Stack direktifi ile
belirliyorsunuz. İşte bu bölge genellikle programdaki dallanma veya altrutinlerin çalışması
sırasında, dönüş adreslerinin ve bayrak kaydedicisinin durumlarını saklamak için
kullanılır. Push komutu bu yığın olarak adlandırılan hafıza bölgesine verileri iterken, pop
komutuda bu bölgeden veri almada kullanılır.
push reg16
pop reg16
push reg32 (**)
pop reg32 (**)
push segreg
pop segreg (CS hariç)
push memory
pop memory
push immediate_data (*)
pusha (*)
popa (*)
pushad (**)
popad (**)
pushf
popf
pushfd (**)
popfd (**)
enter imm, imm (*)
leave (*)
Push ve pop komutları kullanıldığında yığın hafıza bölgesinin işaretçisi olan SP kaydedicisi
değişir. Tabiî ki bu yığına itilen veya yığından çekilen değerin boyutuna bağlıdır. Bu
komutlar 2 veya 4 byte�lık değerler ile işlem yaptığından yığına 2 bytelık bir değer
itildiğinde (mesela bu AX kaydedicisinin içeriği olabilir) SP�nin değeri 2 byte azalır. Şayet
yığına 4 byte�lık değer itilirse SP�nin değeri 4 azalır. Unutulmaması gereken önemli bir
hususta yığın hafıza bölgesine itilen en son değerin çekilecek olan ilk değer olmasıdır.
Tabiî ki yığına birden fazla word ya da doubleword itildiyse aralardaki değerler ile işlem
yapmak adresleme modlarıyla mümkündür fakat bu SP�de herhangibir değişiklik
yapmaz. Yığın hafıza bölgesi ile ilgili unutulmaması gereken üç önemli kural vardır.
Segment kaydedicilerinden olan SS yığın hafıza bölgesinin segment adresini gösterir.
Yığına bir şeyler itildikçe SP azalır eökildikçe artar. SS:SP her zaman yığının tepesi olarak
tabir edilen noktayı gösterir.
Genişletme İşlemleri
Bazen byte boyutundaki bir değeri word boyutuna veya word boyutundaki bir değeri
doubleword boyutuna genişletmek gerekebilir. Bu gibi durunlarda aşağıdaki komutlar
kullanılır.
cbw (convert byte to word) AL kaydedicisinin 1 byte’lık içeriğini AX’e genişletir. Şayet
AL’deki değer pozitifse AH’ın tüm bitleri ‘0’ değerini alır. AL’deki değer negatifse AH’ın
tüm bitleri ‘1’ olur.
cbw
cwd (convert word to double word) komutu AX’in değerini DX:AX’e genişletir. CBW
komutundaki kurallar bu komut içinde geçerlidir.
cwd
Bu komut 80386 ve sonrası işlemcilere özeldir. CWD komutunda olduğu gibi word
boyutundaki bir değeri double word boyutuna genişletmede kullanılır. CWD AX’i DX:AX’e
genişletirken bu komut AX’i EAX’e genişletir.
cwde
cdq
Örnekler:
cbw
cwd
cbw
cwde
cbw
cwde
cdq
movsx komutuda yukarıdaki komutlara benzer iş yapar, kullanım formatları aşağıdaki
gibidir.
Örnekler:
movzx komutu movsx komutu gibi kullanılır fakat negatif değerleri genişletmek için
kullanılmaz, çünkü movzx komutu genişletme işleminde sadece bitleri ‘0’ yapabilir. Bu
komutun sonundaki zx harfleri İngilizcede zero extend yani sıfır ile genişlet gibi bir anlam
taşır.
BSWAP Komutu
Bildiğiniz gibi x86 hafızası little endian yapıya sahiptir, bununla beraber big endian
hafızaya sahip bilgisayarlarda çok sayıda mevcuttur. Örneğin Apple’ın Machintosh
bilgisayarları big andian hafıza yapısına sahiptir. BSWAP komutu bu farklı hafıza
sistemlerine sahip olan bilgisayarlar arasında veri haberleşmesi yapılması için kullanılır.
BSWAP operandında belirtilen 32 bitlik kaydedicinin içindeki değeri byte-byte ters çevirir.
Düşük değerlikli sekiz biri en yüksek değerlikli bölgeye, 8-15 arasındak bitleri 16-23
arasına, 16-23 arasındaki bitleri 8-15 arasına ve son olarak 24-31 arasındaki bitleride 0-7
arasına yerleştirir.
bswap reg32
XLAT Komutu
Tablo DB 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F
MOV AL, 0Ah ; İndeks değeri (0 dan 0Ah’a kadar 11 tane değer var)
LEA BX, Tablo ; BX’e (taban kaydedicisi) Tablonun ofset adresi
yükleniyor
XLAT ; Tablonun 11. elemanına erişilip ASCII karakterin hex
karşılığı AL’ye yükleniyor (AL=41h)
Şimdi yukarıda açıkladığımız komutlardan birkaçını kullanarak bir program yazalım ve
daha sonra da inceleyelim. Öncelikle Push ve Pop komutlarını DS kaydedicisinin değerini
belirlemek için kullanabilirim. .exe tipi programlarda DS kaydedicisinin değerini
belirlemek için bu güne kadar hep aşağıdaki iki satırı kullandık,
Kullandığım sistemin saat bilgisini ekrana yazdırmak istiyorum. Tabiî ki bu gibi işlemler
için hali hazırda DOS’un kesmeleri mevcuttur. Fonksiyon 2Ch sistem zamanı ile ilgili
bilgileri işlemcinin kaydedicilerine getirir.
MOV AH,2Ch
INT 21h
Çevirme işlemleri için çarpma, bölme gibi komutları bilmeniz gerekir ama henüz o
komutlarla ilgili örnekler çözmedik. Bunun yerine biz hex-decimal tablolar oluşturup bu
tablolardan desimal değerleri bulalım ve ekrana yazdıralım. Fakat tabloların sınırlarını
belirlemek için elimizdeki bilgiyi iyi tanımamız gerekir.
En büyük saat bilgisi 23:59 yani 0 ile 59 elemanlarını kapsayan bir tablo oluşturmam
yeterli.
SaatTablo DB
"00$","01$","02$","03$","04$","05$","06$","07$","08$","09$"
DB
"10$","11$","12$","13$","14$","15$","16$","17$","18$","19$"
DB
"20$","21$","22$","23$","24$","25$","26$","27$","28$","29$"
DB
"30$","31$","32$","33$","34$","35$","36$","37$","38$","39$"
DB
"40$","41$","42$","43$","44$","45$","46$","47$","48$","49$"
DB
"50$","51$","52$","53$","54$","55$","56$","57$","58$","59$"
Yukarıdaki tablo bize 60 elamanlı gibi görünebilir fakat hafızada 60*3=180 byte yer
kaplar, çünkü x86 hafızası byte adreslenebilir ve her rakam veya karakter hafızada 1
byte’lık yer kaplar.
Diyelim ki CH’taki saat bilgisi 0Ah, bu saat 10 demek. Tabloda “10$” olan kısım acaba
tablonun hangi adresidir? Sayacak olursanız 30. elemanın 1 31. elemanın 0 yani 30
elemandan itibaren 10 değerinin mevcut olduğunu görürsünüz. Tabloda her saat değeri
için 3 byte’lık değer ayrılmıştır ve bu programda CH veya CL deki değerler yardımıyla
indeks adresi hesaplanırken 3 ile çarpmak gerekir.
CH’ta saat 10’u temsilen 0Ah değeri bulunuyorsa 0Ah*3 = 1Eh = 30 hesabı yapılmalıdır.
Tüm bu işlemleri yapıp ekrana yazdırılıcak değeride DX kaydedicisine yüklemeliyiz. Çünkü
ekranda bir karakter dizisini yazdırmak için INT 21h’ın 9. fonksiyonu karakter dizisinin
başladığı adresi DX kaydedicisinde bulunmasını ister. Tüm bu işlemleri yapan kod satırları
aşağıdaki gibidir.
Uc DB 3
Bununla beraber “:” karakterinide saat bilgisini yazdırırken saat ile dakika arasına
yerleştirelim, tabiî ki bu bilgide data segmentinde bir adreste saklı kalsın.
Ayirac DB ‘:’
Saat DB ‘Saat $’
4- Ekrana yazdır
7- Ekrana yazdır
8- Dur
Şekil 1 - Saat bilgisini ekranda gösteren program.
Şekil 2 - Programın ekran çıktısı.
Elbette bu program daha kısa veya pratik bir şekilde yapılabilirdi. Bunun için komut
bilgimizi genişletmeliyiz. Ayrıca değişken bildirimlerini de şu ana kadar tam olarak
anlatmadım. Bu konularla ilgili makalelerimiz yolda fakat şu anda sizlerin mevcut
bilgilerinizle bu programa birde salise kısmını ekleyebilmeniz gerekir.
Bu programa ait assembly kaynak kodlarını buradan, kodların derlenmiş halini buradan
download edebilirsiniz. Bir sonraki makalede görüşmek üzere.
Aritmetik ve mantık (lojik) işlemler mikroişlemcinin ALU (Arithmetic Lojic Unit) denen
kısmında yapılır. ALU bir dizi elektronik toplama, çıkarma ve mantık devrelerinden
oluşmuştur. Bu devrelerin çalışma mantıkları ise sayma temelinden geçer. Bizlerde ilkokul
sıralarında temel işlemleri parmaklarımızla sayarak yapardık. 3 ile 5’i toplarken 3’ün
üzerine 5 tane parmak sayardık. Mikroişlemcide her saykılda (saat darbesinde) ALU’da bir
sayma işlemi yapar. Bu saat darbesi ne kadar hızlı olursa işlemler o kadar hızlı
gerçekleşir. Örneğin 1 GHz. hızında bir işlemci saniyede 1 milyar elektronik darbe
üretebilir ve bu saniyede milyonlarca işlem yapabileceği anlamına gelir.
Toplama ve elde ile toplama komutlarıdır. ADD komutu işlemci durum kaydedicisinin C
bitini hesaba katmazken ADC toplama işlemini C bitinide dahil ederek yapar.
MOV AX, 5
ADD AX, 6
MOV AX, 5
ADC AX, 6
Bu işlemden sonra şayet C=0 ise sonuç 000Bh C=1 ise sonuç 000Ch olacaktır.
x := y + z + t işlemini;
MOV AX, Y
ADD AX, Z
ADD AX, T
MOV X, AX
şeklinde yapabilirsiniz. Tabiki bu x,y,z,t’ler birer hafıza konumu veya kaydedici olabilir.
1.yol
MOV AX, X
MOV BX, Z
ADD AX, BX
MOV X, AX
Yukarıdaki şekilde bu işlemi yapabilrsiniz ama bu çokta iyi bir yol değildir.
2.yol
MOV AX, X
ADD AX, Z
MOV X, AX
Bu yol daha iyi gibi görünsede bundan daha iyi çalışacak kodlar aşağıdaki gibidir.
3. yol
MOV AX, Z
ADD X, AX
Adresleme modlarını akıllı bir şekilde kullanabilirseniz çok hızlı çalışan programlar
hazırlayabilirsiniz. Yukarıdaki üç program parçası aynı işi yapmasına rağmen en hızlı
çalışanı 3. südür. Günümüzde kullanıcıya daha yakın ve program yazması daha kolay olan
üst seviye programlama dillerine göre assembly dilinin en büyük avantajı budur.
ADD ve ADC komutları işlemcinin bayrak kaydedicindeki bitlere şöyle etki ederler.
6. Elde edilen sonuçtaki binary 1 ler çift sayıda olursa, örneğin 0000
0101 değerinde 2 tane 1 vardır bu durumda P=1 olur.
INC Komutu:
ADD X, 1 gibi çalışır. X kaydedici veya hafıza alanı olabilir. Kısaca hedefi 1 arttırır.
Döngülerde çok kullanılan bir komuttur. Bu yüzden çok önemlidir. INC komutunun 1,2
veya 4 bytelık operandı olabilir. Yani bu komutu aşağıdaki formatlarda kullanabilirsiniz.
INC komutu genelde ADD mem,1 veya ADD reg,1 formatına tercih edilir çünkü daha
hızlıdır, buna rağmen peşpeşe 1 den fazla inc komutu kullanmak gerekirse komut setinin
incelenmesinde fayda vardır. Çünkü bu işi ADD reg,2 veya ADD mem,2 şeklindede
yapabilirsiniz ve bu durumda sadece 1 adet komut satırı yazarsınız. Ayrıca INC bayrak
kaydedicisinin C bitine etki etmez. Bu yüzden dizi işlemleri için çok uygun bir komuttur.
Oysa ADD ve ADC C bitine etki ederler ve büyük dizilerde bu komutlar kullanılırsa dizinin
içindeki elemanları işaret etme işleminde bazen yanlış sonuç gösterebilirler.
XADD Komutu:
80486 ve sonrası işlemciler için geçerli bir toplama komutudur. Bu komutu aşağıdaki
örnek ile daha iyi anlayacağınız kanaatindeyim.
SUB (Subtract) yani çıkartma SBB ise borç ile çıkart (SuBtract with Borrow) anlamına
gelir. Her iki çıkartma işlemi bir çıkartma sonucu üretmenin yanında bayrak
kaydedicisinin C bitinide etkilerler. Bu komutların genel kullanım formatları aşağıdaki
gibidir;
Ayrıca bu sonuç incelenirken, kaydedicideki FFFAh değerinin 15. biti 1 olduğundan sonuç
negatif olarak değerlendirilmeli ve yukarıdaki işlem yapılarak sonucun gerçek değeri
hesaplanınca;
-6 değerine ulaşabilirsiniz.
FFFAh
000Ah
-------
0004h
MOV AX, X
SUB AX, Y ; x-y işlemi yapılıyor, sonucu AX’e yükleniyor.
SUB AX, Z ; x - y - z işlmi yapılmış oluyor
MOV X, AX ; sonuç x’e yüklenerek x = x - y - z işlemi yapılmış oluyor.
MOV AX, Y
ADD AX, Z ; y - z işlemi yapılıyor
SUB X, AX ; x - y - z işlemi yapılıp sonuç x’e yükleniyor.
DEC komutu:
Decrement yani azalt anlamına gelir. hedef operandını 1 eksiltir, başka bir deyişle -1
ekler. Kullanım formatları aşağıdaki gibidir.
DEC reg
DEC mem
DEC reg16
C biti hariç çıkartma komutların etkilediği bayrakları etkileyen bir komuttur. INC komutu
gibi genelde döngülerde her iterasyondan sonra sayacı azaltmak için kullanılır.
CMP komutu:
SUB komutu ile aynı işi yapar fakat çıkarma işleminin sonucunu herhangi bir kaydediciye
yüklemez. Bu komut genelde şartlı dallanma komutlarından önce bayrakları etkilemek
için kullanılır. CMP’nin anlamı "compare" yani karşılaştır demektir. Bakın neleri
karşılaştırabiliyoruz;
A ara elde biti yani işlem yapılırken 3. bite gelindiğinde eldenin olup olmadığı hakkında
bilgi verir ve P işlem sonucundaki değeri binary olarak düşündüğümüzde 1’ler tekmi
yoksa çift mi durumunu gösterir. A ve P bayraklarından ziyade programcılar Z, C, O ve S
bitlerinin durumları ile ilgilenirler. Bu bayrakları değerlendirirken de işlemlerin işaretli
yada işaretsiz sayılar ile yapıldığının bilinmesi büyük önem taşır.
1- Z bayrağı sayılar ister işaretli ister işaretsiz olsun eşitlik yada eşit olmama durumunu
gösterir.
mov ax,5
mov bx,5
CMP ax,bx ; Z=1 yani operandlar eşit.
C=1 ise 2. operand 1.operand dan büyük demektir. C=0 ise 1. operand büyüktür.
Şartlı dallanma komutlarının ilk harfi J ile başlar ve takip eden 1,2 yada 3 harf şartı
gösterir. Bu tür komutları bundan sonra JXXX komutları olarak kullanacağım.
JXXX komutlarının da CMP komutları gibi işaretli ve işaretsiz değerler için farklı anlamları
vardır. Tüm bu anlamlar ve komutları aşağıdaki 3 tabloda özetleyebiliriz.
Tüm programlama dillerinde kullanılan if, for, while vb. deyimler aslında şartlı dallanma
komutları ile düzenlenmiş bir kaç assembly komutuna benzetilebilir. Hatta işi biraz daha
netleştirelim C dilinde kullandığımız "if" deyimi aslında bir adet cmp ve bir adet Jxxx
komutundan oluşur, xxx kısmı ise şarta göre değişir. Aşağıdaki örnekle bu olayı daha iyi
anlayabilirsiniz.
Şekil 1 - C Dili ile yazılmış bir program.
Şekil 2 - Programın assembly kodlarına baktığımızda if deyimine karşılık CMP ve JLE komutlarını görüyoruz.
JLE satırına dikkat edin, JLE’nin anlamı less or equal yani düşük yada eşitse. Yani a’nın
değeri olan 5 b’nin değeri olan 4’ten düşük yada eşitse, program 00411A3E adresinden
devam edip, önce eax’e k karakterini yükleyip (daha doğrusu k’nın ASCII kod karşılığını
yükleyecek), bunu hafızadaki "sonuc" adlı yere (sonuc da aslında bir adres :))
kaydedecektir. Tabi 5 4’ten büyüktür. Hal böyle olunca bu programda JLE komutu
işlendikten sonra bir dallanma söz konusu değildir. Yani program normal akışını devam
ettirecek ve eax’e "b" karakterini ardından da sonuc adlı adrese kopyalayacaktır.
Bu programın assembly kodlarını daha önce görmediğimiz bir biçimde birazcık farklı
görmeniz doğaldır. Bunun nedeni Visual Studio.NET’in C derleyicisinin 32 bitlik
olduğundandır. Yani bu güne kadar biz 16 bitlik TASM veya MASM’ı kullandık. Bu yüzden
eax kaydedicisini göremedik veya adresleri buradaki gibi 32 bitlik değil de hep 16 bitlik
offsetler halinde gördük. Bu yüzden assembly kodları biraz farklı. 32 bitlik kodlama bizim
için henüz çok erken bir kavram, bu yüzden bundan sonraki makalelerimizde 16 bitlik
kodlamaya devam edeceğiz.
NEG komutu
NEG negatif kelimesinin kısaltmasıdır. Tek operandı vardır. Kullanım formatı aşağıdaki
gibidir.
neg reg
neg mem
yani operandı herhangi bir kaydedici veya hafıza adresi olabilir. Yaptığı iş operandın
değerinin negatifini almaktır. Daha doğru bir deyişle operandını 0’dan çıkartır. Binary
düzende düşünecek olursanız 1’lein yerine sıfır 0’ların yerine 1 getirir ve bu sonuca 1
ekler.
mov al,0fh
neg ax
MUL çarpma IMUL ise işareti dikkate alarak çarpma işlemlerini yapar. Kullanım formatı
aşağıdaki gibidir.
İşaretsiz çarpma:
mul reg
mul mem
İşaretli çarpma:
imul reg
imul mem
imul reg, reg, imm (*)
imul reg, mem, imm (*)
imul reg, imm (*)
imul reg, reg (**)
imul reg, mem (**)
IMUL komutunun 286 ve 386 dan sonraki kullanım formatlarına bakacak olursanız, bu
güne kadar gördüğümüz formatlardan biraz farklı olduğunu görürsünüz. Bu kullanım
şekilleri programcıların kafasını biraz karıştırmakla beraber tek bir komut satırı ile
çabucak çarpma işlemini yapmasını sağlar. Nede olsa Intel karmaşık komut setini
benimsemiştir.
Ayrıca çarpma işlemi toplamadan daha büyük sonuçlar çıkarabilir. Yani 2 basamaklı bir
değeri başka bir 2 basamaklı değer ile toplarsanız sonuç en fazla 3 basamaklı çıkarken
çarpmada bu 4 basamağa kadar çıkabilir. Daha fazla basamaklı sayıların çarpımında
sonuç çarpılan veya çarpandan çok daha fazla basamaklı çıkabilir. Bu gerçeği göz önüne
alarak işlemci tasarımcıları sonucu her zaman çarpan ve çarpılanın boyutundan daha
büyük bir kaydedicide saklama yoluna gitmişlerdir. Bunları aşağıdaki şekil ve
açıklamalarla daha iyi anlayacaksınız.
Byte Çarpma
komutlarını kullanabilirsiniz. Burada "mul dl" komutu ile DL*AL işlemi yani bu
kaydedicilerdeki değerler olan 17h ve 27h sayıları çarpılır. Peki sonuç nerede? Yukarıdaki
şekle baktığınızda sonucun AX içinde olacağını görebilirsiniz. Bu çarpma işleminden sonra
AX’te 0381h değeri görülür.
mov al,sayi
mul al
komutlarını kullanabilirsiniz.
Word Çarpma
Bu tür bir çarpma işleminde operand AX ile çarpılır ve sonuç DX-AX kaydedicilerinden
okunur. DX’te daha önce ne olduğu önemli değildir çünkü çarpmadan sonra buraya
sonucun yüksek değerlikli byte’ı yerleşir. Sonucun düşük değerlikli byte’ı ise AX
kaydedicisinde saklanır.
bu işlemden sonra DX=0023h ve AX=4500h olur. Yani asıl sonuç olan 234500h değerinin
yüksek değerlikli word’u DX’te düşük değerlikli kısmıda AX’te görülür. Fakat çarpılan
değer yani 2345h bu işlemden sonra kaybolacaktır. Şayet bu çarpılan değer sizin için
önemliyse;
carpilan db 2345h
..
..
..
mov ax, carpilan
mov bx,100h
mul bx
böylece 2345h değeri daima "carpılan" ismi ile hafızada korunur. Aynı şeyi tabi ki çarpan
için yani 100h değeri içinde yapabilirsiniz.
carpilan db 2345h
carpan db 100h
..
..
..
mov ax, carpilan
mov bx, carpan
mul bx
Double word boyutundaki verilerin çarpımında da word dekine benzer bir yapı kullanılır.
Çarpılan değer EAX kaydedicisine yerleştirilip, MUL veya IMUL komutunun peşinden gelen
operand ile bu değer çarpılır. Daha sonra elde edilen sonucun yüksek değerlikli
doubleword’u EDX’te düşük değerlikli doubleword’ü ise EAX’te saklanır. Yani sonuç 64
bitliktir.
carpilan dd 12345678h
carpan dd 34522344h
..
..
.386
..
..
mov ax, carpilan
mov bx, carpan
mul bx
Buradaki .386 32 bitlik kaydedicileri kullanmak için assembler’a verilen bir direktif
(talimat) dır. EDX ve EAX gibi 32 kaydedicilerin 32 bitlik alanlarını kullanmak için nu
talimatı vermeniz gerekir. 32 bitlik programlama, 16 bitlik programlama nedir bunlar?
Şimdilik sadece 32 bitlik programların 16 bitliklere göre daha avantajlı olduğunu
görebilirsiniz. Çünkü 32 bitlik programlama ile kaydedici boyutlarımı 2 katına çıkıyor ve
bir kaydedicide hesaplayabileceğimiz değerlerde aynı oranda artıyor, bu işlemi 16 bitlik
bir programlama ile de halledebilmemize rağmen 2 katı daha fazla komut yazmamız
gerekir.
1- Byte boyutundaki bir operand AL ile çarpılırsa sonuç AX’te görülür. AH=0 ise C ve O
sıfır olur, aksi durumlarda bu bayraklar set (1) olur.
2- Word çarpmada C ve O sıfır ise DX’te sıfır demektir, aksi durumlarda bu bayraklar set
olur.
3- Double word çarpmada ise C ve O sıfır ise EDX’te sıfır demektir, aksi durumlarda bu
bayraklar yine set olur.
Yukarıdaki üç durum soldaki sıfırların çarmada bir değeri olmadığından, sonucu optimize
etmenize yardımcı olacaktır.
IMUL (Integer Multiplication) komutu ile yukarıdaki MUL komutu için verilmiş kalıpları
kullanabilirsiniz, bununla beraber IMUL komutuna özel çok operandlı kullanım
formatlarıda mevcuttur. Tüm kullanım formatları bu makalenin başında verildiği gibidir
fakat kaydedicilerin 8 16 ve 32 bitlik durumları da göz önüne almamız gerekir. Şimdi
henüz açıklamadığımız 286 ve 386 sonrası işlemcilerde kullanılabilen komut formatlarını
aşağıdaki örneklerle inceleyelim.
ve 2 operandlı olanlarıda
şeklinde çalışır. Her zaman son kullanılacak olan operandın "imm" yani sayısal bir değer
olduğuna dikkatinizi çekerim.
Bu komutlar ile 8x8 bit çarpım söz konusu değildir, imm8 olarak yukarıda gördüğünüz
operand sadece komutunun makine kodunun olmasını sağlar. Ayrıca bu çarpma
işlemlerinde sonucun boyutu operandta belirtilen kaydedicilerin boyutuyla aynıdır, yani
makalenin başında anlattığımız mul komutu gibi sonuç operandın 2 katı olmaz. Bu
durumda sonucun hedef kaydediciye sığmaması durumuna karşı C ve O bitleri birlikte
kontrol edilmelidir, bu durumlara Intel’in komut setinden bakabilirsiniz.
Bununla beraber bu formattaki çarpma komutları Z bitini her zaman doğru bir şekilde
etkilemeyebilir, şayet sonucun sıfır olup olmadığı sizin için önemli ise ancak sonucu sıfır
ile karşılaştırdıktan sonra Z bitini kontrol etmelisiniz. Aynı şekilde sonucun işaretini
öğrenmek için işaret bayrağı yerine C ve O bitlerinin sıfır olup olmadığı kontrol
edilmelidir.
IMUL komutu için Intel’in 80286 ve sonrası işlemcilere koyduğu bu adresleme biçimleri
çok boyutlu diziler ile yapılan işlemleri hatırı sayılır biçimde kolaylaştırmıştır. Bu konuya
çok boyutlu dizileri ve karmaşık veri yapılarını anlatırken bir daha değinmeyi
düşünüyorum.
DIV division yani bölme kelimesinin kısaltmasıdır. Bölme işlemi çarpmanın tersine
bölünen’e göre küçük sonuç üretir, bu yüzden bu komutları kullanırken bölünen’in boyutu
bölen’in boyutunun iki katı olmak zorundadır, an azından Intel bu komutlar için böyle bir
form öngörmüştür. Bu yüzden bölünenin boyutu en az word türünde olmalıdır, çünkü x86
Assembly dilinde en küçük veri tipi byte’dır. Bu durumda byte türünden bir değeri
bölmek isterseniz bunu CBW komutu ile word boyutuna dönüştürmeniz gerekir,
hatırlarsanız bu tür komutları daha önceki makalelerimizde açıklamıştık.
bu işlemden sonra AX, 0105h olur. AH’taki 01h kalan ve AL’deki 05h ise bölümdür.
Nitekim 16’nın (yani 10h) 3’e bölümü ile de bu sonuç üretilir. Bu tür bölme işleminde elde
edebileceğiniz en büyük bölüm 255 (FFh işaretsiz değerler için) yada 127 (7Fh işaretli
değerler için) değerleridir.
Mesela 11h’ı (yani 17’yi) 3’e bölerseniz sonuç normalde 5,666... şeklinde olur, bu işlemi
div komutu ile yaptığınızda ise AX’te 0205h değerini görürsünüz, yani bölüm 5 ve kalan
2. Özetle 17’nin içinde 5 tane 3 ve 1 tane’de 2 vardır ve div ve idiv komutları tamsayı
bölme işlemlerini gerçekleştirebilir. Ondalıklı bölme işlemleri için floating point
kaydedicileri kullanılır ve bu kaydediciler matematik işlemcisinin içindedir. 80486 DX
işlemcisine kadar matematik işlemci normal işlemcinin yanına opsiyonel olarak konulurdu
mesela işlemci 80386 ise matematik işlemcisi 80387 olurdu. Artık matematik işlemcisi
normal işlemcinin içine gömülü olarak geliyor. Bu arada floating point ünitelerini
kullanmak için yüksek seviyeli assembly kodları yazmak gerekir ve bu iş bizim için henüz
çok erken. Fakat kalanı 10 ile çarpıp sonrada bölünen ile karşılaştırıp şayet bölünenden
büyükse tekrar bölene bölme işlemine gidebilirsiniz, aynı kağıt üzerinde normal bölme
işlemi yapar gibi, fakat floating point ünitelerini kullanmak inanın bu işten daha pratiktir
ve daha kolay sonuç verir.
Representation yani gösterme veya sunum işlemi çok geniş bir yelpazede incelenebilir, bu
tamamen kullanıcının hayal gücüne kalmış bir olaydır. Benim burada anlatmaya
çalışacağım olay ise ekran text modunda iken hafızadaki binary ifadelerin ASCII karakter
karşılıklarını ekranda göstermek olacaktır.
Diyelim ki bir işlem yaptınız ve sonucunu 20h (32) olarak AL kaydedicisinde saklamayı
başardınız ve bunu ekranda göstereceksiniz, bunu direk olarak ekrana yazdırırsanız
sadece imleci 1 kez ilerletmiş olursunuz çünkü 20h ascii kod tablosunda space yani
boşluk karakterine karşılık gelir, klavyedeki en büyük tuş yani. Peki 32yi nasıl
yazdıracağız? Unutmayın ki ascii kod tablosunda sadece rakamların, harflerin ve bir dizi
kontrol karakterinin kod karşılıkları vardır sayıların kod karşılıkları yoktur. Bu bağlamda
biz 32 yi değil 3 ve 2 yi yan yana ekranda göstermeyi düşünmeliyiz. Ama şu anda AL’de
ne 3 ne 2 var sadece 20h var. Diğer bir gerçek 3’un ascii kod karşılığı 33h ve 2 nin ki ise
32h dır. 30h ile 39h arası ascii kod tablosunda rakamlar için ayrılmıştır.
Böyle bir komut varmı yokmu oraya geleceğiz ama 20h ile 3332h arasında güzel bir bağ
var. 20h ı 0ah yani 10’a bölsek zaten hex’den decimal’e dönüşüm işlemi yapmış oluruz;
birde al ile ah’ı yer değiştirsek! Acaba buna gerçekten gerek var mı? Hatırlarsanız ekrana
bir string yazdırmak için DOS kesmelerinden 09h nolu fonksiyonunu kullanmıştık ve bu
fonksiyon hafızadaki stringleri yazdırıyordu, yani biz AX’teki bu 3233h değerini önce bir
hafızaya atalım sonra 32h ile 33h’ın yerini değiştirmek gerekiyor mu düşünürüz;
x86 tabanlı işlemciler hafızayı adreslerken little endian byte sıralamasını kullanırlar, bu
yüzden al’deki düşük değerlikli byte hafızanın düşük numaralı adresine ax’teki yüksek
değerlikli byte’ta hafızanın yüksek numaralı adresine yerleşmiş olur, böylece ax ile al’nin
içeriklerini takas etmemize de gerek kalmaz. Artık sonuc değişkenini referans göstererek
ekrana yazdırma kesmesini kullanabilirsiniz.
lea dx,sonuc
mov ah,9
int 21h
Klavyeden giriş yapıldığında, basılan tuşa ait kod hafızada ascii formatta saklanır,
mikroişlemci hesaplamaları binary yapar bu yüzden ascii’den binary’ye dönüşüm yapmak
gerekir. Şayet ekrana bir karakter basılacaksa bu karakter ekrana gönderilmeden önce
ascii forma dönüştürülmelidir.
Bizler günlük yaşamımızda desimal değerleri kullanırız, bu desimal değerleri hafızada
binary rakamlar olan 1 ve 0’ları kullanarak gösterebiliriz, buna BCD (Binary Coded
Decimal) kodlama diyoruz. Desimal numaralar hafızada BCD olarak gösterilmenin
yanında ASCII olarakta gösterilebilir. İşte AAA, DAA, AAS gibi komutları bunun için
kullanıyoruz.
Örneğin klavyeden girilen 1234 hafızada 31 32 33 34 olarak ASCII formda saklanır. BCD
gösterimin ise iki farklı çeşidi vardır, bunlar packed BCD (paketlenmiş BCD) ve unpacked
(paketlenmemiş) BCD’dir.
AAA (Ascii Adjust After Addition) komutu toplama komutundan sonra sonucu ascii ye
ayarlar. Aşağıdaki örnekleri inceleyin;
Sonuç 09 olması gerekir AAA komutu 6 değerini siler. Sonuç AL�de 09 olarak görülür.
36 = 0011 0100
35 = 0011 0101
+_______________
6B = 0110 1011
Sonuç 11 olması gerekir AAA komutu B değerinin yerine 1 koyar ve toplama sonucu 9
değerini aştığından AH kaydedicisine de 1 koyar, yani AX = 0101 olur.
34h artık ascii olarak 4 demektir. Tabi burada AH kaydedicisini de kontrol edip şayet 01
ise bu değeri de 30h ile OR işlemine tabii tutmalıyız. Bu tür işlemler için genelde ekrana
tek bir karakter basma fonksiyonu kullanılır yani AL’deki değer teker teker ekrana
bastırılır. Henüz mantıksal komutları görmedik bu yüzden aşağıdaki programı tam olarak
anlamayabilirsiniz, bu yüzden açıklamalara dikkat edin.
Şekil - Makine dilinden insan diline dönüşüm
Pekte güzel bir ekran çıktısı olmasa da bu program dönüşümleri anlamak için iyi bir
örnektir. Yukarıdaki şekilde program ilk çakıştırıldığında klavyeden sırayla 2 ve 3 girilmiş
ve sonuç 5 olarak ekrana basılmış. Daha sonra 7 ve 8 değerleri girilmiş ve sonuç 15
olarak ekrana basılmış. Gerçektende sonuçlar doğru :) Unutulmaması gereken bir nokta
AAA komutunun C bitini etkilediğidir, şayet AAA komutundan önceki bayrakların durumu
sizin için önemliyse bayrakların durumunu saklamanız gerekir.
AAS (Ascii Adjust after Subtract) komutuda AAA gibi çalışır, çıkartma komutundan sonra
sonucu ASCII’ye ayarlamak için kullanılır. AAS komutu sekiz bitlik AL kaydedicisinin
yüksek değerlikli 4 bitini kontrol eder, şayet AF bayrağı 1 ise (başka bir deyişle bu 4 bit
Ah...Fh arasında ise) AL’den 6, AH’tan 1 çıkartılır. AH’tan 1 çıkartmak normalde 00 olan
AH’ı FF yapmak yani rakamın negatifliğini ayarlamak demektir.
yukarıdaki gibi bir durumda SUB komutu ile A ve C bayrakları set (1) olur. Bu durumda
sonucu doğrudan 30h ile OR işlemine tabi tutmak hatalı olacaktır. FCh sonucu desimal -4
değerine eşittir bu yüzden burada programcı OR komutu ile doğru ascii değeri ekrana
yazdırmadan önce NEG komutu ile tersini alabilir. Bu durumda sonuc 4 olacaktır. Tabiki
bu 4 değeri ekrana yazdırılmadan önce önüne - işareti konulmalı.
mov al,34h
sub al,38h
jnc devam
neg al
devam: aas
or al,30h
yukarıdaki kod parçası sonuç negatif olsa da pozitif olsa da, sonucun mutlak değerini
doğru bir şekilde ascii değere dönüştürür.
Unutmayalım ki klavye bize ekran text moundayken daima ascii değerler verecektir, bu
çarpma ve bölme işlemlerinde de problem oluşturur. Klavyeden girilen değerlere sanki 30
eklenmiş gibi geleceğinden çarpma işleminin sonucu girilen değerde bir modifikasyon
yapmadığımız sürece yanlış hesaplarız. Bu tür modifikasyonları yapmak için başka
mantıksal komutlara ihtiyacımız olacak, bu yüzden bu komutları anlatmadan DAA, DAS,
AAD ve AAM gibi komutları açıklamak istemiyorum.
80x86 KOMUT SETİ (Bölüm 4)
Mantıksal (logical) komutlar AND, OR, XOR ve NOT adıyla bilinen ve matematiksel
hesaplamalarda çok kullanılan komutlardır.
AND komutu
1 ve 1 = 1
1 ve 0 = 0
0 ve 1 = 0
0 ve 0 = 0
sonuçlarını üretir.
bu işlemlerden sonra AL’in yüksek değerli 4 biti (nibble) sıfırlanacaktır yani AL binary
olarak ifade edersek 0000 0101 olacaktır. Buna düşük değerlikli 4 bite dokunmadan diğer
bitleri sıfırlamakta denilebilir.
Bu komutu elektronikçiler çok kullanırlar, mesela paralel porttan alınacak olan verinin
sadece 5 bitini kontrol etmek için;
Diyelim ki AL’ye IN komutu ile alınan byte 1011 0001 olsun, bu durumda;
1011 0001
0010 0000
v
__________
0010 0000
yukarıdaki işlem yapılır ve AND komutu bayrak kaydedicisinin Z bitini 0 olarak kurar,
çünkü sonuç 0’dan farklıdır. İşte burada maske olarak sadece 5. biti 1 olan bir byte
seçilmiştir.
Not: Windows XP altında paralel port’a doğrudan erişim işletim sisteminin kerneli
tarafından engellendiğinden bu programı windows XP öncesi işletim sistemlerinde
çalıştırabilirsiniz. IN ve OUT komutlarının kullanımı sonraki makalelerin konusudur.
OR komutu
1 | 1 = 1
1 | 0 = 1
0 | 1 = 1
0 | 0 = 0
XOR komutu
XOR olmadan şifreleme işlemleri sanırım çok zor yapılırdı. Çok fazla kullanım alanı
olmakla beraber veri paketleme ve şifreleme işlemleri için hayat kurtarıcı bir komuttur.
eXclusiveOR (özel veya) kelimesinin kısaltmasıdır. Doğruluk tablosu;
1 ^ 1 = 0
1 ^ 0 = 1
0 ^ 1 = 1
0 ^ 0 = 0
Ben genelde öğrencilerime bu komutun yaptığı işi anlatırken "aynılarda 0 farklılarda 1"
sonucunu veren mantıksal ifade derim.
XOR komutu ile şifreleme ve paketleme işlemleri için örnekler şu anda belki de sizin için
biraz ağır kaçabilir, bunun yerine swap (yer değiştirme) ve kaydedici sıfırlama örneklerini
XOR komutunu ile neler yapılabildiğine örnek teşkil edeceğini düşünüyorum.
xor ax,ax ; ax=0 olur ve "mov ax,0" dan kat kat hızlı çalışır
Bayrakların Durumu
NOT komutu
not reg
not mem
not bl
Bu makalemde sizlere örnek teşkil etmesi için konsol ekranındaki görüntüyü (tüm
yazıları) önce karma karışık bir hale getiren daha sonrada tekrar orijinal haline çeviren bir
programı anlatmayı düşünüyorum. Öncelikle Windows’un cmd.exe yada komut istemi ile
açılan pencerenin görüntüsünün aslında B800:0000 adresinin bir yansıması olduğunu
hatırlatmak isterim. Debug ile bu alana göz atalım.
Şekil 1 - Aslında ekran yansıması da hafızadaki byte’lardan ibarettir.
Konsol penceresi açılınca şayet işletim sisteminiz Windows XP ise "Microsoft Windows
XP....." ile başlayan bir yazı görülür. Debug’ı çalıştırıp b800:0000 adresini ekrana
döktüğümüzde bu yazı hala aynı yerindedir. B800h segmenti de burada görünen yazının
bulunduğu text video alanıdır. Şekilde B800:0000 adresindeki 4Dh değerinin aslında M
harfi olduğunu ve C karakterininde yine aynı segmentte bir değer olduğunu
görüyorsunuz. Yukarıdaki resimdeki hafıza dökümü, en üst satırın tamamını ve sonraki
satırın yarısından biraz fazlasını gözler önüne seriyor. Hafıza dökümünde tek numaralı her
offsette (0001h,0003h,00F3h gibi) 07h değerini görüyorsunuz. Bu text mode özelliği olup
siyah zemin üzerine beyaz yazı anlamına gelir. Gerçektende ekrandaki karakterlerin hepsi
beyaz ve zemin siyah değil mi?
Konsol ekranının tamamı karakterlerle dolsa, ekranda kaç karakter görürüz? Tabi ki
80x25=2000 adet. Şimdi bu karakterlerin tek tek adreslerine erişip (özellik içeren 07h
byte’larını atlayarak) bunları lojik bir işlemden geçirsem ekranın o anki görüntüsü değişir,
hem de bizim boş olarak tabir ettiğimiz ama aslında 20h olan yerler bile. Böyle bir
durumda ekranda karman çorman anlaşılmaz bir görüntünün belirmesi olası bir
durumdur.
Peki değiştirdiğim her byte’ı tekrar eski haline getirebilir miyim? Bütün bunların cevabı
aşağıdaki programda saklı.
Şekil 2 - 80x25 text mode da görünen ekrana müdahele.
Şekil 3 - Program çalıştırılmadan önceki durum.
Aslında video işlemleri için daha pratik interrupt’lar mevcuttur, burada anlatmaya
çalıştığım, "şayet assembly dili ile hafızanın her yerine erişebilecek deneyime sahipseniz
yapamayacağınız şey yoktur" gerçeğiydi.
Makalemizde, 80X86 komut sistemine ait aritmetik ve lojik komutları açıklamaya devam
edeceğiz.
AAM komutu, çarpma işlemi sonucunda, AX registerinde oluşan değeri ASCII formata
dönüştürme amaçlı kullanılır. Daha önce anlatılan AAA ve AAS komutlarında olduğu gibi
AAM ve AAD komutlarıda operandsız yazılırlar. AH ve AL registerlerini gizli operand olarak
kulanmaktadırlar.
AAM komutu, çarpma işlemi ile ax registerinde oluşan değerin ardından kullanılır. Al’nin
değerini desimal 10 ile böler, elde edilen değeri ah’a aktarır. Bölümden kalan değeride
tekrar al’ye aktarır.Yapılan işlemin sonucunda, elde edilen sayının sol dijiti ah’da, sağ
dijiti ise al’de yer alır.AAM komutu, parity, sign ve zero flaglarını al’ye aktardığı değere
göre uygun şekilde etkiler.
Örneğimizde mul komutu, çarpım sonucu olan 00 51 değerini ax registerine aktarırken,
aam komutuda bu değeri 00 81 olarak ASCII formatına dönüştürmüştür.
51h = 0101
I. adım
0001b
al
registerinin
_________________
değerini
= 08h
0Ah’a böl
0Ah = 0000
0Ah=d’10’
1010b
II.adım
bölümün
sonuç
ah = 08h
değerini
al = 01h
ah’a ,
kalan’ı
al’ye aktar
III.adım
Sonuç ax = 0801h
AAD komutu ise bölme işleminde bölünenin sonucunda, AX registerinde oluşan değeri
ASCII formata dönüştürme amaçlı kullanılır.AAD komutu, bölünen değerin yüksek seviyeli
kısmını (ah registerinin içeriğini) desimal 10 ile çarpar ve al (düşük seviyeli kısmı) ile
toplayarak al içerisine aktarır.
Yapılan işlem sonucunda ah registeri sıfırlanır, al registerinde bulunan BCD sayısı, başka
bir BCD sayısı ile bölünebilir duruma getirilmiş olur. AAD komutu, parity, sign ve zero
flaglarını al’ye aktardığı değere göre uygun şekilde etkiler.
Örneğimizde desimal 35 sayısı, desimal 5’e bölünmekte ve sonuç yine desimal olarak
elde edilmektedir. AAD komutunu kullanarak, ax üzerine paketlenmemiş olarak aktarılan
sayıyı, bölme işlemi için uygun hale getiriyoruz.
I. adım
ah 03h = 0000 0011b
registerinin 0Ah = 0000 1010b
içeriğini *_________________
0Ah ile çarp 1Eh = 0001 1110b
0Ah=d’10’
II.adım 1Eh = 0001 1110b
çarpım 05h = 0000 0101b
sonucundaki +_________________
değere 23h = 0010 0011b
al’nin
değerini
ekle ve
al’ye yaz
23h = 0010 0011b
III.adım
_________________
Sonuç
=7
05h = 0000 0101b
DAA komutu ile yapılan toplama işlemi sonucunda her bir byte’da iki adet desimal dijit
bulunur. ADD komutunun arkasından kullanılan DAA komutu, al’nin içeriği olan değere ve
AF (auxilary flag) ile CF (carry flag)’nin durumuna bağlı olarak iki farklı şekilde işleme
sokulur.
I. Durum: al registerinin düşük seviyeli 4 biti 9’dan büyük veya AF set ise, al registeri 6
ile toplanarak, AF set edilir.
II.Durum: al registerinin yüksek seviyeli 4 biti 9’dan büyük veya CF set ise, al registeri ile
60h toplanarak CF set edilir.
mov al,09 ; al = 9
add al,05 ; al = 0E
daa ; al =14
I. adım
09h = 0000 1001b
al
05h = 0000 1001b
registerlerini
+__________________
topla
0Eh = 0000 1110b
II.adım
al
registerinin
düşük
seviyeli biti 0Eh = 0000 1110b
0’dan küçük 06h = 0000 0110b
olduğu için +_________________
6 değerini 14h = 0001 0100b
ekledik ve
sonuç
değere
ulaştık
DAS komutu ile yapılan çıkartma işleminin al registeri üzerindeki sonucuna ve auxuiliary
ile carry flaglarının durumuna bağlı olarak iki farklı şekilde kullanılır.
I. Durum: al registerinin düşük seviyeli 4 biti 9’dan büyük veya AF set ise, al
registerinden 6 çıkartılarak, AF set edilir.
II.Durum: al registerinin yüksek seviyeli 4 biti 9’dan büyük veya CF set ise, al
registerinden 60h çıkartılarak CF set edilir.
DAS komutu, auxiliary, carry, parity, sign ve zero flaglarını etkiler.
Örneğimizde, desimal 13 ’den 5 ’i çıkartma işlemini al registerinde yaptığımız için sonuç
0E oluyor. al registerindeki değerin desimal sayı olması için çıkartma komutunun
ardından DAS düzenleyici komutu kullanılıyor.
13h = 0001
I. adım 0011b
al 05h = 0000
registerindeki 1001b
değerden bl -
registerindeki __________________
değeri çıkart 0Eh = 0000
1110b
II.adım
al
registerinin
düşük 0Eh = 0000 1110b
seviyeli biti 06h = 0000 0110b
0’dan küçük -
olduğu için 6 __________________
değerini 08h = 0000 1000b
çıkardık ve
sonuç değere
ulaştık
Aritmetik işlemler sırasında operandlar farklı uzunluklara (byte,word veya double word)
sahip olabilirler. Bu gibi durumlarda, işlem öncesinde operandların uzunluklarının
düzenlenmesi gerekir.Küçük uzunluğa sahip operandın uzunluğunu, büyük uzunluğa
sahip operandın uzunluğuna denkleştirilmektedir.İşaretsiz sayılarla temsil edilen
operandlar, rahat bir şekilde düzenlenir. Fakat işaretli sayılarla temsil edilen operandlarda
değişim o kadar rahat olmamaktadır.
İşaretli sayıları içeren operandların uzunluklarının düzenlenmesi için CBW ve CWD
komutları geliştirilmiştir. CBW komutu al ve ah, CWD komutu ise ax ve dx’i gizli operand
olarak kullanırlar.
CBW komutu, al içerisindeki işaretli sayının işaret bitini ah’ın tüm bitlerine, CWD komutu
da ax içindeki sayının işaret bitinin değerini dx’in tüm bitlerine aktararak sonuca
ulaşmamızı sağlar.
TEST Komutu:
Test komutunun çalışma prensibi ve flagları etkileme biçimide dahil olmak üzere And ile
aynıdır. Farklı olarak, komut ile birlikte kullanılan operandların değerlerini değiştirmez.
Diğer lojik komutlarında olduğu gibi, carry, owerflow bayrakları test işleminin sonrasında
reset edilirler, auxiliary dışındaki diğer bayraklarda işlem sonucuna uygun şekilde
etkileneceklerdir.
Farkı olmamasına rağmen, hangi durumda and, hangi durumda test komutunu
kullanmalıyız sorusu aklımıza gelecektir. Örneğimizi inceleyelim;
and
al,01
I. Durum jz *
...
...
test
al,01
II.Durum jz *
...
...
SHR Komutu ile, soldaki operandın en düşük seviyeli biti carry bayrağına kopyalanır ve
operandın tüm bitleri sağa doğru 1’er bit kayar. En soldaki bitin değeri 0 yapılır. Bu işlem
SHR ile verilen ikinci operandın değeri kadar tekrarlanır.
Örneğimizde, 30h değerini iki kere 1’er biti sağa kaydırarak sonucun nasıl değiştiğini
inceleyeceğiz.
Programda da görüldüğü gibi her bir sağa kaydırma işlemi ile sayıyı ikiye bölmektedir.Her
bir shr işlemi sonrasında carry bayrağı 0 ise çift sayı, 1 ise tek sayıdır.
SHL Komutu ile, soldaki operandın en yüksek seviyeli biti carry bayrağına kopyalanır ve
operandın tüm bitleri sola doğru 1’er bit kayar. En soldaki bitin değeri 0 yapılır. Bu işlem
SHL ile verilen ikinci operandın değeri kadar tekrarlanır.
Örneğimizde 0Ch değerini iki kere 1’er bit sola kaydırarak sonuç üzerindeki değişiklikleri
inceleyeceğiz.
Programda da görüldüğü gibi her bir sola kaydırma işlemi ile sayıyı iki ile çarpmış
oluyoruz.
RCR Komutu, işleme tabi tutulan bitlerin herbirini bir pozisyon sağa kaydırır. Düşük
seviyeli bit pozisyonundaki değer carry bayrağına, carry bayrağının değeride yüksek
seviyeli bit pozisyonuna aktarılır. İşlem ikinci operand değeri kadar tekrarlanır.
RCL Komutu, işleme tabi tutulan bitlerin herbirini bir pozisyon sola kaydırır. Carry
flağının değeri düşük seviyeli bit pozisyonuna, yüksek seviyeli bit pozisyonundaki değer
ise carry flağına aktarılır. İşlem ikinci operand değeri kadar tekrarlanır.
aamsonuc db ’00$’,10,13
dassonuc db ’00$’
knm db 10,13,’$’
;....................................................
.code
mov ax,@data
mov ds,ax
mov es,ax
call aamornek
call dasornek
;....................................................
aamornek proc
mov al,12h
shr al,01 ;al registerinin içeriğini bir bit sağa kaydır, al=9h
mov ah,09h
lea dx,aamsonuc ;sonucu yaz
int 21h
mov ah,09h
lea dx,knm ;alt satıra geç
int 21h
aamornek endp
;....................................................
dasornek proc
mov al,28h
rcr al,01 ;al registerinin içeriğini bir bit sola kaydır, al=14h
mov ah,02h
mov dl,al
int 21h ;sonucu yaz
dasornek endp
;....................................................
mov ah,4ch
int 21h
end ;programdan çıkış
;....................................................
Çeşitli bilgilerin birleşerek biraya gelmelerine string denir. �Merhaba� kelimesi bir
string�dir. ASSEMBLY�de stringleri kontrol edebilen komutlar vardır ve bu komutlar
hafıza blokları üzerinde çalışırlar.
Movsb ;si registerinde bulunan byte dizisini di registeri ile gösterilen adrese
byte olarak taşı
MOVS Komutu:
movs komutunun kullandığı veri tipine göre farklı kullanım şekilleri vardır, data
segmentinde ve ofset adresi SI rejisterinde bulunan byte / word / double word dizilerini
ES ve DI registerleri ile gösterilen adrese taşır. CX registerinde tutulan deger adedince
taşıma işlemi tekrarlanır.movsb ile byte dizileri, movsw ile word dizileri ve movsd ile
double word dizileri üzerinde işlemler yapılır.
REP: kendisinden sonra gelen string komutunun cx registerinin değeri 0 olana dek
tekrarlar.
CMPS Komutu:
REPE, REPZ, REPNE ve REPNZ komutlarının parametresi olarak bellekteki iki stringi
birbirleriyle karşılaştırma işleminde kullanılır. Stringlerden biri Data Segmentte diğeri ise
Extra Segmentte tutulur. Data Segmentindeki stringin ofset adresi SI registerine, Extra
segmentteki stringin ofset adresi ise DI registerine yerleştirilir. Cx registerindeki değer
adadince karşılaştırma işlemi tekrarlanır.
Not:REPE: eşit oduğu zaman tekrarla REPZ: zero flag �0� oldugu zaman tekrarla
REPNE: eşit olmadığı zaman tekrarla REPNZ: zero flag �0� olmadıgı zaman tekrarla
LODS Komutu:
Lods komutu string komutları içinde tektir. Komut öncesinde herhangi bir komut
kullanmaya gerek yoktur. Ofset adresi DS:SI rejisterinde bulunan byte / word / double
word dizilerini al, ax, eax registerlerine, si registerinin değerini arttırarak veya azaltarak
kopyalar.
SCAS Komutu:
Ofset adresi DS:SI rejisterinde bulunan byte / word / double word dizileri içerisindeki
değerle al, ax, eax registerlerindeki değerleri karşılaştırarak arama yapmak için kullanılır.
Scas komutu AF, Cf, OF, PF, SF ve ZF bayraklarını ayarlarak arama işlemini gerçekleştirir.
STOS Komutu:
Stos komutu es:di ile gösterilen adreste bulunan akümülator içerisinde belirtilen değeri
saklar. Bu işlemden sonra yön bayrağının durumuna göre işlemci di registerini arttırır
veya azaltır. Stos komutu byte, word yada double word üzerinde işlemler yapmak için
uygundur.
Programın ekran çıktısından da gördüğünüz gibi klavyeden ilk harf grubunun tamamı
küçük harf ikinci harf grubunun bir elemanı ise büyük harf olarak girilmiştir. Sonuçta
enter tuşuyla harf girişi tamamlanıp sonuç değeri elde edilmiştir. Umarım string
komutlarının kullanımı konusunda faydalı olur.
.model small
.stack 64
.data
.code
mov ax,@data
mov ds,ax
mov es,ax
dizi_al:
mov ah,01 ;klavyeden karakter isteniyor
int 21h ;girilen al�de
degis:
sub al,20h ;al değerinden 20h çıkar
sbt:
sub al,0h ;al değerinden 0h çıkar
yaz:
mov ah,09h ;ekrana snc�de tutulan değer yazılıyor
lea dx,snc
int 21h
Aslında string komutları ile yapılan işlemler bilgisayar kullanıcılarına o kadar yakın ki,
mesela msword uygulamalarında kullanılan kopyala-yapıştır gibi.
Yorumlarınızı bekliyorum.
Stack Memory (Yığın Hafıza Bölgesi)
Stack memory geçici değerlerin saklandığı bir hafıza alanıdır. Mikroişlemci SS:SP
kaydedicileri ile bu hafıza alanına erişir. SS stack alanının segment adresini tutarken SP
bu alandaki offsetleri işaret eder. Stack hafıza alanını kullanmama diye bir lüksümüz yok,
bu makaleye kadar stack memory ile ilgili hiç bir komut yazmasakta, bu hafıza alanını
yadığımız tüm programlar kullandı.
LIFO
Yine yukarıdaki şekilde orijinal SP yığın boşken SP kaydedicisinin aldığı değerdir. Mesela
.stack 32 diye direktif verdiyseniz SP=0020h ı gösterir. Yığına veri itildikçe SP’nin değeri
azalır, bu değerler yığından çekildikçe SP artar. x86 mimarisinde yığın hafızaya 16 bitlik
çalışma modunda 16 bitlik değerler, 32 bitlik çalışma modunda ise 32 bitlik değerler
saklayabilirsiniz.
PUSH ve POP komutları
Yığın hafıza alanını etkileyen komutların genel kullanım formatları aşağıdaki gibidir;
push reg16
pop reg16
push reg32 (**)
pop reg32 (**)
push segreg
pop segreg (CS hariç)
push mem
pop mem
push imm (*)
pusha (*)
popa (*)
pushad (**)
popad (**)
pushf
popf
pushfd (**)
popfd (**)
Programda üç tane prosedür bulunmakta ve ana prosedüre baktığınızda klasik kodlar olan
segment ayarlama ve dosa dönüşün dışında sadece CALL komutlarını görüyorsunuz. Ana
prosedürden 3 kere alt prosedürlere dallanma gerçekleşiyor. Her dallanma sırasında geri
dönüş adresi yığında saklanır. Alt prosedürlerden dönerkende ret komutu ile bu adres
geri yüklenir. Şayet dallanma bir başka segmente tapılıyorsa CS ve IP, buradaki gibi aynı
segment içindeyse sadece IP yani offset adresi yığında saklanır. Offset ve segment
adreslerinin 2 byte olduğunu düşünecek olursanız bunun stackın yapısınada uygun
olduğunu anlarsınız.
Yığın hafıza yada stackın kullanılmadığı bir program yok gibidir. Tüm kesmeler (bunlarda
bir çeşit alt prosedür gibi düşünüle bilir) çağrılmadan önce, o anki CS ve IP ayrıca tüm
kaydediciler yığında saklanmak zorundadır. Aksi halde geri dönüş yapıldığında alt
prosedürün değiştirdiği kaydediciler ile karşılaşılır, hatta bu bile mümkün olmaz çünkü
mevcut CS ve IP saklanmadan geri dönüş söz konusu değildir.
Satırlar yığın hafızayı anlatmakta biraz yetersiz kalıyor olabilir, bu konuyu anlamamış
olabilirsiniz diye sizlere birde video hazırladım. Yukarıdaki programın örnek alındığı
videoyu buradan download edip izlediğinizde sanırım problem çözülecek :)
1- Programlar genelde 3 adet hafıza alanından oluşur, bunlar code, data ve stack tır.
2- CPU bu alanlara adresleme yaparak erişir,
3- CPU CS ve IP kaydedicilerini kodlara erişip onları işlemek için,
4- DS, SI ve DI’yı değişkenlere datalara erişmek için,
5- SS, SP ve BP’yi yığın hafıza alanına erişmek ve biçimlendirmek için kullanılır.
Bu makaleyi iyice anlamak için mutlaka videoyu izleyin ve bu örnek programın kaynak
kodlarını kendiniz derleyip çalıştırın ve bu programa başka prosedürler ekleyip onları
başka prosedürlerden yada ana prosedürden çağırarak programı inceleyin. İyi
çalışmalar... :)
Şekil 1�de Turbo Pascal içerisindeki ASM....END; blokları arasında, Şekil 2�de
C++ Builder 6.0 içerisindeki asm{...} blokları arasında tasm�da olduğu şekliyle
kodlarımızı yerleştirebiliyoruz.
Şekil 1 ve 2�deki assembly blokları içerisinde, iki sayının toplanması işleminin nasıl
gerçekleştiğine bakalım:
C ++ Builder Componentleri :
Etiket (TLABEL): Form içerisinde bulunan Metin Kutusu�na ne tür değer girileceğini
belirtmek için kullanılmaktadır.
Metin Kutusu (TEdit): Kullanıcının bilgi girişi yapması ya da elde edilen sonucun
görüntülenmesi için kullanılır.
C++ Builder 6.0 ile gerçekleştirdiğimiz örnek kodu buradan , Turbo Pascal 7.0 ile
gerçekleştirdiğimiz örnek kodu buradan indirebilirsiniz.
Edit: ★BLλCK-
λCK-CΘDΣ
goa@hackermail.com