32 Bit Assembler
32 Bit Assembler, Intel IA-32 mimarisindeki 80386 ve üzeri işlemcilerin programlanması için kullanılan makine dilidir. Bugün kullanmakta olduğumuz Pentium 4, Celeron ve Pentium M (Centrino) teknolojili işlemciler, bu dil ile programlanmaktadır. 16 Bit Assembler ile geriye dönük olarak tam uyumlu olmasının yanında, korumalı kip ve 32 bitlik yazmaçların kullanımı ile işlemci gücünün tamamından faydalanılmasını sağlar.
Bu bölümün okunmasının evvelinde, aynı yazı dizisindeki 3. bölüm olan "Turbo Assembler ile Çalışmak" konusunun incelenmesinde fayda görüyorum. Zira şimdiki konunun içeriği, 3. bölümde işlenen tüm genel yapıları tekrarlamayacak; bunun yerine daha çok ek ve/veya gelişmiş özelliklerin irdelemesi yapılacaktır.
32 Bit Assembler Derleyicisi
32 Bit Assembler içerikli .asm dosyalarının derlenmesi ve .obj uzantılı nesne kodunun oluşturulma işlemini Microsoft Corporation tarafından sunulan "Microsoft Macro Assembler" (ml.exe) programı yapmaktadır. Oluşturulan nesne kodunun .exe uzantılı uygulamaya dönüştürülmesi ise, yine Microsoft Corporation tarafından sunulan "Microsoft Incremental Linker" (link.exe) tarafından yapılmaktadır. Her iki program da komut satırında çalışmakta ve dosyayı yardımcı parametrelerde belirtilen şekilde işlemektedir. Bu programlara tekil olarak ulaşmak pek zordur; dolayısıyla internette şu sitede MASM32 adlı kabinenin bulunması ve indirilmesi gerekir: http://www.movsd.com
Bu kabine içerisinde, 32 bit assembler programlama ile ilgili tüm yardım dosyaları, programlama araçları, include ve lib şeklinde yardımcı kütüphaneler ve örnekler mevcuttur. Kabine sürümü zaman içerisinde güncellenmektedir. Bu tarih itibariyle elimdeki en son sürüm 8.2'dir. Zip'li biçimde 3MB olan bu kabine, açıldığında disk üzerinde 20MB'tan daha fazla yer işgal etmektedir.
Bu yazı dizisinde, 32 bit assembler'in 32 Bit Windows ortamındaki kullanımı işlenecektir. Zira ait olunan ortam ile etkileşim kuralları, kodlama özgürlüğünü etkileyebilmektedir.
32 Bit Assembler'a Giriş
32 Bit Assembler'daki kodlama kurallarının altyapısı genel olarak 16 Bit Assembler'a dayanmaktadır. Ancak özellikle işlem verimliliği ve bellek erişimi hususunda 16 bitlik ortama göre büyük üstünlük ve kolaylık göstermektedir.
32 Bitlik İşlemcinin Yazmaçları
Adından da anlaşılacağı üzere, 32 bitlik işlemcideki temel yazmaçlar da 32 bit uzunluktadır. Bu yazmaçlar, amaç ve kullanımlarına göre aşağıdaki gibidir:
Genel amaçlı yazmaçlar: EAX(accumulator), EBX(base), ECX(counter), EDX(data)
İşaretçi yazmaçları: ESI(source index), EDI(destination index), EIP(instruction pointer)
Yığın yazmaçları: ESP(stack pointer), EBP(base pointer)
Durum yazmacı: EFLAGS
Genel amaçlı yazmaçlar, hemen hemen tüm komutlarda parametre (operand) olarak kullanılabilirler. IA-32 mimarisindeki 80386 işlemcisinden itibaren bu yazmaçlar da, bellek erişimi amacıyla köşeli parantez içerisinde kullanılabilmektedir. Yani eskiden sadece BX, SI, DI ve BP'nin özgürce kullanılabildiği bu amaca artık EAX, ECX ve EDX de hizmet etmektedir. Yine de uyumluluk ve kod anlaşılırlığı açısından kimi ortak kurallara haiz olmak yerinde bir fikir olabilir.
İşaretçi yazmaçları, isimlerini eski 16 bitlik yapıdaki görevlerinden almışlardır. Artık bu yazmaçların bellek gösterme görevi, yukarıda belirtildiği üzere genel amaçlı yazmaçların tümü tarafından gerçekleştirilebilmektedir. Ancak bir görev vardır ki sadece buradaki ESI ve EDI yazmaçlarına özgüdür: Dizi ve dize işlemleri. Bu tip işlemlerde ESI kaynak işaretçi, EDI ise hedef işaretçi olarak kullanılmaktadır.
Yığın yazmaçları, temel olarak işlevlere giriş ve çıkışlarda konum bilgisinin korunmasına yararlar. Bunlardan ESP yazmacı, hemen hemen sadece sistem tarafından kullanılır ve değiştirilir. Ancak aynı bölge üzerinde çalışan EBP yazmacı ise sadece kullanıcı/programcı tarafından değiştirilir. EBP'nin görevi, yığındaki bilgileri, daha özel olarak aktarılan parametreleri okumak veya yazmaktır.
Yukarıda adı geçen yazmaçlardan EIP ve EFLAGS dışında kalanlar, pek çok mantıksal ve aritmetik komut tarafından parametre olarak kabul edilmektedir. EIP, komut işaretçisi olduğu için sadece dallanma komutları(Jxx, CALL, RET ve IRET) ile değiştirilmesi mümkündür. EFLAGS yazmacının herhangi bir kullanıcı değerine atanması da dolaylı yollardan mümkün olmaktadır.
Bu 10 adet yazmaç, çoklu görev sisteminin temelini oluşturan, farklı görevlere ilişkin özgün bilgilerin temelini özgün yığın ile birlikte teşkil etmektedir. Yani çoklu görev anahtarlamaları esnasında yukarıdaki 10 yazmaç görev başına muhafaza edilir.
32 bitlik Intel işlemcilerinde sadece bu yazmaçlar değil, çeşitli görevlere yardımcı olmak amacıyla şu özel yazmaçlar da bulunur. İlk üç grup, programcı tarafından aktif olarak kullanılabilir. Sonraki 6 grup ise işletim sistemi tarafından kullanılır.
x87 FPU Yazmaçları: ST0, ST1, ST2, ST3, ST4, ST5, ST6, ST7 ve öteki denetim yazmaçları
MMX Yazmaçları: MM0, MM1, MM2, MM3, MM4, MM5, MM6, MM7
XMM Yazmaçları: XMM0, XMM1, XMM2, XMM3, XMM4, XMM5, XMM6, XMM7, MXCSR
Segment Yazmaçları: CS, SS, DS, ES, FS, GS
Kontrol Yazmaçları: CR0, CR2, CR3 + CR4(Mimariye Pentium işlemcisi ile dahil olmuştur)
Sistem Tablosu İşaretçi Yazmaçları: GDTR, LDTR, IDTR, Görev Yazmacı
Hata Ayıklama Yazmaçları: DR0, DR1, DR2, DR3, DR6, DR7
Test Yazmaçları: TR4, TR5, TR6, TR7
MSR Yazmaçları
Burada adı geçen son 6 grup özel yazmaçlar, sisteme bağlı çalışan veya iş yapmaya yönelik uygulamalar yazan bir programcı tarafından değiştirilmemeli veya kullanılmamalıdır. Söz konusu yazmaçlar, işletim sistemleri tarafından kullanılmakta ve örneğin IDE(Integrated Development Environment)'ler tarafından talep edildiğinde, çalışması durdurulan program ile ilgili bilgi sağlamaktadır.
FLAT Bellek Modeli
16 bitlik işlemciler, günümüzdeki uygulamaları göz önüne aldığımızda bellek erişimi ve kullanımı bakımından son derece zayıf kalmaktadır. Zira 16 bitlik yazmaçlar ile 64KB'lık bellek bölgesine doğrudan yakın erişim sağlanabilmekte ve ancak segment yazmaçlarının değiştirilmesi ile maksimum 1MB'lık uzak bölge kontrol edilebilmekteydi. Oysa 32 bitlik işlemcinin yapısında varolan FLAT bellek modeli sayesinde 4GB'lık fiziksel bellek doğrusal olarak denetlenebilmektedir.
32 bit Windows ortamı için yazılan uygulamalar FLAT bellek modeline haiz olmalıdır. Bu bellek modeli kullanıldığında, program başlangıcından sonuna kadar segment yazmaçları aynı değeri göstermektedir. Bu yapısıyla bir COM programını andırmaktadır. Gerçekten de FLAT bellek modelinde yapılan erişimler NEAR (yakın) bellek adreslemesi dahilindedir(bkz. Bölüm - 4: COM ve EXE Dosya Biçimleri). Bellek erişiminde herhangi bir hesaplamanın yapılmaması, işlem kolaylığını ve dolayısıyla hızı arttırmaktadır. Ayrıca bu modelde, aynı olan bu segment yazmaç değerlerinin hiçbir önemi yoktur; zira sistemde varolan fiziksel belleğin ilk 4GB'lık bölümü doğrusal olarak adreslenmektedir. Yani, herhangi bir CS değeri için IP'nin göstermiş olduğu değerin bellekteki yeri sabittir. Bu durum, kimi zaman mutlak erişimlerin yapılmasına gerek duyduran "görecelilik" yapısını ortadan kaldırmakta; dolayısıyla tüm adreslemeler mutlak olabilmektedir. Yani fiziksel bellekteki 5. hücreye herhangi bir değer atamak için bu modelde adres yerine 0x00000005 yazmak yeterlidir. Oysa eski 80286 modelinde FAR (uzak) bellek adreslemesi kullanılarak bunun 0x0000:0x0005 olarak belirtilmesi gerekmektedir. Ya da daha uzun ve zahmetli bir yolla önce DS'ye 0x0000 verilmeli, daha sonra da SI'nin 0x0005 değerini göstermesi sağlanmalıdır.
FLAT bellek modelinin kullanımı, ESI ve EDI gibi işaretçi yazmaçlarının çalıştığı segmentlerin kimi durumlara özgü olarak farklı belirtilmesini de gereksiz kılar. Zira normal bellek gösterimlerinde, SEGMENTED (parçalı) bellek modelinde ESI işaretçisi DS üzerinde, EDI işaretçisi ise ES üzerinde çalışır. Kimi durumlarda varsayılan segmentlerin farklı belirtilmesi gerekebilir. Örneğin MOV [EDI], ES:[ESI]. Ancak program dahilindeki tüm segment yazmaçlarının aynı değeri göstermesinden ötürü bu farklı belirtmenin bir anlamı olmayacaktır ki buna ihtiyaç da olmayacaktır.
Korumalı Kip (Protected Mode)
İlk DOS zamanlarında, sistemde varolan belleği denetleyen ve izinsiz erişimleri durdurabilen herhangi bir mekanizma yoktu. Dolayısıyla yanlış veya acemice yazılmış bir program, normalde kendisine ait olabilecek 64KB'lık belleğin dışındaki bölgelere kontrolsüz erişimde bulunabilmekte ve istenmeyen sonuçlara neden olabilmekteydi(bkz. Bölüm 4: COM ve EXE Dosya Biçimleri). Zira 0xA000 segmentinde bulunan VGA bellek bölgesi, programların görsel öğeleri görüntülemek için kullandığı paylaşımlı bir alandır. Bu bölgedeki veriler, görüntü arabirimi tarafından sürekli okunur ve ekrana piksel olarak yansıtılır. Bu alana, belirli bir döngü dahilinde veri yazmakta olan bir kod, döngü yapısındaki bir aksaklık nedeniyle hedefini aşabilir ve yine paylaşımlı olarak sunulan DOS işletim sistemi kodunu ve hatta bellekte kayıtlı olan BIOS ayarlarını değiştirebilir. Bu durumda sistem, herhangi bir açıklama dahi yapamadan kilitlenebilir ve yeniden başlatılması gerekebilir. Kaydedilmemiş herhangi bilgiler de kaybedilir.
Korumalı Kip, programa tahsis edilmiş bölgelerin dışına erişilmesi durumunda buna izin vermeyen ve gerekirse sistemin bütünlüğünün ve güvenliğinin korunması adına programı sonlandırabilen bir mekanizmadır. Buna benzer bir yapı, 16 bit Windows (Windows 3.1) zamanında da mevcuttu. Ancak eski Windows'ların "Benzetimli Çoklu Görev (Simulated Multitasking)" sağlamasından ötürü kimi zaman, sistem hatayı oluşturan programa müdahalede bulunana kadar program, farklı bellek bölgelerini değiştirebiliyordu. Dolayısıyla çoklu görev ortamı ve güvenliği tam olarak sağlanabilmiş değildi. Bu durum, fazla miktarda bellek kullanımına ihtiyaç duyan mühendislik veya simülasyon uygulamalarının, öteki programlarla paralel çalıştırılabilmesini tehlikeye sokuyordu. Hatalı bir programın, işletim sistemine ait bölgelere müdahalede bulunması sonucunda herhangi izinsiz denetim gerçekleştirmeyen, ancak tamamen masumane biçimde, evvelden hatalı programın değiştirmiş olduğu bölgede yer alan bir işletim sistemi fonksiyonunu çağıran bir program, sistemin "Siyah Ekran" ile kilitlenmesine yol açabiliyordu. Dikkat edilirse, Windows sisteminde oluşan hatalarda kullanıcıyı bilgilendirmek amacıyla "Mavi Ekran" çıkar ve hatanın kaynağını, kullanılan çözüm yöntemini belirtir. "Siyah Ekran", sistemin tamamen aciz kaldığının bir göstergesidir.
Bugünkü Windows işletim sistemlerinde varolan anlamdaki Korumalı Kip, donanım destekli olarak sağlanmakta ve öncekine göre mukayese edilemez bir üstünlük sağlamaktadır. Görevlere tahsis edilmiş bellek bölgelerinin kullanım amaçları da belirtilmekte, dolayısıyla Sayfa Hatası (Page Fault) olaylarının niteliği de belirtilebilmektedir. Zira bir program, sistemden 16KB'lık bir bölge talep eder ve 20K'lık bir bölgeyi okumaya veya yazmaya çalışırsa burada, yapılmaya çalışılan işin niteliğine göre hata mesajı verilir. Oluşabilecek hata, eğer program dahilinde hesaba katılmış ve ele alınabiliyorsa (Error Handling, Error Trapping, Trappable Error, Exception Handling), yapılan hataya işletim sistemi tarafından müdahalede bulunulmaz. Ancak böyle bir durum söz konusu değilse, işletim sisteminin tek bir müdahalesi vardır: Uygulamayı sonlandırmak. Sonlandırma işlemi esnasında, programın daha önceden işletim sisteminden talep ettiği tüm bellek bölgeleri de yeni kullanımlara açılır. Bu sayede bellek sızıntısı veya kaybı önlenir. Eski sistemlerde, kimi zaman programların sonlandırılmasındaki bellek kayıpları nedeniyle, herhangi bir uygulama tarafından kullanılmadığı veya rezerve edilmediği halde sistemdeki bellek çok düşük miktara inebiliyor; başka bir tabirle sistem kaynakları tükenebiliyordu.
32 Bit Assembler'da Kodlamaya Giriş
32 Bit Assembler'deki kodlama kuralları, genellikle 80286 işlemcisine hitap eden 16 Bit Assembler'dekine benzer. Ancak buradaki makro üstünlükleri, kodun anlaşılabilirliğine katkıda bulunur. Makro konusuna daha ileriki başlıklarda değinilecektir. Şimdilik sadece genel kodlama kuralları verilecektir.
Komut Parametreleri
32 bit assembler'da komutlar, birlikte kullanıldıkları parametre sayısına göre 0, 1 ve 2 olmak üzere 3 grupta incelenebilir. Parametrelerin arasında virgül bulunmalıdır. Komutların ve yazmaç adlarının yazımında büyük/küçük harf kuralı önem taşımamaktadır. Ancak değişken adları, 16 bit assembler'daki kurallara haizdir. Aşağıda bunlara ilişkin örnekler yer almaktadır.
mov eax, ecx ; Komut: MOV, ECX yazmacının içeriği, EAX yazmacına kopyalanır.
AND ESI, sayac ; Komut: AND, ESI ile "sayac" adlı değişkenin içerikleri bit düzeyinde AND işlemine tabi tutulur; sonuç yine ESI yazmacına yazılır.
Yukarıdaki örneklerde olduğu gibi, 2 parametre alan komutlarda 1. parametreye hedef, 2. parametreye ise kaynak denir. Yapılan işlemin sonucu her zaman hedefe yazılır.
mul dword ptr [ESI] ; Komut: MUL, EAX yazmacının içeriği ile ESI ile gösterilen adresteki 4 baytlık veri çarpılır; sonuç EDX:EAX çiftinde bulunur.
NEG ebx ; Komut: NEG, EBX yazmacının içeriği, 2'ye tümleyen aritmetiğine göre ters işaretli yapılır.
CBW ; Komut: CBW, AL yazmacının içeriği, işareti AH'a aktarılacak şekilde AX'e yazılır.
pushad ; Komut: PUSHAD, Tüm 32 bitlik genel amaçlı yazmaçlar yığına kopyalanır.
Komutların kullanımında, parametrelerin uyumluluğu çok önemlidir. Genellikle her iki parametrenin boyutunun da aynı olması gerekir. Örneğin:
mov eax, edx
Bu işlemde 32 bitlik EDX yazmacının içeriği, 32 bitlik EAX yazmacına kopyalanmaktadır. Ancak aşağıdaki kodlama yanlıştır ve assembler tarafından hata olarak nitelenir:
mov eax, dx
Burada, programcının asıl niyetinin ne olduğu assembler'i ilgilendirmez. Parametrelerin (operandların) uyumsuz olduğu bir hata mesajı ile belirtilir.
Komutların yazımındaki MOV, AND, MUL gibi ibareler, aslında donanımda ikili sayılarla belirtilen işlemlerin daha anlaşılır olması amacıyla kullanılmaktadır. Sadece Intel'in değil, tüm öteki mikroişlemci ve mikrodenetleyici üreticilerinin sunmuş oldukları assembler'lar da bu şekilde MNEMONIC adı verilen kelimeleri çözümlerler. Bu komutlardan sonra gelen parametrelere göre ikili sayı dizeleri oluşturulur. Gerçekte 32 bitlik EAX yazmacına 0x12345678 değerinin yüklenmesi için kullanılan işlem kodu şöyledir:
MOV EAX, 0x12345678 => A1 78 56 34 12
Bu sistemin kullanımı, herhangi bir işlem belirtmede hassasiyet kaybına yol açmaz. Tüm modern assembler'larda bu teknik kullanılır.
Yazmaç Yapılarında Geriye Dönük Uyumluluk
32 Bitlik İşlemcinin Yazmaçları başlığı altında belirtilen tüm genel amaçlı yazmaçların ilk 16 bitlik kısımları, eskiden kullanılan ve başlarında (E) harfi bulunmayan adlarla anılabilir. Örneğin 32 bitlik EAX yazmacının ilk 16 bitlik kısmı AX adını alır ve işlemlerde bu şekilde kullanılır. Aşağıdaki kod örneklerinde, 32 bitlik yazmaçların her bir baytına erişim yöntemi verilmektedir.
|
EAX: 32 Bit |
|||
|
16 Bit |
AX: 16 Bit |
||
|
8 Bit |
8 Bit |
AH: 8 Bit |
AL: 8 Bit |
8 bitlik (1 baytlık) BYTE değişkenine EAX'in ilk 8 bitlik bölümünü aktarmak için:
MOV BYTE, AL
16 bitlik(2 baytlık) WORD değişkenine EAX'in ilk 16 bitlik bölümünü, daha bilimsel tabirle düşük kelimesini (LOW WORD) aktarmak için:
MOV WORD, AX
16 bitlik WORD değişkenine EAX'in yüksek kelimesini (HIGH WORD) aktarmak için doğrudan bir komut yoktur. Programlama tekniklerinin kullanılması gerekir:
ROL EAX, 16 ; 32 bitlik EAX yazmacı, sola doğru 16 bit döndürülür. Dolayısıyla kelimeler yer değiştirmiş olur.
MOV WORD, AX ; Bu yer değişiminden sonra eskiden yüksek olan kelime düşün olmuştur ve WORD değişkenine doğrudan aktarılabilir.
Yukarıda belirtilen işlem yapıldıktan sonra EAX yazmacının eski haline getirilmesi için bir kere daha 16 bitlik döndürme işlemine tabi tutulması gerekir. Döndürmedeki miktar, yazmaç boyutunun yarısı olduğundan yönün herhangi bir önemi yoktur.
Daha önceki başlık altında belirtilmiş olan "Parametrelerin Uyumluluğu" konusunda verilen örneğe geri dönmek gerekirse:
MOV EAX, DX
işlemindeki amacın DX'in, EAX yazmacına aktarılması olduğunu düşünelim. Bu işlemin kurallar dahilinde yapılabilmesi için şu olanaklar mevcuttur:
MOVZX EAX, DX ; DX yazmacının içeriğini, boyutu aşan bölümünü 0 yapmak suretiyle EAX yazmacına kopyalar.
MOVSX EAX, DX ; DX yazmacının içeriğini işaretli bir sayı kabul ederek, işaret bitini EAX'in yüksek kelimesindeki tüm bitlere ve kendisini de düşük kelimesine kopyalar. Bu şekilde 2'ye tümleyen sisteminde sayının büyüklüğü ve işareti korunur.
Şu iki yöntem de bu işe yaramakta, ancak daha zahmetli olmaktadır:
XOR EAX, EAX ; EAX yazmacının içeriğini 0 yapar.
MOV AX, DX ; DX yazmacının içeriğini AX yazmacına kopyalar. Dolayısıyla EAX = AX olduğundan amaca ulaşılmış olur. Ancak sayı işaretsiz kabul edilmiştir.
MOV AX, DX ; DX yazmacının içeriğini AX yazmacına kopyalar.
CWDE ; AX yazmacının içeriğini işaretli bir sayı kabul ederek, işaret bitini EAX'in yüksek kelimesindeki tüm bitlere(16-31) kopyalar.
Buradaki örnekler, veri taşıma ile ilgili ekstralar niteliğinde olup, tüm işlemler için bu farklı yollar mümkün olmamaktadır.
Yazmaçların Değerlerinin Korunumu
Daha önce de belirtildiği gibi, uygulama kodunun yazımı esnasında malik ortamın kurallarına uymak gerekir. 32 Bit Assembler ile Windows ortamında çalışacak kod yazarken çoğunlukla Windows API(Application Programming Interface)'den faydalanılır. Dolayısıyla çağırılan fonksiyonlar ile birlikte x86 işlemcisinin sınırlı sayıdaki kaynağını, buradaki konu olarak yazmaçlarını güvenli şekilde paylaşmak gerekir.
32 bitlik işlemcide, programcının serbestçe değiştirebildiği 8 yazmacın varlığından söz edilmişti: EAX, EBX, ECX, EDX, ESI, EDI, ESP, EBP. Bu yazmaçlardan ESP ve EBP, çok özel amaçlar için kullanılmaktadır. Buradaki özel amaç, işlevlere giriş ve çıkış konumlarını muhafaza etmek, parametrelere ve yerel değişkenlere erişmektir. Dolayısıyla bu yazmaçların alt işlevler tarafından değiştirilmesi, üst kod tarafından yanlış kullanılmalarına yol açar. Bu iki yazmacın, işlev giriş ve çıkışında bulunduğu gibi bırakılması gerekir. Eğer işlev dahilinde mutlaka kullanılmaları gerekiyorsa eski değerlerinin muhafaza edilmesi ve kullanım bittiğinde yüklenmesi gerekir. Geriye serbest kullanım için 6 yazmaç kalır. Bu yazmaçlardan EBX, ESI ve EDI, eski özelliklerindeki bellek erişimi ve adresleme üstünlüklerinden ötürü dizi işlemlerinde sıklıkla kullanılabilmektedir. Dolayısıyla üst kod, alt kodu çağırırken buradaki değerlerinden faydalanabilir ve alt kod tarafından bu yazmaçların değiştirilmesi, yanlış adreslemelere yol açar. Geriye sadece şu 3 yazmaç kalır: EAX, ECX ve EDX. Bunu bir kurala dönüştürmek gerekirse, Windows API kullanılırken herhangi bir işlev çağırıldığında, dönüşte EAX, ECX ve EDX yazmaçlarının eski değerlerinde olması beklenmemelidir. Dolayısıyla kritik değer taşımaları durumunda bu yazmaçların yığında saklanmaları ve ardından işlevlerin çağırılması gerekir.
Herhangi bir işlevin, yaptığı işlemin gereksinimlerinden ötürü ESI, EDI ve EBX yazmaçlarını kullanması gerektiğini varsayalım. Bu durumda aşağıdaki gibi bir kod eklentisi kullanılmalıdır:
Function1 proc
push esi
push edi
push ebx
; Buraya işlev kodu yazılır.
pop ebx
pop edi
pop esi
Function1 endp
ESP ve EBP yazmaçları aracılığıyla yerel değişkenlere ve işlev parametrelerine erişim, bu yazı dizisinin 3. bölümünde belirtildiği gibidir. Ancak 32 bit assembler'de yerel değişken ve parametrelere erişim için bu yazmaçların programcı tarafından değiştirilmesine gerek bırakmayacak yapılar mevcuttur. Assembler, gerekli adres hesaplamalarının tamamını kendi yapmakta ve kullanıcıyı daha çok işin yapımına yöneltmektedir. Bu konu ile ilgili ayrıntılara ileriki başlıklarda girilecektir.
Veri Türleri
32 bit assembler'da kod yazarken, hangi komutun hangi tür veriyi işleyeceğini ve bu verilerin boyutlarının ne olması gerektiğini iyi bilmek gerekir. Aksi takdirde çalışır görünen bir kod, programcının istediği amaca hizmet etmeyebilir.
1. Komut Parametre Veri Türleri (Instruction Operands)
Assembler'da, komutlar ile birlikte kullanılmak üzere 3 tip parametrik veri türü mevcuttur:
Mutlak Veri (Immediate)
Mutlak veriler, tamamen rastgele nitelikli ve programcı tarafından tanımlanan sayısal büyüklükler olup 8, 16 veya 32 bit boyutlarında olabilmektedir.
mov eax, "c" ; "c" harfinin ASCII karşılığı, EAX yazmacına yüklenir.
mov eax, "ATIL" ; "ATIL" kelimesindeki harflerin ASCII karşılıkları EAX yazmacına yüklenir. Kelimenin 4 harfi geçmesi hataya yol açar, çünkü EAX yazmacının boyutu 4 bayttır.
mov edx, 175 ; 175 sayısı EDX yazmacına yüklenir.
mov ecx, 3692af2dH ; 3692af2dH sayısı, ECX yazmacına yüklenir.
Yukarıdaki örneklerde 32 bitlik yazmaçlar kullanılmış olmasına karşın, 16 bitlik veya 8 bitlik yazmaçlar için de notasyon aynı şekildedir. Mutlak veri, komut ile birlikte gelmesinden ötürü işlemci arabelleğinde doğrudan varolduğundan, yazmaçlara aktarım hızı en yüksek seviyededir.
Bellek Verisi (Memory)
Bellek verisi, sistemde varolan fiziksel veya mantıksal bellek üzerindeki hücrelerde varolan verilerdir. İşlemci, belleğe erişim sağlamak suretiyle bu verileri komutların yanında parametre olarak kullanabilir.
mov eax, [edi] ; EDI yazmacının göstermiş olduğu bellekten itibaren 32 bitlik veri EAX yazmacına aktarılır.
mov ax, word ptr [edi] ; EDI yazmacının göstermiş olduğu bellekten itibaren 16 bitlik veri AX yazmacına aktarılır. Burada boyut ayarlaması kullanılmıştır. Boyut ayarlaması, özellikle tek parametre alan işlemlerde kullanılır. Buradaki kullanımın sağladığı bir düzeltme yoktur; zira assembler zaten 16 bitlik olduğunu varsayacaktır.
mov [mutlakadres], al ; Mutlak adres ile belirtilen adresteki 1 baytlık hücreye AL yazmacının içeriği aktarılmıştır.
Ne 16 bitlik işlemcilerde, ne de 32 bitlik işlemcilerde doğrudan doğruya bellekten belleğe işlem yapan bir komut yoktur. Ancak şu iki özel durum, 32 bitlik işlemci ile gelen yenilikler arasındadır:
mov dword ptr [adres], mutlakdeğer ; Bu işlem ile herhangi bir bellek gözüne bir mutlak değer doğrudan doğruya yazılabilir.
push dword ptr [adres] ; Bu işlem ile herhangi bir bellek gözündeki değer, doğrudan doğruya yığına atılabilir.
Bir bellek gözündeki değeri, başka bir bellek gözüne kopyalamanın iki yolu vardır:
mov eax, [adres1] ; 1 çevrim
mov [adres2], eax ; 1 çevrim
Bu işlemde EAX dışında herhangi bir genel amaçlı yazmaç da kullanılabilir, ancak yazmaç içeriğinin değişeceği unutulmamalıdır. Aşağıdaki işlem ise herhangi bir yazmaç kullanımını gerektirmez, ancak yukarıdakine göre nispeten daha yavaştır. Dolayısıyla döngüler içerisinde kullanımı hız ve performans kaybına yol açar.
push [adres1] ; 4 çevrim
pop [adres2] ; 6 çevrim
Her iki yöntemin kombinasyonu ele alınarak başarımı en yüksek bellek kopyalama işlemi gerçekleştirilebilir. Önce EAX yığına atılır; daha sonra ise ilk yöntemde olduğu gibi bellek verileri aktarılır. Son olarak da EAX yığından geri alınır. Bu işlem toplam 7 çevrim sürecektir. Döngüler içinde kullanılması durumunda ise sadece 1 kereliğine EAX'in yığına atılması ve sonra işlemler bittiğinde geri alınması yeterli olacak; bu da başarımı mümkün olan en yüksek düzeye çıkaracaktır.
Yazmaç Verisi (Register)
mov eax, ecx ; ECX yazmacının içeriği, EAX yazmacına kopyalanır.
mov esi, edi ; EDI yazmacının içeriği, ESI yazmacına kopyalanır.
mov ds, ax ; AX yazmacının içeriği, DS segment yazmacına kopyalanır.
mov dl, bl ; BL yazmacının içeriği, DL yazmacına kopyalanır.
Yazmaçlar üzerinde işlem yapmanın, boyut uyumluluğu dışında özel kuralları yoktur. Ancak segment işaretçilerinin (CS, SS, DS, ES, FS, GS) genel amaçlı yazmaç niteliğinde olmadığı ve her komut dahilinde kullanılamayacağı unutulmamalıdır.
2. Temel Veri Türleri (Fundamental Data Types)
Komutların işleme tabi tutacağı verilerin büyüklüklerine göre de bir sınıflandırma mevcuttur. Aşağıda temel veri türlerinin büyüklük sınıflandırmasına ilişkin tablo görülmektedir.
|
Veri |
Boyut |
Alternatif İfade Biçimi |
|
|
Nibble |
4 Bit |
- |
|
|
Byte |
8 Bit |
High Nibble |
Low Nibble |
|
Word |
16 Bit |
High Byte |
Low Byte |
|
Doubleword |
32 Bit |
High Word |
Low Word |
|
Quadword |
64 Bit |
High Doubleword |
Low Doubleword |
|
Double Quadword |
128 Bit |
High Quadword |
Low Quadword |
Bu veri türlerinden Quadword, IA-32 mimarisine Intel486
işlemcisi ile; Double Quadword ise Pentium III işlemcisindeki
SSE(Streaming SIMD Extensions) komutları ile girmiştir. Bu veri türlerine
ilişkin boyutların nasıl belirtileceğine ilişkin konuya ileriki başlıklarda
değinilecektir.
Bu veriler, genellikle bellek üzerinde ele alınır ve
komutlar tarafından işleme tabi tutulabilmeleri için işlemcinin ilgili bellek
bölgesine erişmesi gerekir. Bu noktada "Doğal Sınırlar (Natural
Boundaries)" kavramı ortaya çıkar. Doğal sınır, bir veri türünün boyutuna
göre bellek üzerinde bulunmasının daha verimli olduğu yerlerdir. Bu
yerler, veri türünün büyüklüğü ile tam bölünebilen noktalardır. Örnek olarak
Word veri türüne ilişkin doğal sınırlar, 2'ye bölünebilen adreslerdir.
Dolayısıyla adres değerinin ikili ifadesindeki en düşük bit her zaman sıfır (0)
olmalıdır. Aynı şekilde Doubleword veri türü için 4, Quadword veri türü için 8
ve Double Quadword veri türü için 16 baytlık doğal sınırlar tanımlıdır.
IA-32 mimarisindeki işlemcilerin normal komut seti, söz
konusu veri türlerinin doğal sınırlar dahilinde bulunmalarını gerektirmez.
Ancak doğal sınırlarda bulunmayan bir veriye erişim için işlemcinin art arda 2
bellek erişimi yapması gerekir. Bu da işlem başarımını düşürür. Özellikle yığın
konusunda bu ayrıntı büyük önem taşır. Dikkat edildiyse PUSH ve POP
işlemleri ile yığına atılabilecek en küçük veri boyutu 16 bittir. Dolayısıyla
herhangi bir ekstra müdahalede bulunulmaksızın yığın işaretçisinin 16
bitlik doğal sınırlarda erişim yapacağı açıktır. Ancak 32 bit assembler'da,
normalde 4 baytlık doğal sınırlarda bulunan yığına 2 baytlık bir veri
atılmasının ardından, yeni bir hizalama yapılmaksızın sürekli olarak 4
baytlık verilerin yüklenmesi ve boşaltılması ile çalışıldığında işlem başarımı
%50'ye kadar düşebilecektir. Dolayısıyla programcının bu ayrıntıyı da göz
önünde bulundurarak ekstra hizalama yapması gerekir.
IA-32 mimarisine Pentium III işlemcisi ile gelen SSE alt
komut setinde, Double Quadword veri türü üzerinde işlem yapan kimi komutlar
ise, doğal sınırlar dahilinde bulunmayan veriler üzerinde işlem yapılmaya
çalışıldığında genel koruma hatası vermektedir. Dolayısıyla bu komutların
işleyeceği verilerin adres ifadelerindeki en düşük 16'lı rakam sıfır (0)
olmalıdır.
Not: Yüksek seviyeli programlama dillerine ilişkin derleyicilerin ayarlarında,
yapı üyelerinin hizalamalarına ilişkin seçenekler bulunur. Örneğin 16 bitlik
DOS platformu için programların yazıldığı Turbo
C++'ta Options->Compiler->Code Generation menüleri vasıtasıyla
erişilen ayarlarda "Word Alignment" seçeneği, yapı üyelerinin her
halükarda 16 bitlik doğal sınırlarda hizalanmasını sağlar. Benzer şekilde 32
bitlik Windows platformu için programların yazıldığı Microsoft Visual C++'ta
ise Project->Settings->C/C++->Code Generation->Struct Member
Alignment yoluyla erişilen ayarlarda 1, 2, 4, 8 ve 16 bayt seçenekleri
bulunur. Ancak bu hizalamaların bir çeşit israfa yol açarak bellek kullanımını
arttıracağı unutulmamalıdır.
3. Sayısal Veri Türleri (Numeric
Data Types)
Yukarıda belirtilmiş olan bellek veri türleri, çeşitli
komutlar tarafından farklı nitelikli sayısal veri türleri olarak kabul edilir
ve işleme tabi tutulur. Aşağıda, bu veri türlerinin yorumlanışına ilişkin tablo
görülmektedir.
|
Sayısal
Tür |
Boyut |
C++
Karşılığı |
Tanım
Aralığı |
|
İşaretli
Tamsayı |
8 Bit |
char |
-128...127 |
|
İşaretli Tamsayı |
16 Bit |
short |
-32768...32767 |
|
İşaretli Tamsayı |
32 Bit |
int |
-231...231-1 |
|
İşaretli Tamsayı |
64 Bit |
__int64* |
-263...263-1 |
|
İşaretsiz Tamsayı |
8 Bit |
unsigned char |
0...255 |
|
İşaretsiz
Tamsayı |
16 Bit |
unsigned
short |
0...65535 |
|
İşaretsiz
Tamsayı |
32 Bit |
unsigned
int |
0...232-1 |
|
İşaretsiz Tamsayı |
64 Bit |
unsigned __int64* |
0...264-1 |
|
Ondalık
sayı |
32 Bit |
float |
1.18 x 10-38...3.40
x 1038 |
|
Ondalık sayı |
64 Bit |
double |
2.23 x 10-308...1.79 x 10308 |
|
Ondalık sayı |
80 Bit |
long double |
3.37 x 10-4932...1.18 x 104932 |
*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.
Tüm sayısal veri türlerinde, en yüksek bit işaret bitini
temsil eder. Tamsayı türlerinde sayı, 2'ye tümleyen sisteminde yorumlanır.
Ondalık sayı türleri ise "IEEE Standart 754 for Binary Floating-Point
Arithmetic" dahilindedir. Burada söz konusu ondalık sayı türlerinin bit
düzeyindeki temsillerine yer vermeyeceğim.
4. İşaretçi Veri Türleri (Pointer
Data Types)
Bellek üzerindeki konumları gösteren veri türlerine işaretçi
denir. IA-32 mimarisinde iki tür işaretçi tanımlıdır: Yakın İşaretçi (NEAR
POINTER, 32 Bit) ve Uzak İşaretçi (FAR POINTER, 48 Bit). Yakın işaretçi,
üzerinde çalışılan segment değiştirilmeksizin bellekteki bir noktanın
gösterilmesidir ve FLAT bellek modelinde kullanılan tek türdür. Parçalı
(segmented) bellek modelinde ise, gösterilmek istenen veri eğer üzerinde
çalışılan segment dahilinde değilse uzak işaretçiler yardımıyla bunun
gösterilmesi gerekir. Bu veri türünün kullanımına pek az rastlanır. Genel
olarak 16 bitlik platformda kesme vektörlerinin çağırılmasında ve dolaylı
adreslemede; 32 bitlik platformlarda işletim sisteminin kullandığı
yönetimsel komutlarda ve kesmelerde uzak işaretçiler kullanılır. Bellekteki
hücrede yer alan değer, hedeflenen segment işaretçisi(16 bit) ve offset
konumudur(32 bit).
5. BCD Veri Türü ve Paketlenmiş BCD
Tamsayılar (Binary Coded Decimal and Packed BCD Integers)
BCD veri türü, 4 bit ile ifade edilen ve 0-9 arasındaki
rakamları gösteren türdür. ifadede ve anlaşılırlıkta kolaylık sağlaması için ve
onluk düzene benzerliğinden ötürü kullanılmaktadır. Bir BCD rakamının ifade
edilmesi için 4 bit gerektiğinden, bir baytlık büyüklükteki bir BCD sayının
yüksek 4 bitinin anlamı olmayacaktır. Ancak belleği ekonomik kullanmak adına bu
yüksek 4 bite de anlam yüklenmesi durumunda bu yapıya Paketlenmiş BCD Tamsayı
(Packed BCD Integer) denir. IA-32 mimarisinde 10 bayta kadar paketlenmiş BCD
tamsayılar tanımlamak mümkündür. Bu şekildeki bir yapıda ancak ilk 9 bayt
sayısal olarak anlamlı, dizideki 80. bit ise tamsayının işaretini
belirtmektedir. Bu şekilde 18 basamaklı ve işaretli tamsayılar ifade etmek
mümkün olmaktadır. Mimaride paketlenmiş baytlar üzerinde işlem yapan temel set
komutları ve daha uzun paketler üzerinde işlem yapan SSE komutları
bulunmaktadır.
Yukarıda açıklanan tüm bu veri türlerinin yanında bit alanı,
bit dizisi ve SIMD(Single Instruction Multiple
Data) verileri bulunmaktadır. Bitler ile ilgili işlem yapan test ve
atama komutları temel set dahilindedir. SIMD veri türü ise MMX ve SSE olmak
üzere ikiye ayrılır. MMX yazmaçları MM0'dan MM7'ye kadar 64 bitlik yazmaçlar,
SSE yazmaçları ise XMM0'dan XMM7'ye kadar olan yazmaçlardır. Bu yazmaçlar özel
amaçlı olduğundan işlemci dahilindeki çalışma yazmaçları arasında gösterilmez.
SIMD yazmaçlarının hemen hemen tamamı paketlenmiş bellek verileri üzerinde
işlem yaparlar.
Bellek Üzerinde Çalışmak
Bellek, bir işlemcinin sınırlı sayıdaki kaynaklarının
yetmediği durumlarda kullanılmak üzere hazır bulunan ve işlemci tarafından
doğrudan erişilebilen fiziksel bölgedir. Bilgisayarın icadından bu yana
geliştirilen hemen hemen her uygulama ve kod, çeşitli verileri işleme tabi
tutabilmek amacıyla geçici depolama alanlarına ihtiyaç duymuştur. Bugünkü
uygulamaların bellek ihtiyaçları ise ortalama olarak 40-50 MB arasındadır.
32 bit assembler'da FLAT bellek modelinde erişilen bellek
offset'tir. Herhangi bir offsetin ifadesi için aşağıdaki taslak biçim
ele alınabilir:
Offset = Base + ( Index * Scale) + Displacement
Bu yapıdaki öğelerin açıklaması şu şekildedir:
Base: Herhangi bir genel amaçlı yazmaç; yani EAX, EBX, ECX, EDX, ESI, EDI,
ESP veya EBP'den biri.
Index: ESP dışındaki herhangi bir genel amaçlı yazmaç.
Scale: 1, 2, 4 veya 8.
Displacement: Herhangi bir 32 bitlik sayı.
Offset olarak yazılan ifade, işlemci tarafından çözümlenir
ve ortaya çıkan sonuca Efektif Adres denir. Bu şekilde esnek bir yapıyla
belirtilen bellek erişim modeli sayesinde pek çok yapısal değişkene döngüler
dahilinde zahmetsizce erişmek mümkün olmaktadır. Belleğe erişmek için, offset
ifadesinin köşeli parantez içerisinde belirtilmesi gerekir. Aksi takdirde bu
şekildeki bir yapı, assembler tarafından kodlama hatası olarak algılanır.
Aşağıda bellekten yazmaçlara aktarım ile ilgili örnekler mevcuttur.
mov eax, [ebx +
ecx*4] ;
EAX yazmacına, EBX + ECX * 4 ile belirtilen adresteki 32 bitlik veri aktarılır.
mul word ptr [edx + ebp]
;
EDX + EBP yazmacı ile gösterilen adresteki 16 bitlik bellek verisi AX yazmacı
ile çarpılır. Sonuç DX:AX çiftinde depolanır.
Bu tür adreslemede, FLAT bellek modelinin kullanımından
ötürü tüm işaretçi yazmaçları aynı segment üzerinde çalışmakta ve sabit
bir yer işaret edilmektedir. Ancak bellek modelinin parçalı (segmented) olması
durumunda, base ve index parametrelerinin her ikisi de ESP ve/veya EBP'den
oluştuğu takdirde üzerinde çalışılan segment SP olarak kabul edilmektedir.
Normalde, aksi belirtilmedikçe varsayılan segment DS'dir. Aşağıdaki
kodlama şekli ile, varsayılan segment geçici olarak değiştirilebilir:
mov edx, es:[eax + ebx * 8 + 4]
Bu kodlamada, sadece bu komut için üzerinde
çalışılan segmentin ES olduğu varsayılmaktadır. Dolayısıyla fiziksel
adres hesabında ES'nin değeri kullanılır. FLAT bellek modelinde bu kodlamanın
herhangi bir etkisi olmadığı halde parçalı (segmented)
bellek modelinde, fiziksel adres hesabındaki şu yöntemden ötürü büyük
farklılıklar ortaya çıkmaktadır:
PA = (SEGMENT << 20) + OFFSET
Bu sayede, parçalı bellek modelinde 4GB'a sınırlı
işaretçiler ile maksimum 64GB'lık fiziksel bellek erişimi mümkün
olmaktadır. 16 bitlik platformda bu hesap şöyleydi:
PA = (SEGMENT << 4) + OFFSET
Bu hesap yöntemi, 64K'ya sınırlı işaretçiler ile
erişilebilecek maksimum belleğin 1MB olmasını sağlıyordu. Her iki yöntemde de
toplam erişimin işaretçi boyutuna oranı aynıdır: 16.
Normalde işlemcinin hesaplamış olduğu efektif adresin
yazmaçlarda saklanabilmesi için işlemci tarafından sunulan bir
komut da vardır: LEA (Load Effective Adress). Şu şekilde kullanılır:
lea eax, offset
Hedef parametre, herhangi bir genel amaçlı yazmaç da
olabilir. Bu komutun açıkça yapmış olduğu işlem, offset değerini hedef yazmaca
yazmaktır. Offset ile belirtilen belleğe erişim dahi yapılmaz. Bu komut,
belirtilen nokta yakınlarındaki bölgeye, sadece displacement değeri
değiştirilmek suretiyle fazla sayıda erişim yapılacaksa hızdan kar sağlamak
amacıyla kullanılır. Zira işlemcinin karmaşık yapıdaki bir offset ifadesini çözümlemesi
zaman almaktadır.
Yığın (The Stack)
Yığın temel olarak ve önem sırasına göre şu 4 amaca hizmet
eder:
Genel amaçlı yazmaçlardan ESP(Stack Pointer) ve EBP(Base
Pointer) yığın üzerinde çalışmaktadır. Yığın üzerinde işlemcinin yaptığı
işlemler ESP'nin değişmesine yol açar. Aynı şekilde, programcının yığın
üzerinde serbestçe çalışabilmesi için EBP'yi değiştirmesi gerekir.
Bilindiği üzere alt programların çağırılması için CALL
komutu kullanılmaktadır. Bu komutun çalışması esnasında EIP değeri, ESP'nin
göstermiş olduğu konumdan itibaren aşağıya doğru 4 baytlık alana yazılır. Bu
sayede, alt programı çağıran üst programın çalışma konumu korunmuş olur.
Ardından da EIP'ye, parametre olarak belirtilen yeni değer yüklenir ve
çalışmaya o noktadan devam edilir. Bu komutun zıttı olan RET komutu ise,
ESP'nin göstermiş olduğu konumdan itibaren yukarı doğru 4 baytlık alanı EIP
yazmacına aktarır ve işlemcinin çalışma noktasını değiştirir. Burada dikkat
edilmesi gereken en önemli kural, yığının düşük adreslere doğru büyüdüğü ve
yüksek adreslere doğru küçüldüğüdür.
Benzer şekilde, işlemci dahilindeki kesmelerin çağırılması
için kullanılan INT komutu da, çalışma noktasının değişecek olmasından ötürü
EIP yazmacını yığına atar. Ancak kesme çağrımında bir farklılık da söz
konusudur: Yazılım kesmelerinin programcı tarafından kontrollü biçimde
çağırılmasına rağmen donanım kesmelerinin ne zaman ve hangi işlem yapılırken
meydana geleceği önceden kestirilemez. Dolayısıyla EFLAGS yazmacının konumuna
göre işlem yapacak olan bir kod, kesme rutininin yapacağı değişikliklerden
ötürü yanlış çalışabilir. Her iki tür kesmenin de dönüşü IRET komutuyla
sağlandığından, her iki türlü çağrım işleminde de EFLAGS yazmacının değerinin
korunması gerekir. Aynı zamanda kesmeler, uzak adresleme yöntemiyle
çağırılırlar. Dolayısıyla yığına önce EFLAGS, sonra CS, sonra da EIP yazmaçları
atıldığından toplam 10 baytlık bir kullanım söz konusu olacaktır. IRET işlemi,
yığına atılan yazmaçları ters sırada geri alır ve çalışmanın eski konumdan
devamını sağlar.
Alt programlara parametre aktarımı 16 bit assembler'da
olduğu gibidir. Dolayısıyla bu konu hakkında detaylı bilgi edinmek isteyenler,
bu yazı dizisinin 3. bölümü olan "Turbo Assembler ile Çalışmak"
konusunun ikinci yarısındaki "Alt Programlar" başlıklı
bölümü inceleyebilirler.
Alt programlar dahilinde iki tür yerel değişken söz konusu
olabilir: Parametre olarak aktarılanlar ve yerel olarak tanımlananlar. Bu
değişkenlerden parametre olarak aktarılanlar, zaten üst program tarafından
yığın veya yazmaçlar üzerinden gönderilmiştir. Dolayısıyla önemli olan yerel
olarak tanımlananlar için ayırılacak bölgedir. Şu bir gerçektir ki çoklu görev
dahilindeki her bir görevin kendine ait yazmaçları ve belirli büyüklükte yığını
vardır. Bu yığın, tamamen programcının denetimi altındadır. Dolayısıyla,
yığının kullanılmayan bölgelerinin söz konusu yerel değişkenleri depolamak
amacına hizmet etmesi akıllıca olacaktır. Yani, EBP'nin ESP'nin son konumuna
eşitlenmesinin ardından, artık EBP'nin göstermiş olduğu değerden küçük olan
adres bölgeleri yerel değişkenleri barındırmak amacıyla kullanılabilir.
Aşağıdaki örnekte bu duruma değinilmektedir.
|
0004FFFC |
Parametre 1 |
|
0004FFF8 |
Parametre 2 |
|
0004FFF4 |
Parametre 3 |
|
0004FFF0 |
Parametre 4 |
|
0004FFEC -> ESP |
Eski EIP |
|
0004FFE8 |
? |
|
0004FFE4 |
? |
|
0004FFE0 |
? |
|
0004FFDC |
? |
|
0004FFD8 |
? |
|
0004FFD4 |
? |
|
0004FFD0 |
? |
|
0004FFCC |
? |
|
0004FFC8 |
? |
|
0004FFC4 |
? |
|
0004FFC0 |
? |
|
0004FFBC |
? |
|
0004FFB8 |
? |
|
0004FFB4 |
? |
|
0004FFB0 |
? |
Görüldüğü üzere ESP'nin değeri, üst program tarafından 4
adet parametrenin yığına atılması ve ardından alt programın çağırılması sonucu
20 bayt azalmış ve 0004FFEC'ye gelmiştir. Şu kesindir ki alt programın bu yığın
üzerinde ihtiyaç duyacağı bölge, ESP'nin şu anki değerinin 4 fazlasından
itibarenki 16 bayttır. Buradaki parametrelere erişim için aşağıdaki ilk atama
yapılmalıdır:
push ebp
; 32 bitlik EBP yazmacı yığına atılıyor.
ESP 4 azalır.
mov ebp,
esp ; ESP, EBP'ye
kopyalanıyor.
Artık ESP'nin değeri 0004FFE8'dir. Bu noktanın yukarısı
kritik önem taşır ve genellikle sadece okunmalıdır. Ancak, üst program
tarafından aktif olarak kullanılmayacağı bilindiğinde aşağı bölgenin hiçbir
önemi yoktur. Alt programın, üzerinde işlem yapmak ve sonuç saklamak için 32
baytlık yerel belleğe ihtiyaç duyduğu varsayılsın. Bu durumda 0004FFC8
adresinden 0004FFE8 adresine kadar olan 32 baytlık bölge güvenli biçimde
kullanılabilir. Bu bölge üzerinde güvenli işlem yapmak için EBP yazmacı
kullanılmalıdır. Dolayısıyla 0004FFC8 adresi, [EBP - 32] biçiminde gösterilmelidir(32d
= 20H).
Yazmaçların veya kimi verilerin değerlerini geçici olarak
muhafaza etme işlemi, PUSH ve POP komutları ile klasik olarak yapılan
işlemlerdir. Dolayısıyla özel önem teşkil etmezler. Bu noktada dikkat
edilmesinin önem taşıyabileceği en büyük unsur, bu komutların 16 veya 32 bitlik
yazmaç, bellek veya mutlak verileri taşıyabildikleridir. Dolayısıyla, 32 bitlik
veriler üzerinde işlem yapılan bir uygulamada, yığın işaretçisinin kazara
32 bitlik doğal sınırlar dışına çıkarılması başarımın azalmasına yol
açacaktır. Olası tehlikelerin önüne geçmek amacıyla, doğal hizayı
bozabilecek özel durumların ardından ESP'nin yeniden hizalanması gerekir.
Aksi takdirde işlemcinin çalışmasında hiçbir teknik hata olmadığı halde kodun
işleyişi yavaşlayabilecektir.
Bu bölümün 2. kısmına burada nokta koyuyorum. Çünkü bundan
sonraki başlıklar, doğrudan kod yazımı ve teknikleri ile ilgili olacak. Sonraki
yazımı yayınlayana dek ilgilenen arkadaşların bu yazıdaki başlıklar
dahilinde soru işaretleriyle kalmamasını ümit ediyorum. Kafanıza
herhangi bir şey takıldığında bunu grup üzerinden sormakta tereddüt etmeyin.
Çünkü soracağınız soru, kimilerinin sormaya üşendiği bir soru da olabilir.
Dolayısıyla herhangi bir zahmete girmeden daha fazla kişiye faydalı oluruz. Yazmış
olduğum bu teknik makaleyi bilgisel hatalara karşı incelemiş olmama rağmen,
tespit edebileceğiniz hatalardan ötürü şimdiden herkesten özür diliyorum.
Hepinize iyi günler ve çalışmalar dilerim.
Cihan Atıl Namlı