osa1 feed

Assembly programları için kod organizasyonu hakkında

November 12, 2012 - Tagged as: tr.

Düzenlenip yayınlanmayı bekleyen süper yazılar olmasına rağmen şu anda mikroişlemciler sınavı çıkışı bunu yazıyor olamam epey garip.

Her ne kadar 2. vizeden çıkmış olsak da, henüz umudu kesmeyenlerin ve gelecek dönemlerin işine yarayabileceğini düşündüğüm, derste -bana göre yanlış olarak- hiç bahsedilmeyen birkaç şeyden bahsedeceğim.

Ana fikir şu, bu ders kapsamında veya genel olarak x86 ASM kodlarken, bazı _convention_lar takip etmek program organizasyonu açısından çok faydalı oluyor ve ben birazdan bahsedeceğim conventionlar olmadan ben programlayamazdım.

Bunlardan ilk bahsedeceğim tamamen kozmetik, kodun okunaklığını arttıran ve aslında tamamen programcının yorumlaması gereken basit bir düzenleme.

Etiketleri yerleştirirken, kendi uydurduğum scope kuralları uyguluyorum. Yazdığım kod şu şartları sağlıyor:

Örnek olarak aşağıdaki kod, henüz deadline’ı gelmemiş bir ödevin çözümünün bir parçası. Programın tamamını koyamıyorum o yüzden, sadece etiketleri yerleştireceğim:

...

kopyala:
    ...
    
    kopyala_dongu:
        ...
    
    ...

sirala:
    ...

    sirala_dongu:  
        ...
   
    sirala_son:     
        ...

kaydir:
    ...
    
    kaydir_dongu:
        ...
            
    kaydir_son:
        ...

Yukarıdaki maddeleri açıklamak için birkaç örnek: sirala_donguden sirala_sona atlanabilir, ama kaydir_donguye atlanamaz, çünkü arada daha az girintilenmiş kod var(kaydir prosedürüne ait). Her yerden kaydir, sirala ve kopyala çağırılabilir(call), ancak hiçbir yerden zıplanamaz. Bunun gibi. Bunları takip ettiğinizde, kod çok daha okunaklı bir hale geliyor diye düşünmekteyim.

İkinci kısım aslında daha önemli, prosedür oluşturma ve çağırma hakkında.

Anlatacağım convention, cdecl olarak bilinen, x86-32 sistemlerde C fonksiyonlarının derlenme şekli.

Yapılan şey şu, fonksiyon parametreleri, sondan başlanarak(örneğin 3 parametre varsa, ilk önce 3. parametre) stacke pushlanır. Daha sonra call yapılır. Fonksiyon, önce bp’yi stacke atarak yedekler(push bp), daha sonra parametrelere ve local değişkenlere erişmek için, bp(base pointer)ye sp(stack pointer)ı atar(mov bp, sp). Bu aşamadan sonra, artık [bp+2] bp’nin eski değerini, [bp+4] birinci parametreyi, [bp+6] 2. parametreyi verir.

Bir sonraki aşama olarak, fonksiyon içerisinde kullanılacak local değişken kadar stackde yer açılır. Örneğin 1 değişken varsa, sub sp, 2, 2 değişken varsa sub sp, 4 ile stack’de yer açılır. Bu sayede fonksiyon içerisinden başka bir fonksiyon çağırdığımızda, local değişkenlerin üzerine birşey yazılmaz. Aslında bpyi de benzer bir sebeple yedeklemiştik. Başka bir fonksiyon çağırıldığında, bp’yi kendi değişklenlerine ve parametrelerine erişmek için değiştirecek. Birazdan göreceğimiz gibi fonksiyon dönüş yaparken bpyi eski haline getirecek.

stack’de yer açtıkdan sonra da local değişkenlere, 1. değişken için [bp-2], 2. değişken için [bp-4] ile erişebiliyoruz.

NOT: Bu arada eğer farkedilmediyse belirteyim, 16bit 8086 işlemcilerden bahsediyorum. 32 bit sistemlerde stack pointerını 1 değer için 2 değil 4 azaltmanız gerekecek. Bir diğer farkedilmesi gereken şey de, stack pointer’ın pushlandığında azaldığı.

Fonksiyon işini bitirdiğinde, spye local değişkenleri silmek için bpyi atamalı(mov sp, bp, hatırlarsanız fonksiyon çağırıldığında mov bp, sp yapmıştık, ve daha sonra local değişkenler için spyi kaydırmıştık). Bu aşamada stack’in tepesinde bp’nin eski değeri var. pop bp ile bunu bpye yükledikten sonra ret ile dönüş yapabiliriz.

Bu arada fonksiyon dönüş değerini axe koyuyor.

Bundan sonra son olarak yapılması gereken şey, fonksiyonu çağıran kod parametreleri stacke atmıştı, ama temizleyen olmadı. 2 parametre için add sp, 4 gibi bir kod ile stack temizlenebilir.

Anlaşılması için bir üs alma fonksiyonu yazacağım. Fonksiyonun adı power olsun. Çağırılışı şu şekilde:

push 2          ; ikinci parametre
push 5          ; birinci parametre
call power
add sp, 4

En sonunda stackin temizlendiğine dikkat. Bu koddan sonra axde fonksiyonun dönüş değeri olmuş olacak.

Fonksiyon ise şöyle:

power:
    push bp             ; bp'nin eski değerini yedekle
    mov bp, sp          ; bp := sp.
    sub sp, 2           ; 1 local değişken için stackde yer aç
    
    mov bx, [bp+4]      ; birinci parametreyi bx'e yükle
    mov cx, [bp+6]      ; ikinci parametreyi cx'e yükle
    
    mov [bp-2], bx      ; bx'i birinci local değişkene ata

    power_loop_start:
        cmp cx, 1
        je power_end
        
        mov ax, [bp-2]
        mul bx
        
        mov [bp-2], ax
        dec cx
        jmp power_loop_start        
        
    power_end:
        ; birinci local değişkenimiz fonksiyonun dönüş değeri
        ; bu değeri ax'e yükle
        mov ax, [bp-2]
        mov sp, bp        ; sp'yi eski haline getir
        pop bp            ; bp'yi eski haline getir
        ret               ; dön

İşte x86-32’de C fonksiyonları buna benzer bir şekilde derleniyor. 64bit sistemlerde ekstradan 8 yazmaç olduğundan, parametreler direkt olarak stacke atılmaktansa yazmaçlara yazılıyormuş. Başka dillerde de, dilin ihtiyaçlarına göre farklı yollar izleniyor. Örneğin C++ dilindeki this pointerları her seferinde stacke atılmaktansa, sürekli sabit bir yazmaça yükleniyor olabilir.

Bazı calling conventionlar için özet bilgiye Wikipedia sayfalarından ulaşabilirsiniz: X86 calling conventions, bazı farklı mimariler için conventionlar, Linux ortamında kullanılan çeşitli conventionlar. Onun dışında AMD64 için ABI. Intel IA manuallarında da ABIlardan bahsediliyordu yanlış hatırlamıyorsam.

Bu arada her fonksiyon çağırılışında stacki temizlemek, fonksiyonların kaç parametre aldığının fonksiyonun kendisinin her zaman bilmesi durumunda, gereksiz. Yukarıda C conventionlarından bahsettiğimden, ve C’de örneğin sscanf gibi fonksiyonlar değişik sayılarda parametre alabildiğinden, temizleme işlemini parametreleri gönderen taraf yapıyor.