Öncelikle hemen söyleyeyim bu yazı programlamayla EN AZ orta
seviye ilgilenenlere yöneliktir. Yani bu kritere uymuyorsanız bence
hiç bulaşmayın, sonra program yazma işinden tiksinebilirsiniz :).
Tabi tiksinme kısmı şaka ama yine de size çok sıkıcı gelebilir.
Neyse uzatmadan başlayalım yazımıza.
Programlarınızı yazdığınız ortamları merak ettiniz mi hiç? Acaba
onlar nasıl yapıldılar? Ya da ilk programlama dili nasıl ve hangi
dille yazıldı(biliyorum biraz garip bir cümle oldu)? İşte bütün bu
sorulara genel olarak verilebilecek cevap ASSEMBLY'dir. Assembly,
aslında makina dili diye de geçer. Bu dili kullanırken -ki aslında
tam olarak bir dil demek de yanlış- işlemcinin temel komut setinde
ne varsa onları kullanırsınız. Aynı zamanda bildiğiniz şekilde
değişken oluşturma ve yönetme şansınız da yoktur(Örn "int i=10;"
gibi komutlarınız yok). Tabi bunlar yüzeysel yorumlar. Çünkü
Assembly kullanırken bütün bunları -birçok fonksiyon, değişken
yönetimi vs.- siz yaparsınız. Yani bir değişkenin saklanacağı bellek
adresi, uzunluğu vs. tamamen sizin denetiminiz ve
sorumluluğunuzdadır.
Şimdi önce isterseniz gelin programlama dillerinin
sınıflandırılmasına bakalım. Programlama dilleri makinaya
yakınlığından insana yakınlığına göre sıralanır. Mesela .NET
dillerinden C# insana en yakın dillerden biridir. Bir çok alt seviye
işlem derleyici tarafından otomatik yapılır veya yapılması için
gerekli kod programa eklenir. Bunun yanında C en alt seviye
dillerden biridir. Bellek denetimi gibi şeyler programcıya
bırakılmıştır ama bu dille yapabilecekleriniz ve alacağınız
performans C#'tan çok daha fazla olacaktır. İşte assembly bu
sınıflandırmada makinaya en yakın yerde bulunuyor. Eğer bu dili iyi
öğrenirseniz ve kullanabilirseniz deyim yerindeyse bilgisayara takla
bile attırabilirsiniz. :)
Bilgisayarlar verileri 2'lik sistemde işler ancak 16'lık sistemde
saklarlar. Örneğin 2 sayıyı toplamak istediğinizde bunlar işlemciye
bitler şeklinde iletilir ki bu 2'lik sistemdir. Ancak bunları bir
dosyaya yazmak isterseniz bu sefer bir byte'lık alanlara yazarsınız
ki bu alanlardaki bilgiler de 16'lık sistemde saklanır. Biz de
bilgisayarda artık EN ALT seviyede uğraşacağımıza göre bu sistemleri
iyi bilmeli, bu sistemler ve 10'luk sistem arasındaki dönüşümleri
rahatça yapabilmeliyiz.
Bilgisayarda saklanan ve işlenen verilerden bahsetmişken hemen
bunların çalışan programlar için nerelerde yapıldığına bakalım.
Normal dillerle yazılan programların tamamı değişkenleri bellekte
saklar. Her değişkene ait bir bellek adresi vardır. Hatta
pointerlar'la haşır neşir olanlar bu bellek adresilerini
anımsayacaklardır. İşte assembly kullanırken bu pointerlar bizim
değişkenlerimizi temsil edecek dersek çok da yanlış olmaz. Ancak
işlemci komutları genelde doğrudan bellek üzerinde işlem yapmak
yapmak yerine genelde REGISTER adını verdiğimiz işlemci çekirdeği
üzerindeki sabit değişkenler üzerinde değişiklikler yaparlar.
Bellekle ilgili bilmeniz gereken bir başka şey ise belleğin
segmentlere ayrılmış olduğudur. Segment sözcüğünü dilimize kabaca
KATMAN olarak çevirebiliriz. Bu katmanlarda benzer tip veriler bir
arada tutulur. Örneğin Code Segment(CS)'ta programınızın kodları
bulunurken Data Segment(DS)'ta kullandığınız bazı değişkenlerin
verileri tutulur. Bunların yanında Stack Segment(SS) ve Extra
Segment(ES) gibi başka katmanlar da bulunur. Tüm bu katmanların
başlangıç adresleri yanlarında parantez içinde belirttiğim
kısaltmalarla aynı adlardaki özel registerlarda tutulur. Bunları
yeri geldiğinde göreceğiz. Segmentlerin içindeki hücrelere ulaşmak
için de offsetleri(adresleri) kullanırız. Veri yazma ve oku
işlemleri DS üzerinde yapılır. O zaman biz bir bellek okuması veya
yazması yapacağımız zaman sadece işlemi yapacağımız offseti bilmek
zorundayız. İşlemci otomatik olarak DS'de tutlan değere bizim
verdiğmiz offset değerini ekler ve ilgili hücreye veriyi yazar yada
bu hücreden veriyi okur.
Assembly dilindeki komutlar yani işlemcinin temel komutları
genelde 2 parametre kullanır -ki bunlara artık OPERAND diyeceğiz- ve
bunlar register adı, bellek bölgesi ya da sabit bir sayı
olabilirler. Örnek vermek gerekirse: MOV komutu bri register ya da
bellek bölgesine değer atamak için kullanılır. İlk operand atama
yapılacak bellek bölgesi ya da register adı, ikinci operand da oraya
atanacak değeri içeren bir başka bellek bölgesi, register ya da
sabit sayı olabilir. Yeri gelmişken operandları ayırmak için
aralarına ","(yazıyla=virgül, rakamla=?!?! :)) koyuluyor. O zaman
offseti 100 olan bellek bölgesine 10 değerini atamak için "MOV
[$64],$0A" komutunu vermemiz gerekiyor. Dikkat ettiyseniz 100 yerine
$64 ve 10 yerine de $0A yazdım. Çoğu assembler(asm kodlarını
derleyen program) direk onluk tabandaki sayıları kabul eder ve siz
16'lık tabanda bir sayı girmek istediğiniz zaman başına $ işaretini
koymanızı ister. Ancak debug gibi basit ve alt seviye bir derleyici
kullanırsanız bütün sayılar standart oalrak 16'lık sitemde
yorumlanacaktır. Bu arada komutta da gördüğünüz gibi bellek
bölgelerine ulaşmak veya oralarda işlem yapmak için offsetini köşeli
parantez içinde yazmamız gerekiyor. Ve son olarak o komut aslında
hatalı. Çünkü bellek bölgesine byte cinsinden mi word(2 byte'lık
veri) cinsinden mi yazacağımızı belirtmedik. O ne ki derseniz hemen
açıklayayım. Bellek bölgeleri aslında 1 word'lük kapasiteye
sahiptirler ancak oralara sabit sayılar atanırken 1 byte
uzunluğundaki veriler de yazılabilir. Bunu açıklığa kavuşturmak için
operandlardan önce "BYTE PTR" ya da "WORD PTR" yazmamız gerekiyor.
Vay be, bir komutla amma çok şey anlatmış oldum! :)
Aslında buraya öyle detaylı bir kullanım rehberi ya da birçok
komutun kullanım şeklini falan yazmayı düşünmüyorum. Çünkü hem ben o
konuda anlatabilecek kadar yetkin değilim hem de zaten bu yazının
amacı giriş yapmanızı sağlamak. Zaten sadece assembly kullanarak bir
program yazmak aslında biraz çılgınlık :)(kabul ediyorum zamanında
16bitlik programlar yazdık biz arkadaşımla ama cidden kafayı yeme
noktasına geldik :)). Bu sebeple buradan öğrendiğiniz genel bilgiler
ve komutları öğrenebileceğiniz bir kaynağın da yardımıyla
programlarınızda hız artışı veya boyut azaltışı(!) yapabilirsiniz.
Bu kadar gevezelikten sonra genel kullanım ve birkaç komut
açıklamsıyla yazımızı bitrelim. Daha önce de dediğim gibi burada
komutlardan sonra operand giriyorsunuz. Bazı komutlar operandsız da
olabiliyor. Ve bir komut en çok 2 operand alabiliyor(aslında daha
çok alan da olabilir ama bildiğim kadarıyla 2 ile sınırlı). Daha
önce bahsettiğim MOV komutu 2 operand alır. İlk operand atama
yapılacak yeri belirtirken ikinci operand atama yapılacak değeri
belirtir. Buraya sabit bir sayı yazabileceğinizi gibi birbellek
bölgesi ya da register adı da yazabilirsiniz.
XOR komutu ilk operand ile ikinci operandı XOR mantıksal
işleminden geçirir ve sonucu ilk operanda atar. AND, OR gibi diğre
mantıksal operasyon komutları da aynı şekilde çalışır.({a:=a xor b}
deyimi assemblyde {xor a,b}'ye denk gelir)
PUSH komutu arkasından verilen operandın değerini bellek yığınına
atar ve arkasından çağırılan POP komutuyla bu değeri POP komutunun
operandına geri yükler. Yanlız burada işlemin sırayla olduğunu
unutmamanız gerekiyor. Yani
PUSH EAX
PUSH EBX
.
.
POP EAX
POP EBX
gibi bir komut yazarsanız EAX'ın değeri EBX'e, EBX'in değeri de
EAX'a atanmış olur. Çünkü en son EBX'in push edilmesine rağmen ilk
POP işleminde EAX kullanıldı. Özetlersek ilk giren son çıkar ya da
son giren ilk çıkar diyebiliriz.
CMP komutu kıyaslama yapmak için kullanılır. Verilen ilk
opranddan ikinci operand çıkartılır ancak operandların değerlerinde
bir değişiklik olmaz. Bu işlem sadece FLAG adı verilen bazı kontrol
bitlerinin değerlerinin değişmesine sebep olur ki bu da zaten
kıyaslama sonucunda yapılacak işlemi berlemek için yeterlidir. Şöyle
ki: burada BASIC'tenbildiğiniz "GO TO"ya benzer bir yapı
kullanılıyor. Yani şu şöyleyse buraya git ve oradan devam et gibi
bir yapı var. Bunları da GO TO değil JMP, JNZ gibi atlama
komutlarıyla yapıyorsunuz. JMP komutu hiçbir koşul gözetmeden
operand olarak verilen adrese atlar ve oradan devam eder. JNZ ise "Zero
Flag"i olarak geçen ve son işlemin sonucu 0 ise değeri 1 yapılan bir
bitin değerine göre atlama yapar. CMP işleminde de iki operandın
değeri birbirinden çıkarıldığında sonuç sıfır ise bu iki değer
birbirine eşit demektir ki bu durumda JNZ komutu(Jump if Not Zero) hiçbirşey
yapmaz çünkü "Zero Flag"i 1'dir. Yani sonuç sıfırdır. JZ komutu ise
JNZ'nin tam aksine Jump if Zero yani sonuç sıfırsa şu adrese git
anlamına gelir. Yine az önceki durumu düşünürsek bu komutu
kulanırsanız program belirtilen adrese sıçrayacak ve buradan devam
edecektir.
Son olarak for ve/veya while döngülerine karşılık gelen LOOP
komutunu da verelim. For döngülerinde kullandığınız değişken sanırım
genel de "i" oluyor. Burada ise bu iş için standart olarak ECX
register'ı kullanılıyor. Bu register'a döngünün kaç kez
çalıştırlacağını atadıktan sonra işletilecek komutların sonuna LOOP
{adres} komutunu yazıp adres olarak da komutların başlangıç adresini
verirseniz tam bir for döngüsü elde etmiş olacaksınız. Yalnız burada
dikkat etmeniz gereken birkaç nokta var. Öncelikle döngü komutu
içerisinde ASLA ECX'in değerini değiştirmemelisiniz. Değiştirmek
zorunda kalırsanız da mutlaka PUSH ve POP komutlarını gerekli
şekilde kullanıp orijinal değerini geri döndürmeniz gerekir. Aksi
takdirde döngünün uzunluğu alakasız şekilde değişebilir. İkinci
olarak, LOOP'a operand olarak verdiğiniz adres ECX'registerına döngü
sayısını atadığınız yerden sonra olmazsa programınız sonsuz döngüye
girer. LOOP'un 2 farklı versiyonu daha var: LOOPZ ve LOOPNZ. Bunlar
da hem ECX'in değerine bakıyorlar hem de "Zero Flag"inin değerine.
LOOPZ, Loop if zero; LOOPNZ ise loop if not zero yani LOOPZ sıfır
olduğu sürece, LOOPNZ de sıfır olmadığı sürece döngüye devam et
anlamına geliyor. Eğer döngü içinde bir CMP işlemi varsa bu
komutlardan birini kullanarak döngü sayısı bitmeden döngüden
çıkılmasınıa sağlayabilirsiniz.(Bu da döngü içinde kullandığımız
"break;" komutuna benziyor.)