Mikroişlemciler · Ders Notu
Bölüm 9.2: emu8086.inc Kütüphanesi
Assembly ile yazılan hemen her program ekrana bir şey yazar, klavyeden bir şey okur ya da bir sayıyı onluk tabanda gösterir. Bu işlerin her biri birkaç BIOS ve DOS hizmetinin doğru sırayla çağrılmasını gerektirir. emu8086, sık kullanılan bu işleri emu8086.inc adlı hazır bir kütüphanede toplar. Not, kütüphanenin INCLUDE direktifiyle programa nasıl katıldığını ve programın boyutunu neyin belirlediğini anlatarak başlar. Ardından ekran makrolarını (PUTC, GOTOXY, PRINT, PRINTN, CURSOROFF, CURSORON) açılımlarıyla birlikte ele alır. Sonra DEFINE_ ve CALL ile kullanılan prosedürlere geçer: dizgi yazan PRINT_STRING ve PTHIS, klavyeden dizgi okuyan GET_STRING, ekranı temizleyen CLEAR_SCREEN, sayı okuyan SCAN_NUM ve sayı yazan PRINT_NUM ile PRINT_NUM_UNS. Her prosedürün mantığı sade bir eşdeğer kodla yazmaç yazmaç izlenir. Son kesimler bütün parçaları birleştiren örnek programlara ve kütüphane kullanılırken en sık yapılan hatalara ayrılmıştır.
Öğrenme çıktıları
- INCLUDE direktifini açıklamak
- Bir kütüphane dosyasının kaynağa derleme sırasında metin olarak eklendiğini, assembler'ın dosyayı hangi klasörlerde aradığını ve program boyutunu yalnız kullanılan makroların ve tanımlanan prosedürlerin belirlediğini açıklamak.
- Ekran makrolarını kullanmak
- PUTC, GOTOXY, PRINT, PRINTN, CURSOROFF ve CURSORON makrolarının hangi BIOS hizmetine ve hangi komut dizisine genişlediğini göstermek; imleç konumunu, satır sonunu ve ekran kaymasını önceden hesaplamak.
- Prosedürleri doğru tanımlayıp çağırmak
- DEFINE_ makrolarıyla prosedürleri programa eklemek, CALL ile çağırmak, her prosedürün girdi ve çıktı yazmaçlarını bilmek ve hangi yazmaçların korunduğunu söylemek.
- Dizgi ve sayı prosedürlerini izlemek
- PRINT_STRING, PTHIS, GET_STRING, SCAN_NUM, PRINT_NUM ve PRINT_NUM_UNS'nin mantığını yazmaç, yığın ve tampon düzeyinde adım adım izlemek; işaretli ve işaretsiz yorum farkını açıklamak.
- Örnek programlar yazmak
- Kütüphane makro ve prosedürleriyle sayı okuyan, hesaplayan, sonucu yazan ve ekranı düzenleyen programlar yazmak; sık yapılan hataları tanımak.
9.2.1Kütüphane ve INCLUDE
9.2.1.1Ortak işlevler kütüphanesi
Ekrana tek bir karakter yazmak bile 8086'da birkaç komut ister. Karakter AL'ye, hizmet numarası AH'ye konur ve BIOS'un ekran kesmesi INT 10h çağrılır. Bu sırada AX'in eski değeri bozulur; değer gerekiyorsa önce yığına itilip sonra geri alınmalıdır. Bir sayıyı onluk tabanda yazmak daha da uzundur: sayı 10'a bölünür, kalanlar basamak olur, basamaklar ASCII koduna çevrilir ve doğru sırayla yazılır. Klavyeden çok basamaklı bir sayı okumak da aynı işin tersidir. Bu kalıplar hemen her programda yeniden gerekir. Her seferinde yeniden yazılmaları hem zaman kaybıdır hem de hata kaynağıdır.
emu8086 bu sorunu bir kütüphaneyle çözer. Kurulumla gelen emu8086.inc dosyası ekran, klavye ve sayı işlemleri için hazır makrolar ve prosedürler içerir. Programcı bu dosyayı kaynağın başına ekler ve içindeki işlevleri adlarıyla kullanır. Kütüphane yeni bir yetenek getirmez: içindeki her şey BIOS ve DOS kesmeleriyle, yani programcının kendisinin de yazabileceği komutlarla kuruludur. Kazanç, sınanmış kodun yeniden kullanılmasıdır.
Kütüphane (library), hazır makro ve prosedürleri içeren ve başka programlara eklenerek kullanılan kaynak dosyasıdır. emu8086'nın kütüphanesi emu8086.inc dosyasıdır; uzantısındaki inc ekleme (include) dosyası olduğunu gösterir.
Kütüphanenin bir sınırı da baştan bilinmelidir. emu8086.inc, emu8086 ortamına özgüdür. MASM ya da TASM gibi başka bir assembler bu dosyayı tanımaz; kütüphanenin makroları da o assembler'ların makro diliyle birebir uyumlu olmayabilir. İçerideki kod ise sıradan 8086 komutlarından oluşur. Bu yüzden kütüphanenin nasıl çalıştığını anlamak, başka ortamlarda aynı işi yapan kodu yazmayı da öğretir. Notun izleme kesimleri bu amaçla her prosedürün mantığını sade bir eşdeğer kodla gösterir.
9.2.1.2INCLUDE direktifi
Başka bir dosyadaki tanımlar kaynağa INCLUDE direktifiyle alınır. emu8086.inc için satır şöyledir:
include 'emu8086.inc'INCLUDE, assembler'a belirtilen dosyanın metnini bu satırın yerine koymasını söyleyen direktiftir (directive). Direktif işlemci komutu üretmez; işi derleme sırasında biter. Çalışan programda “INCLUDE” diye bir komut yoktur.
Dosya adı tırnak içinde ya da tırnaksız yazılabilir: include 'emu8086.inc' ve include emu8086.inc aynı işi görür. Büyük-küçük harf farkı da önemsizdir. Assembler dosyayı iki yerde arar (Şekil 9.2.1). Önce derlenen kaynak dosyanın bulunduğu klasöre bakar. Dosya orada yoksa emu8086 kurulumundaki Inc klasörüne bakar. emu8086.inc kurulumla birlikte bu klasöre yerleştirildiği için çoğu programda ikinci adımda bulunur. Programcı kendi kütüphanesini yazıp kaynakla aynı klasöre koyarsa ilk adımda bulunur. Aynı adla bir dosya kaynak klasöründe de varsa, kaynak klasöründeki kazanır.
Direktifin yeri de önemlidir. Bir makro ancak tanımından sonra kullanılabilir; assembler kaynağı yukarıdan aşağı okur ve tanımını görmediği bir makro adını komut sanıp hata verir. Bu yüzden include satırı kaynağın en başına, ORG 100h'den önce yazılır. Satırın kendisi bayt üretmediği için başa konması programın adreslerini değiştirmez.
include satırını programın sonuna, END'den hemen önceye yazmak. Assembler PUTC 'A' gibi bir satıra geldiğinde PUTC'nin tanımını henüz görmemiştir ve bilinmeyen komut hatası verir. Makroların tanımı kullanımlarından önce gelmelidir.
9.2.1.3INCLUDE sonrası kaynak
INCLUDE'un ne yaptığını görmenin en kolay yolu, assembler'ın gördüğü kaynağı düşünmektir. Assembler include satırını siler ve yerine emu8086.inc'in bütün metnini koyar. Ardından ortaya çıkan tek parça kaynağı derler. Sadeleştirilmiş görüntü şöyledir:
; include satırının yerine:
PUTC MACRO char ; şablon: 0 bayt
...
ENDM
GOTOXY MACRO col, row
...
ENDM
DEFINE_SCAN_NUM MACRO
... ; prosedür şablonu
ENDM
; programın kendisi:
ORG 100h
PUTC 'A' ; burada 8 bayt
RETBu görüntüden üç sonuç çıkar. Birincisi, kütüphanenin büyük bölümü MACRO … ENDM bloklarından oluşur. Bir makro tanımı yalnız bir şablondur; assembler onu bir yere not eder ama karşılığında hiç bayt üretmez. Kullanılmayan bir makro programa tek bayt eklemez. İkincisi, kod makronun kullanıldığı yerde oluşur. Yukarıdaki programda PUTC 'A' satırı açılınca beş komutluk, 8 baytlık bir dizi üretir. Programın tamamı bu 8 bayt ile RET'in 1 baytından oluşur: 9 bayt.
Üçüncü sonuç prosedürlerle ilgilidir. Kütüphanedeki prosedürler de doğrudan kod olarak değil, makroların içinde saklanır. Örneğin SCAN_NUM prosedürünün bütün kodu DEFINE_SCAN_NUM adlı bir makronun gövdesidir. Program DEFINE_SCAN_NUM satırını yazmazsa makro hiç açılmaz ve SCAN_NUM'ın kodu programa girmez. Böylece kütüphanenin tamamını eklemek programı büyütmez. Program boyutunu yalnız iki şey belirler: kullanılan makroların her kullanımı ve DEFINE_ ile tanımlanan prosedürler.
| Kaynakta | Üretilen kod | Ne zaman? |
|---|---|---|
include 'emu8086.inc' | yok | yalnız tanımlar okunur |
makro kullanımı (PUTC 'A') | makronun gövdesi, parametreleri yerleştirilmiş olarak | her kullanımda yeniden |
DEFINE_SCAN_NUM | prosedürün bütün kodu | bir kez, satırın bulunduğu yerde |
CALL scan_num | 3 baytlık CALL komutu | her çağrıda |
Çalışan programın içinde “emu8086.inc” diye bir şey kalmaz. Bellekte yalnız açılmış makro komutları ve tanımlanmış prosedürler bulunur. emu8086'nın bellek penceresinde ya da sökücüsünde (disassembler) bakıldığında PUTC'nin yerinde PUSH, MOV, INT ve POP komutları görülür. Bu durum hata ayıklamada önemlidir: tek adım çalıştırırken bir makro satırı birden çok komut adımına karşılık gelir.
9.2.1.4Kütüphanede neler var?
emu8086.inc'in içeriği iki gruba ayrılır (Tablo 9.2.2, Tablo 9.2.3). Makrolar doğrudan adlarıyla yazılır. Prosedürler önce bir DEFINE_ makrosuyla tanımlanır, sonra CALL ile çağrılır.
| Makro | Görev | Arka planda |
|---|---|---|
PUTC char | imlecin bulunduğu yere tek karakter yazar | INT 10h, AH = 0Eh |
GOTOXY col, row | imleci verilen sütun ve satıra götürür | INT 10h, AH = 02h |
PRINT string | dizgiyi yazar | 0Eh ile karakter karakter |
PRINTN string | dizgiyi yazar, sonra yeni satıra geçer | 0Eh ile karakter karakter; sona 13, 10 |
CURSOROFF | imleci gizler | INT 10h, AH = 01h |
CURSORON | imleci gösterir | INT 10h, AH = 01h |
| Prosedür | Görev | Tanım satırı |
|---|---|---|
PRINT_STRING | DS:SI'deki 0 ile biten dizgiyi yazar | DEFINE_PRINT_STRING |
PTHIS | CALL'ın hemen ardındaki dizgiyi yazar | DEFINE_PTHIS |
GET_STRING | klavyeden dizgi okur | DEFINE_GET_STRING |
CLEAR_SCREEN | ekranı temizler | DEFINE_CLEAR_SCREEN |
SCAN_NUM | klavyeden işaretli sayı okur, CX'e yazar | DEFINE_SCAN_NUM |
PRINT_NUM | AX'i işaretli sayı olarak yazar | DEFINE_PRINT_NUM + DEFINE_PRINT_NUM_UNS |
PRINT_NUM_UNS | AX'i işaretsiz sayı olarak yazar | DEFINE_PRINT_NUM_UNS |
İki grubun farkı makro ile prosedür arasındaki temel farktır. Makro kullanıldığı her yere kopyalanır; kısa ve sık kullanılan işler için uygundur. Prosedürün kodu bellekte bir kez bulunur ve her kullanımda CALL ile oraya gidilir; uzun işler için uygundur. Kütüphane bu ayrımı mantıklı biçimde yapar. PUTC 8 bayttır ve bir CALL ile RET'in maliyeti işin kendisine yakın olurdu; bu yüzden makrodur. SCAN_NUM ise eksi işaretini, geri silmeyi ve taşmayı denetleyen yüzlerce baytlık bir koddur. Her kullanımda kopyalanması programı hızla büyütürdü; bu yüzden prosedürdür.
Prosedür çağrısının süresi de hesaplanabilir. 8086'da aynı kesim içindeki doğrudan bir CALL 19, yakın RET 8 saat turu sürer. 5 MHz'lik bir 8086'da bir tur 200 ns olduğundan çağırıp dönmenin bedeli 27 × 200 ns = 5,4 µs'dir. Bir karakter yazan INT 10h ise BIOS'un içinde yüzlerce komut çalıştırır. Dolayısıyla ekran ve klavye işlerinde CALL/RET'in süresi önemsizdir; asıl belirleyici, bellekte kaplanan yerdir.
9.2.2Ekran makroları
9.2.2.1Metin ekranı, imleç ve BIOS hizmetleri
emu8086'nın çıktı penceresi PC'nin standart metin ekranını taklit eder. Ekran 80 sütun ve 25 satırdan oluşur. Her hücrede bir karakter durur. Hücrelerin konumu (sütun, satır) çiftiyle verilir: sol üst köşe (0, 0), sağ alt köşe (79, 24)'tür. Sayma sıfırdan başlar; bu yüzden “10. sütun” soldan on birinci hücredir.
İmleç (cursor), ekranda bir sonraki karakterin yazılacağı konumdur. BIOS imlecin sütununu ve satırını kendi veri alanında saklar. Karakter yazan hizmetler karakteri imlecin bulunduğu hücreye koyar ve imleci bir sağa ilerletir.
Ekranın arkasında bir bellek bölgesi vardır. Renkli metin modunda video kartının belleği işlemcinin adres uzayında B800h kesiminden başlar. Her hücre iki bayt tutar: karakterin ASCII kodu ve rengini belirleyen öznitelik baytı. Video kartı bu belleği saniyede onlarca kez okur ve ekrana çizer. Bir karakteri ekrana koymanın en doğrudan yolu bu belleğe MOV ile yazmaktır. İşlemci için bu sıradan bir bellek yazma yol çevrimidir; adres yolunda B8xxxh gibi bir fiziksel adres, veri yolunda karakter kodu görünür.
Kütüphanenin makroları bu belleğe doğrudan yazmaz; BIOS'un ekran hizmetlerini kullanır. INT 10h komutu işlemciyi anakarttaki ROM'da bulunan BIOS yordamına götürür. Yordam, AH'deki hizmet numarasına bakarak ne yapacağına karar verir. İmleci ilerletmek, satır sonunda alt satıra geçmek ve son satırda ekranı kaydırmak gibi ayrıntıları BIOS üstlenir. Kütüphanenin kullandığı üç hizmet Tablo 9.2.4'tedir.
| AH | Hizmet | Girdiler | Kullanan |
|---|---|---|---|
| 01h | imleç şeklini ayarla | CH = başlangıç, CL = bitiş tarama satırı | CURSOROFF, CURSORON |
| 02h | imleç konumunu ayarla | BH = sayfa, DH = satır, DL = sütun | GOTOXY |
| 0Eh | karakter yaz ve imleci ilerlet (teletype) | AL = karakter | PUTC, PRINT, PRINTN |
0Eh hizmetinin adı eski daktilo-telgraf makinelerinden gelir. Bu makineler gibi hizmet de karakteri yazar ve kağıdı, yani imleci, ilerletir. Kontrol karakterlerini de makine gibi yorumlar: 13 imleci satır başına, 10 alt satıra götürür, 8 bir sola alır, 7 ses çıkarır. Bu özellik ekran makrolarının davranışının büyük bölümünü açıklar.
9.2.2.2PUTC ve GOTOXY neye genişler?
PUTC tek bir parametre alır: yazılacak karakter. Parametre bir sabit (PUTC 65), bir karakter sabiti (PUTC 'B') ya da 8 bitlik bir yazmaç veya değişken (PUTC AL) olabilir. Assembler makroyu açarken parametrenin yazıldığı yere bu metni koyar. PUTC 'A' şu beş komuta genişler:
PUSH AX ; AX'i koru
MOV AL, 'A' ; parametre
MOV AH, 0Eh ; teletype
INT 10h
POP AX ; AX geriKomutlar sırayla şunları yapar. PUSH AX SP'yi 2 azaltır ve AX'in o anki değerini yığına yazar. MOV AL, 'A' AL'ye 41h koyar; AH'ye dokunmaz. MOV AH, 0Eh hizmet numarasını AH'ye koyar; artık AX = 0E41h'dir. INT 10h bayrakları, CS'yi ve IP'yi yığına iter ve BIOS'a geçer. BIOS 'A'yı imlecin bulunduğu hücreye yazar, imleci bir sağa alır ve IRET ile döner. POP AX yığındaki eski değeri AX'e geri yükler ve SP'yi 2 artırır. Makrodan çıkıldığında AX, SP ve bayraklar girişteki değerlerine dönmüştür. Çağıran kod AX'in geçici olarak değiştiğini fark etmez.
Açılımın boyutu makine kodlarından hesaplanır: PUSH AX 1 bayt (50h), MOV AL, 41h 2 bayt (B0 41), MOV AH, 0Eh 2 bayt (B4 0E), INT 10h 2 bayt (CD 10), POP AX 1 bayt (58h). Toplam 8 bayttır.
GOTOXY iki parametre alır: önce sütun, sonra satır. GOTOXY 10, 5 şöyle açılır:
PUSH AX
PUSH BX
PUSH DX
MOV AH, 02h ; imleç konumla
MOV DH, 5 ; satır (row)
MOV DL, 10 ; sütun (col)
MOV BH, 0 ; sayfa 0
INT 10h
POP DX
POP BX
POP AXMakro AH, BH, DH ve DL'yi kullanır; bu yüzden AX, BX ve DX'i saklar. POP'lar PUSH'ların ters sırasıyla yapılır: en son itilen DX ilk geri alınır. Açılım 16 bayttır: üç PUSH ve üç POP 6 bayt, dört MOV r8, sabit 8 bayt, INT 10h 2 bayt. BH = 0, metin modunun ilk görüntü sayfasını seçer. Renkli metin kartının belleğinde birden çok ekran sayfası sığar; emu8086 programları hep 0. sayfayı kullanır.
Parametre sırası en sık karışan noktadır. Makro matematikteki (x, y) sırasını kullanır: önce yatay konum (sütun), sonra dikey konum (satır). BIOS ise satırı DH'de, sütunu DL'de bekler. Makro bu dönüşümü kendisi yapar: ikinci parametreyi DH'ye, birinciyi DL'ye koyar.
GOTOXY 5, 10 yazıp imlecin 5. satıra gitmesini beklemek. Makro 5'i sütun, 10'u satır olarak alır; imleç 10. satırın 5. sütununa gider. Benzer bir tuzak, konumu AH'de tutmaktır: GOTOXY AL, AH açılımında önce MOV AH, 02h çalışır, ardından MOV DH, AH satıra 2 yazar. Örneğin AL = 20, AH = 7 iken imleç 7. satıra değil 2. satıra, (20, 2) konumuna gider. Konum önce makronun kullanmadığı yazmaçlara alınmalıdır.
9.2.2.3PRINT ve PRINTN
PRINT bir dizgiyi yazar. Parametresi kaynakta tırnak içinde verilen sabit bir metindir: PRINT 'Hello World!'. Makro bu metni koda gömer ve 0Eh hizmetiyle karakter karakter yazar. emu8086.inc'teki PRINT'in açılımı şu yapıdadır (yerel etiket adları sadeleştirildi):
PUSH AX
PUSH SI
JMP atla ; dizginin üstünden atla
dizgi DB 'Hello World!', 0
atla:
LEA SI, dizgi
sonraki:
MOV AL, CS:[SI] ; karakteri kod kesiminden oku
CMP AL, 0
JZ bitti
INC SI
MOV AH, 0Eh
INT 10h
JMP sonraki
bitti:
POP SI
POP AXAçılımın en ilginç yanı dizginin yeridir. Dizgi kodun ortasında durur ve işlemci onu komut sanmasın diye üstünden bir JMP ile atlanır. Makroya özgü etiketler LOCAL direktifiyle her açılımda benzersiz adlar alır; PRINT ikinci kez kullanıldığında etiket çakışması olmaz. Dizgi kodun içinde olduğu için kod kesimindedir ve karakterler CS:[SI] ile okunur. .COM programında CS = DS olduğu için önek fark yaratmaz; ama önek sayesinde makro DS'nin başka bir kesimi gösterdiği durumlarda da doğru çalışır.
Açılım SI'yi ve AX'i saklar. Boyutu dizginin uzunluğuna bağlıdır: n karakterlik bir dizgi için n + 1 bayt veri ve 23 bayt kod, toplam 24 + n bayt. PRINT 'Hello World!' 12 karakter için 36 bayt tutar. Döngü kodu PRINT'in her kullanımında yeniden kopyalanır.
PRINTN aynı işi yapar, yalnız dizginin sonuna 13 ve 10 baytlarını ekler. Makro dizgiyi DB 'metin', 13, 10, 0 biçiminde tanımlar. 13 (CR, satır başı) imleci satırın 0. sütununa, 10 (LF, satır besleme) bir alt satıra götürür. İkisi birlikte yeni satıra geçişi sağlar (Şekil 9.2.2). PRINT 'a' ardından PRINT 'b' ekranda ab bırakır. PRINTN 'a' ardından PRINT 'b' ise a'yı bir satıra, b'yi alt satırın başına yazar. PRINTN'in açılımı PRINT'inkinden 2 bayt uzundur.
PRINT'in parametresi derleme anında bilinen sabit bir metin olmalıdır. Makro metni koda gömdüğü için çalışma sırasında oluşan bir dizgiyi, örneğin klavyeden okunan bir adı yazamaz. PRINT ad yazıldığında assembler DB ad, 0 satırını üretir. Bu satır ad değişkeninin adresini bayt olarak tanımlamaya çalışır; sonuç ya bir hata ya da anlamsız bir bayttır. Çalışma sırasında oluşan dizgiler PRINT_STRING ile yazılır.
9.2.2.4Örnek: makrolarla ekran
emu8086'nın kütüphane belgesindeki ilk örnek üç makroyu birlikte kullanır:
include emu8086.inc
ORG 100h
PRINT 'Hello World!'
GOTOXY 10, 5
PUTC 65 ; 65 - is an ASCII code for 'A'
PUTC 'B'
RET ; return to operating system.
END ; directive to stop the compiler.İlk iki satır kod üretmez. include makro tanımlarını ekler, ORG 100h assembler'a programın 0100h bağıl konumundan başlayacağını söyler. .COM programlarında ilk 256 bayt (0000h–00FFh) DOS'un program kesimi önekine (PSP) ayrıldığı için kod 0100h'den başlar. Program başladığında ekran boştur ve imleç (0, 0)'dadır.
PRINT 'Hello World!' 12 karakteri 0. satırın 0–11. sütunlarına yazar. Her karakterden sonra BIOS imleci bir sağa aldığı için imleç (12, 0)'da kalır. GOTOXY 10, 5 imleci 10. sütun, 5. satıra taşır; ekrana bir şey yazmaz. PUTC 65 sayıyı karakter kodu olarak kullanır: 65 = 41h, ASCII'de 'A'dır. 'A' (10, 5) hücresine yazılır ve imleç (11, 5)'e geçer. PUTC 'B' aynı işi karakter sabitiyle yapar; assembler 'B''yi 42h'ye çevirir. 'B' (11, 5)'e yazılır, imleç (12, 5)'e geçer. RET programı bitirir. END direktifi yalnız assembler'a kaynağın bittiğini söyler; kod üretmez.
| Satır | İmleç (sütun, satır) | Ekrana yazılan | Açıklama |
|---|---|---|---|
include emu8086.inc | (0, 0) | — | makro tanımları eklenir, kod yok |
ORG 100h | (0, 0) | — | .COM programı 0100h'den başlar |
PRINT 'Hello World!' | (12, 0) | Hello World! (0–11. sütun, 0. satır) | 12 karakter |
GOTOXY 10, 5 | (10, 5) | — | yalnız imleç taşınır |
PUTC 65 | (11, 5) | A, (10, 5) hücresine | 65 = 41h = 'A' |
PUTC 'B' | (12, 5) | B, (11, 5) hücresine | karakter sabiti |
RET | (12, 5) | — | DOS'a dönüş |
Programın makine kodu 0100h'den 0144h'ye kadar uzanır. 36 baytlık PRINT açılımı 0100h–0123h'yi, 16 baytlık GOTOXY açılımı 0124h–0133h'yi, iki PUTC açılımı 0134h–0143h'yi kaplar; RET 0144h'dedir. Kaynakta yedi satır görünse de işlemci 40'tan fazla farklı komut çalıştırır. PRINT'in döngüsü 12 kez döndüğü için yürütülen komut sayısı yüzü aşar.
9.2.2.5CURSOROFF ve CURSORON
Metin ekranındaki imleç, video kartının yanıp söndürdüğü küçük bir çizgidir. Çizginin yüksekliği karakter hücresinin tarama satırlarıyla verilir. Renkli metin modunda bir karakter hücresi 8 ya da 16 tarama satırı yüksekliğindedir; imleç normalde hücrenin alt kısmındaki bir iki satırı kaplar. INT 10h'nin 01h hizmeti imlecin şeklini ayarlar: CH başlangıç, CL bitiş tarama satırıdır.
CH'nin 5. biti özel bir anlam taşır. Bu bit 1 yapılırsa imleç ekrana hiç çizilmez. Kütüphanedeki CURSOROFF, CH'ye 28h yazar: 28h = 0010 1000b'dir ve 5. biti 1'dir. CURSORON aynı hizmetle 5. biti 0 olan normal bir şekil yükler. İki makro da AX ile CX'i saklayıp geri yükler.
PRINTN 'Ad Soyad:' ; yazıp alt satıra geç
CURSOROFF
GOTOXY 30, 12
PRINT 'Merhaba'
CURSORONBu parçada önce “Ad Soyad:” yazılır ve imleç alt satırın başına geçer. CURSOROFF imleci gizler. GOTOXY imleci (30, 12)'ye taşır ve “Merhaba” 30–36. sütunlara yazılır. Bu sırada yanıp sönen imleç ekranda dolaşmaz. CURSORON imleci yeniden görünür yapar; imleç (37, 12)'dedir. İmleci gizlemek yalnız görüntüyü değiştirir; imlecin konumu BIOS'ta tutulmaya ve yazı hizmetleri onu ilerletmeye devam eder. Oyun ve menü ekranlarında yanıp sönen imleç dikkat dağıttığı için gizlenir.
9.2.2.6Kontrol karakterleri
ASCII tablosunun ilk 32 kodu yazdırılabilir bir şekle karşılık gelmez. Bu kodlara kontrol karakteri (control character) denir. 0Eh hizmeti bunların dördünü daktilo gibi yorumlar (Tablo 9.2.6). PUTC bu kodlarla kullanıldığında ekrana bir şekil yazmaz, imleci yönetir.
| Kod | Ad | Etkisi |
|---|---|---|
| 07h | BEL (zil) | hoparlörden kısa bir bip; imleç yerinden oynamaz |
| 08h | BS (geri) | imleç bir sola gider; karakteri silmez; 0. sütunda etkisizdir |
| 0Ah | LF (satır besleme) | imleç bir alt satıra iner, sütun değişmez; son satırda ekran bir satır yukarı kayar |
| 0Dh | CR (satır başı) | imleç aynı satırın 0. sütununa döner; satırdaki yazı yerinde kalır |
Yeni satır bu yüzden iki karakterle yazılır: önce CR, sonra LF. PRINTN'in eklediği 13, 10 çifti bu iki hareketin birleşimidir. Sıra ters olsa da (10, 13) imleç aynı yere varır: önce bir alt satıra iner, sonra o satırın başına döner. DOS ve Windows metin dosyalarında satır sonu bu yüzden CR LF çiftidir.
Ekrandan bir karakteri silmek için BS tek başına yetmez; imleci sola alır ama karakter yerinde kalır. Silme üç adımla yapılır. PUTC 8 imleci sola alır. PUTC ' ' karakterin üstüne boşluk yazar ve imleci yine sağa geçirir. İkinci PUTC 8 imleci boşalan hücreye geri getirir. Kütüphanenin geri silme tuşunu işleyen prosedürleri ekranı bu üçlüyle düzeltir.
9.2.2.7CR ve LF ayrı ayrı: kod izleme
CR ile LF'nin ayrı ayrı ne yaptığını görmek için aşağıdaki program INT 10h'yi makrosuz kullanır. AH = 0Eh bir kez yüklenir; BIOS 0Eh hizmetinde AH'yi değiştirmediği için sonraki çağrılar için de geçerli kalır.
ORG 100h
MOV AH, 0Eh ; teletype
MOV AL, 'a'
INT 10h
MOV AL, 'b'
INT 10h
MOV AL, 10 ; yalnız LF
INT 10h
MOV AL, 'c'
INT 10h
MOV AL, 13 ; yalnız CR
INT 10h
MOV AL, 'd'
INT 10h
RET| Komutlar | AL | İmleç (sütun, satır) | Ekran |
|---|---|---|---|
MOV AH, 0Eh / MOV AL, 'a' / INT 10h | 61h | (1, 0) | a |
MOV AL, 'b' / INT 10h | 62h | (2, 0) | ab |
MOV AL, 10 / INT 10h | 0Ah | (2, 1) | ab |
MOV AL, 'c' / INT 10h | 63h | (3, 1) | ab / ··c |
MOV AL, 13 / INT 10h | 0Dh | (0, 1) | ab / ··c |
MOV AL, 'd' / INT 10h | 64h | (1, 1) | ab / d·c |
Tablodaki “/” satır ayrımını, “·” boş hücreyi gösterir. Yalnız LF gönderildiğinde imleç alt satıra iner ama 2. sütunda kalır; 'c' alt satırın 2. sütununa yazılır ve satırlar merdiven gibi kayar. Yalnız CR gönderildiğinde imleç aynı satırın başına döner; satırdaki 'c' silinmez. 'd' 0. sütuna yazılır ve ekranda “ab”nin altında “d c” görünür (Şekil 9.2.4). Düzgün bir yeni satır için iki kod birlikte gerekir.
INT 10h/0Eh AX dışında yazmaç değiştirmez; bu programda AH de çağrılar boyunca 0Eh kalır. Bu davranış BIOS'un belgelenmiş özelliğidir, ama başka hizmetler için genelleştirilmemelidir. Bazı INT 10h hizmetleri AX'i, bazıları BP'yi değiştirir. Kütüphane makrolarının AX'i her kullanımda saklaması bu yüzden güvenli bir alışkanlıktır.
9.2.2.8Satır sonu ve ekran kayması
0Eh hizmeti 79. sütuna bir karakter yazdıktan sonra imleci kendiliğinden alt satırın başına geçirir. PRINT uzun bir dizgiyi bu sayede kesmeden sürdürür. İmleç son satırdayken (24) alt satıra geçmek gerekirse BIOS ekranın tamamını bir satır yukarı kaydırır. En üst satır kaybolur, en alta boş bir satır gelir ve imleç bu yeni satıra geçer. Kaydırma, video belleğindeki alt 24 satırın bir satır yukarı kopyalanmasıdır.
GOTOXY 75, 24 ardından PRINT 'Merhaba' çalışıyor. Yazının ekranda nerede kaldığını ve imlecin son konumunu bulalım.
- İmleç (75, 24)'tedir. “M”, “e”, “r”, “h”, “a” sırasıyla 75, 76, 77, 78 ve 79. sütunlara yazılır.
- 79. sütuna yazıldıktan sonra imleç alt satıra geçmelidir. İmleç son satırda olduğu için ekran bir satır yukarı kayar. “Merha” 23. satıra çıkar; 24. satır boşalır ve imleç (0, 24)'e gelir.
- “b” ve “a” 24. satırın 0 ve 1. sütunlarına yazılır. İmleç (2, 24)'te kalır.
Sonuçta “Merha” 23. satırın 75–79. sütunlarında, “ba” 24. satırın başında, imleç (2, 24)'tedir (Şekil 9.2.5).
GOTOXY ve PUTC konumları denetlemez. GOTOXY 85, 3 gibi bir çağrıda BIOS'un davranışı tanımlı değildir; imleç görünmeyen bir yere gidebilir ve sonraki yazılar beklenmedik yerlerde çıkar. Programcı konumları 0–79 (sütun) ve 0–24 (satır) aralığında tutmalıdır.
9.2.2.9GOTOXY ve video belleği
Ekranda seçilen bir hücre üç farklı biçimde anlatılabilir: makro çağrısıyla (GOTOXY 10, 5), BIOS'a giden yazmaç değerleriyle (AH = 02h, DH = 5, DL = 10, BH = 0) ve hücrenin video belleğindeki adresiyle. Sonuncusu BIOS'un içinde ne olduğunu anlamak için yararlıdır.
Video belleğinde hücreler satır satır, soldan sağa dizilir. Her hücre iki bayt kapladığı için (sütun, satır) hücresinin bağıl konumu şöyle hesaplanır:
bağıl konum = (satır × 80 + sütun) × 2
Çift adresteki bayt karakterin ASCII kodu, tek adresteki bayt öznitelik baytıdır. GOTOXY 10, 5 ve ardından PUTC 'A' için hesap şöyledir: (5 × 80 + 10) × 2 = 410 × 2 = 820 = 0334h. 'A' (41h) B800:0334h'ye, öznitelik baytı (07h, siyah üstüne açık gri) B800:0335h'ye yazılır. Fiziksel adres B8000h + 0334h = B8334h'dir. Ekranın sağ alt köşesi (79, 24) için bağıl konum (24 × 80 + 79) × 2 = 3998 = 0F9Eh olur. 80 × 25 hücrelik bir sayfa 4000 bayt tutar.
Bu adresleri bilmek, makroların yaptığı işi doğrudan bellek yazımıyla yapmayı da mümkün kılar. Örneğin MOV AX, 0B800h, MOV ES, AX, MOV BYTE PTR ES:[0334h], 'A' dizisi 'A'yı aynı hücreye yazar. Fark, imlecin ilerlememesi ve ekranın kaymamasıdır: doğrudan yazım BIOS'un üstlendiği bütün ayrıntıları programcıya bırakır. Kütüphane makroları daha yavaştır ama bu ayrıntıları düşünmeyi gerektirmez.
9.2.2.10İki basamak yazdırma: kod izleme
PUTC'nin AX'i koruması küçük bir ayrıntı gibi görünür. Aşağıdaki örnek bu ayrıntının neden yararlı olduğunu gösterir. Program AL'deki 0–99 arası bir sayıyı iki onluk basamak olarak yazar. Basamakları ayırmak için AAM komutu kullanılır. Kodda iki PUTC'nin açılımı makro yerine açıkça yazılmıştır:
ORG 100h
MOV AL, 58
AAM ; AH = 5, AL = 8
ADD AX, 3030h ; '5', '8'
PUSH AX ; PUTC AH açılımı
MOV AL, AH
MOV AH, 0Eh
INT 10h
POP AX ; AX geri: 3538h
PUSH AX ; PUTC AL açılımı
MOV AL, AL
MOV AH, 0Eh
INT 10h
POP AX
RETAAM (ASCII adjust after multiply), AL'yi 10'a böler: bölümü AH'ye, kalanı AL'ye yazar. 0–99 arası bir sayının onlar basamağı AH'de, birler basamağı AL'de kalır. SF, ZF ve PF AL'ye göre kurulur. 8086'da AAM 83 saat turu sürer.
MOV AL, 58 AL'ye 58 = 3Ah koyar. AAM 58'i 10'a böler: AH = 5, AL = 8, yani AX = 0508h. Yalnız AL bölündüğü için sayı 0–99 aralığında olmalıdır; 100 ve üstünde bölüm de tek basamak olmaz. ADD AX, 3030h iki bayta birden 30h ekler. Bir basamağa 30h eklemek onu ASCII rakamına çevirir: 5 + 30h = 35h = '5', 8 + 30h = 38h = '8'. Toplamada iki bayt arasında elde oluşmaz, çünkü 08h + 30h = 38h 8 bite sığar.
İlk açılım onlar basamağını yazar. PUSH AX 3538h'yi yığına koyar, SP = FFFCh. MOV AL, AH AL'ye '5'i kopyalar ve MOV AH, 0Eh hizmet numarasını yükler; AX = 0E35h olur. Birler basamağı yazmaçtan silinmiştir. INT 10h ekrana 5 yazar. POP AX 3538h'yi geri getirir; '8' yeniden AL'dedir. İkinci açılım aynı kalıpla birler basamağını yazar. MOV AL, AL hiçbir şey değiştirmez ama zararsızdır; makro parametre AL olduğunda bu komutu üretir.
| Komut(lar) | AH | AL | SP | [FFFCh] | Ekran |
|---|---|---|---|---|---|
MOV AL, 58 | 00h | 3Ah | FFFEh | — | |
AAM | 05h | 08h | FFFEh | — | |
ADD AX, 3030h | 35h | 38h | FFFEh | — | |
PUSH AX | 35h | 38h | FFFCh | 3538h | |
MOV AL, AH / MOV AH, 0Eh / INT 10h | 0Eh | 35h | FFFCh | 3538h | 5 |
POP AX | 35h | 38h | FFFEh | — | 5 |
| ikinci açılım (5 komut) | 35h | 38h | FFFEh | — | 58 |
Programın sonunda ekranda 58 yazılıdır ve AX yine 3538h'dir (Tablo 9.2.8). PUTC AX'i koruduğu için iki basamak ayrı bir yazmaca kopyalanmadan art arda yazılabildi. Makro AX'i korumasaydı ilk PUTC '8'i silerdi ve programcı birler basamağını önceden başka bir yazmaca almak zorunda kalırdı. Kaynakta makrolarla yazıldığında aynı program yalnız PUTC AH ve PUTC AL satırlarını içerir.
9.2.3Prosedürleri kullanmak
9.2.3.1DEFINE_ ve CALL
Kütüphanedeki bir prosedürü kullanmak iki adım ister. Önce prosedür bir kez tanımlanır: bunun için adı DEFINE_ ile başlayan makro yazılır. Sonra programın gereken yerlerinde prosedür CALL ile çağrılır. Tipik bir iskelet şöyledir:
include 'emu8086.inc'
ORG 100h
...
CALL print_num ; kullan
...
RET
DEFINE_PRINT_NUM ; tanımla
DEFINE_PRINT_NUM_UNS
ENDDEFINE_ makrosu, gövdesinde bir kütüphane prosedürünün bütün kodunu taşıyan makrodur. Makro açıldığı yere prosedürü PROC … ENDP bloğu olarak yerleştirir. Her prosedür programda en çok bir kez tanımlanır; iki kez tanımlanırsa aynı etiket iki kez oluşur ve assembler hata verir.
Makronun açıldığı yer, prosedürün bellekteki yeridir. Kaynakta DEFINE_ satırları genellikle ana programın sonundaki RET ile END direktifi arasına yazılır (Şekil 9.2.6). Bellekte önce ana kod, ardından RET, ardından prosedürlerin kodu gelir. İşlemci ana kodu yukarıdan aşağı çalıştırır, RET ile DOS'a döner ve prosedürlerin koduna yalnız CALL ile girer.
Bu yerleşim alışılmış olandır ama tek güvenli yer değildir. emu8086.inc'teki her DEFINE_ makrosu bir koruma içerir: gövdenin başında prosedürün üstünden atlayan bir JMP vardır. DEFINE_SCAN_NUM'ın yapısı kabaca şöyledir:
JMP skip_proc_scan_num ; akış gövdeye girmesin
SCAN_NUM PROC NEAR
... ; prosedürün kodu
RET
SCAN_NUM ENDP
skip_proc_scan_num:Bir DEFINE_ satırı yanlışlıkla ORG 100h'nin hemen altına yazılsa da program doğru çalışır. İşlemcinin ilk çalıştırdığı komut bu JMP'dir ve gövdenin sonrasına, ana programın başına atlar. Bedeli, programda fazladan bir JMP komutunun bulunmasıdır. Yine de DEFINE_ satırlarının RET ile END arasında toplanması programı okunur tutar ve gereksiz atlamaları önler.
Prosedür adları kaynakta küçük harfle yazılır: CALL print_num, CALL scan_num. emu8086 etiketlerde büyük-küçük harf ayırmaz; CALL PRINT_NUM da aynı etikete gider. Küçük harf, belgelerdeki yazımdır ve makro adlarıyla (DEFINE_PRINT_NUM) prosedür adını görsel olarak ayırır.
Bir prosedür başka bir prosedürü çağırıyorsa ikisi birlikte tanımlanmalıdır. PRINT_NUM sayının işaretini yazdıktan sonra mutlak değeri PRINT_NUM_UNS'ye yazdırır. Yalnız DEFINE_PRINT_NUM yazılırsa assembler PRINT_NUM'ın içindeki CALL PRINT_NUM_UNS satırında etiketi bulamaz ve tanımsız etiket hatası verir. Ters durum hata vermez: PRINT_NUM_UNS tek başına tanımlanabilir.
CALL scan_num yazıp DEFINE_SCAN_NUM'ı unutmak. Kütüphanenin eklenmiş olması prosedürün tanımlı olduğu anlamına gelmez; makro açılmadıkça scan_num etiketi yoktur. emu8086 derleme sırasında bu etiket için “undefined” hatası verir.
Makro ile prosedürün bellekteki maliyeti somut bir örnekle karşılaştırılabilir. Bir programda on farklı ileti yazılsın. PRINT ile her ileti kendi döngü kodunu taşır: her açılımda 23 bayt kod ve iletinin baytları bulunur. PRINT_STRING ile döngü bellekte bir kez bulunur; her ileti için yalnız 3 baytlık LEA SI, ileti ve 3 baytlık CALL print_string eklenir. Yedi karakterlik on iletiyle program PRINT kullanıldığında yaklaşık 310 bayt, PRINT_STRING kullanıldığında yaklaşık 160 bayt tutar. Kazancın karşılığı süredir: her çağrıda CALL ve RET fazladan 27 saat turu harcar.
9.2.3.2Girdi ve çıktı yazmaçları
Bir prosedür parametrelerini yazmaçlarda alır ve sonucunu yazmaçta döndürür. Hangi yazmacın ne anlama geldiği prosedürün belgesinde yazar ve programcı buna uymak zorundadır (Tablo 9.2.9).
| Prosedür | Girdi | Çıktı | Ayrıntı |
|---|---|---|---|
| PRINT_STRING | DS:SI = dizgi adresi | — | dizgi 0 ile bitmeli |
| PTHIS | dizgi CALL'ın hemen ardında | — | dönüş adresini kendisi ilerletir |
| GET_STRING | DS:DI = tampon, DX = boyut | tamponda 0 ile biten dizgi | en fazla DX − 1 karakter |
| CLEAR_SCREEN | — | — | imleç (0, 0)'a gider |
| SCAN_NUM | klavye | CX (işaretli) | CX'teki eski değer kaybolur |
| PRINT_NUM | AX (işaretli) | — | PRINT_NUM_UNS de tanımlı olmalı |
| PRINT_NUM_UNS | AX (işaretsiz) | — | 0 … 65 535 |
Kütüphanenin prosedürleri kullandıkları öteki yazmaçları girişte yığına iter ve çıkışta geri alır. Örneğin GET_STRING AX, CX, DI ve DX'i, SCAN_NUM DX, AX ve SI'yi saklar. Çağıran program için değişen tek yazmaç çıktı yazmacıdır. Bayraklar ise korunmaz: prosedürün içindeki CMP ve aritmetik komutlar bayrakları değiştirir. Bir CALL'dan önce kurulan bayrağa CALL'dan sonra güvenilmemelidir.
Çıktı yazmacının bir sonucu da vardır. SCAN_NUM sonucu CX'e yazdığı için CX'teki eski değer kaybolur. CX'te bir döngü sayacı tutan program, SCAN_NUM'ı çağırmadan önce sayacı kendisi saklamalıdır:
MOV CX, 3 ; üç sayı okunacak
oku: PUSH CX ; sayacı sakla
CALL scan_num ; CX = okunan sayı
MOV AX, CX
ADD BX, AX ; toplama ekle
POP CX ; sayacı geri al
LOOP okuPUSH CX olmasaydı LOOP, kullanıcının girdiği sayıyı sayaç olarak kullanırdı. Kullanıcı 0 girerse LOOP CX'i FFFFh'ye indirir ve döngü 65 535 kez daha döner.
9.2.3.3PRINT_STRING ve PTHIS
Kütüphanede dizgi yazan iki prosedür vardır. İkisi de sonu 0 baytıyla işaretlenmiş dizgiyi yazar; farkları dizginin adresini nereden aldıklarıdır.
Sıfırla biten dizgi (null-terminated string), karakterlerin ardından değeri 0 olan bir baytla biten dizgidir. Uzunluk ayrıca saklanmaz; dizgiyi okuyan kod 0 baytını görünce durur. C dilindeki dizgiler de bu biçimdedir.
PRINT_STRING dizginin adresini DS:SI'den alır. Kullanımı iki komuttur:
LEA SI, msg
CALL print_string
...
msg DB 'Merhaba', 0LEA SI, msg, msg'nin bağıl konumunu SI'ye yükler. emu8086 doğrudan adresli bu LEA'yı MOV SI, OFFSET msg ile aynı 3 baytlık koda (BE ll hh) derler; gerçek MASM'da aynı komut 4 bayttır. Prosedür AX ve SI'yi saklar, sonra SI'nin gösterdiği baytları sırayla okur. Her bayt 0 değilse 0Eh hizmetiyle yazılır ve SI bir artar. 0 okununca SI ve AX geri alınır ve prosedür döner. Dizgi programın veri alanında durur; aynı dizgi programın farklı yerlerinde yeniden yazdırılabilir.
PTHIS adını İngilizce print this (bunu yaz) sözünden alır. Dizginin adresini bir yazmaçtan değil, yığından alır. Dizgi CALL komutunun hemen ardına yazılır:
CALL pthis
DB 'Merhaba', 0
; buradan devam ederPRINT makrosu her kullanımda döngü kodunu kopyalar; PRINT_STRING ve PTHIS ise tek kopya kodla istenen sayıda dizgi yazar. PTHIS'in PRINT_STRING'e üstünlüğü yazım kolaylığıdır: dizgiye ad vermek ve onu ayrı bir veri alanında tanımlamak gerekmez.
9.2.3.4Dizgi yazdırma yolları
Kütüphaneyle birlikte bir dizgiyi ekrana yazmanın dört yolu vardır (Tablo 9.2.10). Dördüncüsü kütüphane dışındaki DOS hizmetidir ama karşılaştırma için tabloya alınmıştır.
| Yöntem | Dizgi nerede? | Sonlandırıcı | Adres | Uygun durum |
|---|---|---|---|---|
PRINT 'metin' | kodun içinde (makro açılımı) | 0 (makro ekler) | makro kendisi alır (CS:SI) | bir kez yazılan sabit ileti |
| PRINT_STRING | veri alanında | 0 | DS:SI | birden çok yerde yazılan ya da çalışırken oluşan dizgi |
| PTHIS | CALL'ın hemen ardında | 0 | yığındaki dönüş adresi | çok sayıda kısa sabit ileti |
| INT 21h, AH = 09h | veri alanında | '$' | DS:DX | kütüphanesiz DOS programı |
Sonlandırıcı kendisi yazdırılamaz. '$' ile biten bir dizgide '$' karakteri, 0 ile biten bir dizgide 0 baytı dizginin sonu sayılır. Fiyat yazan bir programda “100 $” iletisi INT 21h/09h ile yazılamaz; ileti '$'tan önce biter. Aynı ileti PRINT_STRING ile sorunsuz yazılır.
Tampon ile yöntemin uyumu da önemlidir. GET_STRING'in doldurduğu tampon 0 ile biter ve doğrudan PRINT_STRING ile yazılır. Aynı tampon INT 21h/09h ile yazılırsa DOS '$' arar. Tamponda '$' olmadığı için DOS tamponun ötesindeki baytları da ekrana döker ve ilk '$'a rastlayana kadar sürdürür. Ekranda anlamsız karakterler belirir.
9.2.3.5PTHIS nasıl çalışır?
PTHIS, CALL komutunun yığına ne ittiğini bilmeye dayanır. CALL, kendisinden sonraki komutun adresini, yani dönüş adresini (return address) yığına iter ve prosedüre atlar. İşlemci “sonraki komutun” orada bir komut olup olmadığını denetlemez; yalnız CALL'ın uzunluğunu IP'ye ekler. CALL'ın hemen ardında bir dizgi duruyorsa itilen adres dizginin ilk baytının adresidir. PTHIS bu adresi dizginin adresi olarak kullanır.
Prosedür dört iş yapar (Şekil 9.2.7). Önce dönüş adresini POP SI ile yığından SI'ye alır. Sonra SI'nin gösterdiği baytları 0'a kadar okur ve yazar; her okumadan sonra SI bir artar. 0 okunduğunda SI, 0 baytının bir ötesini gösterir: bu adres dizgiden sonraki ilk komuttur. Prosedür bu adresi PUSH SI ile yığına geri koyar. Son olarak RET yığındaki yeni adresi IP'ye alır. İşlemci dizgiyi atlayarak ondan sonraki komuttan devam eder; dizginin baytları hiçbir zaman komut sanılıp çalıştırılmaz.
Kütüphanedeki PTHIS çağıranın SI ve AX değerlerini de korur. AX yığına itilir. SI ise yığına itilemez, çünkü yığının üstünde dönüş adresi durur ve onun önce alınması gerekir. Prosedür bu yüzden SI'nin eski değerini kod kesimindeki 2 baytlık bir değişkende (CS:temp1) saklar ve sonunda oradan geri yükler. Dizginin baytları da kod kesiminde olduğu için CS:[SI] ile okunur.
Adreslerle izlemek düzeneği somutlaştırır. Sayı okuyup yazan örnek programda CALL pthis 010Bh'dedir ve 3 bayttır. Ardından 010Eh'den başlayan 21 baytlık dizgi gelir: 13, 10, “You have entered: ” ve 0. Dizgi 010Eh–0122h'yi kaplar; CALL print_num 0123h'dedir.
| An | SP | Yığının üstü | SI | Açıklama |
|---|---|---|---|---|
CALL pthis öncesi | FFFEh | 0000h | msg1 | IP = 010Bh |
CALL pthis sonrası | FFFCh | 010Eh | msg1 | dönüş adresi = dizginin ilk baytı |
POP SI sonrası | FFFEh | 0000h | 010Eh | eski SI CS:temp1'de |
| 0 okununca | FFFEh | 0000h | 0123h | 21 bayt okundu, 20 karakter yazıldı |
PUSH SI sonrası | FFFCh | 0123h | 0123h | yeni dönüş adresi |
RET sonrası | FFFEh | 0000h | msg1 | IP = 0123h: CALL print_num |
Tablodaki “0000h”, DOS'un .COM programı başlarken yığına koyduğu sözcüktür (Tablo 9.2.11). Kütüphane sürümünde POP SI'den sonra AX de itildiği için yığının iç durumu tabloda gösterilenden bir sözcük daha derindir; tablo yalnız dönüş adresiyle ilgili adımları gösterir.
Aynı fikir PRINT makrosunda da vardır: PRINT de veriyi kodun içine gömer. Fark, PRINT'in verinin üstünden bir JMP ile atlaması, PTHIS'in ise RET'in döneceği adresi değiştirmesidir. Dönüş adresini değiştirmek güçlü ama tehlikeli bir tekniktir. Dizgi 0 ile bitmezse PTHIS dizgiden sonraki komut baytlarını da yazar ve 0 bulduğu rastgele bir yere döner; program kilitlenebilir.
9.2.3.6GET_STRING ve CLEAR_SCREEN
GET_STRING kullanıcıdan bir satır metin okur. Okunan karakterler DS:DI'nin gösterdiği bellek alanına yazılır. Bu alana tampon (buffer) denir; programcı onu önceden DB n DUP(0) gibi bir tanımla ayırır. DX tamponun bayt cinsinden boyutudur. Prosedür dizginin sonuna 0 koyduğu için en fazla DX − 1 karakter alır. Kullanıcı Enter'a basınca okuma biter.
CLEAR_SCREEN ekranı temizler ve imleci sol üst köşeye, (0, 0)'a götürür. Girdi ve çıktı yazmacı yoktur. Prosedür önce INT 10h'nin 06h hizmetiyle (pencereyi yukarı kaydır) bütün ekranı boş satırlarla doldurur, sonra 02h hizmetiyle imleci (0, 0)'a taşır. Ekranın satır ve sütun sayısını BIOS veri alanından okur: 0040:0084h'deki bayt satır sayısının bir eksiğini, 0040:004Ah'deki sözcük sütun sayısını tutar. Bu yüzden DS'yi geçici olarak 0040h yapar; DS'yi ve kullandığı öteki yazmaçları yığında saklar.
LEA DI, ad ; tampon
MOV DX, 20 ; boyut
CALL get_string
CALL clear_screen
LEA SI, ad
CALL print_string
RET
ad DB 20 DUP(0)Bu program en çok 19 karakterlik bir ad okur, ekranı temizler ve adı sol üst köşeye yazar. GET_STRING'in tamponu 0 ile bitirmesi sayesinde aynı tampon hiçbir dönüşüm yapılmadan PRINT_STRING'e verilir. Programın kullandığı üç prosedür için DEFINE_GET_STRING, DEFINE_CLEAR_SCREEN ve DEFINE_PRINT_STRING satırları RET'ten sonra eklenmelidir.
9.2.3.7GET_STRING mantığı: kod izleme
Kütüphanedeki GET_STRING geri silmeyi ve dolu tamponu da işlediği için uzundur. Okuma döngüsünün özü aşağıdaki sade eşdeğerle izlenebilir. Tampon 4 bayttır ve kullanıcı “Ece” yazacaktır.
LEA DI, ad ; tampon adresi
MOV DX, 4 ; tampon boyutu
oku: CMP DX, 1 ; 0 için 1 bayt kalsın
JBE bitti
MOV AH, 01h
INT 21h ; AL = tuş (yankılı)
CMP AL, 13 ; Enter mi?
JE bitti
MOV [DI], AL ; tampona yaz
INC DI
DEC DX
JMP oku
bitti: MOV BYTE PTR [DI], 0
RET
ad DB 4 DUP(?)Program 0100h'den başlar. LEA DI, ad 3, MOV DX, 4 3, CMP DX, 1 3 bayttır; döngünün kalanı ve MOV BYTE PTR [DI], 0 ile RET 011Ch'de biter. Tampon 011Dh'den başlar: ad+0 = 011Dh, ad+3 = 0120h.
Döngünün başındaki CMP DX, 1 / JBE bitti çifti kalan yeri denetler. DX kalan bayt sayısıdır. DX ≤ 1 ise yalnız sondaki 0'a yer kalmıştır ve okuma biter. JBE işaretsiz karşılaştırma yapar; DX'in 0 olduğu durumda da atlar. INT 21h'nin 01h hizmeti bir tuş bekler, tuşun ASCII kodunu AL'ye yazar ve karakteri ekrana yankılar. Enter (13) değilse karakter tampona yazılır, DI bir ilerler ve kalan yer DX bir azalır.
| Adım | AL | DI | DX | ZF | Tampon (ad+0…3) | Ekran |
|---|---|---|---|---|---|---|
LEA DI, ad / MOV DX, 4 | 00h | 011Dh | 0004h | 0 | ? ? ? ? | |
CMP DX, 1 / JBE (4 > 1, atlamaz) | 00h | 011Dh | 0004h | 0 | ? ? ? ? | |
INT 21h ('E'), CMP AL, 13 | 45h | 011Dh | 0004h | 0 | ? ? ? ? | E |
MOV [DI], AL / INC DI / DEC DX | 45h | 011Eh | 0003h | 0 | E ? ? ? | E |
| 2. tur ('c') | 63h | 011Fh | 0002h | 0 | E c ? ? | Ec |
| 3. tur ('e') | 65h | 0120h | 0001h | 0 | E c e ? | Ece |
CMP DX, 1 / JBE bitti (atlar) | 65h | 0120h | 0001h | 1 | E c e ? | Ece |
MOV BYTE PTR [DI], 0 | 65h | 0120h | 0001h | 1 | E c e 0 | Ece |
Dördüncü turun başında DX = 1'dir: yalnız sondaki 0 için yer kalmıştır (Tablo 9.2.12). CMP DX, 1 eşitlik bulur ve ZF = 1 olur; JBE atlar. Kullanıcı daha fazla yazsa da okunmaz; en fazla DX − 1 = 3 karakter alınmıştır. Tampon 'E', 'c', 'e', 0 olur ve PRINT_STRING ile yazdırılabilir. Sade sürüm tampon dolunca kendiliğinden biter; kütüphanedeki sürüm ise Enter'ı bekler.
9.2.3.8GET_STRING ayrıntıları ve geri silme
Kütüphanedeki GET_STRING sade sürümden dört noktada ayrılır.
- Yankı denetimi. Tuş yankısız okunur: INT 16h'nin 00h hizmeti tuşu AL'ye (ASCII) ve AH'ye (tarama kodu) yazar ama ekrana bir şey basmaz. Prosedür yalnız tampona aldığı karakteri 0Eh hizmetiyle ekrana yazar. Böylece kabul edilmeyen tuşlar ekranda iz bırakmaz.
- Dolu tampon. DX − 1 karakter alındıktan sonra yeni harfler yok sayılır. Prosedür kendiliğinden bitmez; Enter ya da geri silme bekler. Kullanıcı yanlış yazdığı son harfi silip düzeltebilir.
- Geri silme (8). Tamponda harf varsa DI bir geri alınır ve ekrana 8, ' ', 8 yazılır. Tampon boşsa tuş yok sayılır; imleç girdinin başından geri gitmez ve ekrandaki istem silinmez.
- Enter (13). Enter tampona yazılmaz. Yerine 0 konur ve prosedür döner. DX ≤ 1 verilirse hiç okuma yapılmaz ve tampona dokunulmaz.
Klavyeden okumanın donanım tarafı da bilinmelidir. Bir tuşa basıldığında klavye denetleyicisi 8259 kesme denetleyicisinin IRQ1 hattını etkinleştirir. İşlemci o anki işini bırakıp BIOS'un INT 09h yordamını çalıştırır. Yordam tuşun tarama kodunu port 60h'den okur, ASCII karşılığını bulur ve ikisini 0040h kesimindeki klavye tamponuna yazar. INT 16h/00h bu tampondan okur; tampon boşsa tuş gelene kadar bekler. GET_STRING'in “beklemesi” bu döngünün içindedir.
GET_STRING döndüğünde imleç girilen metnin hemen sağındadır. Prosedür Enter'ı ekrana yazmaz; sonraki çıktı aynı satırda başlar. Alt satıra geçmek için çağrıdan sonra PUTC 13 ve PUTC 10 eklenir.
Geri silmenin mantığı yine sade bir eşdeğerle izlenir. Aşağıdaki kod tuşları yankısız okur, harfleri tampona ekler, geri silmede son harfi tampondan ve ekrandan siler, Enter'da tamponu 0 ile bitirir. Kullanıcı sırasıyla A, b, geri silme, c ve Enter tuşlarına basacaktır.
ORG 100h
LEA DI, ad
oku: MOV AH, 0
INT 16h ; AL = tuş (yankısız)
CMP AL, 13 ; Enter mi?
JE bitti
MOV AH, 0Eh
CMP AL, 8 ; geri silme mi?
JNE ekle
CMP DI, OFFSET ad
JE oku ; silinecek harf yok
DEC DI
INT 10h ; 8: imleç sola
MOV AL, ' '
INT 10h ; harfin yerine boşluk
MOV AL, 8
INT 10h ; yine sola
JMP oku
ekle: MOV [DI], AL
INC DI
INT 10h ; yankı
JMP oku
bitti: MOV BYTE PTR [DI], 0
RET
ad DB 6 DUP(?)Kodun iki ayrıntısı dikkat ister. MOV AH, 0Eh karşılaştırmalardan önce yapılır; böylece hem yankı hem de silme üçlüsü için AH hazırdır. MOV bayrakları değiştirmediği için ardından gelen CMP'nin sonucu etkilenmez. Geri silme dalındaki INT 10h, AL'de hâlâ 8 bulunduğu için imleci sola alır; tuşun kodu doğrudan kontrol karakteri olarak kullanılmıştır.
| Olay | AL | DI | İmleç sütunu | Tampon | Ekran |
|---|---|---|---|---|---|
LEA DI, ad | 00h | ad+0 | 0 | ? ? ? ? ? ? | |
| INT 16h: 'A'; Enter ve geri silme değil | 41h | ad+0 | 0 | ? ? ? ? ? ? | |
ekle: yaz, ilerle, yankı | 41h | ad+1 | 1 | A ? ? ? ? ? | A |
| 'b' için aynı yol | 62h | ad+2 | 2 | A b ? ? ? ? | Ab |
INT 16h: geri silme; CMP AL, 8 → ZF = 1 | 08h | ad+2 | 2 | A b ? ? ? ? | Ab |
CMP DI, OFFSET ad (eşit değil) / DEC DI | 08h | ad+1 | 2 | A b ? ? ? ? | Ab |
| 8, ' ', 8 üçlüsü | 08h | ad+1 | 1 | A b ? ? ? ? | A |
| 'c': b'nin baytına yazılır | 63h | ad+2 | 2 | A c ? ? ? ? | Ac |
Enter: JE bitti, 0 yazılır | 0Dh | ad+2 | 2 | A c 0 ? ? ? | Ac |
DEC DI b'nin baytını bellekten silmez; o bayt yalnız dizginin dışında kalır ve sonraki harf onun üstüne yazılır (Tablo 9.2.13). Silme işi iki yerde birden yapılır: tamponda DI'yi geri almak, ekranda üçlüyü yazmak. İkisinden biri unutulursa ekran ile tampon birbirini tutmaz. Örneğin üçlü unutulursa ekranda “Abc”, tamponda “Ac” kalır. Kütüphane sürümü aynı girdiyle de tamponu 'A', 'c', 0 yapar; ekrana 'A', 'b', 8, ' ', 8 ve 'c' gönderir ve ekranda “Ac” görünür.
9.2.3.9Tampon boyu ve taşma
GET_STRING tamponun gerçek boyunu bilmez; yalnız programcının DX'te verdiği sayıya güvenir. DX tamponun gerçek boyundan büyük verilirse prosedür tamponun ardındaki değişkenlerin üstüne yazar.
Bellekte ad DB 4 DUP(0), ardından deger DW 0 ve son DB 5 tanımlı. Program MOV DX, 8 ile GET_STRING'i çağırıyor ve kullanıcı “Istanbul” yazıp Enter'a basıyor.
- DX = 8 olduğu için prosedür en fazla 7 karakter alır: “Istanbu”. Sekizinci harf 'l' yok sayılır.
- İlk dört karakter “Ista” tamponun dört baytına yazılır.
- 'n' (6Eh) ve 'b' (62h) deger'in iki baytına yazılır. Sözcük little-endian okunduğu için deger = 626Eh olur.
- 'u' (75h = 117) son'un üstüne yazılır; son = 117 olur.
- Sondaki 0, son'dan sonraki bayta yazılır. O bayt da başka bir değişkene ya da koda ait olabilir.
Programda hiçbir hata iletisi çıkmaz. deger ve son sessizce bozulmuştur.
Bu hataya tampon taşması (buffer overflow) denir. Assembly'de dizi sınırlarını denetleyen bir mekanizma yoktur; her bellek yazımı yalnız adrese bakar. Güvenli kural basittir: DX her zaman tamponun DUP ile ayrılan boyuna eşit verilir. Bunu kendiliğinden sağlamanın bir yolu, tamponun hemen ardına bir etiket koyup MOV DX, ad_son - ad yazmaktır; böylece tampon büyütüldüğünde DX de kendiliğinden değişir.
DX'i “okunacak karakter sayısı” sanmak. DX tamponun boyudur ve sondaki 0'ı da içerir. 10 karakterlik ad okumak için tampon DB 11 DUP(0), DX = 11 olmalıdır. DB 10 DUP(0) ve DX = 10 ile ancak 9 karakter alınır.
9.2.4Sayı prosedürleri
9.2.4.1SCAN_NUM, PRINT_NUM ve PRINT_NUM_UNS
İşlemci sayıları ikili tutar; kullanıcı ise sayıları onluk rakamlarla yazar ve okur. Klavyeden gelen “307”, üç ayrı ASCII kodudur: 33h, 30h ve 37h. Programın hesap yapabilmesi için bu kodların 0133h ikili sayısına çevrilmesi gerekir. Ekrana sayı yazarken iş tersine döner: 0133h'nin basamakları bulunur ve her biri ASCII koduna çevrilir. Kütüphanenin üç sayı prosedürü bu iki dönüşümü yapar (Tablo 9.2.14).
| Prosedür | Ne yapar? | Girdi / çıktı | Tanım |
|---|---|---|---|
| SCAN_NUM | klavyeden çok basamaklı, işaretli bir sayı okur; Enter ile biter | çıktı: CX | DEFINE_SCAN_NUM |
| PRINT_NUM | AX'teki işaretli sayıyı onluk yazar | girdi: AX | DEFINE_PRINT_NUM + DEFINE_PRINT_NUM_UNS |
| PRINT_NUM_UNS | AX'teki işaretsiz sayıyı onluk yazar | girdi: AX | DEFINE_PRINT_NUM_UNS |
İki yazdırma prosedürünün farkı, AX'teki 16 bitin nasıl yorumlandığıdır. İşaretsiz yorumda 16 bit 0 … 65 535 aralığını gösterir. İşaretli yorumda ikiye tümleyen gösterimi kullanılır ve aralık −32 768 … 32 767'dir. En anlamlı bit 0 olan sayılarda iki yorum aynıdır. En anlamlı bit 1 olan sayılarda işaretli değer, işaretsiz değerden 65 536 çıkarılarak bulunur.
İkiye tümleyen (two's complement) gösteriminde −x, x'in bütün bitleri ters çevrilip 1 eklenerek yazılır. 16 bitte bu, 65 536 − x'e eşittir. 8086'nın NEG komutu bir sayının ikiye tümleyenini alır.
Örneğin FFE7h'nin işaretsiz değeri 65 511'dir. En anlamlı biti 1 olduğu için işaretli değeri 65 511 − 65 536 = −25'tir. Tersinden bakıldığında 25 = 0019h; bitleri ters çevrilince FFE6h, 1 eklenince FFE7h elde edilir. Aynı FFE7h, PRINT_NUM ile “-25”, PRINT_NUM_UNS ile “65511” olarak yazılır. Bitler değişmez, yalnız yorum değişir.
9.2.4.2SCAN_NUM mantığı: kod izleme
Onluk bir sayıyı soldan sağa okurken her yeni rakam önceki sonucu bir basamak sola kaydırır. “307” için 3 okunur; 0 gelince sonuç 3 × 10 + 0 = 30 olur; 7 gelince 30 × 10 + 7 = 307 olur. Kural kısaca sonuç ← sonuç × 10 + rakam'dır. Bu yöntem Horner yöntemi olarak da anılır ve sayının uzunluğunu önceden bilmeyi gerektirmez. SCAN_NUM'ın özü aşağıdaki sade eşdeğerdir; eksi işareti ve taşma denetimi çıkarılmıştır.
ORG 100h
XOR CX, CX ; sonuç = 0
oku: MOV AH, 01h
INT 21h ; AL = tuş (yankılı)
CMP AL, 13 ; Enter mi?
JE bitti
SUB AL, '0' ; ASCII → rakam
CBW ; AX = rakam
XCHG AX, CX ; AX=sonuç, CX=rakam
MOV BX, 10
MUL BX ; AX = sonuç × 10
ADD CX, AX ; CX = sonuç×10 + rakam
JMP oku
bitti: RETKodun her satırı bir amaca hizmet eder. XOR CX, CX CX'i sıfırlar; bir yazmacı kendisiyle XOR'lamak MOV CX, 0'dan kısadır (2 bayta karşı 3 bayt) ve ZF'yi 1 yapar. SUB AL, '0' ASCII kodundan 30h çıkararak rakamın değerini bulur. CBW AL'yi işaretli olarak AX'e genişletir; 0–9 arası rakamlarda AH = 0 olur. XCHG AX, CX iki yazmacı takas eder: sonuç çarpma için AX'e geçer, rakam CX'te bekler. Takas gereklidir, çünkü MUL çarpılanı her zaman AX'te bekler. MUL BX DX:AX ← AX × BX işlemini yapar; sonuç 16 bite sığdığı sürece DX = 0 kalır. ADD CX, AX çarpımı rakama ekler ve yeni sonucu yine CX'e yazar.
| Adım | AX | CX | BX | ZF | CX (onluk) | Ekran |
|---|---|---|---|---|---|---|
XOR CX, CX | 0000h | 0000h | 0000h | 1 | 0 | |
MOV AH, 01h / INT 21h: '3' | 0133h | 0000h | 0000h | 1 | 0 | 3 |
CMP / JE / SUB AL, '0' | 0103h | 0000h | 0000h | 0 | 0 | 3 |
CBW / XCHG AX, CX | 0000h | 0003h | 0000h | 0 | 3 | 3 |
MOV BX, 10 / MUL BX / ADD CX, AX | 0000h | 0003h | 000Ah | 0 | 3 | 3 |
'0' okunur, SUB, CBW, XCHG | 0003h | 0000h | 000Ah | 1 | 0 | 30 |
MUL BX / ADD CX, AX | 001Eh | 001Eh | 000Ah | 0 | 30 | 30 |
'7' okunur, SUB, CBW | 0007h | 001Eh | 000Ah | 0 | 30 | 307 |
XCHG / MUL BX / ADD CX, AX | 012Ch | 0133h | 000Ah | 0 | 307 | 307 |
Enter: AL = 0Dh, CMP AL, 13 | 010Dh | 0133h | 000Ah | 1 | 307 | 307 |
JE bitti / RET | 010Dh | 0133h | 000Ah | 1 | 307 | 307 |
Tablonun birkaç noktası açıklama ister (Tablo 9.2.15). INT 21h/01h AL'ye tuşu yazar ama AH'ye dokunmaz; AH, hizmet numarası olan 01h'de kalır ve AX = 0133h görünür. '0' okunduğunda SUB'ın sonucu 0'dır ve ZF = 1 olur. 30 × 10 = 300 = 012Ch'dir; 7 eklenince CX = 307 = 0133h olur. Enter okunduğunda CMP eşitlik bulur, ZF = 1 olur ve JE döngüden çıkar. Sonuç SCAN_NUM'daki gibi CX'tedir.
Çarpma bu döngünün en pahalı adımıdır. 8086'da MUL r16 118–133 saat turu sürer; 5 MHz'de yaklaşık 25 µs. Üç basamaklı bir sayı için üç çarpma yapılır. Bu süre kullanıcının tuşa basma hızı yanında önemsizdir. Yine de 10 ile çarpma kaydırmayla da yapılabilir. x × 10 = x × 8 + x × 2 olduğundan çarpım SHL ve ADD komutlarıyla çok daha az turda hesaplanır.
9.2.4.3SCAN_NUM'ın denetimleri
Kütüphanedeki SCAN_NUM, sade eşdeğerin üzerine beş denetim ekler:
- Eksi işareti. '-' tuşu bir işaret bayrağı kurar. Enter'a basıldığında bayrak kuruluysa
NEG CXuygulanır. - Rakam olmayan tuş. Prosedür her tuşu önce ekrana yazar, sonra denetler. Tuş '0'–'9' aralığında değilse ekrandan silinir (8, ' ', 8) ve sayıya katılmaz.
- Geri silme. Son basamağı atar: CX ← CX / 10. Ekranda da son karakter silinir.
- Taşma. Sayı 16 bite sığmayacaksa son basamak kabul edilmez. Çarpmadan sonra DX ≠ 0 ise ya da toplamada CF = 1 olursa prosedür önceki değeri geri yükler ve son rakamı ekrandan siler.
- Enter'ın yankısı. Enter da ekrana yazılır. 13 kodu imleci satır başına döndürür ama alt satıra indirmez.
Son madde uygulamada en çok şaşırtan davranıştır. SCAN_NUM döndüğünde imleç girilen sayının bulunduğu satırın başındadır. Hemen ardından yazılan her şey girilen sayının ve istemin üstüne yazılır. Örnek programlarda SCAN_NUM'dan sonra PUTC 13, PUTC 10 eklenmesi ya da bir sonraki iletinin 13, 10 ile başlaması bu yüzdendir.
| Girdi | CX | PRINT_NUM | Açıklama |
|---|---|---|---|
| 307 | 0133h | 307 | olağan durum |
| -25 | FFE7h | -25 | Enter'da NEG: 0019h → FFE7h |
| 12a3 | 007Bh | 123 | 'a' ekrandan silinir, sayıya katılmaz |
| 40000 | 9C40h | -25536 | 16 bite sığar ama işaretli aralığı aşar |
| 70000 | 1B58h | 7000 | son 0 kabul edilmez: 7000 × 10 = 70 000 > 65 535 |
| -40000 | 63C0h | 25536 | 9C40h'nin ikiye tümleyeni 63C0h |
Tablodaki son üç satır prosedürün bir sınırını gösterir (Tablo 9.2.16). Basamaklar işaretsiz 16 bit olarak biriktirilir ve işaret en sonda uygulanır. Taşma denetimi yalnız 65 535 sınırına bakar. Bu yüzden 32 768 ile 65 535 arasındaki girdiler hata vermeden kabul edilir ama işaretli yorumda negatif görünür. Örneğin 40 000 = 9C40h'dir; işaretli değeri 40 000 − 65 536 = −25 536'dır. −40 000 girildiğinde 9C40h'nin ikiye tümleyeni 63C0h = 25 536 olur ve eksi işaretli bir girdi pozitif bir sonuç verir. Doğru sonuç için kullanıcının girdisi −32 768 … 32 767 aralığında kalmalıdır.
9.2.4.4PRINT_NUM_UNS mantığı: kod izleme
Bir sayının onluk basamakları 10'a art arda bölünerek bulunur. Her bölmenin kalanı bir basamaktır, bölüm bir sonraki adımın sayısıdır. Sorun, basamakların sağdan sola, yani ters sırayla çıkmasıdır: 1205'in ilk kalanı 5, son kalanı 1'dir. Ekrana ise önce 1 yazılmalıdır. Yığın bu ters çevirme için doğal bir araçtır: kalanlar sırayla itilir, sonra ters sırayla çekilir.
ORG 100h
MOV AX, 1205 ; yazdırılacak sayı
MOV BX, 10
XOR CX, CX ; basamak sayacı
bol: XOR DX, DX
DIV BX ; AX=bölüm, DX=kalan
PUSH DX ; basamak yığına
INC CX
OR AX, AX ; bölüm 0 mı?
JNZ bol
yaz: POP AX ; önce en anlamlı
ADD AL, '0'
MOV AH, 0Eh
INT 10h
LOOP yaz
RETDIV BX 32 bitlik DX:AX'i BX'e böler: bölümü AX'e, kalanı DX'e yazar. Bölmeden önce XOR DX, DX zorunludur. DX'te bir önceki kalan durursa bölünen DX:AX çok büyük olur; bölüm 16 bite sığmazsa işlemci 0 tipi kesmeyi (bölme hatası) üretir. CX kaç basamak itildiğini sayar; ikinci döngüde LOOP aynı sayıda POP yapılmasını sağlar. OR AX, AX AX'i değiştirmez ama bayrakları kurar: bölüm 0 ise ZF = 1 olur ve döngü biter.
| Adım | AX | DX | CX | SP | Yığın (üstten) | Ekran |
|---|---|---|---|---|---|---|
başlangıç (MOV, MOV, XOR) | 04B5h | 0000h | 0000h | FFFEh | — | |
| 1. bölme: 1205 / 10 | 0078h | 0005h | 0001h | FFFCh | 5 | |
| 2. bölme: 120 / 10 | 000Ch | 0000h | 0002h | FFFAh | 0, 5 | |
| 3. bölme: 12 / 10 | 0001h | 0002h | 0003h | FFF8h | 2, 0, 5 | |
| 4. bölme: 1 / 10 | 0000h | 0001h | 0004h | FFF6h | 1, 2, 0, 5 | |
OR AX, AX → ZF = 1, JNZ atlamaz | 0000h | 0001h | 0004h | FFF6h | 1, 2, 0, 5 | |
| 1. yazma: POP, '0' ekle, INT 10h, LOOP | 0E31h | 0001h | 0003h | FFF8h | 2, 0, 5 | 1 |
| 2. yazma | 0E32h | 0001h | 0002h | FFFAh | 0, 5 | 12 |
| 3. yazma | 0E30h | 0001h | 0001h | FFFCh | 5 | 120 |
| 4. yazma, LOOP biter | 0E35h | 0001h | 0000h | FFFEh | — | 1205 |
Dört bölmeden sonra yığının üstünde en anlamlı basamak, 1 durur (Tablo 9.2.17). Yazma döngüsü basamakları 1, 2, 0, 5 sırasıyla çeker ve yazar. Her POP'tan sonra ADD AL, '0' basamağı ASCII'ye çevirir ve MOV AH, 0Eh hizmet numarasını yükler; AX'in 0E31h, 0E32h gibi değerleri bundandır. Döngü bittiğinde SP yeniden FFFEh'dir: dört PUSH'a dört POP karşılık gelmiştir. Yığın tablosu DOS'un FFFEh'deki 0000h sözcüğünü göstermez.
9.2.4.5Kütüphanenin yöntemi: azalan bölen
Kütüphanedeki PRINT_NUM_UNS aynı çıktıyı başka bir yöntemle üretir. Sayıyı hep 10'a bölmek yerine basamak değerlerine böler: önce 10 000'e, sonra 1000'e, 100'e, 10'a ve 1'e. Her bölmenin bölümü bir basamaktır ve hesaplandığı anda yazılır. Kalan bir sonraki turun sayısı olur. Basamaklar soldan sağa çıktığı için yığına ve ters çevirmeye gerek kalmaz.
- Bölen BX 10 000 ile başlar ve her turdan sonra 10'a bölünür. BX 0 olunca yazdırma biter.
- Bölüm (0–9) bir basamaktır;
ADD AL, 30hile ASCII'ye çevrilip PUTC ile yazılır. 65 535 ÷ 10 000 = 6 olduğu için bölüm her zaman AL'ye sığar. - CX bir bayrak olarak kullanılır. CX = 1 iken sayı bölenden küçükse tur atlanır; böylece baştaki sıfırlar yazılmaz. İlk basamak yazılınca CX = 0 olur ve aradaki sıfırlar artık yazılır.
- AX = 0 için ayrı bir dal vardır ve doğrudan '0' yazılır. Bu dal olmasaydı bütün turlar atlanır ve ekrana hiçbir şey yazılmazdı.
| Bölen (BX) | Bayrak (CX) | Sayı (AX) | İşlem | Ekran |
|---|---|---|---|---|
| 10 000 | 1 | 3007 | sayı < bölen ve henüz basamak yok: atla | — |
| 1000 | 1 | 3007 | 3007 ÷ 1000 = 3, kalan 7; CX = 0 | 3 |
| 100 | 0 | 7 | 7 ÷ 100 = 0, kalan 7 | 30 |
| 10 | 0 | 7 | 7 ÷ 10 = 0, kalan 7 | 300 |
| 1 | 0 | 7 | 7 ÷ 1 = 7, kalan 0 | 3007 |
| 0 | 0 | 0 | bölen 0: bitti | 3007 |
3007 örneği bayrağın neden gerektiğini açıkça gösterir (Tablo 9.2.18). İlk turda 3007 < 10 000 olduğu için bölüm 0 olurdu; bu sıfır baştaki bir sıfırdır ve yazılmamalıdır. 100'e ve 10'a bölünen turlarda da bölüm 0'dır ama bu sıfırlar sayının içindedir ve yazılmalıdır. Aradaki fark yalnız “ilk basamak yazıldı mı?” sorusuyla ayırt edilir; CX bu bilgiyi tutar. 40 000 için ilk turda 40 000 ÷ 10 000 = 4 yazılır ve CX = 0 olur. Sonraki dört turda dört 0 yazılır; ekranda 40000 görünür.
Böleni küçültmek de bir bölmedir: kütüphane BX ← BX / 10 işlemini DIV ile yapar. Bu sırada AX'teki sayı yığında saklanır. Prosedür AX, BX, CX ve DX'i girişte iter ve çıkışta geri alır; çağıran için hiçbir yazmaç değişmez.
9.2.4.6PRINT_NUM ve eksi işareti: kod izleme
PRINT_NUM işaretli sayıyı yazmak için PRINT_NUM_UNS'yi kullanır. Sayı negatifse önce '-' yazılır, sonra sayının mutlak değeri işaretsiz olarak yazdırılır. Aşağıdaki kod kütüphanedeki PRINT_NUM'ın eşdeğeridir:
ORG 100h
MOV AX, -25 ; AX = FFE7h
CALL print_num
RET
print_num PROC
PUSH AX
OR AX, AX ; SF = işaret biti
JNS pozitif
NEG AX ; AX = 0019h
PUSH AX ; PUTC '-' açılımı
MOV AL, '-'
MOV AH, 0Eh
INT 10h
POP AX
pozitif: CALL print_num_uns
POP AX ; AX = FFE7h geri
RET
print_num ENDPMOV AX, -25 satırında assembler −25'in ikiye tümleyenini hesaplar ve AX'e FFE7h yükleyen komutu üretir. CALL print_num 0103h'dedir ve 3 bayttır; yığına dönüş adresi 0106h itilir. Prosedür önce AX'i saklar, çünkü NEG AX'i değiştirecektir. OR AX, AX sayıyı değiştirmeden bayrakları kurar: en anlamlı bit 1 olduğu için SF = 1 olur. JNS (işaret yoksa atla) atlamaz. NEG AX 0 − FFE7h = 0019h = 25 hesaplar; sonuç pozitif olduğu için SF = 0 olur. PUTC '-' açılımı eksi işaretini yazar. CALL print_num_uns 25'in basamaklarını yazar. Son olarak POP AX AX'e FFE7h'yi geri yükler.
| Komut(lar) | AX | SP | SF | Yığın (FFFAh, FFFCh) | Ekran |
|---|---|---|---|---|---|
MOV AX, -25 | FFE7h | FFFEh | 0 | —, — | |
CALL print_num | FFE7h | FFFCh | 0 | —, 0106h | |
PUSH AX / OR AX, AX | FFE7h | FFFAh | 1 | FFE7h, 0106h | |
JNS (atlamaz) / NEG AX | 0019h | FFFAh | 0 | FFE7h, 0106h | |
| PUTC '-' açılımı (5 komut) | 0019h | FFFAh | 0 | FFE7h, 0106h | - |
CALL print_num_uns (dönüşten sonra) | 0019h | FFFAh | 0 | FFE7h, 0106h | -25 |
POP AX | FFE7h | FFFCh | 0 | —, 0106h | -25 |
RET (print_num'dan) | FFE7h | FFFEh | 0 | —, — | -25 |
Sayı yazdırıldıktan sonra da AX'te durur (Tablo 9.2.19). Çağıran program aynı sayıyı başka bir işte kullanmaya devam edebilir. PRINT_NUM_UNS kullandığı bütün yazmaçları sakladığı için onun içindeki adımlar tabloda tek satıra indirgenmiştir. Kütüphanedeki PRINT_NUM ayrıca AX = 0 için doğrudan '0' yazan bir dal içerir. İşareti de OR yerine CMP AX, 0 ile sınar; iki komut SF'yi aynı biçimde kurar.
Bu tasarımın bir sonucu dikkat çekicidir: −32 768 (8000h) için NEG yine 8000h üretir, çünkü +32 768 16 bitlik işaretli aralıkta yoktur. PRINT_NUM önce '-' yazar, sonra 8000h'yi PRINT_NUM_UNS'ye verir. PRINT_NUM_UNS 8000h'yi işaretsiz 32 768 olarak yazar ve ekranda doğru sonuç, −32768 görünür. Mutlak değerin işaretsiz yazılması bu uç durumu kendiliğinden çözer.
9.2.4.7Basamak ayırma: iki yöntemin karşılaştırması
| Ölçüt | Yığınlı yöntem (10'a böl, it, ters sırayla çek) | Azalan bölen (10 000, 1000, …) |
|---|---|---|
| Bölme sayısı | basamak sayısı kadar | beşe kadar; ayrıca böleni küçültmek için birer bölme |
| Yığın kullanımı | basamak başına 2 bayt | yalnız saklanan yazmaçlar |
| Baştaki sıfırlar | kendiliğinden oluşmaz | bayrakla atlanır |
| Sabit genişlik (00042) | ek kod gerekir | bayrak kaldırılınca kendiliğinden |
| Başka taban | bölen 2, 8 ya da 16 yapılır; 16'da 9'dan büyük basamaklar 'A'–'F' olur | bölen dizisi tabana göre değişir (16'da 4096, 256, 16, 1) |
| Çıkış sırası | önce yığına, sonra ters sırayla ekrana | soldan sağa, hesaplandığı anda |
İki yöntem aynı çıktıyı verir: 1205 için ikisi de 1, 2, 0, 5 basamaklarını yazar (Tablo 9.2.20). Seçimi yığın kullanımı, sabit genişlik ve taban gereksinimi belirler. Süre açısından DIV'in maliyeti belirleyicidir. 8086'da DIV r16 144–162 saat turu sürer. 1205 için yığınlı yöntem 4 bölme yapar. Azalan bölen yöntemi 1000, 100, 10 ve 1'e dört bölme ile böleni beş kez küçültmek için beş bölme yapar; ilk turda bölme atlanır. Yığınlı yöntem bu yüzden yaklaşık iki kat hızlıdır. Kütüphanenin seçimi yığın kullanmama ve sabit genişliğe kolay uyarlanma yönünden yapılmıştır.
9.2.4.8Aynı bitler, iki yorum
AX'e verilen bir değer için iki prosedürün ne yazacağı yan yana konduğunda yorum farkı açıkça görülür (Tablo 9.2.21).
| AX | En anlamlı bit | PRINT_NUM | PRINT_NUM_UNS |
|---|---|---|---|
| 0000h | 0 | 0 | 0 |
| 7FFFh | 0 | 32767 | 32767 |
| 8000h | 1 | -32768 | 32768 |
| 9C40h | 1 | -25536 | 40000 |
| FFE7h | 1 | -25 | 65511 |
| FFFFh | 1 | -1 | 65535 |
Kural tektir: en anlamlı bit 0 ise iki prosedür aynı şeyi yazar; 1 ise PRINT_NUM işaretsiz değerin 65 536 eksiğini yazar. Program hangi yorumun doğru olduğunu bilmek zorundadır; işlemci bilmez. Toplama ve çıkarma komutları iki yorum için aynı bitleri üretir; yalnız bayrakların okunuşu değişir. İşaretsiz taşmayı CF, işaretli taşmayı OF gösterir. Bir sayaç, adres ya da uzunluk gibi negatif olamayan değerler PRINT_NUM_UNS ile, sıcaklık ya da fark gibi negatif olabilen değerler PRINT_NUM ile yazılır.
32 767'yi aşabilen işaretsiz bir değeri PRINT_NUM ile yazmak. AX = 40 000 (9C40h) PRINT_NUM ile −25536 görünür. Değerin anlamı işaretsizse PRINT_NUM_UNS kullanılmalıdır.
9.2.5Örnek programlar
9.2.5.1Sayı oku ve yazdır
emu8086'nın kütüphane belgesindeki ikinci örnek dört prosedürü birlikte kullanır. Program kullanıcıdan bir sayı ister, sayıyı okur ve geri yazar:
include 'emu8086.inc'
ORG 100h
LEA SI, msg1 ; ask for the number
CALL print_string ;
CALL scan_num ; get number in CX.
MOV AX, CX ; copy the number to AX.
CALL pthis ; print the following string:
DB 13, 10, 'You have entered: ', 0
CALL print_num ; print number in AX.
RET ; return to operating system.
msg1 DB 'Enter the number: ', 0
DEFINE_SCAN_NUM
DEFINE_PRINT_STRING
DEFINE_PRINT_NUM
DEFINE_PRINT_NUM_UNS ; required for print_num.
DEFINE_PTHIS
END ; directive to stop the compiler.Programın akışı kısadır. LEA SI, msg1 istem dizgisinin adresini SI'ye yükler ve PRINT_STRING onu yazar. SCAN_NUM sayıyı okuyup CX'e koyar. MOV AX, CX sayıyı PRINT_NUM'ın beklediği yazmaca taşır. PTHIS, CALL'ın ardındaki dizgiyi yazar: dizgi 13, 10 ile başladığı için önce alt satıra geçilir. PRINT_NUM sayıyı işaretli olarak yazar ve RET programı bitirir. msg1'den sonraki satırlar veri ve prosedür tanımlarıdır; akış onlara RET'ten sonra hiç ulaşmaz.
Ana programın bellekteki yerleşimi Tablo 9.2.22'dedir. Bütün CALL'lar 3 baytlık yakın çağrılardır (E8 ve 16 bitlik uzaklık). Dizginin kod alanının ortasında durduğuna dikkat edilmelidir: 010Eh–0122h arası komut değil veridir.
| Adres | Bayt | Satır |
|---|---|---|
| 0100h | 3 | LEA SI, msg1 (BE 27 01) |
| 0103h | 3 | CALL print_string |
| 0106h | 3 | CALL scan_num |
| 0109h | 2 | MOV AX, CX (8B C1) |
| 010Bh | 3 | CALL pthis |
| 010Eh | 21 | DB 13, 10, 'You have entered: ', 0 |
| 0123h | 3 | CALL print_num |
| 0126h | 1 | RET (C3) |
| 0127h | 19 | msg1 DB 'Enter the number: ', 0 |
| 013Ah | … | DEFINE_ makrolarıyla açılan prosedürler |
Kullanıcının -25 yazıp Enter'a bastığını varsayalım. İzleme tablosunda CALL satırları için iki an gösterilir: prosedürün içi (yığında dönüş adresi varken) ve dönüş (Tablo 9.2.23).
| Satır | SI | CX | AX | SP | Ekran |
|---|---|---|---|---|---|
LEA SI, msg1 | 0127h | (boyut) | 0000h | FFFEh | |
CALL print_string (içinde) | 0127h | (boyut) | 0000h | FFFCh | Enter the number:␣ |
CALL scan_num (içinde) | 0127h | … | 0000h | FFFCh | Enter the number: -25 |
CALL scan_num (dönüş) | 0127h | FFE7h | 0000h | FFFEh | Enter the number: -25 |
MOV AX, CX | 0127h | FFE7h | FFE7h | FFFEh | Enter the number: -25 |
CALL pthis | 0127h | FFE7h | FFE7h | FFFCh | … |
| dizgi yazılır | 0127h | FFE7h | FFE7h | FFFCh | … / You have entered:␣ |
CALL print_num (içinde) | 0127h | FFE7h | FFE7h | FFFCh | … / You have entered: -25 |
RET | 0127h | FFE7h | FFE7h | 0000h | … / You have entered: -25 |
Tablonun birkaç ayrıntısı açıklama ister. SI program boyunca 0127h'de kalır, çünkü PRINT_STRING ve PTHIS SI'yi saklayıp geri yükler. AX de SCAN_NUM'dan sonra 0000h'dir; SCAN_NUM AX'i saklar. Değişen tek yazmaç çıktı yazmacı CX'tir: −25'in ikiye tümleyeni FFE7h. CX'in ilk değeri “(boyut)” olarak gösterilmiştir. emu8086 bir .COM programını yüklerken CX'e programın bayt cinsinden boyutunu koyar. Bu değer SCAN_NUM CX'i sıfırladığında kaybolur ve sonucu etkilemez. Son RET yığından DOS'un koyduğu 0000h'yi IP'ye alır; SP = FFFEh + 2 = 0000h olur. İşlemci PSP'nin başındaki INT 20h'ye gider ve program biter.
Ekranın son durumu iki satırdır. Birinci satırda “Enter the number: -25”, ikincisinde “You have entered: -25” yazar. SCAN_NUM Enter'ı yalnız CR olarak yankıladığı için imleç birinci satırın başına dönmüştü. PTHIS dizgisinin başındaki 13 ve 10 imleci ikinci satırın başına götürür. Dizgi 13, 10 ile başlamasaydı “You have entered: ” birinci satırın üstüne yazılır ve istem silinirdi.
9.2.5.2İki sayının toplamı
İkinci örnek iki sayı okur ve toplamlarını yazar. İşaretli taşma da denetlenir.
include 'emu8086.inc'
ORG 100h
CALL scan_num ; 1. sayı → CX
MOV BX, CX ; sakla
PUTC 13
PUTC 10 ; alt satır
CALL scan_num ; 2. sayı → CX
PUTC 13
PUTC 10
MOV AX, BX
ADD AX, CX ; işaretli toplam
JO tasma
CALL print_num
RET
tasma: PUTC '?'
RET
DEFINE_SCAN_NUM
DEFINE_PRINT_NUM
DEFINE_PRINT_NUM_UNS
ENDProgramın üç ayrıntısı önemlidir. Birincisi, ikinci SCAN_NUM da sonucu CX'e yazar; birinci sayı kaybolmasın diye önce BX'e alınır. BX'in seçilmesi rastgele değildir: SCAN_NUM BX'i kullanmaz, PUTC yalnız AX'e dokunur. İkincisi, Enter yalnız satır başına döndürdüğü için her SCAN_NUM'dan sonra PUTC 13 ve PUTC 10 gelir; ikinci sayı ve sonuç ayrı satırlara yazılır. Üçüncüsü, JO işaretli taşmayı yakalar.
Taşma bayrağı (OF, overflow flag), işaretli bir işlemin sonucu 16 bitlik işaretli aralığa (−32 768 … 32 767) sığmadığında 1 olur. Toplamada iki işlenen aynı işaretli, sonuç ise ters işaretliyse OF = 1'dir. JO komutu OF = 1 ise atlar.
| Girdiler | BX | CX | AX = BX + CX | OF | Son satır |
|---|---|---|---|---|---|
| 120, −45 | 0078h | FFD3h | 004Bh | 0 | 75 |
| −7, −8 | FFF9h | FFF8h | FFF1h | 0 | -15 |
| 30000, 5000 | 7530h | 1388h | 88B8h | 1 | ? |
İlk iki satırda sonuç işaretli aralıktadır (Tablo 9.2.24). 120 + (−45) toplamasında CF = 1 olur, çünkü işaretsiz olarak 0078h + FFD3h 16 biti aşar. Bu elde işaretli hesapta anlamsızdır ve OF = 0 kalır. Üçüncü satırda iki pozitif sayının toplamı 35 000'dir ve 32 767'yi aşar. Sonuç 88B8h'nin en anlamlı biti 1'dir; iki pozitif sayının toplamı negatif görünür ve OF = 1 olur. JO atlar ve ekrana '?' yazılır. JO olmasaydı PRINT_NUM 88B8h'yi −30536 olarak yazardı.
9.2.5.3Çerçeveli başlık
Üçüncü örnek ekranı temizler ve 10. sütundan başlayan çerçeveli bir başlık çizer. Kütüphane makroları programcının kendi prosedürüyle birlikte kullanılır.
include 'emu8086.inc'
ORG 100h
CALL clear_screen
GOTOXY 10, 2
CALL cizgi
GOTOXY 10, 3
PRINT '| BASLIK |'
GOTOXY 10, 4
CALL cizgi
GOTOXY 0, 6 ; imleç alta
RET
cizgi PROC ; +----------+
PUTC '+'
MOV CX, 10
tire: PUTC '-'
LOOP tire
PUTC '+'
RET
cizgi ENDP
DEFINE_CLEAR_SCREEN
ENDcizgi prosedürü bir '+' yazar, on kez '-' yazar ve bir '+' ile biter: 12 karakterlik “+----------+”. Başlık satırındaki dizgi de 12 karakterdir; iki kenar çizgisi, iki boşluk, “BASLIK” ve iki boşluk. Bu yüzden üç satır 10–21. sütunlarda üst üste hizalanır (Şekil 9.2.10). Son GOTOXY 0, 6 imleci çerçevenin altına alır; program bittiğinde emu8086'nın yazacağı bir şey başlığın üstüne binmez.
Programın iki noktası makro ile döngünün ilişkisini gösterir. Döngüdeki PUTC '-' kaynakta bir kez yazıldığı için bir kez açılır; 8 baytlık açılım 10 kez çalışır ama 10 kez kopyalanmaz. Makronun kopyalanması derleme anında, döngünün yinelenmesi çalışma anında olur. İkinci nokta yazmaçlarla ilgilidir. LOOP sayacı CX'tedir. PUTC yalnız AX'i kullanıp geri yüklediği için CX'e dokunmaz ve sayaç güvendedir. Döngüde CX'i bozan bir prosedür, örneğin SCAN_NUM çağrılsaydı sayacın PUSH CX / POP CX ile korunması gerekirdi. cizgi'nin tanımı da kütüphane prosedürleri gibi RET'ten sonradır.
9.2.6Sık yapılan hatalar
Kütüphane kullanılırken yapılan hatalar çoğu zaman derleme sırasında fark edilmez; program çalışır ama yanlış sonuç verir. Aşağıdaki hatalar emu8086 ile çalışan öğrencilerin en sık karşılaştıklarıdır.
DEFINE_ satırını unutmak. CALL scan_num var ama DEFINE_SCAN_NUM yoksa derleyici etiketi bulamaz ve “undefined” hatası verir. PRINT_NUM için DEFINE_PRINT_NUM_UNS da gerekir.
Dizgiyi 0 ile bitirmemek. PRINT_STRING ve PTHIS 0 görene kadar yazar. msg DB 'Sayi: ' gibi 0'sız bir tanımda, dizgiden sonra gelen baytlar da ekrana dökülür. Bunlar çoğu zaman prosedür kodlarıdır ve ekranda anlamsız karakterler görünür. PTHIS'te ayrıca dönüş adresi yanlış yere kayar ve program kilitlenebilir.
Sonucu AX'te aramak. SCAN_NUM sonucu CX'e yazar. PRINT_NUM'dan önce MOV AX, CX unutulursa AX'teki eski değer yazılır. Genel kural: bir prosedürün hangi yazmacı koruduğu belgesinde yazmıyorsa korunduğu varsayılmamalıdır.
Enter'dan sonra alt satır beklemek. SCAN_NUM'dan sonra imleç girdinin bulunduğu satırın başındadır. Sonraki çıktı girilen sayının ve istemin üstüne yazılır. “Sayi: ” istemiyle 42 okunup hemen PRINT_NUM çağrılırsa ekranda “42yi: 42” kalır. Araya PUTC 13, PUTC 10 eklenmelidir.
Tampon boyunu büyük vermek. DX, tamponun gerçek boyundan büyük verilirse GET_STRING tamponun ardındaki değişkenlerin üstüne yazar. Hata ancak o değişkenler kullanıldığında ortaya çıkar.
DEFINE_ satırlarının yeri ise bir hata kaynağı değildir. Her DEFINE_ makrosu prosedür gövdesinin üstünden atlayan bir JMP ile başladığı için satır programın başına konsa da işlemci gövdeye girmez (Şekil 9.2.11). Bedeli fazladan bir atlama komutudur. Alışılmış yer RET ile END arasıdır.
9.2.7Alıştırmalar ve çözümleri
Alıştırma 1. Ekranın ortasına (sütun 35, satır 12) “MERKEZ” yazan programı makrolarla yazınız.
include 'emu8086.inc'
ORG 100h
GOTOXY 35, 12
PRINT 'MERKEZ'
RET“MERKEZ” 12. satırın 35–40. sütunlarına yazılır ve imleç (41, 12)'de kalır. Altı karakterlik bir metnin tam ortada durması için başlangıç sütunu (80 − 6) / 2 = 37 olmalıdır; 35 yaklaşık bir orta noktadır. Ortalamak için genel kural: başlangıç sütunu = (80 − uzunluk) / 2.
Alıştırma 2. Kullanıcıdan iki sayı okuyup toplamını yazdıran programı yazınız.
Çözüm, iki sayının toplamı örneğidir. Gereken parçalar: CALL scan_num → MOV BX, CX → PUTC 13, PUTC 10 → CALL scan_num → MOV AX, BX → ADD AX, CX → CALL print_num; tanımlar DEFINE_SCAN_NUM, DEFINE_PRINT_NUM ve DEFINE_PRINT_NUM_UNS. PUTC 13 ve PUTC 10 unutulursa ikinci sayı birincinin üstüne yazılır ve ekranda yalnız son yazılanlar okunur.
Alıştırma 3. PRINT makrosu 10 kez kullanılırsa dizgiler ve döngü kodu kaç kez bulunur? PRINT_STRING ile karşılaştırınız.
PRINT ile 10 kopya oluşur: her açılım kendi dizgisini ve kendi 23 baytlık döngü kodunu taşır. PRINT_STRING ile döngü kodu bir kez bulunur; bellekte 10 dizgi ve her dizgi için bir LEA SI ile bir CALL (3 + 3 bayt) yer alır. Yedi karakterlik on iletiyle PRINT'li program yaklaşık 310, PRINT_STRING'li program yaklaşık 160 bayttır. İleti sayısı arttıkça fark büyür; buna karşılık her CALL/RET çifti 27 saat turu ekler.
Alıştırma 4. AX = 8000h iken PRINT_NUM ve PRINT_NUM_UNS ne yazar?
8000h'nin işaretsiz değeri 32 768'dir; PRINT_NUM_UNS “32768” yazar. En anlamlı biti 1 olduğu için işaretli değeri 32 768 − 65 536 = −32 768'dir; PRINT_NUM “-32768” yazar. Bu, 16 bitlik işaretli aralığın en küçük sayısıdır ve NEG onu değiştirmez; PRINT_NUM yine de doğru yazar, çünkü mutlak değeri işaretsiz yazdırır.
Alıştırma 5. DEFINE_PTHIS, ORG 100h'nin hemen altına yazılırsa ne olur?
Program yine doğru çalışır. Makronun ilk komutu JMP skip_proc_pthis'tir; işlemci PTHIS'in gövdesinin üstünden atlar ve ana programın ilk komutuna gelir. Bedeli fazladan bir JMP'dir ve ana programın adresleri gövdenin boyu kadar ileri kayar. Koruyucu JMP olmasaydı işlemci doğrudan PTHIS'in koduna girerdi: POP SI yığından DOS'un 0000h sözcüğünü alır, PSP'nin baytlarını dizgi diye yazar ve geçersiz bir adrese dönerdi.
Alıştırma 6. Kullanıcının adını GET_STRING ile alıp “Merhaba, ad!” yazdırınız.
include 'emu8086.inc'
ORG 100h
LEA DI, ad
MOV DX, 20
CALL get_string
PUTC 13
PUTC 10
PRINT 'Merhaba, '
LEA SI, ad
CALL print_string
PUTC '!'
RET
ad DB 20 DUP(0)
DEFINE_GET_STRING
DEFINE_PRINT_STRING
ENDKullanıcı “Ece” yazıp Enter'a basarsa ekranın ilk satırında “Ece”, ikincisinde “Merhaba, Ece!” görünür. PUTC 13 ve PUTC 10 olmasaydı GET_STRING'den sonra imleç “Ece”nin hemen sağında kalır ve ekranda tek satırda “EceMerhaba, Ece!” görünürdü. Tampon 20 bayttır ve DX = 20; en çok 19 karakterlik bir ad alınır.
9.2.8Özet
- emu8086.inc, ekran, klavye ve sayı işleri için hazır makrolar ve prosedürler içeren kütüphanedir. İçindeki her şey BIOS ve DOS kesmeleri üzerine kurulu sıradan 8086 kodudur.
- INCLUDE bir direktiftir: dosyanın metnini derleme sırasında satırın yerine koyar, kod üretmez. Dosya önce kaynak klasöründe, sonra emu8086'nın Inc klasöründe aranır. Satır kaynağın başına yazılır.
- Makro tanımı ve DEFINE_ yazılmamış prosedür programa bayt eklemez. Program boyutunu kullanılan makroların her kullanımı ve DEFINE_ ile tanımlanan prosedürler belirler.
- Ekran makroları INT 10h üzerine kuruludur: PUTC ve PRINT 0Eh, GOTOXY 02h, CURSOROFF ve CURSORON 01h hizmetini kullanır. Makrolar kullandıkları yazmaçları saklar; PUTC 8 bayt, GOTOXY 16 bayttır.
- GOTOXY sütun, satır sırasını kullanır; BIOS satırı DH'de, sütunu DL'de bekler. Sol üst (0, 0), sağ alt (79, 24)'tür. Hücrenin video belleğindeki adresi B800h:(satır × 80 + sütun) × 2'dir.
- PRINT dizgiyi koda gömer ve üstünden JMP ile atlar; her kullanımda döngü kodu kopyalanır. PRINTN sona 13, 10 ekler. Yeni satır için CR (13) ve LF (10) birlikte gerekir. 79. sütundan sonra imleç alt satıra geçer, son satırda ekran kayar.
- Prosedürler DEFINE_ ile bir kez tanımlanır, CALL ile çağrılır. Her DEFINE_ makrosu gövdenin üstünden atlayan bir JMP ile başlar; alışılmış yer RET ile END arasıdır. PRINT_NUM, PRINT_NUM_UNS'yi de gerektirir.
- Girdi ve çıktı yazmaçları: PRINT_STRING DS:SI, GET_STRING DS:DI ve DX, SCAN_NUM → CX, PRINT_NUM ve PRINT_NUM_UNS ← AX. Öteki yazmaçlar korunur, bayraklar korunmaz.
- PTHIS, CALL'ın yığına ittiği dönüş adresini dizginin adresi olarak kullanır; dizgiyi yazdıktan sonra adresi 0'ın ötesine taşır ve RET dizgiden sonraki komuta döner.
- GET_STRING en fazla DX − 1 karakter okur, tamponu 0 ile bitirir, geri silmeyi işler ve Enter'ı yazmaz. DX tamponun gerçek boyundan büyük verilirse tamponun ardındaki değişkenler ezilir.
- SCAN_NUM rakamları sonuç ← sonuç × 10 + rakam kuralıyla biriktirir; eksi işaretini en sonda NEG ile uygular, rakam olmayan tuşları ve 65 535'i aşan basamakları reddeder. Enter yalnız satır başına döndürür.
- PRINT_NUM_UNS basamakları azalan bölenle (10 000, 1000, …) soldan sağa yazar; baştaki sıfırları bir bayrakla atlar. Yığınlı yöntem aynı sonucu 10'a bölüp basamakları yığından ters sırayla çekerek verir. PRINT_NUM negatif sayıda '-' yazıp mutlak değeri işaretsiz yazdırır.
- Aynı 16 bit iki biçimde okunur: en anlamlı bit 1 ise işaretli değer = işaretsiz değer − 65 536 (FFE7h = −25 = 65 511).
9.2.9Kavram listesi
Terimlerin İngilizce karşılıkları ve öteki bölümlerdeki kullanımları terim sözlüğünde bulunur.
- kütüphane library
- direktif directive
- makro macro
- makro genişletme macro expansion
- prosedür procedure
- prosedür çağrısı procedure call
- dönüş adresi return address
- parametre geçirme parameter passing
- yazmaçların korunması register preservation
- imleç cursor
- dizgi string
- kontrol karakteri control character
- ASCII
- BIOS
- hizmet service
- video belleği (metin modu) video memory
- klavye tamponu keyboard buffer
- işaretli / işaretsiz sayı signed / unsigned number
- ikiye tümleyen two's complement
- etiket label
9.2.10Kendini Dene
GOTOXY 3, 7açılımında INT 10h çağrılmadan hemen önce AH, BH, DH ve DL nedir? Açılım kaç bayttır ve hangi yazmaçları saklar?Cevap
AH = 02h, BH = 0, DH = 7 (satır), DL = 3 (sütun). Açılım 16 bayttır: üç PUSH ve üç POP 6 bayt, dört
MOV r8, sabit8 bayt,INT 10h2 bayt. AX, BX ve DX yığında saklanıp ters sırayla geri alınır.Renkli metin modunda (35, 12) hücresindeki karakter video belleğinin hangi bağıl konumunda durur? Fiziksel adresi nedir?
Cevap
(12 × 80 + 35) × 2 = 995 × 2 = 1990 = 07C6h. Karakter B800:07C6h'de, öznitelik baytı B800:07C7h'dedir. Fiziksel adres B8000h + 07C6h = B87C6h'dir.
Boş bir ekranda imleç (0, 0)'da iken
PRINTN 'Selam've ardındanPUTC 'X'çalışıyor. X hangi hücreye yazılır? İmleç sonunda nerededir?Cevap
“Selam” 0. satırın 0–4. sütunlarına yazılır. PRINTN'in eklediği 13 imleci (0, 0)'a, 10 ise (0, 1)'e götürür. X (0, 1)'e yazılır ve imleç (1, 1)'e geçer.
Kullanıcı SCAN_NUM çalışırken sırasıyla 5, b, 0 tuşlarına ve Enter'a basıyor. Prosedür döndüğünde CX nedir? Ekranda ne kalır?
Cevap
'b' rakam olmadığı için ekrana yazılır, sonra 8, ' ', 8 ile silinir ve sayıya katılmaz. Sayı 5 × 10 + 0 = 50 olur: CX = 0032h. Ekranda “50” kalır; Enter imleci satırın başına döndürür.
Yığınlı basamak ayırma kodu (10'a böl, it, LOOP ile çek) AX = 0 için ne yazar? Kütüphanedeki azalan bölen yöntemi neden AX = 0 için ayrı bir dal ister?
Cevap
Yığınlı kod döngüye en az bir kez girer: 0 / 10 = 0, kalan 0 yığına itilir ve CX = 1 olur. OR AX, AX ZF'yi 1 yapar, yazma döngüsü bir kez döner ve “0” yazılır. Azalan bölen yönteminde bayrak CX = 1 iken sayı bölenden küçükse tur atlanır. AX = 0 her bölenden küçük olduğu için bütün turlar atlanır ve hiçbir şey yazılmaz; ayrı dal bu yüzden '0' yazar.
GET_STRING,
MOV DX, 1ile çağrılıyor ve kullanıcı “abc” yazıp Enter'a basıyor. Tampona ne yazılır?Cevap
Hiçbir şey. DX ≤ 1 iken tamponda yalnız sondaki 0'a yer vardır ve prosedür hiç okuma yapmadan döner; tampona 0 da yazılmaz, eski içerik olduğu gibi kalır. Basılan tuşlar klavye tamponunda bekler ve sonraki okuma hizmeti onları alır.
Bir .COM programında
CALL pthis0100h'de, hemen ardındaDB 'Tamam', 13, 10, 0var. CALL yığına hangi adresi iter? PTHIS'in RET'i hangi adrese döner?Cevap
CALL 3 bayttır; yığına 0103h itilir, bu dizginin ilk baytı 'T'nin adresidir. Dizgi 5 + 2 + 1 = 8 bayttır ve 0103h–010Ah'yi kaplar. PTHIS 0'ı 010Ah'de okuduğunda SI = 010Bh olur; bu değer yığına konur ve RET 010Bh'ye döner.
AX = C350h iken PRINT_NUM ve PRINT_NUM_UNS ne yazar?
Cevap
C350h = 50 000'dir; PRINT_NUM_UNS “50000” yazar. En anlamlı bit 1 olduğu için işaretli değer 50 000 − 65 536 = −15 536'dır; PRINT_NUM “-15536” yazar.
9.2.11İleri okuma
- emu8086 kurulumundaki
Inc/emu8086.incdosyası: kütüphanedeki bütün makroların ve prosedürlerin kaynak kodu. SCAN_NUM'ın taşma denetimi ve PRINT_NUM_UNS'nin azalan bölen döngüsü burada satır satır okunabilir. - emu8086 yardım belgeleri, “emu8086.inc — library of common functions” ve “Macros” sayfaları: kütüphanenin kullanım örnekleri ve makroların genel kuralları.
- B. B. Brey, The Intel Microprocessors (8. baskı): prosedürler, makrolar ve BIOS video hizmetleri; onluk sayıların ikiliye ve ikiliden onluğa dönüştürülmesi.
- R. Brown, “Ralf Brown's Interrupt List”: INT 10h, INT 16h ve INT 21h hizmetlerinin girdi ve çıktı yazmaçlarıyla ayrıntılı listesi.
9.2.12Kaynaklar
- emu8086 yardım belgeleri: “emu8086.inc — library of common functions” ve “Macros” sayfaları.
- emu8086 kurulumundaki Inc/emu8086.inc dosyası (makro ve prosedürlerin kaynak kodu).
- B. B. Brey, The Intel Microprocessors, 8. baskı, Pearson, 2009 — prosedürler ve makrolar.
- A. S. Tanenbaum, Modern Operating Systems, 3. baskı, Pearson, 2008.
- Intel, The 8086 Family User's Manual, 1979 — CALL, RET, MUL, DIV, AAM komutlarının tanımları ve saat turu sayıları.
İlgili sayfalar: Bölüm 9.2 sunumu · Çalışma soruları · Terim sözlüğü · © Sercan Külcü. Tüm hakları saklıdır.