Harbe uğramış bir CD'yi kurtarmaya uğraşmak müthiş sabır isteyen bir iş. CD-ROM'a koyuyorsunuz, Nero'yu açıyorsunuz, sonra da menülerden "Save Track" bölümüne girip CD'nin bir kopyasını kaydetme komutunu veriyorsunuz. Daha önceki gün bilgisayarımı 10 saatliğine bu işe tahsis ettim, ama netice alamadım. Dün yeniden denemeye koyuldum, CD'yi biraz daha sildim, belki aşama kaydederiz. Son denememin başlangıcı: 15 Ekim 2004 Cuma, 15.21...

 

Geçen bölümün sonunda da bahsettiğim gibi, bu bölüm TASM(Turbo Assembler) üzerine kurulu. Bu programı elimden geldiğinde enine boyuna irdeleyeceğim. Eksik kalan veya (olur ya) yanlış betimlenen kısımlar olabilir, her zaman uyarıya ve öneriye açığım.

 

Bölüm 3: Turbo Assembler ile Çalışmak

 

TASM(Turbo Assembler), Borland firması tarafından Turbo C++ ve Borland Pascal programlama dili IDE paketleri içinde elimize ulaşır. Ayrıca kendi başına olan bir paketini de bulmak mümkündür. Söz konusu yazılım dillerine sahip iseniz, kurulu oldukları dizin altında Bin klasöründe yer alırlar. TASM.EXE, TLINK.EXE, TD.EXE bu bölümde ilgileneceğimiz temel uygulamalardır.

 

Debug programı, bize Windows ile birlikte sunulan bir TEST aracıdır. Debug ile programlar yazmak epey zahmetli olacaktır, zira sembolik bir yönü bulunmadığından dallanma komutlarının parametrelerini doğrudan kendimiz takip etmeliyiz. Ancak her ne kadar Assembler en alt seviye dil ve içeriği olan komutlar dizisi de ikili sayıların kombinasyonlarından oluşsa da, yazılan kodu önce derleyerek gerekli öngörüleri yapacak bir program, bizi tamamen yapılacak olan işe yöneltecek ve optimizasyona ayıracağımız zamanı arttıracaktır. Bu bağlamda Debug, sadece bir dosya oluşturma aracıdır, bizim yazdığımız kod üzerinde herhangi bir derleme işlemi yapmamaktadır(ana programa ilişkin RET komutunu INT 20'ye dönüştürmek dışında).

 

Her programın işleyeceği dosyanın bir formatı olduğu gibi, TASM'ın da içini okuyarak işlediği dosyanın bir düzeni olmalıdır. Genel olarak bir TASM dosyasının içeriğinde yazılacak olan ibarelerin hedeflerine göre sırası şu biçimde olmalıdır:

 

Yukarıdaki 3 maddenin yeterince açıklayıcı olmadıklarını biliyorum. Bu nedenle örnek bir program ile başlayacağım. Aşağıdaki program, klavyeden ardı ardına iki karakter okur, bu karakterleri 1 baytlık hex sayı olarak yorumlar ve sayı değerini DL yazmacına yazar.

 

.286                        ;80286 platformu

.model small            ;Bellek modeli

.stack                      ;Yığına ayırılmış bölge. Burada istenirse genel değişkenler tanımlanabilir. Bu bölgenin boyutu kullanıcı tarafından belirlenebilmektedir.

.code                       ;Programın içeriğinin başlangıcı

        mov AH,1h       ;1 no'lu DOS işlevi: Klavyeden girilen karakterin ASCII kodunu AL yazmacına yazar ve karakteri ekranda görüntüler.

        Int 21h             ;DOS kesmesi

        mov DL,AL       ;ASCII kodunu DL yazmacına aktar

        sub DL,30h      ;Okunan karakteri 0-9 arasında sayı olarak elde etmek için 30h eksilt

        cmp DL,9h       ;Okunan karakterin bir rakam olup olmadığını anlamak için 9 ile karşılaştır

        jle sayi1           ;Küçük veya eşitse bu bir rakamdır, digit1 etiketine atla

        sub DL,7h        ;Büyükse A-F karakterlerinden biri olması ihtimalie karşı 7 eksilt

sayi1:

        mov CL,4h       ;DL'deki sayıyı 16 ile çarpacağız, bunun için DL'yi 4 bit sola kaydırmalıyız

        shl DL,CL        ;Kaydırma işlemini yaparak 16 ile çarpmayı gerçekleştir

        int 21h             ;İkinci karakteri oku

        sub AL,30h      ;Bir önceki karaktere yapılan işlemleri tekrarla

        cmp AL,9h       ;Aynısı, sadece DL'ye aktarım yapılmadığı için işlem AL üzerinde yapılıyor

        jle sayi2           ;aynısı...

        sub AL,7h        ;aynısı...

sayi2:

        add DL,AL       ;DL'nin ilk 4 biti 0'dır, AL'nin de son 4 bitinin 0 olduğunu varsayarak iki sayıyı toplayalım. Artık DL yazmacında girilen hex sayının onluk karşılığı var.

        mov AH,4CH    ;4C(76) no'lu dos işlevi: DOS'a geri dön

        Int 21h             ;DOS kesmesi

        End                 ;Program bitimi, TASM tarafından gerekli görülür.

 

 

Bu programın nasıl çalıştığını anlamak için bir ASCII tablosu bularak rakamların ve A'dan F'ye harflerin kod numaralarına bakmak gerekir. Bu kısa bilgiyi burada yazmam faydalıdır: 0'dan 9'a karakterler 30h-39h arasında, A'dan F'ye harfler ise 41h-47h arasında bulunur. Yukarıda yapılan işlemler ile yazmaçlarda, girilen hex rakamın aynısının görüntülenmesi sağlanmaktadır.

 

Asıl konumuza gelelim ve bu dosya biçiminin öğelerini açıklayalım.

 

Platform

 

Yukarıdaki programda bulunmasa da bir assembler programının ilk öğesi her zaman platform olmalıdır. Burada platformdan kasıt, programın hangi işlemciye göre derleneceğidir. Aşağıdaki gibi bir satır yazmalıyız:

 

.286

 

Bunun anlamı, programın Intel 80286 işlemcisinin komut setine ve bellek erişim mimarisine göre derleneceğidir. Burada Gerçek Kip, Korumalı Kip ve Sanal Kip diye 3 kavram karşımıza çıkar, ancak bunlara ilişkin bilgiler için şu an çok erken. Sizin bilmeniz gereken, işlemcinizin gerçek kipte çalıştığı ve aşağıdaki düzenlerin geçerli olduğudur:

 

Yazmaçlar:    AX    CS    IP                Bellek erişimi 1MB'a sınırlı. Erişim şekli: Gerçek adres = DS x 16 + ofset (SI, DI, BX ve/veya sayı gibi...)

                     BX    ES    DI

                     CX    DS    SI

                     DX    SS    SP

                     FLAGS

 

Bildiğiniz gibi 80286 işlemcisi 16 bitliktir. 2 adet 8 bitlik yazmacın kombinasyonu olan 16 bitlik yazmaçlar ve 20 bitlik adres yolu kullanır. Ancak 80386'dan itibaren günümüze gelen Intel mimarisinin adı IA32'dir ve herşey 32 bitliktir. Dolayısıyla, programın platformunu belirtirken işlemcinin yapmasını istediğiniz işlerin büyük önemi vardır. .386 veya .486 yazmak size daha geniş imkanlar sunacaktır. Ne var ki işlemcinin yazmaçları tamamen kendisine ait olsa da, yine de bellek erişiminin bulunulan ortamla doğrudan ilgisi vardır. Dolayısıyla DOS altında 32 bitlik bellek erişimini doğrudan doğruya kullanamazsınız, ya Korumalı Kipe geçmelisiniz(DPMI, DOS4GW vs... kullanmak), ya da hep o eskiden görüp de ne olduğunu bir türlü anlamadığımız "HIMEM.SYS is testing memory... OK" mesajının kaynağı olan Uzatılmış/Genişletilmiş Bellek Yöneticilerinin(mesela EMM386.EXE) sunduğu işlevleri kullanmalısınız. Nasıl kafanız karışıyor değil mi? Boşverin bunları, siz kendi işinize bakın. Ya .286 yazın, ya da hiçbir şey yazmayın, varsayılan ortam zaten 80286 ortamıdır.

 

Model

 

Modelden kasıt, programın kullanmakta olduğu belleğin ve kodun boyutlarına göre izinlerin ayarlanmasıdır. Buradaki model kavramı, daha önce Turbo C++'ta veye Borland Pascal'da program yazan arkadaşların yine bilmediği, ancak "Options\Compiler\Code Generation" menüsünde bulunan "Memory Model" ile tamamen aynıdır. Aşağıdaki şekilde kullanılır:

 

.model [program bellek modeli]

 

Bu bellek modeli seçeneklerini tanıtalım:

 

Tiny: Yazmakta olduğunuz tüm kod ve kullanacağınız tüm belleğin toplamı 64k'dan küçük ise, yani özel olarak CS, SS, DS ve ES aynı değerde veya kısaca COM programı ise.

 

Small: Yazmakta olduğunuz kod ve kullanacağınız bellek ayrı ayrı 64k'ya sınırlı ise. Derleyici, CS ve DS'yi bu değerlere göre ayarlar. Ancak artık COM programının limitleri aşılmıştır, yani program EXE olacaktır.

 

Medium: Kod 1MB'a, bellek ise 64k'ya sınırlı olacaksa...

 

Compact: Kod 64k'ya, bellek ise 1MB'a sınırlı olacaksa...

 

Large: Kod ve bellek ayrı ayrı 1MB'a sınırlı olacaksa...

 

Huge: Kod 1MB'a, bellek ise mümkün olan en büyük değere uzanabilecekse... Bu uzanım, birden fazla DATA SEGMENT ile sağlanabilir. Ayrıca STACK SEGMENT de 64k olur. Tabii ki 1MB'ın üzerinde bellek erişimini normal yollarla sağlayamazsınız. Başka araçlar da gerekecektir. Karıştırmayın siz bu bölümleri...

 

C++ ve Pascal dilleri için bu seçeneklerin fazladan anlamları da vardır. Bellek erişimi için kullanılacak işaretçi türlerinin near ya da far olacakları bu modeller tarafından tayin edilir. Mesela Huge bellek modelinde işlev çağırımlarında CS bir kez daha yığına atılır(normalde CS ve IP, CALL FAR komutu tarafından yığına atılır).

 

Genel Değişkenler ve Yığın

 

Zaten bu nokta, TASM kullanmamız için en önemli gerekçelerden. Programlarımızda kullanacağımız genel değişkenler için Debug'ta program bittikten sonraki bir bölümde yerleştirme yapmamız gerekirdi. Ancak .data deyimiyle başlatacağımız bir bölüm ile tüm genel değişkenlerimizi belirtebilir ve boyutlarını ayarlayabiliriz. Hangi adreslerde saklanacakları bizi hiç mi hiç ilgilendirmeyecektir. Yeter ki işlem yaparken kullanmakta olduğunuz bellek modelini göz önünde bulundurun.

 

Aşağıda, derlemeye tabi tutulabilecek biçimdeki bir assembler programındaki değişken tanımlama örneklerine yer vereceğim. Bu örneklerde kullanılan öğeler, sadece TASM için değil, MASM ve NASMW gibi derleyicilerde de ortaktır.

 

sayi    db    8h

"sayi" değişkenine 1 baytlık yer ayırılmış ve başlangıç değeri olarak 8h verilmiştir.

 

i        dw    7F56h

"i" değişkenine 1 word'lük(2 baytlık) yer ayırılmış ve başlangıç değeri olarak 7F56h verilmiştir.

 

j        dd    ?

"j" değişkenine 1 doubleword'lük(4 baytlık) yer ayırılmış ve başlangıç değeri atanmamıştır.

 

yazi    db    "Merhaba"

"yazi" değişkeni, "M","e","r","h","a","b","a" karakterlerinin ASCII kodlarından oluşan bir dizinin ilk elemanını temsil etmektedir. Bunun bir metin olarak kullanılabilmesi için daha fazla bilgiye ihtiyacınız olacak.

 

dizi    db    10h dup 8

"dizi" değişkeni, her birinin değeri 8 olan 10h(16) baytlık bir dizinin ilk elemanını göstermektedir. Bu dizinin elemanlarına erişim için ise bilgi + tecrübeye ihtiyacınız var.

 

İthamlarım herkese karşı değil. Zira burada söz ettiklerimden zaten haberi olanlar ve hatta bu konuları iyi bilenler bile olabilir. Amacım, bilgilerimi bilmeyenlere aktarmak.

 

Değişken tanımlamalarında kullanılan isimlerin genel kuralı şudur: Bir harf ile başlamak ve noktalama, boşluk karakterleri içermemek. Boyutlar için kullanılan ara sözcüklerin açılımları akla yatkındır. Zira db, define byte, dw, define word ve dd ise define doubleword anlamlarını taşımaktadır. Son örnekte görmüş olduğunuz dup ifadesi ise çok genel bir assembler makrosu olup, kendisinden önce gelen sayı kadar kendisinden sonraki sayının tanımlanacağını bildirmek için kullanılır. Dup, dizi oluşturmanın çok genel bir yoludur. Değişken tanımlamalarında ilk değerleri atarken ve daha sonra kullanımları göz önünde bulundurarak bellek kullanımını en iyi şekilde ayarlamalısınız. Ama şu ipucunu vermeden geçmemeliyim: 16 bitlik platformlar için sayı değişkenlerini minimum word, 32 bitlik platformlar için ise dword olarak ayarlamaya çalışın. Aksi takdirde aritmetik işlemler yaparken fazladan zahmet çekebilirsiniz. Ne demek istediğimi ileride daha açık biçimde anlatacağım...

 

Örnek olarak verilen programda herhangi bir değişken tanımlaması yoktur. Bu programda işlemcinin yazmaçları doğrudan doğruya değişken görevinde kullanılmıştır. Ancak dikkat edilirse .stack diye belirtilmiş bir bölüm vardır ve bu bölüme hiçbir şey yazılmadan geçilmiştir. Anlaşılacağı üzere burası yığın bölümüdür ve ihtiyacınız olmadığı sürece bu bölgede değişken tanımlamaktan kaçınmalısınız. Çünkü yığın üzerinde tanımlanmış değişkenlere erişmek için fazladan kod yazmak gerekir. Eğer yığını etkin biçimde kullanacaksanız, yani programınızın içinde üst üste çok fazla push ve call komutları geçiyorsa, derleyicinin bir bölge oluşturması amacıyla .stack deyiminden sonra herhangi bir sayı belirtebilirsiniz. Belirteceğiniz sayı, bayt cinsinden yığına ayırılmış bölge olacaktır. Örnekteki belirtmedeki tek amaç, derleyicinin "Warning: No stack" mesajını vermemesini sağlamaktır. Bir bölümün bitirilmesi için yeni bir bölümün başlatılması yeterlidir. Örnek programda .code deyimi, yığın bölümünü bitirmektedir.

 

Bu şekilde işlemci segmentlerindeki bilgileri önceden ayarlamak için tek yol, yukarıda anlatılanlar değildir. Zira TASM ve benzeri dillerin pek çok anahtar kelimesi mevcuttur. Mesela 3 önemli bölümden bahsederken kullanıcı segmentleri diye belirttiğim birşey vardı. Siz sözde isimlerle segmentler tanımlıyorsunuz ve program başlangıcında ds, es, ss ve cs'nin bu segmentleri işaret etmelerini belirtiyorsunuz. Buna benzer bir örneği de sizlere sunacağım.

 

Bu noktaya kadar ifadelerin yazımı için geçerli olan ve bundan sonra da aynı önemi göstermemiz gereken çok önemli bir kural var: Assembler dosyalarında önemli olan şey satırlardır. Çünkü C ve Pascal dillerinde olduğu gibi deyimleri birbirinden ayıran ayraçlar (;) yoktur. Bu nedenle deyimlerin sırası ve yerleşimi önem taşımaktadır. Derleyiciler, bir satırı ele aldıklarında CR(10) karakterini görene dek deyimleri inceler. Satır içerisinde noktalı virgül ile karşılaşıldıktan satırın sonuna dek olan ibareler dikkate alınmaz. Bu nedenle açıklama yazmak için programın herhangi bir satırında noktalı virgül (;) koymak yeterlidir. Unutmayın ki assemblerda, C'de veya Pascal'da olduğu gibi yorum sonlandırma(/* */ ve { } gibi) karakter(ler)i yoktur. Bu nedenle her satırda yorum karakterini koymanız gerekir.

 

Program Kodu

 

Bu bölüm, zaten bildiğimiz veya tasarlayabildiğimiz algoritmanın mikroişlemci komutları ile ifade edildiği ve assembler'in nispeten kolay bölümüdür. Platforma ilişkin bellek ve işlemci değişkenleri ile bağdaşacak komutları yazarak yapılacak işi belirttiğiniz program kodunun yazımı, Debug'takine göre çok daha tehlikesizdir. Çünkü artık değişkenleri gönül rahatlığı içinde kullanabilecek, alt programlar tanımlayabilecek ve dallanma komutlarının hedeflerini belirtmek için etiketler(label) kullanabileceğiz.

 

Kod İçerisinde Değişken Kullanımı

 

Bir önceki bölümde değişkenlerin tanımlanmasından bahsederken boyutları da göz önünde bulundurmuştuk. Şimdi bu boyutların ne şekilde kullanılacağını işleyeceğiz. Aşağıdaki örnekte kullanılan değişkenin (i), bir önceki bölümde tanımlanmış olan değişken olduğunu varsayalım.

 

mov ax, i

add ax, 5

mov i, ax

 

Yukarıdaki kod ile i değişkeninin değeri 5 arttırılmıştır. Önce bu değişken ax yazmacına taşınmış, sonra ax yazmacına 5 eklenmiş, son olarak da ax yazmacındaki değer tekrar i değişkenine yazılmıştır. Peki bu i değişkeni nedir ve nerededir?

 

Assembler dilinde sizin tanımlamış olduğunuz değişkenler, aslında bir bellek gözünü işaret etmektedir. Yani derleme esnasında bu değişkenlerin kullanıldıkları yerlere ilgili bellek adresleri yazılır. Derleyici, herhangi bir değişkenin yerini sabit olarak belirler. Artık bu değişken ile ilgili iki durum vardır:

  1. Değişkenin temsil ettiği bellek gözündeki değer,
  2. Değişkenin temsil ettiği bellek gözü(segment ve offset).

Derleyicinin bildiği tek şey, bu değişkenin yeridir. Bellek gözünün içeriği ile ilgili hiçbir şey bilinemez. Assemblerda bir bellek gözündeki bilgiye erişmenin yolu, adresi köşeli parantez içinde yazmaktır. Dolayısıyla, kod içerisinde i yazdığınızda derleyicinin yaptığı işlem, buraya [i'nin adresi] biçiminde bir bilgi yazmaktır. Yukarıda yazmış olduğunuz kod, derlendiğinde aşağıda yazılan şekle dönüşür. Bellek adresini tamamen keyfi yazıyorum:

 

mov ax, [038b]

add ax, 5

mov [038b], ax

 

AX yazmacı 16 bitlik olduğu için belirtilen bellek gözünden itibaren 2 bayt AX'e yazılmakta ve geri yazma işleminde de aynı prosedür uygulanmaktadır. Siz, i'nin adresini bilemezsiniz, sadece uzmanlığınıza bağlı olarak tahminde bulunabilirsiniz. Bu adresi programın içerisinde kullanmak için aşağıdaki gibi bir ifade yazmalısınız:

 

mov ax, offset i

mov bx, seg i

 

Yukarıdaki belirtimde ax yazmacına i değişkeninin bulunduğu bellek gözünün offset değeri, bx yazmacına ise i'ye ilişkin ofsetin geçerli olduğu segment değeri yazılmaktadır. Fazla sayıda elemanlı bir dizi tanıtıldığında, bu dizinin elemanlarına erişmek için aşağıdaki yöntem kullanılabilir:

 

mov bx, offset dizi

xor si, si

mov ax, [bx+si]

 

Yukarıdaki ifadede SI yazmacının değeri değiştirilmek(daha ziyade arttırılmak) suretiyle dizinin diğer elemanlarına erişilebilir. Mesela yukarıda önce SI sıfırlanmış, sonra da AX yazmacına dizinin ilk elemanı(hatta dizinin elemanları birer baytlık olduğu için ilk iki elemanı) aktarılmıştır. Artık bu elemanlar işlenebilir ve geri yazılabilir.

 

Unutulmaması gereken en önemli şey, Intel işlemcilerinin bellekten belleğe doğrudan kopyalama yapmadıklarıdır. Bu nedenle mov i, j gibi ifadeler hata ile sonuçlanacaktır. Ayrıca değişkenlerin yer aldığı komutların doğrudan bellek ile çalışıp çalışmadığının da bilinmesi gerekir.

 

Bu bölüm devam edecek, ancak konu çok geniş ve uzun olduğu için tamamını tek bir yazıda ele almak istemiyorum. Bir dahaki yazım, bu bölümün devamı biçiminde olacak. O zamana dek isterseniz buradakileri inceleyin, yazının devamı ile de ilk programımızı kendimiz, mantığımızla yaratırız. Şimdilik hoşçakalın...

 


 

Geçen zamanın özeti

 

Şu an 16 Ekim 2004 Cumartesi, saat 22.45 ve bilgisayar, CD'yi okumaya devam ediyor. Şimdilik 666MB toplamın 644MB'ı okunmuş durumda. Ben bu işin yarın sabaha karşı biteceğini umuyorum. Canım sıkılmıyor, çünkü sistemi tehlikeye sokmayacak başka işlerle uğraşabiliyorum(WindowsXP sağolsun, kötülemeyin sonra güzelim işletim sistemini). Ama bir elektrikler kesilse... bir kesilse... Cinnet geçirebilirim!!!

 

Cihan Atıl Namlı

 

16 Ekim 2004 Cumartesi, 22.45