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.

Tanım 9.2.1

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'
Tanım 9.2.2

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.

include'emu8086.inc'1. kaynak dosyanınklasörüyoksa2. emu8086 kurulumundakiInc klasörübulunan dosyanın içeriği, include satırının yerine eklenir
Şekil 9.2.1: INCLUDE satırı önce kaynak dosyanın klasöründe, bulunamazsa emu8086 kurulumundaki Inc klasöründe aranır. Bulunan dosyanın içeriği satırın yerine eklenir.

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.

Sık yapılan hata

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
RET

Bu 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.

Tablo 9.2.1: emu8086.inc'ten programa ne zaman bayt girer?
KaynaktaÜretilen kodNe zaman?
include 'emu8086.inc'yokyalnız tanımlar okunur
makro kullanımı (PUTC 'A')makronun gövdesi, parametreleri yerleştirilmiş olarakher kullanımda yeniden
DEFINE_SCAN_NUMprosedürün bütün kodubir kez, satırın bulunduğu yerde
CALL scan_num3 baytlık CALL komutuher ç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.

Tablo 9.2.2: Kütüphanedeki makrolar (doğrudan kullanılır).
MakroGörevArka planda
PUTC charimlecin bulunduğu yere tek karakter yazarINT 10h, AH = 0Eh
GOTOXY col, rowimleci verilen sütun ve satıra götürürINT 10h, AH = 02h
PRINT stringdizgiyi yazar0Eh ile karakter karakter
PRINTN stringdizgiyi yazar, sonra yeni satıra geçer0Eh ile karakter karakter; sona 13, 10
CURSOROFFimleci gizlerINT 10h, AH = 01h
CURSORONimleci gösterirINT 10h, AH = 01h
Tablo 9.2.3: Kütüphanedeki prosedürler (DEFINE_ ile tanımlanır, CALL ile çağrılır).
ProsedürGörevTanım satırı
PRINT_STRINGDS:SI'deki 0 ile biten dizgiyi yazarDEFINE_PRINT_STRING
PTHISCALL'ın hemen ardındaki dizgiyi yazarDEFINE_PTHIS
GET_STRINGklavyeden dizgi okurDEFINE_GET_STRING
CLEAR_SCREENekranı temizlerDEFINE_CLEAR_SCREEN
SCAN_NUMklavyeden işaretli sayı okur, CX'e yazarDEFINE_SCAN_NUM
PRINT_NUMAX'i işaretli sayı olarak yazarDEFINE_PRINT_NUM + DEFINE_PRINT_NUM_UNS
PRINT_NUM_UNSAX'i işaretsiz sayı olarak yazarDEFINE_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.

Tanım 9.2.3

İ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.

Tablo 9.2.4: Ekran makrolarının kullandığı INT 10h hizmetleri.
AHHizmetGirdilerKullanan
01himleç şeklini ayarlaCH = başlangıç, CL = bitiş tarama satırıCURSOROFF, CURSORON
02himleç konumunu ayarlaBH = sayfa, DH = satır, DL = sütunGOTOXY
0Ehkarakter yaz ve imleci ilerlet (teletype)AL = karakterPUTC, 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 geri

Komutlar 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  AX

Makro 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.

Sık yapılan hata

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  AX

Açı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.

abCR (13): satır başıLF (10): alt satır
Şekil 9.2.2: PRINTN 'a' ardından PRINT 'b': CR imleci satır başına, LF bir alt satıra götürür; b alt satırın başına yazılır.

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.

Tablo 9.2.5: Makrolarla ekran programının izlemesi (her satır çalıştıktan sonraki durum).
Satırİmleç (sütun, satır)Ekrana yazılanAçı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ücresine65 = 41h = 'A'
PUTC 'B'(12, 5)B, (11, 5) hücresinekarakter sabiti
RET(12, 5)—DOS'a dönüş
0510150123456Hello World!… 80 × 250510150123456Hello World!AB… 80 × 25
Şekil 9.2.3: GOTOXY 10, 5'ten sonra (solda) imleç (10, 5)'tedir; iki PUTC'den sonra (sağda) ekranın 5. satırında AB yazılıdır.

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'
CURSORON

Bu 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.

Tablo 9.2.6: PUTC ile verilen kontrol karakterleri.
KodAdEtkisi
07hBEL (zil)hoparlörden kısa bir bip; imleç yerinden oynamaz
08hBS (geri)imleç bir sola gider; karakteri silmez; 0. sütunda etkisizdir
0AhLF (satır besleme)imleç bir alt satıra iner, sütun değişmez; son satırda ekran bir satır yukarı kayar
0DhCR (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
Tablo 9.2.7: CR ve LF'nin ayrı ayrı etkisi.
KomutlarALİmleç (sütun, satır)Ekran
MOV AH, 0Eh / MOV AL, 'a' / INT 10h61h(1, 0)a
MOV AL, 'b' / INT 10h62h(2, 0)ab
MOV AL, 10 / INT 10h0Ah(2, 1)ab
MOV AL, 'c' / INT 10h63h(3, 1)ab / ··c
MOV AL, 13 / INT 10h0Dh(0, 1)ab / ··c
MOV AL, 'd' / INT 10h64h(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.

AL0Ahimleç (sütun, satır)(2, 1)ekranabAL64himleç (sütun, satır)(1, 1)ekranabdc
Şekil 9.2.4: Yalnız LF'den sonra imleç (2, 1)'dedir (solda); yalnız CR ve 'd'den sonra ekranda ab ve altında d c görünür (sağda).

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.

Örnek 9.2.1

GOTOXY 75, 24 ardından PRINT 'Merhaba' çalışıyor. Yazının ekranda nerede kaldığını ve imlecin son konumunu bulalım.

  1. İmleç (75, 24)'tedir. “M”, “e”, “r”, “h”, “a” sırasıyla 75, 76, 77, 78 ve 79. sütunlara yazılır.
  2. 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.
  3. “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).

sütun01234…7576777879satır 23…Merhasatır 24ba…imleç
Şekil 9.2.5: Ekranın son iki satırı: “Merha” 23. satırın 75–79. sütunlarında, “ba” 24. satırın başında; imleç (2, 24)'te.

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
RET
Tanım 9.2.4

AAM (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.

Tablo 9.2.8: AAM ve PUTC ile iki basamak yazdırmanın izlemesi.
Komut(lar)AHALSP[FFFCh]Ekran
MOV AL, 5800h3AhFFFEh—
AAM05h08hFFFEh—
ADD AX, 3030h35h38hFFFEh—
PUSH AX35h38hFFFCh3538h
MOV AL, AH / MOV AH, 0Eh / INT 10h0Eh35hFFFCh3538h5
POP AX35h38hFFFEh—5
ikinci açılım (5 komut)35h38hFFFEh—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
END
Tanım 9.2.5

DEFINE_ 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.

ana kod … CALL print_num …RETprint_numprint_num_unsENDCALLakış yukarıdan aşağıDEFINE_… makrolarının açıldığı yer
Şekil 9.2.6: Bellekte önce ana kod ve RET, ardından DEFINE_ makrolarıyla açılan prosedürler gelir; CALL prosedüre atlar.

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.

Sık yapılan hata

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).

Tablo 9.2.9: Kütüphane prosedürlerinin girdi ve çıktı yazmaçları.
ProsedürGirdiÇıktıAyrıntı
PRINT_STRINGDS:SI = dizgi adresi—dizgi 0 ile bitmeli
PTHISdizgi CALL'ın hemen ardında—dönüş adresini kendisi ilerletir
GET_STRINGDS:DI = tampon, DX = boyuttamponda 0 ile biten dizgien fazla DX − 1 karakter
CLEAR_SCREEN——imleç (0, 0)'a gider
SCAN_NUMklavyeCX (işaretli)CX'teki eski değer kaybolur
PRINT_NUMAX (işaretli)—PRINT_NUM_UNS de tanımlı olmalı
PRINT_NUM_UNSAX (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 oku

PUSH 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.

Tanım 9.2.6

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', 0

LEA 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 eder

PRINT 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.

Tablo 9.2.10: Dizgi yazdırma yollarının karşılaştırması.
YöntemDizgi nerede?SonlandırıcıAdresUygun 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_STRINGveri alanında0DS:SIbirden çok yerde yazılan ya da çalışırken oluşan dizgi
PTHISCALL'ın hemen ardında0yığındaki dönüş adresiçok sayıda kısa sabit ileti
INT 21h, AH = 09hveri alanında'$'DS:DXkü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.

CALL pthis0D0A'You have entered: '00CALL print_numkod kesimi (bellek, soldan sağa artan adres)yığın:adres(0Dh)← SP = FFFChCALL pthis0D0A'You have entered: '00CALL print_numkod kesimi (bellek, soldan sağa artan adres)yığın:adres(CALL print_num)← SP = FFFCh
Şekil 9.2.7: CALL pthis'ten sonra yığında dizginin ilk baytının (0Dh) adresi durur (solda). PTHIS dizgiyi yazdıktan sonra yığındaki adresi 0 baytının bir ötesiyle, CALL print_num'un adresiyle değiştirir (sağda).

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.

Tablo 9.2.11: PTHIS çağrısında yığın ve SI (sayı oku ve yazdır programı).
AnSPYığının üstüSIAçıklama
CALL pthis öncesiFFFEh0000hmsg1IP = 010Bh
CALL pthis sonrasıFFFCh010Ehmsg1dönüş adresi = dizginin ilk baytı
POP SI sonrasıFFFEh0000h010Eheski SI CS:temp1'de
0 okununcaFFFEh0000h0123h21 bayt okundu, 20 karakter yazıldı
PUSH SI sonrasıFFFCh0123h0123hyeni dönüş adresi
RET sonrasıFFFEh0000hmsg1IP = 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.

Tablo 9.2.12: GET_STRING okuma döngüsünün izlemesi (DX = 4, girdi “Ece”).
AdımALDIDXZFTampon (ad+0…3)Ekran
LEA DI, ad / MOV DX, 400h011Dh0004h0? ? ? ?
CMP DX, 1 / JBE (4 > 1, atlamaz)00h011Dh0004h0? ? ? ?
INT 21h ('E'), CMP AL, 1345h011Dh0004h0? ? ? ?E
MOV [DI], AL / INC DI / DEC DX45h011Eh0003h0E ? ? ?E
2. tur ('c')63h011Fh0002h0E c ? ?Ec
3. tur ('e')65h0120h0001h0E c e ?Ece
CMP DX, 1 / JBE bitti (atlar)65h0120h0001h1E c e ?Ece
MOV BYTE PTR [DI], 065h0120h0001h1E c e 0Ece

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.

AL65hDI0120hDX0001hZF1CF0tampon'E'ad+0'c'ad+1'e'ad+20ad+3DIDX = 1ekranEce▌
Şekil 9.2.8: Döngünün sonunda tampon: 'E', 'c', 'e' ve sondaki 0; DI ad+3'ü (0120h) gösterir.

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.

Tablo 9.2.13: Geri silmeyi işleyen okumanın izlemesi (A, b, geri silme, c, Enter).
OlayALDIİmleç sütunuTamponEkran
LEA DI, ad00had+00? ? ? ? ? ?
INT 16h: 'A'; Enter ve geri silme değil41had+00? ? ? ? ? ?
ekle: yaz, ilerle, yankı41had+11A ? ? ? ? ?A
'b' için aynı yol62had+22A b ? ? ? ?Ab
INT 16h: geri silme; CMP AL, 8 → ZF = 108had+22A b ? ? ? ?Ab
CMP DI, OFFSET ad (eşit değil) / DEC DI08had+12A b ? ? ? ?Ab
8, ' ', 8 üçlüsü08had+11A b ? ? ? ?A
'c': b'nin baytına yazılır63had+22A c ? ? ? ?Ac
Enter: JE bitti, 0 yazılır0Dhad+22A 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.

Örnek 9.2.2

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.

  1. DX = 8 olduğu için prosedür en fazla 7 karakter alır: “Istanbu”. Sekizinci harf 'l' yok sayılır.
  2. İlk dört karakter “Ista” tamponun dört baytına yazılır.
  3. '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.
  4. 'u' (75h = 117) son'un üstüne yazılır; son = 117 olur.
  5. 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.

Sık yapılan hata

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).

Tablo 9.2.14: Sayı prosedürleri.
ProsedürNe yapar?Girdi / çıktıTanım
SCAN_NUMklavyeden çok basamaklı, işaretli bir sayı okur; Enter ile biterçıktı: CXDEFINE_SCAN_NUM
PRINT_NUMAX'teki işaretli sayıyı onluk yazargirdi: AXDEFINE_PRINT_NUM + DEFINE_PRINT_NUM_UNS
PRINT_NUM_UNSAX'teki işaretsiz sayıyı onluk yazargirdi: AXDEFINE_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.

Tanım 9.2.7

İ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: RET

Kodun 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.

Tablo 9.2.15: SCAN_NUM mantığının izlemesi (girdi 307, ardından Enter).
AdımAXCXBXZFCX (onluk)Ekran
XOR CX, CX0000h0000h0000h10
MOV AH, 01h / INT 21h: '3'0133h0000h0000h103
CMP / JE / SUB AL, '0'0103h0000h0000h003
CBW / XCHG AX, CX0000h0003h0000h033
MOV BX, 10 / MUL BX / ADD CX, AX0000h0003h000Ah033
'0' okunur, SUB, CBW, XCHG0003h0000h000Ah1030
MUL BX / ADD CX, AX001Eh001Eh000Ah03030
'7' okunur, SUB, CBW0007h001Eh000Ah030307
XCHG / MUL BX / ADD CX, AX012Ch0133h000Ah0307307
Enter: AL = 0Dh, CMP AL, 13010Dh0133h000Ah1307307
JE bitti / RET010Dh0133h000Ah1307307

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 CX uygulanı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.

Tablo 9.2.16: SCAN_NUM'a verilen girdiler, CX'teki sonuç ve PRINT_NUM'ın yazdığı değer.
GirdiCXPRINT_NUMAçıklama
3070133h307olağan durum
-25FFE7h-25Enter'da NEG: 0019h → FFE7h
12a3007Bh123'a' ekrandan silinir, sayıya katılmaz
400009C40h-2553616 bite sığar ama işaretli aralığı aşar
700001B58h7000son 0 kabul edilmez: 7000 × 10 = 70 000 > 65 535
-4000063C0h255369C40h'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
RET

DIV 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.

Tablo 9.2.17: Yığınlı basamak ayırmanın izlemesi (AX = 1205 = 04B5h).
AdımAXDXCXSPYığın (üstten)Ekran
başlangıç (MOV, MOV, XOR)04B5h0000h0000hFFFEh—
1. bölme: 1205 / 100078h0005h0001hFFFCh5
2. bölme: 120 / 10000Ch0000h0002hFFFAh0, 5
3. bölme: 12 / 100001h0002h0003hFFF8h2, 0, 5
4. bölme: 1 / 100000h0001h0004hFFF6h1, 2, 0, 5
OR AX, AX → ZF = 1, JNZ atlamaz0000h0001h0004hFFF6h1, 2, 0, 5
1. yazma: POP, '0' ekle, INT 10h, LOOP0E31h0001h0003hFFF8h2, 0, 51
2. yazma0E32h0001h0002hFFFAh0, 512
3. yazma0E30h0001h0001hFFFCh5120
4. yazma, LOOP biter0E35h0001h0000hFFFEh—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.

AX0000hDX0001hCX0004hAX onluk:0yığınFFF60001hSPFFF80002hFFFA0000hFFFC0005hekran▌
Şekil 9.2.9: Bölmeler bittiğinde yığında FFF6h'den FFFCh'ye doğru 1, 2, 0 ve 5 basamakları durur; SP = FFF6h.

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, 30h ile 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ı.
Tablo 9.2.18: Azalan bölenle 3007'nin yazdırılması (her turun başındaki durum).
Bölen (BX)Bayrak (CX)Sayı (AX)İşlemEkran
10 00013007sayı < bölen ve henüz basamak yok: atla—
1000130073007 ÷ 1000 = 3, kalan 7; CX = 03
100077 ÷ 100 = 0, kalan 730
10077 ÷ 10 = 0, kalan 7300
1077 ÷ 1 = 7, kalan 03007
000bölen 0: bitti3007

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 ENDP

MOV 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.

Tablo 9.2.19: PRINT_NUM eşdeğerinin izlemesi (AX = −25).
Komut(lar)AXSPSFYığın (FFFAh, FFFCh)Ekran
MOV AX, -25FFE7hFFFEh0—, —
CALL print_numFFE7hFFFCh0—, 0106h
PUSH AX / OR AX, AXFFE7hFFFAh1FFE7h, 0106h
JNS (atlamaz) / NEG AX0019hFFFAh0FFE7h, 0106h
PUTC '-' açılımı (5 komut)0019hFFFAh0FFE7h, 0106h-
CALL print_num_uns (dönüşten sonra)0019hFFFAh0FFE7h, 0106h-25
POP AXFFE7hFFFCh0—, 0106h-25
RET (print_num'dan)FFE7hFFFEh0—, —-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ı

Tablo 9.2.20: Yığınlı yöntem ile azalan bölen yönteminin karşılaştırması.
ÖlçütYığı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ı kadarbeşe kadar; ayrıca böleni küçültmek için birer bölme
Yığın kullanımıbasamak başına 2 baytyalnız saklanan yazmaçlar
Baştaki sıfırlarkendiliğinden oluşmazbayrakla atlanır
Sabit genişlik (00042)ek kod gerekirbayrak kaldırılınca kendiliğinden
Başka tabanbölen 2, 8 ya da 16 yapılır; 16'da 9'dan büyük basamaklar 'A'–'F' olurbölen dizisi tabana göre değişir (16'da 4096, 256, 16, 1)
Çıkış sırasıönce yığına, sonra ters sırayla ekranasoldan 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).

Tablo 9.2.21: Aynı 16 bit, iki prosedür.
AXEn anlamlı bitPRINT_NUMPRINT_NUM_UNS
0000h000
7FFFh03276732767
8000h1-3276832768
9C40h1-2553640000
FFE7h1-2565511
FFFFh1-165535

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.

Sık yapılan hata

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.

Tablo 9.2.22: Sayı oku ve yazdır programının ana bölümünün yerleşimi.
AdresBaytSatır
0100h3LEA SI, msg1 (BE 27 01)
0103h3CALL print_string
0106h3CALL scan_num
0109h2MOV AX, CX (8B C1)
010Bh3CALL pthis
010Eh21DB 13, 10, 'You have entered: ', 0
0123h3CALL print_num
0126h1RET (C3)
0127h19msg1 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).

Tablo 9.2.23: Sayı oku ve yazdır programının izlemesi (girdi −25).
SatırSICXAXSPEkran
LEA SI, msg10127h(boyut)0000hFFFEh
CALL print_string (içinde)0127h(boyut)0000hFFFChEnter the number:␣
CALL scan_num (içinde)0127h…0000hFFFChEnter the number: -25
CALL scan_num (dönüş)0127hFFE7h0000hFFFEhEnter the number: -25
MOV AX, CX0127hFFE7hFFE7hFFFEhEnter the number: -25
CALL pthis0127hFFE7hFFE7hFFFCh…
dizgi yazılır0127hFFE7hFFE7hFFFCh… / You have entered:␣
CALL print_num (içinde)0127hFFE7hFFE7hFFFCh… / You have entered: -25
RET0127hFFE7hFFE7h0000h… / 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
END

Programı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.

Tanım 9.2.8

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.

Tablo 9.2.24: İki sayının toplamı programı farklı girdilerle.
GirdilerBXCXAX = BX + CXOFSon satır
120, −450078hFFD3h004Bh075
−7, −8FFF9hFFF8hFFF1h0-15
30000, 50007530h1388h88B8h1?

İ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
END

cizgi 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.

0123456+----------+|BASLIK|+----------+sütun 10 … 21
Şekil 9.2.10: Ekranın sol üst bölümü: 2–4. satırlarda, 10. sütundan başlayan çerçeveli BASLIK yazısı; imleç (0, 6)'da.

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.

Sık yapılan hata

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.

Sık yapılan hata

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.

Sık yapılan hata

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.

Sık yapılan hata

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.

Sık yapılan hata

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.

Başta: gereksiz JMPORG 100hDEFINE_SCAN_NUMana programRETAlışılmış yerORG 100hana programRETDEFINE_SCAN_NUMEND← JMP ile atlanır← yalnız CALL ile
Şekil 9.2.11: DEFINE_SCAN_NUM ORG 100h'nin altına konsa da makronun ilk komutu JMP, prosedür gövdesini atlar (solda). Alışılmış yer RET ile END arasıdır; prosedüre yalnız CALL ile girilir (sağda).

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
END

Kullanı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.

9.2.10Kendini Dene

  1. GOTOXY 3, 7 açı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, sabit 8 bayt, INT 10h 2 bayt. AX, BX ve DX yığında saklanıp ters sırayla geri alınır.

  2. 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.

  3. Boş bir ekranda imleç (0, 0)'da iken PRINTN 'Selam' ve ardından PUTC '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.

  4. 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.

  5. 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.

  6. GET_STRING, MOV DX, 1 ile ç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.

  7. Bir .COM programında CALL pthis 0100h'de, hemen ardında DB 'Tamam', 13, 10, 0 var. 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.

  8. 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.inc dosyası: 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

  1. emu8086 yardım belgeleri: “emu8086.inc — library of common functions” ve “Macros” sayfaları.
  2. emu8086 kurulumundaki Inc/emu8086.inc dosyası (makro ve prosedürlerin kaynak kodu).
  3. B. B. Brey, The Intel Microprocessors, 8. baskı, Pearson, 2009 — prosedürler ve makrolar.
  4. A. S. Tanenbaum, Modern Operating Systems, 3. baskı, Pearson, 2008.
  5. 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.