Microsoft 32 Bit Macro Assembler'da Kodlama
Gerek bu bölümde anlatılacak olan Microsoft ürünü assembler'da, gerekse de öteki benzer nitelikli assembler'larda kodlama, pek çok gelişkin nitelikli üst düzey programlama dilinde olduğu gibi derleyiciye ve sisteme gönderilecek komutların da bulunduğu farklı kısımlarda ele alınır. Microsoft Macro Assembler'daki (kısaca MASM olarak kullanılacaktır) kodlama tekniği, bu yazı dizisinin 3. bölümünde ele alınan Turbo Assembler'dakine kısmen benzemektedir. Dolayısıyla bir yanda söz konusu bölümün de açık tutulması faydalı olabilir. MASM'a uygun bir ASM dosyasının içeriği, amaç ve yöntemlerine göre şu 6 bölümden oluşmaktadır:
Bir assembler programında, yukarıda belirtilen maddeler dahilinde tüm öğelerin bulunması gerekmese de, kaynak dosyanın derlenebilmesi için en azından platform, model belirtilmesi ve kod bulunması gerekir. Daha açıklayıcı olması için aşağıdaki örneğin incelenmesinde fayda vardır. Bu örnek, derlenip bir EXE dosyası oluşturulabilecek en salt kodu temsil etmektedir.
.386
.model flat, stdcall
.code
start:
ret
end start
İlk satır platformu, ikinci satır model ve dili, kalan bölüm ise kodu temsil etmektedir. Bu kod hiçbir iş yapmamakta, program çalıştırılır çalıştırılmaz sona ermektedir. Konu ilerledikçe, yeni konu başlıkları altında daha nitelikli ve işe yarar kodlar geliştirilecektir.
Bir ASM dosyasının içeriği hakkında unutulmaması gereken en önemli şeylerden biri, ifadelerin satır satır işlendiğidir. Yani C/C++'ta veya Pascal'da olduğu gibi ifadelerin (;) gibi herhangi bir karakterle sonlandırılma durumu yoktur.
Aşağıdaki başlıklar altında, assembler dosyası içeriğinin bölümleri oldukça ayrıntılı biçimde irdelenmektedir.
Platform, Model, Dil ve Derleyici Parametreleri
Bu başlık altında anlatılan parametreler, EXE dosyasında kod olarak dönüştürülmezler. Sadece derleyiciye kodun nasıl değerlendirileceğini belirtirler.
Platform
.platform
Platform, derleyicinin kaynak kodu nasıl ve hangi işlemciye göre değerlendireceğini belirten parametredir. Bilindiği üzere Intel I286 ve I386 işlemcileri arasında büyük farklılıklar vardır. 80386 işlemcisinin kaynakları kullanılarak yazılan bir kodun 80286 işlemcisinde çalışabilmesi mümkün değildir. Dolayısıyla bu parametre hayati önem taşımaktadır. Bu parametre sayesinde derleyici, kullanılacak komut setini ve bellek erişimini tayin eder. (.) karakterine bitişik olarak yazılabilecek şu ibareler mümkündür: 8086, 186, 286, 386, 486, 586, 686. Adlarından kolayca anlaşılabilecek bu komutlardan sadece 586 ve 686'nın anlamı yaygınca bilinmediğinden belirtilmesinde fayda görüyorum: 586 = Pentium, 686 = Pentium Pro.
Intel işlemcilerinde, ondalık sayı işlemlerini gerçekleştiren birime Floating Point Unit (FPU) denir. "Math CoProcessor" olarak da adlandırılan bu birim, 80286 işlemcisinden itibaren işlemci kılıfı dahilindedir ve birim adı, işlemci adının 1 fazlasıdır. x87 FPU'ya ilişkin komut seti, işlemci komut setine paralel olarak gelişim göstermiştir ve işlemciden işlemciye değişmektedir. Dolayısıyla, kullanılacak FPU komut setine ilişkin platform tanımlaması da mümkündür. Aynı şekilde (.) karakterine bitişik olarak yazılabilecek şu değerler mümkündür: 8087, 287, 387, NO87. Buradaki değerlerden NO87, FPU komutlarının derlenmesini önler. Bu durumda, derleyici komut satırı parametrelerinden emulation(olmayan bir kaynağın görevinin yerine getirilmesi) belirtilmeli ve ilgili kütüphane dahil edilmelidir.
Model
.model bellekmodeli
Bilindiği üzere x86 işlemcisinde veri, kod ve yığın amaçlarına hizmet eden farklı segment yazmaçları mevcuttur. Tüm bu segmentler, tüm belleği paylaşarak adreslerler. Ancak amacı ve kullanım sınırı belli olan bir programın tüm belleği serbestçe kullanabilmesi mümkün değildir. Bu yüzden program segmentlerinin belirli değerler ile sınırlı olarak kullanılması gerekir. Model parametreleri, kod ve verinin bellek üzerinde ne şekilde yer alacağını belirler.
Bu yazı dizisinin 3. bölümünde, 80286 işlemcisi için önemli olan 6 bellek modelinden söz edilmişti: Tiny, Small, Medium, Compact, Large, Huge. Pratikte gözlenebileceği üzere bu bellek parametreleri, kod ve verinin yakın ya da uzak işaretçiler ile gösterilmesini etkilemektedir. Ancak 32 bitlik işlemcinin yakın adresleme bölgesi 4GB olduğundan bellek ihtiyacı fazlasıyla karşılanabilmekte ve dolayısıyla uzak işaretçilere gerek kalmamaktadır. Hem kodun, hem de verinin aynı yakın adresleme bölgesi dahilinde olduğu Tiny bellek modeli, bu platform için uygundur. Ancak bunun yerine, tüm işlemlerin segment yazmaçlarından bağımsızlaşmasını sağlayan Flat bellek modeli kullanılır. Yukarıdaki en salt kod örneğinde bu kullanım görülmektedir.
Dil
.model dil
Burada dilden kasıt, alt programların(fonksiyonların) varsayılan çağrı yöntemidir. Bilindiği üzere assembler'da alt programların çağırılması esnasında gönderilecek parametreler genellikle yığına atılır. Burada parametrelerin atılış sırası, erişimde kullanılan yazmaçların muhafazası, yığının eski haline getirilmesi gibi unsurlar önem taşımaktadır. Yaygın olarak kullanılan çağrı yöntemleri şunlardır: C, SYSCALL, STDCALL, BASIC, FORTRAN, PASCAL. Aşağıdaki tablo, bu çağrı yöntemlerinin özelliklerini göstermektedir:
|
|
C |
SYSCALL |
STDCALL |
BASIC |
FORTRAN |
PASCAL |
|
Ön altçizgi |
X |
|
X |
|
|
|
|
Tamamı büyük harf |
|
|
|
X |
X |
X |
|
Parametreler soldan sağa |
|
|
|
X |
X |
X |
|
Parametreler sağdan sola |
X |
X |
X |
|
|
|
|
Çağıranın yığını düzeltmesi |
X |
|
* |
|
|
|
|
(E)BP'nin otomatik korunması |
|
|
|
X |
X |
X |
|
Çoklu parametre aktarımı |
X |
X |
X |
|
|
|
*Sadece çoklu parametre aktarımında.
En yaygın kullanılan tür STDCALL'dır. Windows'un kendi uygulamaları, kütüphaneleri ve pek çok durumda Microsoft Visual C++ da bu yöntemi kullanmaktadır. Dolayısıyla, özel bir amaç veya uyumluluk güdülmediği sürece bu parametrenin stdcall olarak kullanılmasını öneririm.
Örnek olarak verilen salt kodda da görülebileceği üzere bellek ve dil parametreleri, .model belirtecini takiben virgül ile ayırılarak yazılabilir.
Derleyici Parametreleri
.option parametre, parametre...
Derleyicide, kaynak kodun farklı şekilde derlenebilmesi için komut satırındaki ibarelere ek olarak yazılabilecek parametreleri mevcuttur. Ancak bu parametreler, kaynak dosya içerisinde de belirtilebilmektedir. Örneğin Windows API'nin doğru biçimde kullanılabilmesi için option casemap: none belirtilmesi gerekir.
Dahil Edilecek Dosyaların ve Kullanılacak Kütüphanelerin Belirtilmesi
Yüksek seviyeli programlama dillerindeki #include, uses gibi dahil etme komutları MASM'da da bulunur. Bu sayede çeşitli ortak dosyalar kullanılabilir ve kaynak dosyanın boyutu küçülür. Aşağıda buna ilişkin taslak biçim yer almaktadır:
include dosyanın tam yolu
include d:\masm32\include\windows.inc gibi...
Kaynak dosyalar, işlev prototiplerini, gövdelerini, global tanımlamaları, makroları ve eşitlikleri içerebilir. include komutu ile herhangi bir dosya dahil edildiğinde derleyici, sanki komutun bulunduğu noktada o dosyanın içeriği varmış gibi işlem yapar. Dolayısıyla içerik sırasına dikkat etmek gerekir. Bilhassa segmentlerin yerleşimi önem taşıyabilmektedir. Teknik bir yöntem olarak, include dosyalarının derlendiğinde kod oluşturmayacak yapıda olmalarına dikkat edilir. Yani içlerinde sadece prototipler, makro fonksiyonlar ve global derleyici eşitlikleri bulunmalıdır.
Yine yüksek seviyeli programlama dillerinde, başlık dosyası adı altındaki kod dosyalarında alt programların gövdeleri değil, sadece prototipleri yer alır. Program gövdeleri ise .lib uzantılı kütüphane dosyalarında veya .dll uzantılı Windows Dinamik Bağlantı Kütüphanelerinde yer alır(DLL dosyalarının içerisindeki kodun kullanımı için yine .lib dosyası gereklidir). Yukarıdaki örnekte olduğu gibi dahil edilen bir prototip kütüphanesindeki fonksiyonların programda kullanılabilmesi için ilgili kütüphanenin de belirtilmesi gerekir. Aşağıdaki taslak biçim buna ilişkindir:
includelib dosyanın tam yolu
includelib d:\masm32\lib\user32.lib gibi...
MASM32 kabinesi ile gelen dosyalar arasında include ve lib adı altında dizinler bulunur. Bu dizinlerden include, tüm Windows DLL'lerindeki işlevlerin prototiplerini ve global tanımlamaları, lib ise bu DLL'lerdeki işlevlerin giriş adreslerini içeren kütüphaneleri barındırır. Bizim programlarımızda kullanılacak dosyaların adresleri de genellikle bu dizinler dahilinde olacaktır.
Not: Söz konusu dizinlerden lib dizini, Windows DLL'lerine statik yöntemle bağlantı sağlar. Bu dizindeki dosyaların oluşturulabilmesi amacıyla MASM'nin kurulduğu dizindeki include dizininde BLDLIBS.BAT adlı bir toplu iş dosyası bulunur. İçinin incelenmesi suretiyle yeni DLL'lerin dış uzantılı işlevlerine statik bağlantı kütüphaneleri kolayca oluşturulabilir.
Eşitlikler, Global Tanımlamalar ve Makro İşlevler
Eşitlikler
MASM dosyasının bu bölümü, önceki bölümlere benzer olarak kod üretmeyen, ancak kod üretilmeden önce yapılacak yer değişimlerini hedefleyen bir bölümdür.
Yüksek seviyeli programlama dillerinden de aşina olunacağı üzere #define önişlemci komutu ile derleme zamanında kullanılabilecek eşitlikler tanımlanabilmektedir. MASM'de aynı amaca hizmet etmek üzere şu belirteç mevcuttur:
hedef ifade equ kaynak ifade
PI equ 3.14
sayi equ 25
wsprintf equ <wsprintfA> gibi...
Bu belirteç ile yapılan eşitlikler, dosyanın derlemesi esnasında geçici bir kopyadaki tüm hedef ifadelerin yerine, belirtmedeki kaynak ifadelerin yazılmasını ve derlemenin bu işlemi takiben devam etmesini sağlar. Dolayısıyla hedef ifadenin tekil olması dışında kaynak ifade kısıtlaması yoktur. < > operatörleri ile kapalı olması koşuluyla kompleks bir ifadeye de yer verilebilir.
Bunun yanı sıra, yine aynı şekilde kullanılan textequ belirteci ile de eşitlik tanımlanabilmektedir. Bu belirtecin bir üstünlüğü, kaynak ifadedeki çeşitli makroları çözümleyerek hedef ifadeye atamasıdır. Bu çözümleme, derleme esnasında yapıldığı için üretilen programın kullanımı esnasında bir devingenlik taşımaz. Aşağıdaki örneklerde bu çözümlemeye değinilmektedir.
libpath textequ @Environ(<LIB>)
; Bu eşitleme ile libpath ifadesine, sistemdeki
varsayılan kütüphane dizin yolu atanmıştır. "D:\Masm32\lib" gibi...
uzunluk textequ %(endadr-staradr)
; Bu eşitleme ile toplam kod uzunluğu, uzunluk ifadesine
atanmaktadır. Kaynak ifade, sayısal bir ifadeye çözümlenmektedir.
Buradaki eşitlikleri değişkenler ile karıştırmamak gerekir. Hedef ifadeler, dosya içerisinde sadece "placeholder" olarak adlandırılan biçimde yer tutarlar ve derleyici sadece kaynak ifadeyi görür. Dolayısıyla hedef ifade için adres veya büyüklük gibi nicelikler söz konusu değildir.
Global Tanımlamalar
Yine yüksek seviyeli dillerdeki struct, union ve typedef belirteçleri, MASM'de mevcuttur. Kullanım biçimleri aşağıdaki gibidir:
hedef yapı struct
yapı üyesi veri türü ?
yapı üyesi veri türü ?
...
hedef yapı ends
dikdortgen struct
x1
dd ?
y1
dd ?
x2
dd ?
y2
dd ?
dikdortgen ends gibi...
Bu örnekte dikdörtgen, sol üst köşesine ve sağ alt köşesine
ait koordinatlar aracılığıyla 2 boyutlu düzlemde tanımlanmaktadır. Yapı
içerisindeki her bir üyenin veri türü dd(define doubleword) olduğundan yapının
toplam boyutu 16 bayttır.
hedef ortak yapı union
ortak1 veri türü ?
ortak2 veri türü ?
...
hedef ortak yapı ends
ogrenci union
ogrencino db 10 dup(?)
tckimlikno db 12 dup(?)
ogrenci ends gibi...
Yukarıdaki örnekte bir öğrenci, her ikisi de kendine özgü olacağı için öğrenci numarası veya T.C. kimlik numarası ile tanımlanabilmektedir. Dikkat edilecek olursa her iki alanın da boyutu birbirinden farklıdır. Bu ortak yapının boyutu, içerdiği alanların en büyüğü olan 12 bayttır.
hedef tür typedef önceden tanımlı tür
ondalik typedef real4
tamsayi typedef dword gibi...
Yukarıdaki örneklerde, yazacağımız programlarda anlaşılırlık açısından kullanabileceğimiz türkçe veri türleri, amaçlarına yönelik olarak önceden tanımlı veri türlerinden türetilmiştir. MASM'da kullanılabilecek varsayılan veri türlerine ileriki alt başlıklarda değinilecektir.
Yapı üyelerine kod ile erişim, yüksek seviyeli dillerde olduğu gibi (.) operatörü ile gerçekleştirilir. Bu açıklanan türler dışında record belirteci ile de bit düzeyinde alan tanımlamaları yapılabilmektedir. Aşağıdaki şekilde kullanılır:
hedef tür record alanadı1: uzunluk1 [=öndeğer] [, alanadı2: uzunluk2 ...]
zaman record saniye: 5, dakika: 6, saat: 5
Yukarıdaki örnek, MS-DOS'taki FAT16 dosya sisteminde, dosya tarihlerinin kodlamasını göstermektedir. Bu örnekte saati göstermek için 5 bit kullanılarak 0'dan 23'e değerler sağlanmış; dakikayı göstermek için ise 6 bit kullanılarak 0'dan 60'a değerler sağlanmıştır. 2 bayta değin kalan 5 bit ile saniyeyi tam hassasiyette (0'dan 60'a) göstermek mümkün değildir. Sadece çift sayılı saniyeleri göstermek suretiyle (2^5=32 > (60/2 = 30)) bu gösterim 2 bayta sığdırılmıştır. Buradaki hassasiyet kaybı yaklaşımı tam bir mühendislik modellemesidir, zira 1 saniye ihmal edilebilecek bir büyüklüktür. Daha öte ve güvenlikli bir modelleme örneği olarak da, zaman kaydının 3 bayt ile yapılarak 10ms'lik hassasiyete ulaşmak mümkün olabilir(saniye tam hassas kaydedilir, geri kalan 7 bit ile 0-100 arası değerler elde edilir).
Makro İşlevler
Makro işlevler, yine yüksek seviyeli dillerdeki #define önişlemci komutuyla tanımlanan makro işlevler ile aynı niteliktedir. Ancak gerek MASM'de, gerekse de bu amaca yönelik öteki assembler uygulamalarındaki makro derleyebilme özellikleri çok gelişmiştir. Değişik nitelikte parametreler alabilme, buradaki en önemli özelliklerdendir. Aşağıda verilen örnekler, MASM'deki makro işlev kullanımının sağladığı kolaylıkları göstermek üzere hazırlanmış sık kullanılan ifade gruplarıdır.
makro adı macro parametre tanımlamaları
makro yerel değişkenleri -> LOCAL
makro gövdesi
...
endm
szText
MACRO Name, Text:VARARG ; Bu makro, Name adıyla tanımlanan bir
dizeyi oluşturur. Program kodunun içerisinde herhangi bir yerde kullanılabilir.
LOCAL lbl
jmp lbl
Name db Text,0
lbl:
ENDM
m2m MACRO M1, M2 ; Bu makro, M2 ile
belirtilen yazmaç/bellek verisini M1 ile belirtilen yazmaca/belleğe kopyalar.
Bellekten belleğe kopyalama için sıklıkla kullanılır.
push M2
pop M1
ENDM
return
MACRO arg ; Bu makro, C dilindeki return(arg) ifadesini C++'taki
parantezsiz biçimiyle temsil eder. Görülebileceği üzere parametre, her türlü
veri olabilir.
mov eax, arg
ret
ENDM
Yukarıdaki makro işlevleri, yazacağınız tüm assembler programlarının başına yerleştirmenizi tavsiye ederim. Mutlaka işinize yarayacaktır. Makro işlev konusu çok ayrıntılı olup, burada daha fazla yer vermeyi düşünmüyorum.
Alt Program Prototipleri
MASM'da yazılacak alt programların çağırılmasının iki yöntemi vardır. Birinci yöntem, kullanılacak işlev çağrı yöntemine dikkat etmek suretiyle parametreleri göndermek ve CALL komutu ile işlev adresini çağırmaktır. Standart olarak kullanılan stdcall yönteminde önce parametreler sondan başa doğru yığına atılır, daha sonra işlev adresine dallanılır. Ancak dilin stdcall olmaması durumunda, bu yazının başlarında verilen tablodaki kuralları iyi bilmek ve çağrıyı ona göre yapmak gerekir. Çünkü mikroişlemci sistemlerinde en küçük hata dahi görevin, programın ve hatta işletim sisteminin durmasına yol açabilir.
Zaten bir ASM dosyasının bölümlerinden "Platform, Model, Dil ve Derleyici Parametreleri" bölümü, bu çoklu standarda rağmen programcının kullanım biçimini standartlaştırmaya yöneliktir. MASM ve benzer pek çok modern assembler'da işlev çağrımı için invoke komutu kullanılır. Bu komutun devamına önce işlevin adı, takiben de işlevin parametreleri yazılır. Alt program prototiplerinin tanımlanması, bu yöntemi kullanarak yapılacak çağrılarda parametre sayı ve tür hatalarını önlemeye yöneliktir. Aşağıda, bu tanımlamaya ilişkin taslak ve örnekler yer almaktadır.
işlev adı proto [uzaklık] [işlevin çağrı dili] [[parametre1] : veri türü] [, [parametre2] : veri türü] [, ...]
Yukarıdaki parametrelerden uzaklık, işlevin segmentasyonu ile ilgilidir. Geçerli parametre türleri near, far, near16, near32, far16 ve far32 'dir. Bu parametre belirtilmediğinde assembler, işlevin uzaklığını kendi belirler. Flat bellek modelindeki uzaklık near32 'dir. İşlevin çağrı dili ise, "Platform, Model, Dil ve Derleyici Parametreleri" başlığı altında incelenen tablodaki dillerden herhangi biridir. Sadece özel amaçlar söz konusu olduğunda veya dış bir işlevin çağrı yönteminin farklı olması durumunda belirtilmesi önerilir. Bu parametre belirtilmediğinde assembler, dosyanın başındaki dil seçimini varsayılan olarak kabul eder.
Bu şekilde tanımlanan bir işlevin basit çağrı yöntemi: invoke işlev adı [, parametre1, parametre2, ...]
Parametre almayan işlev: Func0 PROTO
1 dd parametre alan işlev: Func1 PROTO :DWORD
2 dd parametre alan işlev: Func2 PROTO :DWORD, :DWORD
Çağrı yöntemi pascal, 2 dw ve 1 dd parametre alan işlev: Func3 PROTO pascal :WORD, :WORD, :DWORD
Yukarıdaki örneklerde, parametrelerin varsayılan adlarını belirtmeye gerek duyulmamıştır; zira kodun derlenmesi üzerinde hiçbir etkisi yoktur. Ancak anlaşılırlığı arttırmak adına belirmek istenirse (:) operatörünün öncesinde yazılabilir: param1: DWORD gibi.
Program prototip tanımlaması, alt program adreslerinin oluşturulmasını ve sabitlenmesini sağlar. Bu sayede kodun herhangi bir yerinde herhangi bir yerde tanımlı alt program serbestçe çağırılabilir. Aksi takdirde, derleme işlemi doğrusal ve nedensel bir süreç olduğundan, söz konusu alt programın gövde tanımlamasına ulaşılmadan o bölümün çağırılması mümkün olmayacaktır.
Genel Değişken ve Yığın Tanımlamaları
Genel değişkenler, bilineceği üzere kodun derlenmesi esnasında değerleri ve büyüklükleri, işlem sonucunda oluşturulacak ikili dosyaya yerleştirilen, programın her noktasından serbestçe erişilebilen ve aksi belirtilmedikçe değiştirilebilen bellek bölümüdür. Genel değişkenler bölümünde tanımlanabilecek türler, assembler tarafından sunulan ön tanımlı veri türleri ve genel tanımlamalar ile oluşturulabilecek kompleks veri türleridir.
Assembler programlarında genel değişken olarak tanımlanacak veri türlerinin bulunabileceği farklı bölümler mevcuttur. Genel olarak, söz konusu değişkenlerin tanımlanacağı bölümler şu şekildedir:
.bölüm1
değişken1 tür1 [? | öndeğer]
değişken2 tür2 [? | öndeğer]
...
.bölüm2
değişken3 tür3 [? | öndeğer]
değişken4 tür4 [? | öndeğer]
...
...
Bu bölümler, fiziksel segment yerleşimlerinden (cs, ds, es, fs gs, ss) farklı olarak, derleme esnasında farklı prosedüre tabi tutularak bellek üzerindeki yerleşimleri (RVA - Relative Virtual Address) değişir. Ayrıca yazı dizisinin 5. bölümünün ilk parçasında da belirtildiği üzere bellek bölümlerinin okunma, yazılma ve çalıştırılma izinleri mevcut olduğundan, söz konusu bölgenin erişimini de belirler. Her yeni bölüm, bir öncekinin sonunu işaret eder. Dolayısıyla kendisi de bir bölüm olacak kod ile birlikte bu bölümler, dosyanın en alt bölümünde yer almalıdır. Dosyanın başında belirtilebilecek .SEQ ibaresi ile bu bölümlerin yazım sırasında yerleştirilmesi (ki hiçbir ibare belirtilmediğinde varsayılan budur), .ALPHA ibaresi ile de alfabetik sırada yerleştirilmesi sağlanabilir, ancak kodun işleyişi üzerinde etkisi yoktur. Aşağıda, MASM'da geçerli olan bölümlerin açıklamaları verilmiştir.
.data
Programdaki ön değeri verilmiş genel değişkenlerin yer alacağı bölümdür. ASM dosyasının derlenmesi esnasında, hedef çalıştırılabilir dosyada bu değişkenlerin kapladıkları yer ayırılır. Yani bölümün büyüklüğü dosya büyüklüğünü doğrudan etkiler. PE teknik açıklamasında bu bölümün karşılığı .data 'dır. .data bölümünün izinleri okuma ve yazmadır.
.data?
Programdaki ön değeri verilmemiş genel değişkenlerin yer almasının gerektiği bölümdür. Bölüm dahilinde ne kadar çok veya ne büyüklükte değişkenler tanımlanırsa tanımlansın, çalıştırılabilir dosyanın boyutunu etkilemez, sadece sonraki bölümlerin yerleşimini etkiler. Bu bölümde ön değerli değişkenler de yer alabilir, ancak hedefteki çalıştırılabilir dosyanın boyutunu arttırır. Çünkü PE biçimli dosyaların boyutları 512'nin katları biçimindedir. Dolayısıyla bu bölüme yazılacak ekstradan 1 ön tanımlı değişken dahi dosya boyutuna 512 bayt ekleyecektir. Assembler ile üretilen programların boyutlarının oldukça küçük olduğu göz önünde bulundurulursa, alan ekonomisi adına bu tip hatalardan kaçınmak gerekir. PE teknik açıklamasında bu bölümün karşılığı .bss 'dir. .data? bölümünün izinleri okuma ve yazmadır.
.const
Yüksek seviyeli dillerdeki const ön eki ile tanımlanan değişkenlerin yer alacağı bölümdür. Bu bölümde tanımlanan değişkenlere sadece okuma izni verilir. Genellikle bilimsel sabitlerin, hata ayıklama dizin bilgilerinin yer almasının uygun olacağı bir bölümdür. .const bölümünün .data bölümünden esasında bir farkı yoktur, sadece programcı açısından önlem teşkil eder. Dikkatli ve ne yaptığını bilen bir programcının böyle bir bölüm tanımlama ihtiyacı olmayabilir. PE teknik açıklamasında bu bölümün karşılığı .rdata 'dır. .const bölümünün izni sadece okumadır.
.stack
Uygulamaya tahsis edilecek yığının belirtildiği bölümdür. Bu bölüm, genellikle değişken tanımlaması için kullanılmaz, ancak ibarenin hemen yanına yazılacak boyut, uygulamanın yığın boyutu olarak tahsis edilir: .stack 2048 gibi. PE teknik açıklamasında bu bölümün bir karşılığı yoktur. .stack bölümünün izinleri okuma ve yazmadır.
.code
Program kodunun yer alacağı bölümdür. Programın başlangıç
kodu ve tüm alt programlar, bu bölümün içerisinde yer almalıdır. PE(Portable
Executable) teknik açıklamasında bu bölümün karşılığı .text 'tir. .code bölümünün izinleri okunma ve
çalıştırılmadır.
PE teknik açıklamasında, burada adı geçmeyen 5 bölüm
daha bulunur: .rsrc, .edata, .idata, .pdata ve .debug. Ancak bu bölümler, MASM
tarafından gerek derleyici parametreleri ile, gerekse de dosya dahilindeki
ibareler yardımıyla kendiliğinden oluşturulur.
Bölümler dahilinde tanımlanabilecek verilerin boyut ve
nitelik türleri ayrıca önem taşımaktadır. Yukarıdaki örnekte belirtilen tür1, tür2, tür3... gibi ifadeler,
MASM'nin bölüm oluşturma boyutunu ve değer tahsis biçimini doğrudan
etkileyecektir. Aşağıdaki tabloda bu türlere ve özelliklerine yer
verilmektedir. Verilerin sayısal tanım aralıkları için yazı dizisinin bu
bölümünün ikinci kısmındaki "Temel Veri Türleri" başlığını
inceleyebilirsiniz.
|
Veri Türü |
Kısa İfade |
Boyut |
C/C++ Karşılığı |
|
byte |
db |
8 bit |
char |
|
sbyte |
--- |
8 bit |
unsigned char |
|
word |
dw |
16 bit |
short |
|
sword |
--- |
16 bit |
unsigned short |
|
dword |
dd |
32 bit |
int |
|
sdword |
--- |
32 bit |
unsigned int |
|
fword |
df |
48 bit |
--- |
|
qword |
dq |
64 bit |
__int64* |
|
tbyte |
dt |
80 bit |
--- |
|
real4 |
--- |
32 bit |
float |
|
real8 |
--- |
64 bit |
double |
|
real10 |
--- |
80 bit |
long double |
*Microsoft C/C++ Compiler tarafından tanımlı olan bu veri türü, farklı derleyiciler tarafından tanınmayabilir. MSDN'de bu tür için "Microsoft Specific -->" ibaresi kullanılır.
Yukarıdaki tablolarda yer alan bilgilere dayanarak aşağıdaki örnekleri incelemekte fayda vardır.
bayt_degisken db ? ; Ön değeri verilmeksizin 1 baytlık bayt_degisken değişkeni tanımlanmıştır. Bu değişkenin .data? bölümünde tanımlanması uygundur.
word_degisken dw 35 ; Ön değeri 35 olarak atanmak suretiyle 2 baytlık word_degisken değişkeni tanımlanmıştır. .data bölümünde tanımlanması uygundur.
dword_degisken dd ?, 5125, 0, ? ; Art arda 4 adet dword değişken tanımlanmıştır. Bunlardan ilk ve sonuncusuna ön değer atanmamıştır. Basit bir dizi tanımlamasıdır. Bu değişkenin .data bölümünde tanımlanması daha uygundur, çünkü ön değerli hücreler vardır.
dize db "Bu bir assembler programıdır.", 0 ; Bu şekilde metin dizeleri tanımlanabilir. Derleyici, tırnaklar ile kapsanan dizedeki her karakterin ASCII kodunu sıra ile dizer. En sonda bulunan 0 ise, dizenin sıfır ile sonlandırılmış olması için programcı tarafından eklenmelidir. Windows dizeleri bu şekilde kabul ederken DOS, dizeleri $ karakteri ile sonlandırılmış biçimde kabul eder. Bu dizenin .data bölümünde tanımlanması uygundur.
float_degisken real4 10.85 ; Ön değeri 10.85 olan 4 baytlık float_degisken tanımlanmıştır. dword ve real4 veri türlerinin boyutları aynı olmalarına rağmen, değer atamalarındaki standartlar farklıdır. Tamsayı tür olan dword işaretsiz veya işaretli olarak 2'ye tümleyen standardında, ondalık tür olan real4 ise IEEE 754 standardında kodlanarak bellekteki ilgili yere yazılır. Bu değişkenin .data bölümünde tanımlanması gerekir.
Genel tanımlamalar bölümünde oluşturulan türlerin, yapıların ve ortaklıkların da değişken olarak tanımlanması mümkündür. Ancak bu türler, içlerinde birden fazla eleman barındırdığından tanımlanma biçiminde < > parantezleri kullanılmalıdır.
bolum_ortalamalari float 16 dup(?) ; Bu tanımlamada öncelikle daha evvel değinilmiş başlıklardaki typedef belirteci ile tanımlanan float veri türüne yer verilmiştir. Bu tür, söz konusu başlıklarda real4 veri türüne eş güdülmüştür. dup dahili makrosu ile de 16 adet ön değeri olmayan eleman tanımlanmıştır. Yazı dizisinin önceki bölümlerinden hatırlanacağı üzere bu makro, önünde yazan sayı kadar parantez içindeki değerden tanımlamaktadır. Bu değişkenin .data? bölümünde tanımlanması uygundur.
arazi dikdortgen <0, 0, 512, 384> ; Bu tanımlamada, önceki başlıklar altında yer verilen dikdortgen yapısı kullanılmıştır. Yapıdaki 4 eleman için de ön değerler verilmiştir. Bu değişkenin .data bölümünde tanımlanması uygundur.
msg MSG <?> ; Bu tanımlamada kaynak yapı olarak Windows'taki pencerelere gelen bildirileri barındıracak MSG yapısı kullanılmıştır. Program dahilinde değişkene değer kullanıcı tarafından verilmeyeceği için herhangi bir ön değer de verilmemiştir. Bu sebeple değişkenin .data? bölümünde tanımlanması uygundur.
koordinatlar POINT <0, 0>, <?, 2>, <?> ; Bu tanımlamada 3 adet POINT yapısı art arda tanımlanmış; ilkindeki her iki elemana da ön değer verilmiş, ikincisinde ise sadece Y üyesine 2 değeri verilmiştir. Son elemandaki hiçbir üyeye ilk değer verilmemiştir. Bu şekliyle .data bölümünde tanımlanması uygundur.
data WSADATA 64 dup(<?>) ; Bu tanımlama ile 64 adet ön değersiz WSADATA yapısı tanımlanmıştır. Bu yapı, Windows Sockets içerisinde yer alır. Bu dizinin .data? bölümünde tanımlanması uygundur.
Yukarıdaki örnekler ile genel değişken tanımlamanın tüm ayrıntılarına değinilmiştir. Görülebileceği üzere dizi tanımlamak için, değeri tanımlı elemanları virgül ile ayırmak ya da dup makrosunu kullanmak gerekir.
MASM'da genel değişken tanımlamaları esnasında kullanılan tamsayı ifadeleri varsayılan ayar olarak 10'luk düzende yorumlanır. Ancak kimi zaman ifadelerin 16'lık veya 2'lik düzende yorumlanmaları, anlaşılırlık açısından önem taşıyabilir. Bu durumda aşağıdaki notasyonlar kullanılabilir:
sayi dd 20h ; Bu ifade, sayi adlı değişkene 16'lık düzende 20, 10'luk düzende ise 32 değerinin atanmasını sağlar.
bitalani db 01100010b ; Bu ifade, bitalani adlı değişkene 2'lik düzende 01100010, 10'luk düzende ise 98 değerinin atanmasını sağlar.
onluksayi dw 35587d ; Bu ifade, onluksayi adlı değişkene onluk düzende 35587 değerini atar. Yani yazıldığı gibi okuma koşulunu sağlar.
Yani, sayısal ifadenin sonuna eklenecek 'h', 'b' veya 'd' harfleri ile değerin 16'lık veya 2'lik düzende yorumlanması sağlanabilir. Unutulmamalıdır ki bu kural sadece tamsayılar için geçerli olup, ondalık değişkenlerde bu şekilde bir kullanımın üreteceği değer çok farklıdır. Varsayılan yorumlamanın değiştirilmesi için .radix belirteci, dosyanın başında kullanılabilir. Bu belirteci takiben yazılacak sayı veya sayıya çözümlenebilir ifade, yukarıdaki özel karakterler ile belirtilmeyen tüm sayısal ifadeler için taban teşkil eder.
.radix 16
Ancak burada bir çakışma söz konusu olacaktır. Zira 16'lık tabana rağmen 'b' veya 'd' harfleri kullanılmak suretiyle taban ayarlaması yapılmaya çalışıldığında assembler, bu harfleri de 16'lık düzenin bir parçası olarak kabul edecektir. Dolayısıyla, yine assembler tarafından aynı nitelikte kabul edilen 'b' -> 'Y' ve 'd' -> 'T' dönüşümü yapılmalıdır.
16'lık düzendeki sayılarda, ilk 10 rakamdan sonraki rakamlar A-F harfleri ile ifade edilir. Ancak değişken adları da bu harfler ile başlayabildiğinden derleyici, A-F rakamları ile başlayan 16'lık sayıların yorumlanışında karışıklık yaşar ve bunu değişken adı olarak kabul etmek ister. Dolayısıyla derleme esnasında hataya yol açılır. Bu hatanın önlenmesi için, 16'lık sayıların da en azından '0' (sıfır) rakamı ile başlatılmasında fayda vardır. Derleyici bunu fazladan hane olarak görmez, sadece karışıklık önlenir.
Hatırlanacağı üzere x86 mimarisindeki verilerin doğal sınırlar dahilinde bulunması, işlem başarımını arttırmaktadır. Ancak 1 bayt uzunluğundaki kimi verilerin kullanımı neticesinde bu hizalamanın dışına çıkılabilmekte, takiben kullanılacak dword büyüklüğündeki değişkenler veya veriler ile de bu sorun devam edebilmektedir. Assembler, hizalama işleminin programcı tarafından ele alınabilmesi için align komutunu sunmaktadır. Bu komutu takiben yazılacak sayı ile bir sonraki verinin, o sayı ile tam bölünebilir ilk bellek alanında yer alması sağlanabilir. Eğer içinde bulunulan segment kod niteliği taşımıyorsa, atlanılan alanlar 0 ile, kod özelliği taşıyorsa da NOP (no operation) komutları ile doldurularak süreklilik sağlanır.
align 4
Bu kullanım, 32 bitlik platformda doğal sınır koşulunu gerçekleştirir. Özel SIMD işlemleri yapılmadığı sürece gerektiği hallerde bu hizalama yeterli olacaktır. 16 bitlik platformlarda hizalamanın koşulu doğal olarak 2'nin katları olacaktır. Bu duruma özel olarak assemblerda, parametre gerekmeksizin bir komut bulunur: even. Bu komut, align 2 ile eş anlamlıdır.
Program Kodu
Bu bölüm, yapılacak işin esas alındığı ve makine kodunun yazıldığı bölümdür. Oldukça uzun olabilen bu bölümün başlangıcında, başlangıç etiketi olarak adlandırılan bir belirteç bulunabilir. Bu etiket, hem programın çalışmaya başlayacağı noktayı gösterir, hem de kod bölümünün en sonunda, programın bittiğini işaret etmek için kullanılır. Derleyici, bu sayede kod bölümünün başlangıç ve bitişini hedef ikili dosyaya işler. Bu etiketten önceki verilerin programın çalışması üzerinde etkisi yoktur. Dolayısıyla söz konusu ara bölgede genel değişken tanımlaması bile yapılabilir.
Kod bölümünde yazılan her bilgi, sıra ile derlenir ve aynı sırada kod oluşturulur. Dolayısıyla bölüm başlangıcında serbest olarak yazılabilecek ASM rutinlerinin ardından, evvelinde herhangi bir dallanma komutu kullanılmadan yazılacak bir alt programa doğrudan ve denetimsiz giriş yapılacak, alt programın dönüş komutu ile de programdan denetimsizce çıkılabilecek ve hatta kimi durumlarda sistem kararlılığı bozulabilecektir. Dolayısıyla, hatalı sırada kod yazarak denetimsiz girişlerden kaçınılmalıdır.
Aşağıda en basit çalıştırılabilir kodun daha açık şekli yer almaktadır:
; Cihan
Atıl Namlı 32 bit Assembler Tutorial
; Minimal requirements example
.386 ;
Target platform, forces 32-bit code.
.model flat, stdcall ; Memory model
and calling convention.
.code ; Code segment start
start: ; Start label, used later for closing code segment.
ret ; Return to system instruction
end start ; Code segment close down, end of file.
Bu kod, yazının başındaki ile aynı kod olup, her satırın yanında açıklamalar mevcuttur. Görüleceği üzere kod bölümünün başlangıcında kullanılan etiket, dosyanın sonunda yeniden kullanılmış ve dosyanın bitimi işaret edilmiştir. Aşağıdaki ikinci örnek, ön değerli değişkenlerin tanımlanarak program içerisinde kullanılmasını göstermektedir.
; Cihan
Atıl Namlı 32 bit Assembler Tutorial
; Declaration of initialized Variables
example
.386
.model flat, stdcall
.data ; Starting data segment.
byte_variable db 15 ;
Value is treated as decimal by default.
word_variable dw 1101000100011001b ;
16 bits binary word.
dword_variable dd 0A1B234H ;
Remark: Because of identifier-like treating of hexadecimal numbers which start
with letters, a '0' must be appended to beginning.
.code ; Starting code segment, while closing data segment.
start:
mov al,
byte_variable ; Legal instruction.
mov ax, word_variable ; Legal
instruction.
mov eax, dword_variable ; Legal
instruction.
ret
end start
Yukarıdaki örnekte, tanımlanmış ve ön değerleri verilmiş olan genel değişkenler, eax yazmacının parçaları ile kullanılmıştır. Yukarıdaki kullanımda, farklı boyutlardaki parametrelerin tek komut ile işlenmeye çalışılması hata verecektir.
Aşağıdaki örnekte, ön değer verilmemiş değişkenlerin tanımlanması ve kod içerisinde değişkenlere değer ataması gösterilmiştir.
; Cihan
Atıl Namlı 32 bit Assembler Tutorial
; Declaration of uninitialized variables
example
.386
.model flat, stdcall
.data
byte_variable db 15 ;
These variables are 7 bytes long at total. But
word_variable dw 1101000100011001b ;
they will grow the code by 512 bytes.
dword_variable dd 0A1B234H
.data?
uninitialized_byte db ? ;
These variables are declared but don't affect code size.
uninitialized_word dw ?
uninitialized_dword dd ?
Text_buffer db 256 dup (?) ; Macro that duplicates a value as requested.
.code
start:
mov al,
uninitialized_byte ; Legal instruction, but result is unpredictable.
mov ax, uninitialized_word ; Legal
instruction, but same result as above.
mov eax, uninitialized_dword ; Legal
instruction, but same result as above.
mov
uninitialized_byte, 10 ; Legal assignment for 386 and successors
mov uninitialized_word, 10000 ;
Legal assignment for 386 and successors
mov uninitialized_dword,
100000 ; Legal assignment for 386 and successors
ret
end start
Dikkat edilecek olunursa, ön değer atanmamış değişkenlerin bellekte bulundukları bölgeler herhangi bir temizleme işlemine maruz olmamaktadır.Dolayısıyla bu değişkenlerin başlangıç değerlerini kestirmek mümkün değildir. Ayrıca 386 değin mümkün olmayan, belleğe doğrudan değer yazma işlemine yer verilmiştir.
Aşağıdaki örnek, alt programların prototip ve gövde tanımlamasını, kod içerisinde çağırılmasını ve geri değer döndürme standardını göstermektedir. Kod içerisindeki yorum satırlarının gösterilişine dikkat edilmelidir, zira bilhassa prototip tanımlamalarında kullanılan (:) operatörü ile (;) yorum operatörü birbirine oldukça benzemektedir ve karıştırılmamalıdır.
; Cihan
Atıl Namlı 32 bit Assembler Tutorial
; Subroutine examples
.386
.model flat, stdcall
; Subroutine(procedure, function) prototypes.
Subroutine1 PROTO ;
No parameter.
Subroutine2 PROTO:
DWORD ; 1 DWORD
Subroutine3 PROTO: SDWORD, :
REAL4 ; 2 DWORD
Subroutine4 PROTO: DWORD, :
DWORD, : WORD ; 2 DWORD, 1 WORD
Subroutine5 PROTO: WORD, :
SWORD, : WORD ; 3 WORD
.data
WordVar dw 40000
DwordVar dd 555555
SwordVar SWORD -7868
SdwordVar SDWORD -2000000
FloatVar REAL4 375.857
.data?
RetVal DWORD ? ; Subroutine return parameter, included only for example.
.code
start:
call Subroutine1 ; Legal
push
DwordVar
call Subroutine2 ; Legal
add esp, 4 ; Stack cleanup
(stdcall style)..
push
FloatVar
push SdwordVar
call Subroutine3 ; Legal, parameters
pushed in order right from left.
add esp, 8 ; Stack cleanup.
push
WordVar
push DwordVar
push DwordVar
call Subroutine4 ; Legal. Assembler
will automatically specify WordVar size.
add esp, 10
push
WordVar
push SwordVar
push WordVar
call Subroutine5 ; Legal.
add esp, 6
;--------------------------------------------
; MASM Macro Syntax for Calling Subroutines
;
;--------------------------------------------
; This
style includes parameter count and type checking. It speeds up coding.
; Also, because of the language type, the
stack is automatically cleaned up.
invoke Subroutine1
invoke Subroutine2, DwordVar
invoke Subroutine3, SdwordVar, FloatVar
invoke Subroutine4, DwordVar, DwordVar, WordVar
invoke Subroutine5, WordVar, SwordVar, WordVar
jmp endlabel
;-------------------------------------------------------------------------------
Subroutine1 proc
mov
eax, RetVal
ret
Subroutine1 endp
;-------------------------------------------------------------------------------
Subroutine2 proc Parameter1: DWORD
mov
eax, RetVal
ret
Subroutine2 endp
;-------------------------------------------------------------------------------
Subroutine3 proc Parameter1: SDWORD, Parameter2: REAL4
mov
eax, RetVal
ret
Subroutine3 endp
;-------------------------------------------------------------------------------
Subroutine4 proc Parameter1: DWORD, Parameter2: DWORD, Parameter3: WORD
mov
eax, RetVal
ret
Subroutine4 endp
;-------------------------------------------------------------------------------
Subroutine5 proc Parameter1: WORD, Parameter2: SWORD, Parameter3: WORD
mov
eax, RetVal
ret
Subroutine5 endp
;-------------------------------------------------------------------------------
endlabel:
ret
end start
Görülebileceği üzere alt programlar, sadece farklarını gösterebilmek amacıyla hem klasik yöntemle, hem de MASM'nin sunduğu invoke komutu ile çağırılmıştır. Her iki çağrı yöntemi de geçerli olup, invoke komutunun sağladığı hata denetimi sayesinde daha güvenli bir yöntem izlenebilmektedir. Ayrıca alt program gövdelerinin tanımlanmaya başlanmasından hemen önce kullanılan jmp komutu ile kodun en sonuna gidilmiştir. Aksi takdirde Subroutine1 alt programının içerisine denetimsiz olarak girilecek ve istenmeyen bir işlem yapılabilecektir.
Aşağıdaki kod, yukarıdakilerden farklı olarak ortaya işe yarar veriler çıkarmaktadır. Bu kod, programlamada bir teknik olan "lookup table" yöntemi ile, sıklıkla kullanılması söz konusu olabilecek iki haneli sayıların karelerini hesaplayarak bir diziye yazmaktadır. Programcı, daha sonra isterse bu dizideki değerleri kullanarak işlem yükünden kurtulabilir.
; Cihan
Atıl Namlı 32 bit Assembler Tutorial
; Lookup table preparing example
; Lookup table for squares
.386 ;
Target platform, forces 32-bit code.
.model flat, stdcall ; Memory model
and calling convention.
.data?
Squares dw 100 dup (?) ; Preparing memory for squares of integer numbers from 0 to 99.
.code
start:
xor ecx, ecx ; Assure ECX = 0.
loopstart:
mov
eax, ecx ; Copy source number to EAX.
mul eax ; Get square
of EAX.
mov Squares[ecx*2], ax ; Move
the result to the array.
inc ecx ; Increment
number.
cmp ecx, 100 ; Compare if
it is reached to 100.
jb loopstart ; If not,
process the loop for next number.
ret ; Return to system/caller.
end start
Yukarıdaki kod, istendiğinde bir alt programa da dönüştürülerek, daha kapsamlı bir uygulama içerisinde modül olarak kullanılabilir. Bu durumda yapılması gereken, oluşturulacak alt program için parametre tanımlaması yapmaktır. Squares dizisinin adresinin verilmesi ve istenen değer aralığının belirtilmesi, işin yapılması için yeterli olabilir.
Aşağıdaki kod, şimdiye değin işlenen pek çok bilginin kullanıldığı ve yeni bilgilerin yer aldığı bir programdır. Bu program, 0'dan 360'a dek tüm tamsayı açı değerlerinin sinüsünü hesaplar; sıklıkla kullandığımız açıların sinüs değerlerini de küçük bir ileti penceresinde gösterir. Hesaplamalar için x87 FPU'nun imkanlarından faydalanılmıştır. Alternatif bir yöntem olarak Taylor serilerinden de faydalanılabilirdi. Bu pencerenin kullanılması ve iletinin gösterilmesi için Windows'un çeşitli hizmetlerinden de faydalanılmıştır.
; Cihan
Atıl Namlı 32 bit Assembler Tutorial
; FPU example
; Lookup table for sines
.386 ; Target platform,
forces 32-bit code.
.model flat, stdcall ; Memory model
and calling convention.
includelib
"D:\Microsoft Visual Studio\VC98\Lib\msvcrt.lib"
includelib "d:\masm32\lib\user32.lib"
sprintf PROTO
C :DWORD,:VARARG
MessageBoxA PROTO :DWORD,:DWORD,:DWORD,:DWORD
.data?
SineTable REAL8 360
dup (?) ; Preparing memory for sines of integer numbers from 0 to
359.
i dd ?
n180 dd ?
Text db 1024 dup (?)
.code
string db "Sin(15) =
%f, Sin(30) = %f, Sin(45) = %f",10,"Sin(60) = %f, Sin(75) = %f,
Sin(90) = %f",0
progtitle db "Sine
Table Example",0
start:
mov i,
0 ; i = 0
mov n180, 180 ; n180 = 180
fldpi ; Load pi onto
FPU stack.
loopstart: ; Start of process phase.
fild
i ; Load number onto FPU stack.
fmul st(0), st(1) ; Multiply
number with pi.
fidiv n180 ; Divide result
to 180.
fsin ; Calculate
sine of the number st(0).
mov eax, i ; EAX = i
fstp SineTable[eax*8] ; Copy
calculated number to the array.
inc i ; Increment
number.
cmp i, 360 ; Compare
number with 360.
jb loopstart ; If not
reached, process the loop for next number.
xor eax, eax
invoke
sprintf, ADDR Text, ADDR string, SineTable[15*8], SineTable[30*8], SineTable[45*8],
SineTable[60*8], SineTable[75*8], SineTable[90*8]
invoke MessageBoxA, 0, ADDR Text, ADDR
progtitle, 0
ret ; Return to system/caller.
end start
Yukarıdaki yeni bilgilere değinmekte fayda vardır. Programımız içinde kullanılan MessageBoxA ve sprintf işlevleri, sırasıyla user32.dll ve msvcrt.dll devingen bağlantı kütüphanelerine statik olarak bağlanılarak kullanılmıştır. Dolayısıyla, söz konusu DLL'ler içerisinde bu işlevlerin adreslerine .lib uzantılı ve aynı adlı statik bağlantı kütüphaneleri kullanılarak erişilmiştir. Daha sonra ise, invoke komutu ile kullanılabilmeleri için prototip tanımlamaları, söz konusu işlevlerin MSDN'deki dokümantasyonlarına uygun olarak yapılmıştır. Programın geri kalan kısmında ise tamamen işin yapılmasına yönelik FPU işlemleri ve döngüler söz konusudur. Yukarıdaki kod MASM tarafından derlendiğinde hata vermeyecek ve 1536 baytlık bir EXE dosyası üretecektir. Aynı işi yapmaya yönelik bir C/C++ programının üreteceği kodun boyutu, yukarıdakine göre oldukça büyüktür.
Assembler, bilhassa ondalık sayı işlemlerinin yapılmasında çok etkilidir. Çünkü kimi karışık işlemler ve dönüşümler, yüksek seviyeli dil derleyicileri tarafından oldukça uzun ve gereksiz işlemler ile gerçekleştirilmeye çalışılır. Çünkü FPU işlemlerinin yapılabileceği toplam bölge çok kısıtlıdır (8 eleman) ve işlemler birbirini kolayca etkileyebilmektedir. Yukarıdaki işlevi gerçekleştirecek bir C/C++ kodu derlenir ve hata ayıklama yöntemi ile incelenirse, sistemin kararlılığını korumaya yönelik pek çok, ancak gerekli olmayan komuta rastlanır. Bunun sebebi, derleyicinin ne yapmak istediğinizi tam olarak bilemeyeceği gerçeğidir. Assembler'in en büyük üstünlüğü buradadır. Siz istemeden kolay kolay kod oluşturulmaz. Sadece yerel değişkenler ve alt programlar için bellek ayırma amacıyla yığın hizalamaya yönelik ekstra 1-2 işleme rastlanır.
Assembler ile yapılabilecek en verimli işlem, kod optimizasyonudur. Yüksek seviyeli dil derleyicileri, mümkün olduğunda optimizasyon yapmaya çalışırlar. Ancak eğer bir kod, çok yüksek öncelik ile (neredeyse sistemde tek başınaymış gibi) çok sık çalışacak ve zamana karşı yarışacaksa, kod içeriğinin son derece hızlı ve sadece amaca yönelik bir yapı taşıması gerekir. Bu durumda, ya programın tamamı assembler'da yazılmalı, ya da kodun sadece o bölümü satır içi assembler kullanılarak yazılmalıdır. En güvenli ve hızlı yöntem ise, söz konusu kritik bölümün assembler'da yazılarak bir kütüphane haline getirilmesidir. Bu sayede statik bağlantı ile yüksek seviyedeki dilden fonksiyon çağırılabilir. Aşağıdaki program, Pentium ve sonrası işlemcilerin çalışma hızlarını ölçmeye yardımcı olmaktadır.
.686p
.model flat, stdcall
option casemap: none
include
d:\masm32\include\windows.inc
include d:\masm32\include\user32.inc
include d:\masm32\include\kernel32.inc
includelib
d:\masm32\lib\user32.lib
includelib d:\masm32\lib\kernel32.lib
szText MACRO Name, Text:VARARG ; This macro creates a string and returns its
address.
LOCAL lbl
jmp lbl
Name db Text,0
lbl:
ENDM
.data?
Frequency dq ?
Counter1 dq ?
Counter2 dq ?
Difference dq ?
Time dq ?
nval dd ?
Speed dq ?
Text db 256 dup (?)
.code
start:
szText Cpuspeed, "Cpu Speed"
invoke
GetModuleHandle, NULL
invoke SetPriorityClass, eax, REALTIME_PRIORITY_CLASS
invoke GetCurrentThread
invoke SetThreadPriority, eax,
THREAD_PRIORITY_TIME_CRITICAL
invoke QueryPerformanceFrequency, offset Frequency
.if
eax==0
szText t2, "This system doesn't
support a high performance counter. Application will now quit."
invoke MessageBox, NULL, ADDR t2, ADDR
Cpuspeed, NULL
jmp quit
.endif
szText
starttext, "ECX register will be decreased 2^32 times using loop method.
Click OK to start.",10,"For most reliable results, please avoid user
inputs, even mouse motions."
invoke MessageBox, NULL, ADDR starttext,
ADDR Cpuspeed, NULL
invoke
QueryPerformanceCounter, offset Counter1
xor ecx, ecx
@@: loop @B
invoke QueryPerformanceCounter, offset
Counter2
mov
eax, dword ptr Counter2
mov ecx, dword ptr Counter2 + 4
sub eax, dword ptr Counter1
sbb ecx, dword ptr Counter1 + 4
mov
dword ptr Difference, eax
mov dword ptr Difference + 4, ecx
fild
Difference
fild Frequency
fdivp st(1), st(0)
mov nval, 1000000000
fimul nval
fistp Time
fild Time
mov nval, 8
fidiv nval
mov nval, 1000000000
fidiv nval
mov nval, 40000000H
fidiv nval
fld1
fdiv st(0), st(1)
fistp Speed
szText
endtext, "Performance Counter Frequency: %I64d Hz.",10,"Counted
Ticks: %I64d",10,10,"Time Passed: %I64d ns.",10,10,"Cpu
Speed: %I64d Hz."
invoke wsprintf, ADDR Text, ADDR endtext,
Frequency, Difference, Time, Speed
invoke MessageBox, NULL, ADDR Text, ADDR Cpuspeed, NULL
quit:
invoke ExitProcess, 0
end start
Bu program, öncelikle çoklu görev ortamındaki kendi önceliğini mümkün olan en yüksek seviyeye çıkarmakta, daha sonra da ECX yazmacını 2^32 kere döngü içerisinde azaltarak aradaki süre farkını ölçmektedir. Bu süre farkının ölçümü için Windows tarafından sunulan iki adet işlev kullanılmakta; söz konusu işlevler doğrudan donanıma erişerek 8253/8254 PIT denetleyicinin kanallarını sorgulamaktadır. Bu öğe, standart bilgisayar donanımının mecburi bir öğesi olup 3 kanala sahiptir. Bu kanallardan biri DRAM'in tazelenmesi suretiyle bilginin korunmasını, bir diğeri ise PC speaker'in ses vermesini sağlar. Sonuncusu da Windows tarafından yüksek başarımlı sayaç (high performance counter) olarak kullanılır.
Yukarıdaki işi yapan bir programın yüksek seviyeli bir dil ile aynı sonucu elde edecek biçimde yazılması son derece güçtür.
Döngü ve Şartlı İfade Blokları için Kolaylıklar
Yukarıdaki örnekte dikkat çekebilecek bir diğer unsur ise, şartlı ifadelerin denetimi için kullanılan bloklardır. MASM'da .if, .elseif, .endif, .while, .endw, .repeat, .until, .untilcxz gibi döngü ve şartlı ifade komutlarını üretebilecek iç makrolar bulunur. Bu sayede döngülerin üretilmesinde adı sanı karışacak pek çok etiketin kullanılmasından kurtulmuş olunur.
Bu komutların kullanımındaki taslaklar şu biçimdedir:
.if şartlı ifade
assembler ifadesi
...
[.elseif şartlı ifade
assembler ifadesi
...]
.endif
.while şartlı ifade
assembler ifadesi
...
[.continue [.if şartlı ifade]]
[.break [.if şartlı ifade]]
...
.endw
.repeat
assembler ifadesi
...
[.continue [.if
şartlı ifade]]
[.break [.if
şartlı ifade]]
...
.until şartlı ifade | .untilcxz [şartlı ifade]
Şartlı ifade kısımlarında C/C++'ta geçerli olan herhangi karşılaştırma operatörü kullanılabilir. Operatörün her iki tarafındaki elemanlar, assembler'daki cmp komutuna uygun olacak operandlar olmalıdır. Kolaylık sağlayan bu komutların arasında for komutunun bulunmayışı ilginç gözükse de, while komutunun doğru kullanımı ile aynı kapıya çıkılabilecektir.
Alt Programlarda Yerel Değişken Tanımlamaları
Yerel değişkenler, ön değer verilmediği sürece çalıştırılabilir dosya boyutunu etkilemeyen ve sadece alt program içerisinde dahili olarak erişilebilen bellek bölgeleridir. Assembler, yerel değişken tanımlamalarında yüksek seviyeli diller ile aynı yöntemi kullanır, yani yığın işaretçisinin altında kalan bölgeyi bu amaçla değerlendirir. Yerel değişken tanımlamasına ilişkin taslak ve örnekler aşağıdaki gibidir.
local değişken [[sayı]] [: veri türü] [, değişken [[sayı]] [: veri türü]] ...
local deyimi ile bir satırda birden fazla değişken tanımlaması mümkün olmaktadır. Ancak yine de farklı türdeki değişkenlerin farklı satırlarda yer alması, anlaşılırlık açısından faydalı olacaktır.
WndProc proc hwnd :DWORD, uMsg :DWORD, wParam :DWORD, lParam :DWORD
LOCAL var :DWORD
LOCAL
caW :DWORD
LOCAL
caH :DWORD
LOCAL
Rct :RECT
LOCAL buffer1[128]: BYTE
LOCAL buffer2[128]: DWORD
LOCAL
szDropFileName[260]: BYTE
...
...
ret
WndProc endp
Yukarıdaki örnekte yerel değişken tanımlaması açıkça görülmektedir. Tanımlanan değişkenlerden biri bir yapıdır. MASM, yerel değişkenlere tanımlandıkları yerde ön değer verilmesini desteklememektedir. Ancak şunun da iyi bilinmesi gerekir ki söz konusu ön değer tanımlaması desteklense bile bu işlem, assembler dilindeki mov komutları ile gerçekleştirilmek zorundadır. Çünkü yerel değişkenlerin mutlak yeri yoktur, ESP'ye göre göreceli konumları söz konusudur.
Yerel değişken erişimi, bilinebileceği üzere EBP ile yapılır. Assembler, tanımlanan yerel değişkenlerin toplam boyutunu hesap eder ve ilk hamlede ESP'yi bu değer kadar aşağı çeker. Böylece yığın işlemleri ile yerel değişken erişimleri birbirinden yalıtılmış olur. Daha sonra sadece EBP'nin ayarlanması ile ara bölgedeki belleğe erişilir ve işlemler yapılır.
Kimi zaman alt programlar veya çeşitli işlemler, parametre olarak değişkenin adresini talep edebilir. Bu teknik, değişkenlerin global veya ortak kullanımında son derece gereklidir. Ayrıca bir işlevin birden fazla değer döndürmek zorunda olması durumunda da parametre olarak döndüreceği değerlere ilişkin adresler istemesi doğaldır. Genel değişken olarak tanımlanan bir ifadenin bellek üzerindeki konumu sabittir ve değişkenin önüne offset deyimi yerleştirilmek suretiyle bu sabit ifadeye erişilebilir(bu sabitlik RVA bazındadır, ancak işletim sistemi tüm RVA bloklarını doğrusal olarak işlediğinden konum sabit olacaktır). Ancak yerel olarak tanımlanan bir değişken için bu şekilde bir sabitlik söz konusu değildir. Çünkü yerel değişkenler, değişken değere sahip olan ESP'ye göre göreceli bir konumda tanımlanmaktadır. Eğer herhangi bir işleve bu şekilde tanımlanmış bir değişkenin adresi aktarılacağında offset deyimi hata verecektir, zira bu deyim sabit bir sayıya çözümlemeyi hedefler. Bu durumda yerel değişkenin adresinin aktarımı için ADDR deyimi kullanılmalıdır. ADDR deyimi, ilgili yerel değişkenin mutlak adresine bir assembler komutu olan lea ile ulaşır ve ilgili ifadeye aktarır. Bu konuyu bir örnekle açıklamakta fayda görüyorum.
WinMain proc hInstance: DWORD, hPrevInstance: DWORD, CmdLine: DWORD, CmdShow: DWORD
LOCAL
wc: WNDCLASS
LOCAL msg: MSG
...
...
StartLoop:
invoke
GetMessage,ADDR msg,NULL,0,0
cmp eax, 0
je ExitLoop
invoke
TranslateMessage, ADDR msg
invoke DispatchMessage,
ADDR msg
jmp
StartLoop
ExitLoop:
return msg.wParam
WinMain endp
Yukarıdaki kod parçasında GetMessage, TranslateMessage ve DispatchMessage alt programları, parametre olarak msg yerel değişkeninin adresini almaktadır. Burada ADDR yerine offset kullanılması hata ile sonuçlanacaktır, çünkü msg değişkeninin yeri sabit değildir. ADDR deyimi, ifadeye yeni bir kod ekleyerek amaçlanan işlemi gerçekleştirecektir. Aşağıda, ifadenin assembler tarafından çözümlenmiş hali görülmektedir.
...
...
StartLoop:
push
0
push 0
push 0
lea eax, [ebp-44h]
push eax
call _GetMessageA@16
cmp eax, 0
je ExitLoop
lea eax, [ebp-44h]
push eax
call _TranslateMessage@4
lea eax, [ebp-44h]
push eax
call _DispatchMessageA@4
jmp StartLoop
ExitLoop:
mov eax, [ebp-3Ch]
ret
...
...
Görüldüğü gibi msg değişkenine ait taban + sabit biçimindeki adres, lea komutunda parametre olarak kullanılmış ve eax yazmacına açık adresin yazılması sağlanmıştır. Ardından assembler, eax yazmacını parametre olarak yığına göndermiştir. Assembler, aynı işlemi 3 kere tekrarlamış ve bu başarım kaybına neden olmuştur. Assembler'in bu gibi kompleks işlemlerde en iyisini yapmadığının bilindiği durumlarda kodun programcı tarafından düzenlenmesinde fayda vardır. İşlemin 3 kere tekrarlanmasındaki gerekçe, her alt program çağrısından sonra eax yazmacının eski değerini yitirecek olmasıdır. Hatırlanacağı üzere Windows ortamında programlama yapılırken genel amaçlı yazmaçlardan EAX, ECX ve EDX'in değerleri, alt programlar tarafından korunmamaktadır.
Alt Programlarda Korumalı Yazmaçların Kullanımı
Windows ortamındaki programlarda EBX, ESI ve EDI yazmaçlarının sabit olması gereği kimi zaman programcıyı yapılacak işlemlerde güç durumda bırakabilir. Öyle ki çok karışık adresleme ve tablo erişimlerinde EAX, ECX ve EDX yazmaçları kimi zaman yetersiz kalabilmektedir. Bu durumda programcının iki seçeneği vardır. Bunlardan biri, korumalı 3 yazmaçtan ihtiyacı olanları önce push komutu ile korumak, sonra işlemler için kullanmak ve en son olarak da pop komutuyla geri çekmektir. Assembler'in bu amaca yönelik olarak sunduğu bir deyim bulunur: uses. Bu deyim, alt program gövde tanımlamasında, parametre tanımından hemen önce yer alır.
işlev adı proc [uses yazmaç1 yazmaç2 ...] [uzaklık] [işlevin çağrı dili] [[parametre1] : veri türü] [, [parametre2] : veri türü] [, ...]
Bu işlem, alt program girişinde prolog kodun belirtilen yazmaçları yığında saklamasını ve alt program bitiminde epilog kodun söz konusu yazmaçları yığından çekmesini sağlar. Yığında saklanması istenen yazmaçlar birbirinden boşluk ile ayrılarak yazılmalıdır. Bu kullanım, EBX, ESI ve EDI dışında gerekli değildir; zira üst program EAX, ECX ve EDX 'in bozulmadan dönmesini beklememelidir.
Yazmaçları korumanın diğer bir yöntemi ise pushad ve popad komutlarıdır. Bu komutlar, EFLAGS da dahil olmak üzere bütün yazmaçları yığında saklar. Ancak optimizasyonun gerekli olduğu noktalarda kullanılmamalıdır; zira işlem uzun sürmekte ve fazlaca gereksiz bellek harcamaktadır.
Özel Operatörler ve Deyimler
Yüksek seviyeli dillerde olduğu gibi MASM'da da özel amaçlı operatörler bulunur. Özel amaçlı operatörlerin çoğu derleme zamanında çalışır ve sabit ifade üretirler.
lengthof ve sizeof operatörleri, tanımlanan değişkenler ve değişken dizileri hakkında faydalı bilgiler sunarlar. lengthof operatörü, ardından yazılan dizi ifadesinde kaç eleman bulunduğunu belirtir. Eğer tekil bir tanımlama söz konusuysa doğal olarak 1 değerini döndürecektir. sizeof operatörü ise, bir dizinin bayt cinsinden toplam bellek uzunluğunu belirtir. Tekil tanımlamalarda ise değişkenin bayt cinsinden boyutunu döndürür.
dizi dd 64 dup(?)
degisken dw 5
lengthof dizi -> 64
sizeof dizi -> 64 * 4 = 256
lengthof degisken -> 1
sizeof degisken -> 2
type operatörü, çok işlevli bir operatördür. Dizi olarak tanımlanmış bir değişken için kullanıldığında dizideki her bir elemanın boyutunu verir. Bir yapı değişkeni için kullanıldığında ise yapının toplam boyutunu verir. Bir sabitin önünde kullanıldığında 0, etiketin önünde kullanıldığında ise kod başlangıcına olan uzaklığı verir. Yazmaçlar ile kullanıldığında da yazmacın boyutunu döndürür.
type dizi -> 4
type RECT -> 16
type 5 -> 0
type EAX -> 4
org deyimi, kimi zaman komut işaretçisinin (EIP) konumunu belirli bir değere getirmek için kullanılır. Bu işlem genellikle programların başlangıçlarında yapılır ve başlangıç etiketinin konumunu ayarlamış olur. Hatırlanacak olursa COM dosya biçiminde bu başlangıç değeri, ilk 256 baytın PSP(Program Segment Prefix) olmasından ötürü 100h idi. Tiny bellek modeli ile COM dosyası üretileceği zaman DOS ortamı için org 100h deyimini kullanmak gerekir. Deyimden sonra kullanılacak parametre, sayıya çözümlenebilir bir ifade de olabilir.
high ve low deyimleri, kelime(word) boyutuna çözümlenebilir bir ifadenin yüksek ve düşük baytlarını 2'ye tümleyen düzeninde döndürür. highword ve lowword deyimleri ise çift kelime(doubleword) boyutuna çözümlenebilir bir ifadenin yüksek ve düşük kelimelerini 2'ye tümleyen düzeninde döndürür. Parametre olarak kullanılacak ifadenin sayısal veya sayısala çözümlenebilir bir deyim olması gerekir.
Gerek C/C++'ta, gerekse de MASM'da, tek satır yerine birden fazla satır kullanarak kompleks bir ifade yazılmak istendiğinde, ifadenin bitmemiş olduğu her satırın sonuna ters bölü (\) ekleyerek dikey uzatma sağlanabilir. Yazının başından hatırlanacağı üzere assembler'da ifadeler satır satır işlenmekte, satırın bitimi ifadenin bitimi demektir. Ters bölü işareti, satır sonu iminin yok sayılmasını sağlar.
Sabit veya sabite çözümlenebilir tamsayı ifadeler üzerinde mantıksal veya bit düzeyinde işlemler yapmak için MASM'de and, or, xor, not, shl ve shr operatörleri mevcuttur. Bu operatörlerin kullanımı ile yapılan işlemler neticesinde, varsayılan ifade büyüklüğüne uygun sabit tamsayı ifadeler üretilir. Bu operatörlerden ilk dördü mantıksal, son ikisi ise bit düzeyinde çalışır.
ifade1 and ifade2
ifade1 or ifade2
ifade1 xor ifade2
not ifade
ifade shl basamak
ifade shr basamak
MASM'de sabit dizeler üzerinde işlem yapmak için catstr, substr, sizestr ve instr operatörleri bulunur. Bu operatörler sayesinde dizeler birleştirilebilir, içlerinde arama yapılabilir, belli kısımlarına ulaşılabilir ve boyutları elde edilebilir. Bu operatörlerin kullanım ayrıntılarına, MASM ile birlikte gelen dokümantasyon aracılığıyla erişilebilir.
32 bit assembler için bu noktadan sonrası, tamamen tecrübe ve araştırmaya dayanmaktadır. Programların yazımı için ihtiyaç olunacak teorik ve pratik bilginin %99'unu bu bölümün tamamı ile vermeye çalıştım. Assembler ile yapılacak kodlamanın daha ileri düzeyi OOP(Object Oriented Programming - Nesneye Yönelik Programlama) ve Win32 COM(Component Object Model)'dir. Bu konular, kendi çaplarında özel teknikler olduğundan bu bölümde yer vermedim. Ancak bu bölüm dahilinde verilen bilgiler ile pek çok çeşit Windows uygulaması yazılabilir. Ancak Windows API ile aşina olunması da gerekir.
MASM en iyi assembler derleyicisi değildir, ancak TASM veya benzeri assembler programlarını kullananlar için kullanımı aşinadır. MASM, en son MMX teknolojisini destekleyen komutları içermiş ve Microsoft tarafından geliştirilmesine son verilmiştir. Hali hazır en son çalıştırılabilir sürüm 6.14'tür. Daha gelişmiş komutların kullanılabildiği ve benim de kimi zaman kullandığım başka bir assembler Flat Assembler(FASMW)'dir. Hem konsol, hem de GUI sürümünü barındıran tek bir arşiv olan FASMW http://flatassembler.net/ sitesinden indirilebilir. Bu assembler'in aynı zamanda kaynak kodu açıktır.
Bu bölümün de sonuna geldik. Bu bölümün 3 parçası ile birlikte yazımı çok uzun sürdü ve epey zahmetli oldu. Bu yazılar belge niteliği taşıdığı için bilgi içeriklerinde hata yapmamaya çalıştım ama yine de gözümden kaçan yerler için sizden özür dilerim. Bölüm dahilindeki tüm örnek ve yorumlar tamamen şahsımın ürünü olup, yazım esnasında faydalandığım kaynaklar şunlardır:
Her türlü soru ve eleştirilerinize açığım. İlgilenenler olursa bu konuda birkaç seanstan oluşan bir seminer de verebilirim.
Saygılarımla
Cihan Atıl Namlı
9 Aralık 2005 Cuma