Home > Assembler | Delphi > Fonksiyon Çağırım Mekanizmaları

Fonksiyon Çağırım Mekanizmaları

Posted on 23 Haziran 2007 | 8 Yorum

Gerek C++ programlamada gerekse Delphi ve diğer programlama dillerinde fonksiyonların stdcall, cdecl, pascal gibi terimler ile tanımlandığını görmüşüzdür. Eğer biraz da merakımız olmuşsa bunların az çok neyi ifade ettiklerini bir yerlerden okumuşuzdur. DLL yazanlar ve başka bir dilde yazılmış bir DLL’i kullanmaya çalışanlar mutlaka bu terimlerden en az biri ile aşina olmuştur. Çünkü DLL yazıp bunu dağıtacağınızda bu terimleri biliyor olmalısınız. Bu makalede bu terimlerin derinlerine ineceğiz ve Delphi inline assembler ile kullanımına örnek vermeye çalışacağız.

Giriş

Diyelimki, bir başka dilde kullanılabilecek olan bir kütüphane hazırlıyorsunuz. Fonksiyonlarınız, diğer bir kod parçasından belli parametrelerle çağrılacaktır ve sonucunda da bu kod parçasına çıktı verecektir. Fonksiyonlara parametre geçirebilmek için yerine göre registerlar, yerine göre de stack kullanılmaktadır. Aynı şekilde fonksiyonunuz parametreleri stack üzerine soldan sağa ya da sağdan sola doğru dizilebilmektedir. Bu durumda kodlar, fonksiyonu çağırırken register’lar üzerinden mi parametre yollayacak yoksa stack üzerinden mi? Stack üzerinden yollayacaksa hangi sıra ile yollayacak? İşte bu gibi karışıklıkları önlemek için çeşitli kabullenmelere gidilmiştir. Bu kabullenmelere Fonksiyon Çağırım Mekanizmaları ya da Fonksiyon Çağırım Anlaşmaları(Function Calling Convetion) denilmektedir.

Çağırımlar şu iki madde üzerinde yoğunlaşmaktadır:

  • Parametreler fonksiyon ya da prosedürlere nasıl geçirilecek?
  • Çağırımdan sonra çıktı olarak stack ve registerlar nasıl etkilenecek?

Delphi, beş farklı çağırım mekanizmasını desteklemektedir. Bunlardan pascal çağırımı, Delphi 1′de varsayılan olarak kullanılmıştır. Delphi 2′den sonra varsayılan olarak kullanılan çağırım ise register olmuştur. C/C++ derleyicileri tarafından varsayılan olarak kullanılan çağırım cdecl çağırımıdır. Bu yüzden C/C++ ile yazılmış bir kütüphaneyi kullanırken aksini belirtilmedikçe cdecl kullanılmalıdır. Son olarak Windows API’leri tarafından kullanılan stdcall çağırımı da Delphi derleyicisi tarafından kullanılabilmektedir. Beşinci çağırım tipi olan safecall, interface metodlarında kullanılmaktadır ve bu makalede bahsi geçmeyecektir.

Bu çağırımlar genelde fonksiyon çağrılmadan önce parametreleri stack üzerine yerleştirmektedir. Fonksiyonun da stack kullanabilme ihtimaline karşın fonksiyon için stack ile dışının ayırt edilmesi gerekmektedir. Yani:

push ebp
mov ebp, esp

bu talimatlar ile, ebp fonksiyon çıktısında tekrar eski değerini yüklemek(pop) için stack üzerine atılıyor ve stack taban işareteçisi(ebp), stack’ın tepe noktasını göstermektedir. Yani stack’ın sıradaki boş noktasını işaret eden esp artık stack’ın tabanı olmuş olacaktır. Böylece taban işaretçisine göre(ebp), parametreler ve local değişkenler birbirinden ayrılmış olacaktır. Intel tabanlı işlemcilerde stack işaretçişi geriye doğru işler. Yani stack’ın tepe noktası demek, sayı değeri olarak en düşük nokta demektir. Yukarıdaki iki satırlık assembler talimatını derleyici fonksiyonun girişinde otomatik olarak eklemektedir. Aynı şekilde “pop ebp” ile fonksiyonun çıkışında ebp’yi eski değerine döndüren talimatı da derleyici otomatik olarak ekler. Buna göre fonksiyon çağırımları genelde aşağıdaki yapıda olmaktadır.

Tabi bu şekil, sadece fikir olması için verilmiştir. Birazdan göreceğimiz fonksiyon çağırımları buna benzemekle birlikte farklılıkları mevcuttur.

Şekilde gördüğünüz gibi ebp’den önceki değerler ebp’den sayıca yüksekte olan değerlerdir. Ebp’den önceki değerler, fonksiyon çağrılmadan evvel stack üzerine yüklenmiş değerlerdir. Yani önceki stack alanını göstermektedir. Bu şekilde gördüğünüz gibi 3 adet parametre, çağrımdan önce stack üzerine yüklenmiştir. Ardından fonksiyon çağırımı ile birlikte yukarıda verdiğimiz 2 satırlık kod işletilmiş ve ebp şekilde görülen noktaya taşınmıştır. Artık stack üzerine eklenecek olan yeni değerler, bu ebp değerinden sonraki yani düşük alandaki bölgelere yerleştirilecektir. Ki şekle baktığımızda ebp’den sonra stack üzerinde 3 adet lokal değişken görünmektedir.

Genel yapı itibari ile çağırımlar bunun gibi işlemektedir. Fakat, şimdi göreceğimiz çağırımlara göre bu yapı farklılık arz etmektedir.

Pascal Çağırım Mekanizması

Delphi’de genel olarak pascal çağırımlı bir fonksiyonu aşağıdaki gibi yazarsınız:

function DenemeFonksiyon(b: Boolean; i: Integer; d: Double): Integer; pascal;

Pascal çağırımlı fonksiyonların parametreleri soldan sağa doğru yani sol öncelikli olarak stack üzerine yerleştirilirler. Ayrıca her parametre 4 byte ve katları şeklinde stack üzerine yerleştirilmeliler. Buna göre stack üzerine ilk yüklenecek olan parametre “b” parametresidir. Ardından i ve d parametreleri de stack üzerine yüklenir. Sonrasında ise bu fonksiyonumuz çağrılır. Yani:

push True //b parametresi True olsun
push $64 //i parametresi 100 olsun
push $4014cccc //5.2 sayısının yüksek kısmı
push $cccccccd //5.2 sayısının alçak kısmı
call DenemeFonksiyon

Yukarıdaki assembler talimatları “DenemeFonksiyon(True, 100, 5.2)” fonksiyon çağırımını yapmaktadır.

Buna göre yukarıdaki fonksiyonumuzun parametreleri stack üzerinde aşağıdaki gibi yerleşecektir:

Şekilde sayıları hexadecimal olarak verdim. Hesap makinesi ile ondalık karşılıklarını bulabilirsiniz.

Bir noktayı özellikle hatırlayalım. Bu çağırımda her bir parametre 4 byte ve katları olmak zorundadır. Burada gördüğünüz gibi Boolean tipindeki parametre de 4 byte uzunluğunda yer kaplamıştır.

Fonksiyon içinde mesela “i” parametresini kullanmak istediğimizde:

mov eax, [ebp+$10] //i parametresini eax'a kopyala

Veya 8 bytelık double tipindeki d parametresini virgüllü sayı stack’ına atmak için:

fld qword ptr [ebp+$08] //st(0) register'ı d parametresi ile dolduruldu.

yazmamız yeterlidir.

Tabi burada okunuş kolaylığı bakımından ebp+$10 yerine “i” ve ebp+$08 yerine de “d” yazılabilir. Derleyici otomatik olarak bunları yukarıda yazdığımız biçime dönüştürecektir.

Peki fonksiyon görevini yaptıktan sonra ne olacak? İlk önce ebp’ye eski değerini yükleyecektir. Ardından stack’a yüklenen parametreleri temizlemek için “ret $10″ talimatını gönderecektir. Buradaki $10(16 byte) çıkış değerinden sonra stack üzerine yerleşmiş olan parametrelerin toplam boyutudur. (4byte Boolean + 4byte Integer + 8byte Double = 16byte)

pop ebx
ret $10

Bu talimatı, derleyici otomatik olarak hesaplayıp fonksiyonun sonuna yerleştirecektir. Bizim yazmamıza gerek yok. “Call” komutu işletildiğinde, stack üzerine dönüş adresi yüklenecektir(şekilde [ebp+$04]). “Ret $10″ talimatı ile de [ebp+$04] ‘deki dönüş adresi çekilecek ve $10 byte kadar stack temizlenecektir. Yani dönüş adresi ile birlikte parametreler de stack üzerinden temizlenecek.

Register Çağırım Mekanizması

Register çağırım mekanizması aşağıdaki gibi tanımlanmaktadır:

function DenemeFonksiyon(b: Boolean; i: Integer; d: Double): Integer; register;

Sondaki register; belirticisine ihtiyaç yoktur. Çünkü Delphi 2′den sonraki sürümlerde bu fonksiyon çağırımı varsayılan olarak kabul edilmiştir. Yani son kısma “register;” yazmasanız dahi derleyici bunu öyle kabul edecektir.

Bu çağırım tipinde, bazı parametreler register’lar üzerinde saklanır, bazıları da stack üzerinde saklanır.

Bildiğiniz gibi register’lar ile işlem yapmak, stack ile işlem yapmaktan çok daha hızlıdır. Çünkü register’lar CPU içindedir, stack ise hafızada bulunan bir adrestedir. Bu yüzden CPU, register’lara daha çabuk erişir ve daha hızlı işlem yapar.

Bu nedenle derleyici, ilk olarak parametreleri register’lara atacaktır. Ama bazı parametreler register’lara sığmadığında veya register’lar yeterli gelmediğinde stack kullanılmaktadır. Bu yüzden, Delphi’nin varsayılan olarak kullandığı bu çağırım en verimli ve en hızlı olan seçimdir.

Parametreler register’lara yerleştirilirken soldan sağa eax, edx ve ecx register’larına yerleştirilirler. En soldaki parametre eax içine sonraki edx ve ardından üçüncü parametre de ecx içine yerleştirilir. Eğer yerleştirilecek olan parametre 4 byte’lık bu register’lara sığamayacaksa, pascal çağırımında olduğu gibi stack üzerine yerleştirilir. Yani yukarıdaki fonksiyonu çağırmak için:

push $4014cccc //5.2 sayısının yüksek kısmı
push $cccccccd //5.2 sayısının alçak kısmı
mov al, True //b parametresi True
mov edx, $64 //i parametresi 100
call DenemeFonksiyon

Bu kodlar “DenemeFonksiyon(True, 100, 5.2)” çağırımı ile aynı görevi yapmaktadır. Stack ve register yerleşimleri ise şu şekilde olmuştur:

Dönüş adresi => [ebp + $04]
d: Double Parametresi => [ebp + $08]
b: Boolean Parametresi => eax(al içinde)
i: Integer Parametresi => edx

Pascal çağırımda olduğu gibi, fonksiyonun çıkışında stack’da bulunan parametreleri temizlemek için derleyici tarafından “ret $08″ talimatı eklenir. Buradaki $08 değeri, stack’daki “d” parametresinin boyutudur. Başka parametreler de stack’da olsaydı buraya toplam boyut girilirdi.

pop ebx
ret $08

Cdecl Çağırım Mekanizması

Örnek olarak verdiğimiz fonksiyonu, şimdi de cdecl tanımlayıcısı ile çağıralım:

function DenemeFonksiyon(b: Boolean; i: Integer; d: Double): Integer; cdecl;

Pascal çağırımının tersine, bu çağırımda parametreler sağdan sola doğru stack üzerine kaydedilir. Yani bu fonksiyonu bir yerlerden çağırmak için şöyle bir şey yazmamız gerekir:

push $4014cccc //5.2 sayısının yüksek kısmı
push $cccccccd //5.2 sayısının alçak kısmı
push $64 //i parametresi 100 olsun
push True //b parametresi True olsun
call DenemeFonksiyon

Gördüğünüz gibi en sağdaki parametre ilk başta stack üzerine yerleştirilmektedir. Sonra ikinci sıradaki ve en sonunda ise birinci parametre stack üzerine yerleştiriliyor. Pascal çağırımında olduğu gibi bu cdecl çağırımında da her parametre 4byte ve katları şeklinde stack üzerine yerleştirilir. Yukarıdaki kodlar ile parametrelerin stack üzerindeki yerleşimleri aşağıdaki gibi olmaktadır:

Buna göre d parametresi [ebp+$10] içinde, i parametresi [ebp+$0C] içinde ve b parametresi de [ebp+$08] içindedir. Fonksiyonun dönüş adresi [ebp+$04] üzerinde bulunmaktadır.

Pascal çağırımından tek farkı parametrelerin sağdan sola yerleştirilmesi değildir. Ayrıca fonksiyon çıkışı esnasında da bu tip fonksiyonlar farklılık göstermektedir. Fonksiyon çıkışı esnasında stack üzerindeki parametreler temizlenmez! Yani sadece “ret 0″ talimatı ile çıkılır. Bunun ardından esp 16 byte geriye alınır. Buradaki 16 byte stack üzerindeki toplam parametre boyutudur. esp’nin yani stack işaretçisinin 16 byte geriye alınması ile stack’ın tepe noktası parametrelerin berisine çekilir. Bundan sonra yapılacak her stack atama işlemi(push), parametrelerin üzerine yazacaktır. Dolayısı ile esp değeri fonksiyon çağırımından önceki hale dönecektir. Derleyici aşağıdaki kodları fonksiyonun sonuna yerleştirecektir:

pop ebp
ret

Ayrıca aşağıdaki talimatı da fonksiyon çağırımının sonuna sizin eklemeniz gerekecektir.

...
call DenemeFonksiyon
add esp, $10

Tabi kendiniz inline assembler ile cdecl çağırımı yapacaksanız “add esp, $10″ talimatını call komutundan sonra kendiniz eklemelisiniz.

C/C++ kütüphanelerini kullanmak istediğinizde bu çağırım tipini kullanabilirsiniz. Ama genelde stdcall ve safecall çağrıları tavsiye edilir. Stdcall çağırımları, cdecl’e göre daha verimlidir, bu yüzden tavsiye edilir. Win32 işletim sistemi fonksiyonları da stdcall çağırımı ile çağrılmaktadır. Ama diğer işletim sistemlerinde çağrımlar cdecl olarak tanımlanmıştır. Tabi bunlar arasında en hızlısının register çağırımı olduğunu söylemiştik.

Stdcall Çağırım Mekanizması

Örnek fonksiyonumuzu stdcall kullanarak tanımlayalım:

function DenemeFonksiyon(b: Boolean; i: Integer; d: Double): Integer; stdcall;

Aynen cdecl cağırımında olduğu gibi stdcall çağırımında da parametreler sağdan sola olmak üzere stack üzerine yerleştirilirler. Parametreler 4 byte ve katları olarak stack üzerine atılırlar. Parametreler fonksiyon çağırımının sonunda “ret $10″ talimatı ile temizlenirler. Buradaki $10 değerinin, stack’taki toplam parametre boyutu olduğunu belirtmiştik.

Aşağıdaki tabloyu Delph’nin yardım dosyalarından çevirerek aldım:

Çağırım Mekanizmaları

Direktif

Parametre Sırası

Temizleme

Register’dan parametre gönderiliyor mu?

register

Soldan-Sağa

Fonksiyon tarafından

Evet

pascal

Soldan-Sağa

Fonksiyon tarafından

Hayır

cdecl

Sağdan-Sola

Çağırıcı tarafından

Hayır

stdcall

Sağdan-Sola

Fonksiyon tarafından

Hayır

safecall

Sağdan-Sola

Fonksiyon tarafından

Hayır

Sonuç

Delphi tarafından kullanılacak olan DLL yazdığınızda kesinlikle register çağırımını kullanmalısınız. Çünkü en verimli yöntem register çağırımıdır. Ama dezavantaj olarak Delphi ve C++ Builder dışında fazla bir derleyici tarafından bu çağırım tanınmamaktadır. Register çağırımı, Visual C’de bulunan “_fastcall” çağırımı gibi derleyici’ye bağlı olan çağırımlardır.

Derleyici’den bağımsız fonksiyonlar yazacağınızda stdcall çağırımı tercih sebebiniz olmalıdır. Stdcall çağırımları, WinAPI fonksiyonlarını çağırabilen bütün derleyicilerde bulunan bir özelliktir. Fakat bu durumda, Delphiye özel veri tiplerinden feragat etmeniz gerekmektedir. Mesela String, Booelan, real48 ve nesneleri stdcall çağrımlı fonksiyonlarda kullanamazsınız.

Pascal çağırımı, 16-bit windows zamanında kalma bir çağırım mekanizmasıdır. Win16 api fonksiyonları pascal çağırımı olarak tanımlanmıştır. Artık Win32 api’lerinde bu kullanılmamaktadır.

DLL’de export edilen fonksiyon isimleri, çağırımın tipine göre çeşitli ekler almaktadır. Delphi 5′in altındaki versiyonlarda değişik çağırım tipi kullansanızda export edilen fonksiyonların isimlerine herhangi bir ek getirilmemektedir. Bu isim ekleri Win32′de export edilen fonksiyonlar için önemlidir.

Diğer derleyiciler değişik isimlendirmelere gitmiş olabilirler. Ama genelde bir C derleyicisi cdecl tipindeki fonksiyonlara isim verirken fonksiyonun önüne altçizgi getirir. Ardından bir @ işareti ile fonksiyonun parametrelerinin toplam byte değerini ekler. Cdecl çağırımı ile tanımladığımız yukarıdaki fonksiyonumuz bir DLL’den export edildiğinde “_DenemeFonksiyon@16″ şeklinde isimlendirilir. C++’da bu isimlendirme extern “C” olarak tanımlanmadıkça daha da kötüdür. Extern “C” tanımlanmadıkça, parametrelerin boyutu, tipleri derleyiciye bağlı olarak değişik biçimlerde fonksiyon ismine eklenirler. Pascal çağırımı olarak tanımladığınızda ise, fonksiyon isimlerinin tüm harfleri büyük olarak yazılır. Ama önceden de dediğimiz gibi pascal çağırımı Win32′de kullanılmamaktadır.

DLL’deki fonksiyon isimlerini görebilmek için Delphi ile birlikte bin klasöründe gelen Tdump.exe aracı kullanılabilir. Elinize fonksiyon çağırımı belli olmayan bir DLL geldiğinde bu aracı kullanarak isimlerden bir şeyler çıkarmaya çalışabilirsiniz.

Hepinize hayırlı çalışmalar…

Fatih Tolga Ata © 2007

» Tags: , , , , , , , ,

8 Yorum

  • [...] barındırdığını göreceksiniz. Eğer buradaki Delphi ve Assembler ile ilgili makaleyi ve buradaki fonksiyon çağırım makenizmalarını okudu iseniz şimdi anlatacaklarımız size tanıdık [...]

    • [...] çeşit fonksiyon çağırım mekanizması vardır. Bunların ne olduğunu ayrıntısı ile anlatan buradaki makalemize göz atabilirsiniz. Hatta okumadı iseniz bir göz gezdirmenizi şiddetle tavsiye [...]

      • At 2009.08.13 03:12, Tuna said:

        Ayrıntılı ve akıcı yazı için teşekkürler.

        • At 2010.12.16 16:40, mustafa said:

          verdiğiniz bilgiler için çok teşekkür ederim
          delphi dili gerçekten çok derin ve güçlü bir dil olmasına rağmen maalesef son zamanlarda hakkettiği ilgiyi pek görememekte.
          bundan dolayı dökümantasyon sıkıntısının yaşandığı bu zamanlarda böyle derin ve güzel makaleler bulmak çok zor.
          çalışmalarınızın devamını diliyorum.

          • At 2012.01.20 15:47, Ahmet Yeşilçimen said:

            Merhaba Fatih hocam, makalenizi severek ve isteyerek okudum fakat aklıma takılan bir kaç husus var eğer aklımdaki acizane sorulara cevap verebilirseniz çok sevinirim.Register Çağırım Mekanizması Kısmında push 4014cccc 5,2 sayısının yüksek kısmıpush cccccccd 5,2 sayısının alçak kısmı bu kısmın yüksek ve alçak kısmını anlayamadım ikisinden biri silinince hata veriyor ayrıca hesap makinesndn baktım fakat çok büyük değerler var nasıl elde ettiniz anlam veremedim

            • At 2012.01.20 17:13, Fatih Tolga Ata said:

              Üçüncü parametre double yani kayan nokta tipinde bir parametredir. Kayan noktalı sayılar iki parça halinde stack’a atılırlar. Yani o iki satır “5.2″ değeridir ve stack’a ilk önce yüksek kısmı sonra alçak kısmı yollanır. Yüksek ve Alçak değimleri ise 16byte’ın sol taraftaki 8 byte’ı yüksek sağ taraftakı 8 byte ise alçak oluyor.
              Edit: Assembler ile uğraşmaya çok uzun zaman olmuş unutmuşum bazı şeyleri. Ama şuradaki hesap makinesi işini görür: http://www.binaryconvert.com/result_double.html?decimal=053046050

            • At 2012.01.20 22:07, Ahmet Yeşilçimen said:

              İlginiz için çok teşekkür ederim. Sayın Fatih Hocam, bir sorumda var umarım rahatsız etmiyorumdur.

              mov eax, True //b parametresi True
              mov edx, $64 //i parametresi 100
              push ebx
              push 01000000000100001100110011001100b //4.2
              push 11001100110011001100110011001101b //4.2
              call DenemeFonksiyon
              pop ebx

              Double tipine SizeOf(Double) ile baktım 8 byte olarak bildirdi. 16 byte yazmıssınız kafam karıstı. 8 byte * 4 = 32 bit Yüksek Kısım 32 Alçak Kısım 32 bit
              Hex de ise

              push $4010CCCC //4.2
              push $CCCCCCCD

              simdi burada Yüksek Kısımda 4 byte oluyor sanırsam 4 byte ama 8 karakter var :) biraz saçma oldu ama sanırsam 4 karakteri 2 byte mı yapıyor ?

              birde push $0101010100101 //misal..
              dediğimiz zaman direkt olarak ECX e atıyor sanırsam peki neden direk ECX e atıyor.
              peki baska bir Registeri Stack’a atmak istediğimde ne yapmam gerekicek şu sekilde denedim

              Push eax
              push $cccdd425 //misal
              push $cccddd365 //misal
              pop eax

              bu sekilde eax ı stacka ittiğim halde Double yine 4.2 oldu ayrıca hatada vermedi açıkcası kafam karısık hocam yardım ederseniz benimle bir likte birçok arkadas da öğrenmis olacak Saygılar Sevgiler….

              • At 2012.01.20 22:08, Ahmet Yeşilçimen said:

                pardon
                Double tipine SizeOf(Double) ile baktım 8 byte olarak bildirdi. 16 byte yazmıssınız kafam karıstı. 8 byte * 4 = 32 bit Yüksek Kısım 32 Alçak Kısım 32 bit

                **
                8 byte * 8 = 64 bit yüksek kısım 32 bit alçak kısım 32 bit…

                (Required)
                (Required, will not be published)

                Switch to our mobile site