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_dongu
den sirala_son
a atlanabilir, ama kaydir_dongu
ye 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 bp
yi 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 bp
yi 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, sp
ye local değişkenleri silmek için bp
yi 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 sp
yi kaydırmıştık). Bu aşamada stack’in tepesinde bp’nin eski değeri var. pop bp
ile bunu bp
ye yükledikten sonra ret
ile dönüş yapabiliriz.
Bu arada fonksiyon dönüş değerini ax
e 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 ax
de 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.