> Assembler | Delphi > Delphi ve C++ Builder ile Assembler

Delphi ve C++ Builder ile Assembler

Bu makalede, başlangıç düzeyi için, Borland Inline Assembler(Basm) kullanımını göstermeye çalışacağız. Basm kullanarak Delphi’de ve C++ builderda assembler kullanarak, kodlarınızı daha da hızlandırabilirsiniz. Bildiğinizi gibi Delphi ve C++ Builder’a monte edilen FastMM projesi, temelde çokca kullanılan fonksiyonların, optimize edilmiş assembler hallerini barındırmaktadır. Böylelikle hem derleme performansı artmakta, hem de runtime’da çalışan kod daha verimli ve hızlı çalışmaktadır. Fakat derleyici her ne kadar kodu optimize etse de ileride göreceğiniz gibi bazı yerlerde yapay zeka yetersiz kalmaktadır. Bu yüzden bu kısımlara müdahale edip kodların boyutunu azaltıp, performansını artırmaya çalışacağız.

Ayrıca derleyicinin kod üretimi(code generation) safhasının nasıl işlediği hakkında da fikir sahibi olacaksınız.

Örneklerimizi Delphi üzerinde yapacağız. Az da olsa assembler bildiğinizi kabul ediyorum.

Fonksiyon ve Parametreler

Klasik makale ve kitap yapısına aykırı olarak direk örnekler ile başlamak istiyorum. Çünkü bu makale, assembler diline giriş değil, assembler’ın Delphi ve CB’de kullanımı ile ilgilidir.

Şimdi yeni bir uygulama açalım ve şöyle bir fonksiyon yazalım:



		

Forma bir button ekleyelim ve bir parametre ile birlikte bu fonksiyonu OnClick olayında çağıralım. Şimdi Fonksiyon içinde ilk satıra breakpoint koyalım ve programı çalıştıralım. Button’a basıp breakpoint koyduğumuz yere gidelim. Eğer CPU penceresi açık değilse View menüsünden ya da ctrl+alt+c ile CPU penceresini açalım. CPU penceresinde Ctrl+O ile mevcut satıra gidebilirsiniz.

Buradan gördüğümüz kadarı ile “Sayi” parametresi, eax register’ı ile geliyor ve tekrar çıktı olarak eax olarak çıkıyor. Bu register çağırımı, Delphi’de default olarak kullanılır. Burada yapılan işlem çok basittir. 2 ile çarpmak için, değişkenin kendisini kendisi ile toplatılıyor. Ret ile de tekrar çalışma satırına dönülüyor. Şimdi buna göre bunu inline assembler ile nasıl yapabileceğimize bakalım:



		

Şimdi form üzerine bir edit koyalım ve buttonun OnClick olayını aşağıdaki gibi değiştirelim:


		

Bir breakpoint ile CPU pencersinde şu satırlara bakalım:


		

StrToInt çağırımından sonra sonuç eax içine alınıyor. Bir önceki örnekte CarpBakalimADD fonksiyonumuz da eax’dan parametre alıp, yine eax’a çıktı verdiğini gördük. Böylece ikinci satırdaki fonksiyon çağırımından sonra sonuç, eax içinde olacaktır. En son satırda da eax, esi içine kopyalanıyor.

İki ile çarpma işlemi iki yolla daha yapılabilir. Bir tanesi mul komutunu kullanarak, diğeri shift ile kaydırarak. Mul komutu eax içindeki değeri başka bir register’daki değer ile çarpar ve sonucu edx:eax register çiftinde tutar. Sonuç için iki çift register gereklidir çünkü iki adet 32 bit sayı çarpıldığından maksimum 64 bit olacaktır.

Bir asm bloğu için altın kural, EDI, ESP, ESI, EBX ve EBP registerlarını korumasıdır. Ama EAX, ECX ve EDX registerlarını da özgürce değiştirebilmesidir. Bu açıklamayı Delphi yardım dosyalarından da bulabilirsiniz.

Mul komutunun edx ve eax’ı kullandığından bahsetmiştik. Öyle ise yukarıdaki tanıma göre, fonksiyonumuzu mul ile de yapabiliriz:



		

Ekstradan ecx de kullanıyoruz ama bu sorun değil. Çünkü ecx’i de istediğimiz gibi değiştirebiliriz. Altın kuralı hatırlayın.

Sonucumuz eax’in içine sığabilecek durumda olduğunda, mul komutunun sonucu eax’de olacaktır. Çünkü edx:eax sonuç çiftinde, edx sayının yüksek kısmını, eax ise düşük kısmını tutacaktır. Ama sonuç, eax’e sığamayacak kadar büyükse, yani sonucun yüksek kısmı edx’e aktarıldığında, fonksiyonun çıktısı yanlış olacaktır. Çünkü fonksiyon çıktısı eax’de çıkmaktadır.

Aynı şeyi shift ile kaydırarak yapalım.


		

Şimdi bu üç tanımlamadan hangisinin hızlı olduğu bizim için önemli. Intel ve AMD dökümanlarından hangisinin daha hızlı olduğunu latency(saykıl) değerlerini hesaplayarak anlayabiliriz. P4 işlemci ile add ve mov komutları 0.5 saykıl gecikmeye sahip. Mul komutu ise 14-18 saykıl arasında değişiyor. Shl ise 4 saykıl gecikmeye sahip. Delphi’nin seçimi bu değerlere göre en hızlı ve en az yer tutan seçenektir. Aynı şey P3 ve Amd için de geçerlidir.

Basit bir Optimizasyon

Aşağıdaki bölme işlemine göz atalım:


		

Derleyici bu kodu aşağıdaki gibi derleyecektir.(CPU penceresinden bakabilirsiniz).


		

Eğer sağlam bir optimize işlemi yapmak istiyorsanız, ilk başta optimize yapacağınız kodları güzel bir şekilde analiz yapmalısınız. Şimdiki gelecek bir kaç paragraf kodların nasıl çalıştığını anlatmaktadır.

İlk baştaki begin’den sonra gelen 3 satırlık assembler kodları, bir çeşit hazırlık kodlarıdır. Önceki örnekte altın kuralımızda bahsettiğimiz gibi EBX register’ı serbest bir şekilde değiştirilemez. Serbest bir şekilde sadece EAX, ECX ve EDX register’larını değiştirebildiğimizi hatırlayınız. EBX register’ı ilerleyen satırlarda IDIV tarafında kullanılması gerekecek. Bu yüzden ebx’i stack üzerine almamız gerekecek. Bunu ilk satırdaki "push ebx" talimatı ile yapıyoruz.

Stack’a ebx’i atmak ile, stack işaretçisinin değeri “4” byte düşüyor. Bildiğiniz gibi Intel tabanlı işlemcilerde stack işaretçisi, stack’a bir değer atmakla yükselmez, tam tersine değer düşer. Her register 32 Bit olduğundan, bir register’ı stack’a atmak, işaretçi değerini 32 bit yani 4 byte azaltır.

Fonksiyonun iki parametresi başlangıçta eax ve edx registerlarında saklanırlar. Bolunen parametresi eax içinde, Bolen parametresi de edx içinde olması, Delphi’nin standart register çağrımları tarafından belirlenmiştir. "push ebx" talimatından sonra gelen iki adet mov komutu ile edx ve eax registerları, ebx ve ecx içine kopyalanıyor.

Bu hazırlık aşamasından sonra esas bölme işlemini yapan komutlarımız sonraki gelen 3 satırdır. Bu satırların ilkinde ecx’i tekrar eax üzerine kopyalandığını görüyorsunuz (mov eax, ecx). Bu satır gereksizdir. Çünkü eax değişmemiştir.

Cdq komutuna geçmeden önce idiv’den bahsetmek istiyorum. Idiv komutu, edx:eax değer çiftindeki 64 Bitlik sayıyı, bu örnekteki 32 Bitlik sayı olan ebx’e bölmektedir. Yani Bolen ebx’de ve Bolunen ise edx:eax çiftinde olmalıdır. Idiv komutu edx:eax çiftini, ebx ile böler. İşte cdq burada devreye giriyor. Cdq komutu, eax içinde bulunan sayıyı işareti ile birlikte edx:eax register çiftine taşır. Yani Dword(32Bit) olan bir değeri QWord(64Bit) yapar. Cdq komutu, idiv’den önce gereklidir. Çünkü eax’in 31’inci biti işaret bitidir. Bu yüzden eax’deki değeri edx:eax register çiftine taşıyabilmek için sayının işaretini de göz önünde bulundurmalıyız. Çünkü edx:eax register çifti için, 63’üncü bit yani, edx’in 31’inci biti, sayının işaret biti olmuştur artık. İşareti ile edx:eax’e taşıma işlemini ise cdq yapmaktadır.

Idiv komutunun sonucu yani bölüm değeri, eax içinde bulunur. Bölme işleminin kalan değer ise edx içinde bulununur.

Bizim için önemli olan şey, eax içinde sonuç değerimiz ile fonksiyondan çıkış yapmamızdır. Zaten idiv komutunun sonucu da eax içinde bulunmaktadır. Yani yapacağımız bir işlem kalmadı. Fonksiyonun sonucu eax içinde çıktı vermektedir.

En sonda bulunan iki satırlık kod ise, bitiş kodlarıdır. Hazırlık kodları ile ebx’i stack’a atmıştık, şimdi tekrar ebx’in ilk baştaki değerini stack’dan almalıyız. Bunun için pop komutunu kullanıyoruz ve ret ile de fonskiyondan çıkıyoruz.

Şimdi bu kodlar ile fonksiyonumuzun assembler halini yazalım. Yapacağımız işlem oldukça basit sadece begin yerine asm yazıyoruz ve ret komutu yerine end ile fonksiyonumuzu bitiriyoruz.



		

Kodları incelerken bir satırın fazlalık olduğunu görmüştük. Burayı açıklama satırı ile çıkardık.

Ayrıca farkında iseniz ecx hiç kullanılmıyor. Tek yaptığımız edx:eax’i ebx’e bölmek. Bu yüzden eax’deki Bolunen parametresini ecx’de saklamamıza gerek yok:



		

Farkında iseniz, ecx register’ı boşa çıktı. Altın kurala göre ecx’i serbestçe kullanabildiğimizi biliyoruz. Ama ebx öyle değildi. Bu yüzden ebx’i kullanabilmek için stack’da değerini saklayıp ardından tekrar stack’dan değerini okutmalıydık. Ama ecx boşa çıktığına göre artık ebx kullanmamıza gerek kalmadı. Haliyle ebx yerine ecx kullanacağımızdan, ebx’i stack’de saklamak için pop ve push komutlarına da ihtiyacımız yok artık:



		

Yeni kodlarımıza göre artık edx:eax çiftini ecx register’ına bölüyoruz. Kodları temizlediğimizde, optimize edilmiş son kodumuz şu şekilde olacaktır:



		

Gördüğünüz gibi fonksiyonumuz daha hızlı ve daha az yer kaplayarak optimize edilmiş duruma geldi.

Lokal Sabitler ve Virgüllü Sayılar

Aşağıdaki fonksiyona göz atalım:


		

CPU penceresinden aldığımız derlenmiş kodlar aşağıdaki gibidir:


		

İlk başta “begin” kelimesinin karşılığı olan 3 satırlık koda bakalım:


		

Burada yapılan kısaca, stack üzerinden yer ayırmak içindir. Ayrılan bu parçaya stackframe denilmektedir. Bir stackframe’de iki adet işaretçi bulunur. Birisi, taban işaretçisi, diğeri stack işaretçisidir. Taban işaretçisi(basepointer) ebp içinde bulunurken, stack işaretçisi(stackpointer) ise esp içinde bulunur. Taban işaretçisi, stack’ın hafızadaki ilk hücresine işaret eder. Yani ebp içinde stack’ın başlangıç adresi tutulur. Aynı şekilde, stack işaretçisi ise, stack’ın mevcut pozisyonuna işaret eder. Push ve pop komutu ile stack işaretçisinin gösterdiği yer, azalır ve artar.

İlk satırdaki kod ile ebp stack üzerine saklanıyor. Çünkü bu değer fonksiyondan çıkıldığında eski değerine tekrar kavuşmalıdır. Taban işaretçisinin yeni değeri ikinci satırdaki mov komutu ile esp’deki değer olarak belirleniyor. Yani stack işaretçisinin değeri, taban işaretçisine kopyalanıyor. Böylelikle, taban işaretçisi olan ebp, stack’ın tepe noktasına işaret ediyor. Tabi hatırlarsanız stack, Intel işlemcilerde geriye doğru işlemektedir. Yani stack’ın tepe noktası, sayı değeri olarak en düşük noktadır. Son satırdaki komut ile, stackpointer 8 byte kadar ilerletiliyor.

Bu üç satırlık kod ile fonksiyonumuz için bir stackframe oluşturulmuş oluyor. Şimdi esas işlemi yapan kodlara bakalım:



		

Burada görülenler basit bir virgüllü sayı hesabıdır. İlk satırdaki fld komutu, LocalConst1 isimli sabiti, virgüllü sayı stack’ının tepesine taşır. Buradadaki komutlar, bildiğimiz mul, add gibi komutların virgüllü sayı (floating point) için olan versiyonlarıdır. Başlarına “f” harfi getirilmesi ile bu fonksiyonların floating point yani virgüllü sayı işlemi yapan komutlar olduğunu anlıyoruz.

Bizim için en mühim nokta “qword ptr [ebp+$08]” gibi ifadelerdir. “Qword ptr”, sonrasında gelecek olan değerin tipini belirtmektedir. Qword ile değerin 64 bit yani 8 byte’lık bir değer olduğunu, ptr ile de, değerin aslında başka bir değere işaret eden bir adres olduğunu gösterir. Sonrasında gelen [ ] köşeli parantezleri içine yazılan değer, işaret edilen adrestir. Yani [ebp+$08] ile ifade edilen, ebp’den 8 byte yani 64bit önceki değerdir. Bildiğiniz gibi ebp, bu fonksiyon için oluşturulan stackframe’in başlangıç adresini tutmaktadır. Öyle ise ebp’den 8 byte önceki değer, önceki stackframe’e işaret etmektedir. Fonksiyonumuzun Double tipinde bir parametre aldığını biliyoruz. Önceki örneklerimizde, fonksiyon parametreleri eax, ecx gibi registerlarda saklanıyordu. Ama Double tipi 64bit uzunluğundadır. Ve eax gibi registerlar ise 32bit uzunluğundadır. Double tipi bu registerlara sığmayacağından, Delphi burada bir seçim yapmıştır ve, parametreleri stack üzerine taşımıştır. Tabi burada double tipindeki bir değer için stack yerine, virgüllü sayı registerları kullanılsa idi daha verimli ve hızlı olacaktı. Borland’ın seçimi stack çözümü üzerine olmuştur. Kısacası, [ebp+$08] ile ifade edilen, fonksiyonumuzun ilk ve tek parametresi olan "Param"dır. “qword ptr” ise bu parametrenin 64bit uzunluğunda olduğunu ifade ediyor.

“faddp st(1)” satırına bakalım. “st(1)”, ikinci sıradaki virgüllü sayı registerıdır. İkinici sıradakidir, çünkü birincisi st(0) register’ıdır. Virgüllü sayı registerları bir stack üzerinde birleşik halde bulunurlar. Ve virgüllü sayı komutları, dolaylı olarak bu stack’ın tepesinde bulunan st(0) ile işlem yaparlar. Fld komutu, değeri virgüllü sayı stack’inin tepesine yani st(0)’a yerleştiriyor. Bunu ilk satırdaki fld komutu ile gördük. Fmul işlemleri yapıldığında sonuç yine st(0) üzerinde duracaktır. Çünkü virgüllü sayı komutları st(0) ile işlem yapıyorlardı. İki fmul komutundan sonra gelen fld komutu LocalConst2 değerini st(0)’a yerleştirir. Haliyle st(0)’da bulununan önceki değer st(1)’e kayar. Ardından gelen fmul ile işleminin sonucu st(0)’da depolanır. Devamında gelen “faddp st(1)” ile st(0)’da bulunan değer ile st(1) toplanır ve toplam sonucu st(0) üzerine kaydedilir. st(1) içinde “LocalConst1*Param*Param” sonucu olduğunu biliyoruz. Çünkü birinci sıraya “LocalConst2” fld ile yerleştirilmiş, “LocalConst1*Param*Param” sonucu st(0)’dan st(1)’e kaymıştır.

“faddp” komutundaki “p” harfi, st(1)’ın stack’dan çıkarılması yani “pop” yapılmasını sağlar. Böylece sonuç st(0) üzerinde durmakla birlikte, st(1)’de stack’dan silinecektir.

Buraya kadar olan komutlar “LocalConst1*Param*Param + LocalConst2*Param” işlemi gerçekleştirdi ve sonucu st(0) üzerine kaydetti. Sonrasında gelen fadd ile LocalConst3 değeri st(0) üzerine eklenmiştir. Fonksiyonun çıktısı st(0) üzerinden verilmektedir.

Bu işlemleri FPU ve CPU pencerelerinden debug yaparak takip edebilirsiniz.

Sonrasında gelen 3 satırlık kod gereksiz parçalar taşımaktadır. “fstp” kısmına bakalım. “fstp” ile sonuç, fonksiyonun stackframe’i üzerine taşınırken, fld ile tekrar stacktan yükleniyor. Bu işlem gereksizdir. Arada bulunan wait komutu gereklidir. Çünkü floating point komutları sırasında herhangi bir hata olup olmadığının kontrolü gerekmektedir. Wait komutu için ayrıntılı bilgi Intel’in yardım dosyalarında bulunmaktadır.

Son olarak, fonksiyonumuzu sonlandıran:



		

bu komutlar, stackframe’i ortadan kaldırıyor. Ard arda iki adet pop çağrılmak ile esp’ye başta eklenen 8 byte geriye çekiliyor, yani eski pozisyonuna geliyor. Sonrasında ise ilk başta stack üzerine kaydettiğimiz ebp’nin eski değeri tekrar yükleniyor.

Şimdi bu fonksiyonu assembler versiyonunu yazalım ve optimize edelim:



		

Bunu derlemeye kalktığınızda “faddp st(1)”in tanınamadığına dair bir hata mesajı alacaksınız ve bu kodları derleyemeyeceksiniz. Intel dökümanlarına baktığınızda, faddp komutunun sadece bir çeşitinin olduğunu göreceksiniz. Yani başka kullanımı yoktur. Yani her zaman st(1) ile st(0)’ı toplar ve sonucu st(0)’a yazar. Bu yüzden st(1)’i yazmaya gerek yoktur. st(1) yazan kısmı açıklama satırı yapıyoruz. Bundan sonra program derlenecektir.

Bu fonksiyonu 2 değerini verek çağırdığımızda sonucun “1*2*2 + 2*2 + 3 = 11“ olması gerekirken 3 sonucunu verdiğini göreceğiz. Peki nerede yanlış yaptık?

FPU ve CPU pencereleri ile adım adım debug yaptığımızda bu sorunu çözebileceğiz. CPU View’e bakarsanız baş tarafa eklediğimiz “push ebp” ve “mov ebp, esp” talimatları iki defa tekrar etmiş. Biz kodları direk kopyala yapıştır ile aldığımızdan, derleyicinin otomatik olarak eklediği bu kodlarını da almış olduk. Bu yüzden bu ilk iki satırı açıklama satırı ile kadırmalıyız. Çünkü zaten derleyici bu satırları ekliyor. Fonksiyonun girişinde ebp’nin otomatik olarak stack’a alınması ile haliyle bizim tekrar ebp’yi en son satırda pop yapmamız da gereksizdir.

Bu sorunu hallettikten sonra pascal versiyonu ile aynı sonuca ulaşıyoruz. Artık optimize işlemine geçebiliriz.

İlk başta en son satırlarda bulunan gereksiz fstp ve fld komutlarını çıkaralım. Bunların neden gereksiz olduğunu yukarıda bahsettik.



		

Bu işlemden sonra kodlara baktığımızda tüm işlemlerin virgüllü sayı registerları ile yapıldığını görüyoruz. Yani ek bir stackframe’e ihtiyaç yok. Bu yüzden stack frame ekleyen “add esp, -$08” satırını da kaldırmalıyız. Haliyle esp değişmediğinden en sonda bulunan iki adet pop komutu ile esp’nin eski değerini tekrar yüklememize de gerek yok.



		

Şimdi açıklama satırlarını temizleyip optimize edilmiş kodumuzu analiz etmeye devam edelim.



		

Bu kodlara baktığımızda “Param” parametresi üç defa üst üste fmul komutu ile hafızadan FPU’ya aktarılıyor. Hafızadan alma işlemini bir defa yapıp, sonrasında bunu tekrar kullanmamız daha verimli ve hızlı olacaktır. Bunun için ilk başta “Param” parametresini fld ile st(0) içine alalım ve hep bunu kullanalım.



		

Kodların açıklamaları yanlarında mevcut. Burada ekstradan ffree adlı komutu gördük. Kodlar bu haliyle optimize edilmiş gibi.

Son olarak bir optimizasyon daha yapalım. LocalConst1 sabitimizin değeri 1’dir. Bu değeri fld ile hafızadan yüklemek yerine, st(0)’ı bir yapan fld1 komutu kullanılabilir. Böylelikle hem hafıza kullanımını hem de saykıl gecikmesini azaltmış olacağız. Kodlarımızın son hali şu şekilde olacaktır:



		

Bu bölümde bu kadar yeterli. Gelecek bölümlerde if-else, while-do, for döngüsü gibi yapılara ve sse, sse2 ve mmx gibi teknolojilere de göz atmaya çalışacağız.

» Tags: , , ,

11 Comments

(Required)
(Required, will not be published)