Bu makalemizde 80x86 gerçek mod
komutlarını inceleyeceğiz.
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.
80x86 komutları
genelde 8 grup altında incelenir.
1) Veri taşıma
komutları
mov, lea, les , push, pop, pushf, popf
2) Dönüştürme
komutları
cbw, cwd, xlat
3) Aritmetic
komutlar
add, inc sub, dec, cmp, neg, mul, imul, div, idiv
4) Mantıksal,
kaydırma, çevirme ve bitsel işlemler için komutlar
and, or, xor, not, shl, shr, rcl, rcr
5) I/O
(Giriş/Çıkış) komutları
in, out
6) Karakter dizi (String) komutları
movs, stos, lods
7) Program akış
kontrol komutları
jmp, call,
ret, Jxx (şartlı dallanma komutları)
8) Diğer komutlar
clc, stc, cmc
Veri Taşıma Komutları
Veri taşıma
komutları bir değeri bir yerden başka bir yere
taşımaya yarar. mov, xchg, lds, lea, les, lfs, lgs, lss, push, pusha, pushad, pushf, pushfd, pop, popa, popad, popf, popfd, lahf,
ve sahf komutları veri taşıma
komutlarıdır.
MOV komutu
Bu komutun kullanım
şekilleri aşağıdaki gibidir.
mov
reg, reg
mov
mem, reg
mov
reg, mem
mov
mem, immediate data
mov
reg, immediate data
mov
ax/al, mem
mov
mem, ax/al
mov
segreg, mem16
mov
segreg, reg16
mov
mem16, segreg
mov
reg16, segreg
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.
reg : register
à kaydedici,
mem : memory
à hafıza (RAM-ROM veya Giriş/Çıkış portları olabilir)
immediate data
à acil adresleme ile kullanılan direkt veri
segreg : segment register
à segment kaydedicisi
mem16 : memory 16 bit
à 16 bitlik hafıza alanı (dw direktifi ile tanımlanan veriler)
reg16 : register 16 bit
à 16 bitlik kaydedici (AX, BX .. gibi)
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.
MOV sayi1, sayi2 ;yanlış kullanım
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.
MOV AX,
sayi1
MOV sayi2, AX ;sayi1
ve sayi2 değişkenlerinin word türünden
olduğunu varsayıyoruz.
MOV komutu ile
yapamayacağınız ikinci şey ise segment
kaydedicilerine doğrudan bir veri taşımaktır. Yani acil
adresleme modunu segment
kaydedicilerine uygulayamazsınız.
MOV DS, 1525h ; bu kullanım hatalıdır.
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,
1525h
MOV DS, AX
Bunların dışında operandların boyutları eşit olmak
zorundadır.
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
mov
word ptr [bx], 5
mov
dword ptr [bx], 5
(*)
(*) 80386 ve sonrası işlemcilerde kullanılabilir
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
xchg (exchange)
komutu operandlarındaki değerleri yer
değiştirir.
80x86 ailesi için dört
değişik kullanım şekli vardır;
xchg reg, mem
xchg reg, reg
xchg ax, reg16
xchg eax, reg32 (*)
(*) 80386 ve sonrası işlemcilerde kullanılabilir
LDS, LES, LFS, LGS, ve LSS komutları
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;
LxS
hedef, kaynak
Bu komutları
aşağıdaki gibi kullanabilirsiniz;
lds
reg16, mem32
les
reg16, mem32
lfs
reg16, mem32
(*)
lgs
reg16, mem32
(*)
lss
reg16, mem32
(*)
(*) 80386 ve sonrası işlemcilerde kullanılabilir
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ı
lea dest, source
şeklindedir.
lea reg16, mem
lea reg32, mem (*)
(*) 80386 ve sonrası işlemcilerde kullanılabilir.
MOV
komutunu da LEA komutu yerine kullanabilirsiniz, fakat hangi komutu kullanacağınıza
adresleme moduna göre seçmelisiniz. Bazen MOV komutu LEA dan daha hızlı çalışabilir, tüm bu
bilgilere herhangi bir intel komut setinden
faydalanarak bakabilirsiniz.
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;
Dizi DB
"Merhaba Assembly",0Ah,0Dh,24h
Daha
sonra bu dizinin adresini DX kaydedicisine yüklememiz gerektiğinde
aşağıdaki komutu kullanmıştık;
MOV DX,OFFSET Dizi
Bu
komutun yaptığı işi LEA kullanarakta
yapabiliriz;
LEA DX, Dizi
PUSH ve POP
komutları
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 (*)
(*) 80286 ve sonrası işlemcilerde kullanılabilir
(**) 80386 ve sonrası işlemcilerde kullanılabilir
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.
LAHF ve
SAHF Komutları
Bu komutlar
bayrakları AH kaydedicisine yükler veya AH’a
yüklenen bayrak kaydedicilerinin durumlarını kayar nokta
kaydedicisine ( floating point
register ) yükler. Bu komutlar 8086 zamanından
kalma ve günümüzdeki modern assembly
programlarında pek kullanılmayan komutlardır.
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.
movzx hedef, kaynak ;Hedef kaynağın iki katı büyüklüğünde olmalıdır.
movsx hedef, kaynak ;Hedef kaynağın iki katı büyüklüğünde olmalıdır.
cbw
cwd
cwde
cdq
bswap reg32
xlat
MOVZX, MOVSX, CBW, CWD, CWDE, ve CDQ Komutları
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 komutu EAX
kaydedicisindeki 32 bit’lik değeri EDX:EAX
‘e genişletir. Bu komutda 80386 ve
sonrası işlemlerde kullanılır.
cdq
Örnekler:
; AL’ deki 8 bitlik değeri 32 bitlik dx:ax’e genişletmek için
cbw
cwd
; AL’ deki 8 bitlik değeri 32 bitlik eax’e genişletmek için
cbw
cwde
; AL’ deki 8 bitlik değeri 64 bitlik edx:eax’e genişletmek için
cbw
cwde
cdq
movsx komutuda yukarıdaki komutlara benzer iş yapar,
kullanım formatları aşağıdaki
gibidir.
movsx reg16, mem8
movsx reg16, reg8
movsx reg32, mem8
movsx reg32, reg8
movsx reg32, mem16
movsx reg32, reg16
Örnekler:
movsx ax, al ;CBW komutunun yaptığı işi yapar.
movsx eax, ax ;CWDE komutunun yaptığı işi yapar.
movsx eax, al ;CBW ve CWDE komutlarının birlikte yaptığı işi yapar.
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.
Tüm bu
genişletme komutları genellikle aritmetik işlemlerde ve
özellikle bölme komutlarında kullanılı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.
BU komut sadece
80486 ve sonrası işlemcilerde kullanılabilir. Kullanım
formatı aşağıdaki gibidir.
bswap reg32
XLAT Komutu
Genellikle tablo
olarak tasarlanan dizilere erişmek için kullanılır. AL
kaydedicisine tablonun elemanlarından birini yükler. Bu komutu
aşağıdaki örneğe bakarak daha iyi anlayabilirsiniz.
Tablo DB 0, 1, 2, 3, 4, 5,
6, 7, 8, 9, A,
B, C, D, E, F
Bu tablonun 11.
elemanını AL’ye yüklemek istersek;
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,
MOV
AX, @data
MOV DS,
AX
Yığın
komutlarınıda veri taşımak için
kullanabileceğimizden,
MOV AX, @DATA
PUSH AX
POP DS
Yukarıdaki üç
satırda DATA segmentin adresini önce AX
kaydedicisine yükledik, sonra bu kaydedicideki değeri
yığına ittik son olarakta
yığından bu değeri çekip DS kaydedicisine yükledik.
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
Bu komutlar
işlenince CX ve DX kaydedicileri saat bilgisi ile yüklenirler,
CH
= saat CL = dakika DH = saniye DL = salise
Kaydedicilerin
içindeki değerlerin binary olduğunu ve debugger programlarında bu değerlerin hexadecimal formatta göründüğünü unutmayalım. Biz
bu programda kullanıcı ekranına desimal
formatta değerler yazdıracağımızdan ya çevirme
işlemi yapacağız yada tablo
oluşturup hex bilgilerin
karşılığına tablodan bakacağız.
Ç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 bytelı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.
MOV AL, CH ;Şimdi
saat bilgisi,
MUL Uc ;3
ile çarpılarak tablodaki desimal
karşılığı bulundu.
MOV DI, AX ;Bu
adres indeks olarak düşünüldüğünden DI ya yüklendi.
LEA DX,
SaatTablo[DI] ;Ve BXe saat
bilgisinin desimal karşılığı
olan tablo konumu yüklendi.
MUL
komutunu önümüzdeki makalelerde inceleyeceğiz, çok fazla kullanım
formatı mevcuttur. Bizim için burada, sadece CH veya CL deki
değerleri 3 ile çarpsın yeter. Fakat MUL komutu sadece AL deki
değeri bir değişken yada AL kaydedicisi
ile çarpabildiğinden önce CH yada CL deki bilgileri AL’ye
taşımamız gerekiyor. Daha sonra 3 değeri ile çarpıp
tablodan adresi buluyoruz. Tabiî ki Uc bir
değişken ve data segmentinde
tanımlı olmalıdır.
Uc DB 3
Al
deki değer 3 ile çarpıldığında sonuç 16 bitlik AX
kaydedicisinde saklanır. Bu noktadan sonra bu adresi DX kaydedisine yükleyip ekranda string
yazdırma kesmesini çağırırsak işlem tamam
olacaktı ama, SaatTablo taban adresine göre
hesapladığımız indeks adresi halen AX kaydedicisinin içinde
ve LEA DX, SaatTablo[AX] gibi bir komut satırı kullanamam
çünkü böyle bir adresleme modu mevcut değil. Indeks olarak kullanılacak adresler 8086 assembly dilinde SI, DI veya BX kaydedicilerinin birinde
olmalıdır, bu yüzden bizde hesapladığımız indeks
değerini DI ya yükledik. Son olarak ekranda bir string
yazdıracağımızdan DX kaydedicisinin içine
yazdıracağımız stringin
başlangıç adresini atarak ve daha önce de
kullandığımız INT 21h fonksiyon 9h ı kullanabiliriz.
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 ‘:’
Birde
saat kelimesini data segmentte
tanımlayalım,
Saat DB ‘Saat $’
Şimdi kabataslak
ne yapacağım belli oldu, programımın
algoritmasını da
yazdıktan sonra kodlarımı yazmaya başlayabilirim.
1-
Ekrana “Saat” yazdır
2-
Sistem saatini ilgili kaydedicilere yükle
3-
Tablodan saat bilgisinin desimal
karşılığını bul
4-
Ekrana yazdır
5-
: karakterini ekrana yazdır
6-
Sistem dakikasının
karşılığını bul
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.
Makale:
80x86 KOMUT SETİ (Bölüm 1) Assembly ve X86 Programlama Eren Erener
|