« Back
in compilers read.

Valhallaya gitmek = DLL yazmak (Name Mangling, Object Layout ve dahası).

Ana amacı dinamik kütüphaneler hakkında deneyimler içeren bir yazı planlıyorum.
İlk önce dinamik kütüphanenin ne olduğunun herkes tarafından bilindiğini varsayıyorum, hepimizin windows ortamında geliştirme yaptığını ve x86 mimaride çalıştığını da yine varsayımlarıma ekliyorum.

Bilinen çağrı konvansiyonları aşağıdaki gibi (birkaç adet daha var, kullanmayacağımız)

__cdecl  
__stdcall  

Bu burada dursun.Biz variyadik fonksiyonlar ile başlayalım. Variyadik fonksiyonlar istenilen kadar argüman alabilen fonksiyonlardır. Değişken sayıda argüman alırlar ve kullanılırlar. Variyadik fonksiyonlar her zaman her dilde dinamik kütüphaneler tarafından çağrılamazlar. Neden çağrılamazlar? DLL boundary dediğimiz olayın bir nevi açıklaması olarak elimizde cpp'da yazdığımız Wasabi sınıfımız olduğunu varsayalım:

class WASABI_SHARED Wasabi {  
    int sushi;

public:  
    Wasabi();
    ~Wasabi();

    // setters
    void setSushi(int s);

    //getters
    int getSushi();
};

WASABI_SHARED wasabinin dışarıya fonksiyonlarını vararg kabul ederek export edilmesini sağlamaktadır. Elinizde destructor, constructor, setter ve getter var. Ama managed code için bir sınıf oluşturacak yeteneğiniz yok. Kusura bakmayın az önce çok manasız bir iş yaptınız(DLL tam manasıyla datayı gizli kılan bir yapıya sahip data section her zaman gizli tarafta ama code section-text her zaman kullanıldığı çalıştırılabilirlere açık). Hadi diyelim bunu es geçtik.
Bu Wasabi sınıfını variyadik fonksiyon argümanı olarak aldığımızı varsayarsak

int yetAnotherVariadic(int z, Wasabi a);  

tanımı dll dışında managed code'a ait değildir. Birincisi managed code'da Wasabi nedir bilmiyorsunuz. İkincisi nesne ile erişim dll sınırından geçmiyor. Sadece ve sadece pointer ile erişebilirsiniz. Bunu da geçelim hadi diyelim ki pointer veriyoruz argüman olarak (cpp kullandığınızı varsayalım şimdi de). DLL hiçbir zaman uygulamayla aynı heap'i ve memory region'u kullandığına emin bile değiliz. Nerede olduğunu bilmiyoruz. Bilsek bile dll içerisinde dll tarafından bilinmeyen bir yere isabet eden bir mapping olacak. Nereden biliyorum? calling conventionlarının stack cleanup kodunu içermesi ve dll'in adreslemesinin sabit olarak tanımlanmamış olmasından. Sizin oluşturduğunuz dll lerin sabit bir adresle başlama gibi bir özelliği yok ama sisteme ait dll'lerin var. bir kernel32.dll'e erişmek sıradan bir c dll'ine erişmekten küçük bir miktarda daha kısa sürüyor.

Windowsta olaylar bu şekilde gelişiyor, sadece calling sorunu bizi zorlamıyor ayrıca nesne layout'u da komple farklı, exception mekanizmaları da boundary'den geçemiyor. Diğer işletim sistemlerinde olaylar daha farklı gelişiyor. Olması gereken ise nesne tabanlı yapıyla DLL'den yapılan çağrının ayrılması.

Şimdi yazının başında niye çağrı tiplerini verdiğime gelirsek: aşağıdaki basit bir caller cleanup çağrısı, yani tam anlamıyla bir __cdecl. Çağıran fonksiyon stack'e push ettikten sonra çağrısını yapıyor ve işlem sonrası stack'i kendi temizliyor.

SECTION .text

caller:

    ; ...

    ; Caller responsibilities:
    PUSH  3         ; push the parameters in reverse order
    PUSH  2
    CALL  callee    ; perform the call
    ADD   ESP, 8    ; stack cleaning (remove the 2 words)

    ; ... Use the return value in EAX ...


callee:

    ; Callee responsibilities:
    PUSH  EBP       ; store caller's EBP
    MOV   EBP, ESP  ; save current stack pointer in EBP

    ; ... Code, store return value in EAX ...

    ; Callee responsibilities:
    MOV   ESP, EBP  ; remove an unknown number of local data elements
    POP   EBP       ; restore caller's EBP
    RET             ; return

Üstteki kodu bir wikiden aldım. cdecl bize kolaylık sağlıyor aslında çünkü yukarıdaki kodu export mekanizmasında

__declspec(dllexport) void __cdecl Function1(void);  

bu şekilde tanımlamış oluyoruz. stack setup ve cleanup'ı yapılmayan hafızada yer etmiş bir program olamaz. Aslında olur da OS ile ilgili zorunluluklardan ötürü olamaz. İster intel sentaksı kullanarak bunu deneyin(ms üzerinde) iste at&t sentaksı kullanarak bunu deneyin(unix) üzerinde assembler(gas, masm) her zaman size sorun çıkaracaktır çünkü zaten os bu işleri yönetmek ile de görevlidir. Fonksiyonları cdecl ile export ediyorsak managed code'da c ye uygun değişkenleri rahatlıkla kullanabiliriz. (Yine hepsi değil mesela vektör tipi yok, array'a marshallamanız gerekmekte ama integer hala integer) İşte kolaylık bu.

_stdcalla gelirsek name mangling işte burada başlıyor. ABI farklılıkları ve sorunlar başgösteriyor. Fonksiyonların adı compilerdan compiler'a değişiyor. _stdcall'ın binlerce sorunu var. İlla zorlamanız gerekiyorsa kendinizi name manglingleri her compiler için decode eden bir yapı kullanmak zorundasınız ki bu bayaa zahmetli.

Bu kadar sorunla bitirme tezimi yaparken uğraştım. MinGW, GCC, intel compiler, vscc kullanırken bu sorunlar bitmedi. En sonunda cdecl ve g++ da karar kıldım. Şu anda managed code içerisinde tüm fonksiyonları tanımlayarak çağırabiliyorum. Managed code'da gösterimi yapılamayan tipleri kullanmadım. Class yapısını kullanmak zorundaydım. Bir şekilde de export etmek. Struct kullanarak bu işi pointerlarla oynayarak yapan insanlar var. Kendileri bizzat güvenli olmadığını söylüyorlar.

Bu yazıya zaman geçtikçe eklemeler yapacağım özellikle diğer calling conventionlar ve az bahsettiğim object layout hakkında...

comments powered by Disqus