Mikroişlemciler · Ders Notu

Bölüm 8.2: Prosedürler

Bir programda aynı iş birçok yerde gerekir: ekrana bir metin yazmak, bir sayıyı onluk basamaklara ayırmak, bir dizide değer aramak. Bu işin kodunu her yerde yeniden yazmak hem belleği doldurur hem de hatayı çoğaltır. Prosedür, böyle bir işi bir kez yazıp istenen her yerden çağırmayı sağlar. 8086 bunu iki komutla yapar: CALL dönüş adresini yığına yazıp prosedüre atlar, RET o adresi yığından alıp geri döner. Not önce prosedürün nasıl tanımlandığını ve CALL ile RET'in yazmaçlara, yığına ve makine koduna etkisini anlatır. Ardından yakın ve uzak çağrıları, dolaylı çağrıyı ve çağrının saat turu maliyetini ele alır. Sonra parametrelerin yazmaçla, bellekle, adresle ve yığınla nasıl geçirildiğine, yazmaçların korunmasına, yığın çerçevesine ve yerel değişkenlere geçer. Son kesimler örnek programları, özyinelemeyi ve prosedürlerde sık yapılan hataları işler. Her kod parçası yazmaçlar, bayraklar ve yığınla birlikte adım adım izlenir.

Öğrenme çıktıları

Prosedür tanımlamak
PROC/ENDP ile prosedür yazmak, prosedürü programda doğru yere koymak ve assembler direktifini işlemci komutundan ayırmak.
CALL ve RET'i izlemek
Dönüş adresinin yığına nasıl itildiğini ve RET ile nasıl geri alındığını IP, SP ve yığın değerleriyle adım adım göstermek; CALL'ın makine kodundaki bağıl uzaklığı hesaplamak.
Çağrı türlerini ayırt etmek
Yakın, uzak ve dolaylı çağrıların yığına ne ittiğini, RET, RETF ve RET n'in ne çektiğini açıklamak; prosedür türü ile dönüş komutunun uyuşması gerektiğini göstermek.
Parametre geçirmek
Yazmaçlar, bellek değişkenleri, adresler ve yığın yoluyla prosedüre veri göndermek; sonucu yazmaçla, bayrakla ya da bellekle geri almak; değerle ve adresle geçirmeyi karşılaştırmak.
Yığın çerçevesi kurmak
BP ile parametrelere [BP+4], [BP+6] gibi sabit uzaklıklarla erişmek, SUB SP ile yerel değişken alanı açmak ve çerçeveyi doğru sırayla kapatmak.
Güvenli prosedür yazmak
Yazmaçları PUSH/POP ile korumak, yığını dengede tutmak, tek çıkış noktası kullanmak, özyinelemenin yığın ihtiyacını hesaplamak ve sık yapılan hatalardan kaçınmak.

8.2.1Prosedür kavramı ve tanımlama

8.2.1.1Prosedür nedir?

Assembly programları kısa sürede uzar. Ekrana bir dizgi yazmak, bir sayıyı onluk basamaklarına ayırmak ya da bir dizide değer aramak gibi işler programın birçok yerinde gerekir. Aynı komut dizisini her gerektiği yerde yeniden yazmak iki sorun doğurur. Birincisi, program gereksiz yere büyür; 8086 tabanlı küçük sistemlerde bellek sınırlı bir kaynaktır. İkincisi, kodun bir kopyasında bulunan hata bütün kopyalarda vardır ve her biri ayrı ayrı düzeltilmelidir. Yüksek düzeyli dillerdeki fonksiyon ve alt program kavramı bu sorunları çözer. Assembly dilindeki karşılığı prosedürdür.

Tanım 8.2.1

Prosedür (procedure, subroutine), belirli bir görevi yapan ve programın istenen yerinden çağrılabilen kod parçasıdır. Prosedür CALL komutuyla çağrılır. Görevini bitirince RET komutuyla, çağrıldığı komutun hemen arkasındaki komuta döner. Çağrı anında yığına yazılan bu adrese dönüş adresi (return address) denir.

Tanımdaki en önemli kavram dönüş adresidir. Bir prosedür programın farklı yerlerinden çağrılabilir. Her çağrıda dönülecek yer başkadır, ama prosedürün kodu aynıdır. Prosedür, nereye döneceğini kendi kodunda tutamaz. Bu bilgiyi her çağrıda CALL komutu yığına yazar, RET komutu da oradan okur. Böylece aynı RET komutu bir çağrıda bir adrese, başka bir çağrıda başka bir adrese döner.

ana program…CALL yaz…CALL yaz…RETyaz PROC……RETCALLRET
Şekil 8.2.1: Ana program yaz prosedürünü iki kez çağırır. Her RET, kendisini başlatan çağrının hemen arkasındaki komuta döner.

Şekil 8.2.1 bu durumu gösterir. Ana program yaz prosedürünü iki ayrı yerden çağırır. Birinci çağrının dönüşü birinci CALL'ın arkasına, ikinci çağrının dönüşü ikinci CALL'ın arkasına yapılır. Prosedürün belleğe tek bir kopyası yüklenmiştir.

Prosedür kullanmanın üç faydası vardır. Tekrar kullanım: kod bir kez yazılır, istenen sayıda çağrılır; kod tekrarı ve bellek kullanımı azalır. Okunabilirlik: program, her biri adı konmuş küçük görevlere bölünür. CALL sayi_yaz satırı, yerine gelecek on beş komuttan çok daha kolay okunur. Bakım: hata tek yerde düzeltilir ve prosedür programın geri kalanından ayrı olarak denenebilir. Bu özellikler programı daha yapılandırılmış (structured) ve anlaşılır kılar.

Prosedürün bir bedeli de vardır. Her çağrıda işlemci CALL ve RET komutlarını fazladan yürütür, dönüş adresini belleğe yazar ve geri okur. Bu yük saat turu cinsinden hesaplanabilir. Yük küçüktür, ama çok sık çağrılan çok kısa işlerde fark edilir.

8.2.1.2PROC ve ENDP ile tanımlama

8086 assembly dilinde prosedür, adıyla başlayan bir PROC satırı ile aynı adla biten bir ENDP satırı arasına yazılır. Genel biçim şöyledir:

ad PROC     ; başlangıç (varsayılan NEAR)
    ; prosedür kodu buraya yazılır
    RET
ad ENDP     ; bitiş

Bu kalıbın üç kuralı vardır. Birincisi, ad prosedürün adıdır ve başta ve sonda aynı yazılmalıdır. p1 PROC ile başlayıp p2 ENDP ile biten bir tanımı assembler hata sayar. İkincisi, ad etiket kurallarına uyar: harfle ya da alt çizgiyle başlar, rakamla başlayamaz, bir komut adıyla aynı olamaz. Assembler adı, prosedürün ilk komutunun adresine bağlar. CALL ad yazıldığında bu adres kullanılır. Üçüncüsü, prosedürün içinde en az bir RET bulunmalıdır. İşlemci ENDP'yi görmez. RET yoksa akış prosedürün son komutundan sonra bellekte ne varsa onunla devam eder.

PROC satırına türü de yazılabilir: ad PROC NEAR ya da ad PROC FAR. Tür yazılmazsa prosedür yakın (NEAR) sayılır. Yakın prosedür, onu çağıran kodla aynı kod kesimindedir. Bu notun örneklerinin çoğu tek kesimli .COM programlarıdır ve bütün prosedürleri yakındır. Uzak prosedürün yığına etkisi CALL ve RET türleriyle birlikte ele alınır.

Prosedürün programdaki yeri de önemlidir. İşlemci komutları bellekteki sırasıyla yürütür ve bir PROC satırında durmaz. Prosedür ana programın ortasına yazılırsa ana program oraya geldiğinde prosedürün komutlarını, çağrılmamış olduğu hâlde yürütür. Bu duruma prosedüre düşmek denir. Güvenli yer, ana programın son RET'inden sonrasıdır:

ORG  100h
     CALL p1
     RET        ; ana program biter
p1   PROC       ; RET'ten sonra:
     ; ...      ; sırayla yürütülmez
     RET
p1   ENDP
END

Burada ana program iki komuttan oluşur. CALL p1 prosedürü çağırır, ardından gelen RET programı bitirir. Prosedürün komutları ana programın RET'inden sonra durduğu için akış onlara yalnız CALL ile ulaşır. Prosedür ana programdan önce yazılmak istenirse ana programın başına, prosedürün üzerinden atlayan bir JMP konur. emu8086 örneklerinde ikinci biçim de sık görülür: JMP start, ardından prosedürler, ardından start: etiketi.

Sık yapılan hata

Ana programın sonundaki RET'i unutmak. Aşağıdaki programda CALL ikiyle'den sonra RET yoktur:

ORG 100h
MOV AX, 2
CALL ikiyle
ikiyle PROC
ADD AX, AX
RET
ikiyle ENDP

CALL dönüş adresi olarak 0106h'yi iter, prosedür AX'i 4 yapar ve RET 0106h'ye döner. Oysa 0106h prosedürün kendi ilk komutudur. İşlemci prosedüre bir kez daha “düşer”, AX = 8 olur ve bu kez RET yığındaki 0000h'yi alarak programı bitirir. Prosedür bir kez çağrıldığı hâlde iki kez çalışmış, sonuç 4 yerine 8 çıkmıştır. Assembler bu hatayı yakalayamaz, çünkü kod söz dizimi bakımından doğrudur.

8.2.1.3Direktifler ve komutlar

Prosedür tanımında iki farklı türden öğe yan yana durur. PROC, ENDP ve END assembler'a yöneliktir. CALL ve RET ise işlemcinin yürüttüğü komutlardır. Bu ayrım prosedürlerin nasıl çalıştığını anlamanın anahtarıdır.

Tanım 8.2.2

Direktif (directive), assembler'a çeviri sırasında ne yapacağını söyleyen satırdır. Direktif makine koduna çevrilmez ve bellekte yer kaplamaz. Komut (instruction) ise bir ya da birkaç bayta çevrilen ve çalışma anında işlemcinin yürüttüğü işlemdir.

Tablo 8.2.1: Prosedür tanımında kullanılan öğeler.
ÖğeTürüGörevi
PROC / ENDPassembler direktifiProsedürün başlangıç adresini ve türünü (NEAR/FAR) assembler'a bildirir. Makine koduna çevrilmez.
CALLişlemci komutuDönüş adresini yığına iter, prosedürün adresine atlar. Yakın doğrudan biçimin kodu E8'dir.
RETişlemci komutuDönüş adresini yığından alır ve oraya atlar. Yakın biçimin kodu C3'tür. Hem prosedürden hem .COM programından dönmek için kullanılır.
ENDassembler direktifiKaynak dosyanın bittiğini bildirir.
derleme: PROC · ENDP · END → bayt yokçalışma: CALL = E8 · RET = C3
Şekil 8.2.2: PROC, ENDP ve END yalnız derleme sırasında kullanılır ve bayt üretmez; CALL ve RET işlemcinin çalıştırdığı komutlardır.

Tablo 8.2.1 ve Şekil 8.2.2 bu ayrımı özetler. Sonucu şudur: çalışma anında “prosedür” diye bir nesne yoktur. Bellekte yalnız komut baytları vardır. Prosedürün adı, başı ve sonu yalnız kaynak kodda ve assembler'ın sembol tablosunda yaşar. İşlemci için bir prosedür, CALL ile atlanan ve RET ile terk edilen bir adres aralığından ibarettir. Bu yüzden ENDP satırı bir “bitiş” işi yapmaz. Prosedürü bitiren, içindeki RET komutudur.

RET'in ikinci kullanımı da bu bakışla anlaşılır. Bir .COM programı da DOS'un çağırdığı bir prosedür gibi biter. DOS programı başlatmadan önce programın kesimindeki ilk 256 bayta, yani PSP'ye (program segment prefix, program kesim öneki) INT 20h komutunu yazar. Sonra yığına 0000h sözcüğünü iter ve programa 0100h adresinden başlatır. Programın son RET'i yığındaki 0000h'yi IP'ye alır. İşlemci CS:0000h'daki INT 20h'yi yürütür ve program sonlanır. emu8086'da bir .COM programı başladığında SP = FFFEh'dir ve [FFFEh] = 0000h'dir. Bu notun bütün izleme tablolarında yığının dibindeki 0000h, DOS'un ittiği bu sözcüktür.

8.2.1.4Modüler programlama

Büyük programlarda prosedürler de bir kaynak dosyaya sığmaz. Program, her biri birbiriyle ilgili prosedürleri toplayan kaynak dosyalarına bölünür. Bu dosyalara modül denir. Bir modüldeki prosedürü başka bir modülden çağırmak için iki direktif kullanılır:

; ana.asm
EXTRN yaz:NEAR     ; başka dosyada
      ...
      CALL yaz
; araclar.asm
PUBLIC yaz         ; dışarıya açık
yaz   PROC NEAR
      ...
      RET
yaz   ENDP

PUBLIC yaz, yaz adını başka modüllere açar. EXTRN yaz:NEAR ise assembler'a bu adın başka bir modülde tanımlı olduğunu ve türünün NEAR olduğunu söyler. Assembler ana.asm'yi çevirirken yaz'ın adresini bilmez. CALL komutunun adres alanını boş bırakır ve nesne dosyasına “buraya yaz'ın adresi gelecek” diye bir not düşer.

ana.asmderleana.objaraclar.asmderlearaclar.objbağlayıcı → .EXE
Şekil 8.2.3: İki kaynak dosya ayrı ayrı derlenip nesne dosyalarına dönüşür; bağlayıcı bunları tek programda birleştirir.

Her modül ayrı derlenir ve bir nesne dosyası (.OBJ) olur (Şekil 8.2.3). Bağlayıcı (linker) nesne dosyalarını art arda yerleştirir, her prosedürün son adresini bulur ve CALL komutlarındaki boş alanları doldurur. Sık kullanılan nesne dosyaları bir kütüphane (.LIB) dosyasında toplanabilir. Bağlayıcı kütüphaneden yalnız programın gerçekten çağırdığı prosedürleri alır. Bu yöntemin faydası, değişmeyen modüllerin yeniden derlenmemesi ve aynı kütüphanenin birçok programda kullanılmasıdır.

emu8086 ayrı derleme ve bağlama yapmaz. Ortak kod bu ortamda INCLUDE direktifiyle eklenir. INCLUDE, adı verilen dosyanın metnini kaynağa olduğu gibi katar ve program tek parça hâlinde derlenir. emu8086 ile gelen emu8086.inc dosyası bu yolla kullanılır. İki yöntemin farkı şudur: bağlayıcı nesne kodunu birleştirir, INCLUDE ise kaynak metni birleştirir.

8.2.2CALL ve RET

8.2.2.1CALL ve RET adım adım

CALL ve RET'in işi kısaca “çağır ve dön”dür. İşlemcinin içinde bu iş birkaç küçük adıma ayrılır. Adımları görmek için en küçük örnek kullanılır: ana program m1 prosedürünü çağırır, m1 BX'e 5 yazar ve döner, ana program AX'e 2 yazar ve biter.

ORG    100h
CALL   m1         ; 0100h: E8 04 00
MOV    AX, 2      ; 0103h: B8 02 00
RET               ; 0106h: C3 → DOS
m1 PROC
MOV    BX, 5      ; 0107h: BB 05 00
RET               ; 010Ah: C3 → çağırana
m1 ENDP
END

Açıklamalardaki adresler ve baytlar programın bellekteki görünüşüdür. ORG 100h ilk komutun 0100h adresine konacağını söyler. CALL 3 bayttır ve 0100h–0102h'yi kaplar. Sonraki komut 0103h'de başlar. Prosedürün ilk komutu, MOV BX, 5, 0107h'dedir; dolayısıyla m1 etiketinin değeri 0107h'dir.

Tanım 8.2.3

Yakın CALL iki iş yapar: (1) SP ← SP − 2 ve [SS:SP] ← IP; burada IP, CALL'dan sonraki komutun adresidir. (2) IP ← hedef adres. Yakın RET bunun tersini yapar: IP ← [SS:SP] ve SP ← SP + 2. İki komut da bayraklara dokunmaz.

Tanımdaki “IP, CALL'dan sonraki komutun adresidir” ifadesi işlemcinin çalışma düzeninden gelir. 8086'da veri yolu arayüz birimi (BIU) komut baytlarını bellekten getirir ve 6 baytlık komut kuyruğuna koyar. Yürütme birimi (EU) komutu kuyruktan alır ve çözer. Komut çözüldüğünde IP, o komutun baytlarının hemen arkasını gösterir. CALL'ın yürütülmeye başladığı anda IP = 0103h'dir. Dönüş adresi diye yığına yazılan değer budur. İşlemci ayrıca bir toplama yapmaz.

Tablo 8.2.2: CALL m1 ve RET'in iç adımları (SS = 0700h, başlangıçta SP = FFFEh).
AdımİşlemIPSPYığın
0başlangıç, sıradaki komut CALL m10100hFFFEh[FFFEh] = 0000h
1CALL çözüldü: IP sonraki komutu gösterir0103hFFFEhdeğişmedi
2SP ← SP − 2, [SP] ← IP0103hFFFCh[FFFCh] = 0103h
3IP ← 0103h + 0004h0107hFFFChdeğişmedi
4MOV BX, 5 yürütüldü010AhFFFChdeğişmedi
5RET: IP ← [SP], SP ← SP + 20103hFFFEh[FFFCh]'deki 0103h artık geçersiz
6MOV AX, 2 yürütüldü0106hFFFEh[FFFEh] = 0000h
kod (CS)0100hCALL m10103hMOV AX, 20106hRET0107hm1: MOV BX, 5010AhRET← IPYığın (SS)FFF8hFFFAhFFFCh0103hSPFFFEh0000hyukarı: düşük adresler kod (CS)0100hCALL m10103hMOV AX, 20106hRET0107hm1: MOV BX, 5010AhRET← IPYığın (SS)FFF8hFFFAhFFFChFFFEh0000hSPyukarı: düşük adresler
Şekil 8.2.4: Solda CALL dönüş adresini ittikten sonra: [FFFCh] = 0103h, SP = FFFCh. Sağda RET'ten sonra: IP = 0103h, SP = FFFEh; yığın çağrıdan önceki hâline döndü.

Tablo 8.2.2 adımları sırayla verir. Üç noktaya dikkat etmek gerekir. Birincisi, 8086'nın yığını aşağı doğru büyür. CALL SP'yi önce azaltır, sonra yazar. Dönüş adresi bu yüzden FFFEh'nin bir sözcük altına, FFFCh'ye yazılır. İkincisi, adım 3'teki hedef adres bir toplamla bulunur: 0103h + 0004h = 0107h. 0004h değeri CALL komutunun içindeki bağıl uzaklıktır; makine kodu incelenirken ayrıntılı açıklanır. Üçüncüsü, RET yığından okuduğu değeri silmez. 0103h FFFCh adresinde durmaya devam eder, ama SP artık onun üstünü gösterdiği için geçersiz sayılır. Sonraki ilk PUSH ya da CALL bu sözcüğün üstüne yazar.

Bu adımların donanımdaki karşılığı da izlenebilir. CALL'ın yığına yazması bir bellek yazma yol çevrimidir. Fiziksel adres SS × 16 + SP ile bulunur: 07000h + FFFCh = 16FFCh. Adres çift olduğu için 16 bitlik sözcük tek bir yol çevriminde yazılır. Bu çevrimde A0 = 0 ve BHE = 0'dır, iki veri baytı D0–D7 ve D8–D15 hatlarından birlikte geçer. SP tek bir değerde olsaydı aynı sözcük iki yol çevrimi gerektirirdi. Bu yüzden SP her zaman çift tutulur. RET aynı adresten bir okuma yol çevrimi yapar.

CALL ve RET'in bir donanım bedeli daha vardır. BIU, CALL'dan sonraki baytları, yani 0103h'deki MOV AX, 2'nin baytlarını kuyruğa çoktan getirmiş olabilir. IP 0107h'ye değişince bu baytlar işe yaramaz. İşlemci kuyruğu boşaltır ve BIU getirmeye yeni adresten başlar. Sıradaki komut, baytları bellekten gelene kadar bekler. RET'te de aynı şey olur. Atlama ve çağrı komutlarının saat turu sayısının yüksek olmasının bir nedeni budur.

8.2.2.2Basit prosedür: kod izleme

Aynı program şimdi baştan sona komut komut izlenir. Tablo 8.2.3'in her satırı, o komut yürütüldükten sonraki durumu gösterir. Programın emu8086'da çalıştırılmasıyla elde edilen değerlerle aynıdır.

Tablo 8.2.3: m1 programının izlemesi (her satır yürütüldükten sonraki durum).
SatırKomutIP (sonraki)SPAXBXYığın
—başlangıç0100hFFFEh0000h0000h[FFFEh] = 0000h
2CALL m10107hFFFCh0000h0000h[FFFCh] = 0103h
6MOV BX, 5010AhFFFCh0000h0005h[FFFCh] = 0103h
7RET0103hFFFEh0000h0005h[FFFEh] = 0000h
3MOV AX, 20106hFFFEh0002h0005h[FFFEh] = 0000h
4RET (DOS'a)0000h0000h0002h0005hboş

Satır sırası 2, 6, 7, 3, 4'tür. Akış kaynak koddaki sırayı izlemez: CALL prosedüre atlar, RET geri getirir. Son satırda ana programın RET'i yığındaki 0000h'yi IP'ye alır ve SP, FFFEh + 2 = 10000h'nin 16 bite sığan kısmı olan 0000h olur. İşlemci 0700:0000'daki INT 20h'yi yürütür ve program biter. Sonuç AX = 0002h, BX = 0005h'dir.

Tabloda yazmaçların değişmediği satırlara da bakmak öğreticidir. CALL ve RET, AX ve BX'e dokunmaz. Prosedürün yazdığı BX, dönüşten sonra da 0005h'dir. Prosedürle ana program aynı yazmaç kümesini paylaşır; prosedürün özel yazmaçları yoktur. Bu durum parametre geçirmeyi kolaylaştırır. Ama prosedürün çağıranın yazmaçlarını bozma tehlikesi de buradan doğar. Parametre geçirme ve yazmaçların korunması bu iki yüzü ele alır.

8.2.2.3Bellekteki makine kodu

Assembler kaynak kodu baytlara çevirdiğinde m1 programı bellekte yalnız 11 bayt tutar: E8 04 00 B8 02 00 C3 BB 05 00 C3. ORG, PROC, ENDP ve END'den hiçbir iz kalmaz.

0100hE8CALL0101h040102h000103hB8MOV AX,20104h020105h000106hC3RET0107hBBm1: MOV BX,50108h050109h00010AhC3RETrel16 = 0107h − 0103h = 0004h → 04 00 (little-endian)
Şekil 8.2.5: 0100h–010Ah arasındaki makine kodu baytları. CALL komutunun 16 bitlik uzaklık alanı (rel16) 0004h'dir ve little-endian düzende 04 00 olarak saklanır.

Şekil 8.2.5'deki ilk üç bayt CALL komutudur. E8, “yakın, doğrudan CALL” işlem kodudur. Arkasındaki iki bayt hedefin mutlak adresi değildir. Hedefin, CALL'dan sonraki komuta göre uzaklığıdır.

Tanım 8.2.4

Yakın doğrudan CALL'ın bağıl uzaklığı (rel16), hedef adres ile CALL'dan sonraki komutun adresi arasındaki farktır: rel16 = hedef − (CALL'ın adresi + 3). İşlemci çalışırken IP ← IP + rel16 hesabını yapar. Fark 16 bitlik ikiye tümleyen sayıdır; hedef geride ise rel16 negatiftir.

m1 için rel16 = 0107h − (0100h + 3) = 0107h − 0103h = 0004h'dir. 8086 çok baytlı değerleri little-endian düzende saklar: önce düşük bayt (04), sonra yüksek bayt (00). Bu yüzden komut E8 04 00 olur.

Uzaklık kullanmanın bir faydası vardır. Kod bellekte başka bir yere taşınırsa CALL ile hedef arasındaki fark değişmez ve komut doğru çalışmaya devam eder. Bu özelliğe konumdan bağımsız kod denir. Mutlak adres saklansaydı kodun her taşınmasında adresin düzeltilmesi gerekirdi.

Örnek 8.2.1

İki CALL'ın makine kodunu bulalım: (a) m2 örneğinde 0107h'deki ikinci CALL m2, m2 = 010Bh; (b) 0120h'deki CALL p, p = 0100h.

  1. Sonraki komut. (a) 0107h + 3 = 010Ah. (b) 0120h + 3 = 0123h. Bu adresler aynı zamanda yığına itilecek dönüş adresleridir.
  2. Fark. (a) 010Bh − 010Ah = 0001h. (b) 0100h − 0123h = −23h. 16 bitlik ikiye tümleyende −23h = 10000h − 23h = FFDDh.
  3. Baytlar. Düşük bayt önce yazılır. (a) E8 01 00. (b) E8 DD FF.
  4. Sağlama. (b) için işlemci IP ← 0123h + FFDDh hesabını yapar. Toplam 10100h'dir; 16 bite sığmayan elde atılır ve IP = 0100h olur.

Prosedürün adı da makine kodunda yer almaz. m1 yalnız assembler'ın sembol tablosunda vardır; makine kodunda ondan geriye yalnız 0107h adresinin uzaklık olarak saklanmış hâli kalır. emu8086'nın hata ayıklayıcısı adları gösterebilir, çünkü sembol tablosunu ayrıca saklar.

8.2.2.4CALL ve RET türleri

8086'nın birkaç CALL ve RET biçimi vardır. Biçimler iki soruya göre ayrılır. Hedef aynı kod kesiminde mi (yakın), başka bir kesimde mi (uzak)? Hedef komutun içinde mi (doğrudan), bir yazmaçta ya da bellekte mi (dolaylı)?

Tablo 8.2.4: CALL ve RET biçimleri.
KomutKodBaytYığına / yığındanAçıklama
CALL yakınE8 rel163IPAynı kesimde doğrudan çağrı (varsayılan).
CALL yazmaç/bellekFF /22–4IPDolaylı yakın çağrı: CALL BX, CALL [tablo+SI].
CALL FAR9A ofs seg5CS, sonra IPBaşka kesimdeki prosedür; CS:IP birlikte değişir.
RETC31IPYakın dönüş.
RETFCB1IP, sonra CSUzak dönüş (FAR prosedür).
RET nC2 n163IP; SP += nDönüşte yığındaki n baytlık parametreyi de atar.

Tablo 8.2.4'deki “FF /2” gösterimi, işlem kodunun FF olduğunu ve ardından gelen ModR/M baytının orta alanının 2 olduğunu söyler. ModR/M baytı işlenenin bir yazmaç mı, yoksa bir bellek adresi mi olduğunu belirtir. Bu yüzden dolaylı CALL'ın uzunluğu işlenene göre 2 ile 4 bayt arasında değişir. CALL BX 2 bayttır (FF D3). CALL tablo[SI] 16 bitlik bir yer değiştirme taşıdığı için 4 bayttır (FF 94 10 01). Uzak RET'in bir de RETF n biçimi vardır (CA n16).

Assembler, prosedür içindeki RET'i prosedürün türüne göre kodlar. ad PROC NEAR ya da türü yazılmamış bir prosedürde RET, C3 olur. ad PROC FAR içinde aynı RET kelimesi CB, yani RETF olarak kodlanır. Programcı türü bir kez PROC satırında yazar, dönüş komutlarını assembler uydurur. CALL da hedefin türüne bakar: hedef FAR bir prosedürse assembler 9A ile başlayan 5 baytlık uzak çağrıyı üretir.

CALL near: SP − 2FFFA····FFFCdönüş IPFFFE0000hSPCALL far: SP − 4FFFAdönüş IPFFFCdönüş CSFFFE0000hSP
Şekil 8.2.6: Yakın CALL yığına yalnız IP iter (SP − 2); uzak CALL önce CS'yi, sonra IP'yi iter (SP − 4).

8.2.2.5Uzak prosedür ve uzak dönüş: kod izleme

Uzak (FAR) prosedür, çağıranın kod kesiminden başka bir kod kesimindedir. Ona ulaşmak için hem CS hem IP değişmelidir. Geri dönebilmek için de ikisinin eski değeri saklanmalıdır. Uzak CALL bu yüzden yığına iki sözcük iter: önce CS'yi, sonra IP'yi. RETF bunları ters sırayla çeker: önce IP'yi, sonra CS'yi. Yığındaki sıra RETF'in çekme sırasıyla uyumludur. En son itilen IP yığının tepesindedir ve ilk çekilen odur.

CALL (yakın)FFFCIPSPFFFE····CALL FARFFFAIPSPFFFCCSFFFE····2 bayt4 bayt
Şekil 8.2.7: Yakın CALL'dan sonra yığında yalnız IP vardır (2 bayt). CALL FAR'dan sonra tepede IP, altında CS durur (4 bayt).

Hangi çağrının gerektiği programın yapısına bağlıdır. Bir .COM programı tek bir 64 KB'lık kesimde çalışır; CS, DS, ES ve SS aynı değeri taşır. Böyle bir programda yakın çağrı yeterlidir. Birden çok kod kesimi olan büyük .EXE programlarında ise başka kesimdeki prosedürlere uzak çağrı gerekir. Uzak çağrı daha uzundur (5 bayt) ve daha yavaştır, çünkü yığına iki sözcük yazar ve CS'yi değiştirir.

Uzak dönüşün yığınla ilişkisini görmek için tek kesimli bir programda küçük bir hile kullanılabilir. CALL FAR'ın iki itmesi iki ayrı komutla yapılır: önce PUSH CS, sonra yakın bir CALL. Yığın uzak çağrıdan sonraki düzene girer ve prosedür RETF ile dönebilir.

ORG 100h
MOV AX, 5
PUSH CS        ; uzak çağrının ilk yarısı
CALL uzak      ; IP itilir
ADD AX, 1
RET
uzak PROC
SHL AX, 1
RETF           ; önce IP, sonra CS
uzak ENDP

Kodun bayt düzeni şöyledir: MOV AX, 5 3 bayt (0100h–0102h), PUSH CS 1 bayt (0103h), CALL uzak 3 bayt (0104h–0106h), ADD AX, 1 3 bayt (0107h–0109h), RET 1 bayt (010Ah). Prosedür 010Bh'de başlar: SHL AX, 1 2 bayt, RETF 1 bayt (010Dh, kodu CB). uzak PROC satırında tür yazılmadığı için NEAR'dır; programcı içine bilerek RETF yazmıştır.

Tablo 8.2.5: PUSH CS + CALL ile RETF izlemesi (her satır yürütüldükten sonraki durum).
SatırKomutAXCS:IP (sonraki)SPYığın (tepeden)
2MOV AX, 50005h0700:0103FFFEh0000h
3PUSH CS0005h0700:0104FFFCh[FFFCh] = 0700h, 0000h
4CALL uzak0005h0700:010BFFFAh[FFFAh] = 0107h, 0700h, 0000h
8SHL AX, 1000Ah0700:010DFFFAh0107h, 0700h, 0000h
9RETF000Ah0700:0107FFFEh0000h
5ADD AX, 1000Bh0700:010AFFFEh0000h

İzlemede (Tablo 8.2.5) iki satır önemlidir. Satır 4'ten sonra yığının tepesinde dönüş IP'si (0107h), onun altında CS (0700h) vardır. Bu, gerçek bir CALL FAR'ın bırakacağı düzenin aynısıdır. Satır 9'da RETF iki sözcük çeker: IP = 0107h, CS = 0700h. SP 4 artarak FFFEh olur. CS zaten 0700h olduğu için değişiklik görünmez, ama işlemci CS'yi yığından yeniden yüklemiştir. Program ADD AX, 1'den sürer ve AX = 000Bh ile biter. Gerçek bir uzak çağrı (9A, 5 bayt) aynı iki itmeyi tek komutla yapar.

AX0005hIP010BhCS0700hSPFFFAhyığınFFFA0107hSPFFFC0700hFFFE0000h AX000AhIP0107hCS0700hSPFFFEhyığınFFFA····FFFC····FFFE0000hSP
Şekil 8.2.8: Solda yakın CALL'dan sonra: tepede IP (0107h), altında PUSH CS'nin ittiği 0700h. Sağda RETF'ten sonra: iki sözcük çekildi, SP = FFFEh.
Sık yapılan hata

Prosedürün türü ile dönüş komutunu uyuşturmamak. Yakın CALL ile çağrılan prosedür RETF ile biterse RETF, yığında olmayan bir CS'yi çeker. CALL FAR ile çağrılan prosedür yakın RET ile biterse CS yığında kalır. İki durumda da yığın 2 bayt kayar ve dönüş yanlış adrese yapılır. Bu hata, sık yapılan hatalar arasında adım adım izlenir. Türü PROC satırında doğru yazmak ve dönüş komutu olarak hep RET kullanmak bu hatayı önler; assembler RET'i türe göre kodlar.

8.2.2.6Dolaylı CALL: kod izleme

Şimdiye kadarki CALL'larda hedef, komutun içine yazılmıştı. Hangi prosedürün çağrılacağı program yazılırken belliydi. Bazı durumlarda bu karar çalışma anında verilir. Örneğin bir menüde kullanıcının seçtiği işlem çağrılır. Bu iş için hedef adres bir yazmaçtan ya da bellekten okunur. Böyle çağrıya dolaylı çağrı denir.

MOV AX, 6
MOV SI, 2               ; 2. işlem (× 2)
CALL tablo[SI]          ; bellekle dolaylı
MOV BX, OFFSET bir_ekle
CALL BX                 ; yazmaçla dolaylı
RET
tablo DW bir_ekle, iki_kat
bir_ekle: INC AX
RET
iki_kat: SHL AX, 1
RET

tablo, iki prosedürün adresini tutan bir sözcük dizisidir. Assembler DW bir_ekle, iki_kat satırına iki etiketin adresini yazar: 0114h ve 0116h. Tablo 0110h'de başlar; ilk eleman 0110h'de, ikinci eleman 0112h'dedir. Her eleman 2 bayt olduğu için i. elemanın uzaklığı 2 × i'dir. SI = 2, tablonun ikinci elemanını, yani iki_kat'ın adresini seçer.

CALL tablo[SI] etkin adresi tablo + SI = 0110h + 2 = 0112h olarak hesaplar ve oradaki sözcüğü okur: 0116h. Dönüş adresini (010Ah) yığına iter ve IP'ye 0116h'yi yükler. Burada iki bellek erişimi vardır: hedefi okumak için DS kesiminden bir okuma, dönüş adresini yazmak için SS kesimine bir yazma. CALL BX ise hedefi doğrudan BX'ten alır ve bellekten hedef okumaz.

Tablo 8.2.6: Dolaylı CALL izlemesi (her satır yürütüldükten sonraki durum).
SatırKomutAXBXIP (sonraki)SP[FFFCh]
1MOV AX, 60006h0000h0103hFFFEh—
2MOV SI, 20006h0000h0106hFFFEh—
3CALL tablo[SI]0006h0000h0116hFFFCh010Ah
10iki_kat: SHL AX, 1000Ch0000h0118hFFFCh010Ah
11RET000Ch0000h010AhFFFEh(geçersiz)
4MOV BX, OFFSET bir_ekle000Ch0114h010DhFFFEh—
5CALL BX000Ch0114h0114hFFFCh010Fh
8bir_ekle: INC AX000Dh0114h0115hFFFCh010Fh
9RET000Dh0114h010FhFFFEh(geçersiz)
AX0006hSI0002hBX0000hIP0116h0114htablo+00116htablo+2SIbir_ekle = 0114h · iki_kat = 0116hyığınFFFC010AhSPFFFE0000h
Şekil 8.2.9: CALL tablo[SI] anında: hedef [tablo+2] = 0116h'den okunur, dönüş adresi 010Ah yığına itilir.

Sonuçta AX = 13 (000Dh) olur: önce 6 × 2 = 12, sonra 12 + 1 = 13. İzlemede (Tablo 8.2.6) iki çağrının dönüş adresleri farklıdır: birincisi 010Ah, ikincisi 010Fh. İkisi de aynı yığın sözcüğüne, [FFFCh]'ye yazılır, çünkü iki çağrı arasında SP FFFEh'ye dönmüştür. Hangi prosedürün çağrılacağı çalışma anında SI ya da BX ile seçilmiştir. SI = 0 olsaydı ilk çağrı bir_ekle'ye giderdi ve aynı kod başka bir sonuç üretirdi.

Kodun bir ayrıntısı daha vardır. bir_ekle ve iki_kat PROC/ENDP ile değil, sıradan etiketlerle tanımlanmıştır. İşlemci için fark yoktur: CALL bir adrese atlar, RET geri döner. PROC/ENDP, programcıya ve assembler'a düzen ve tür bilgisi sağlar.

8.2.2.7Atlama tablosuyla menü

Dolaylı çağrının en yaygın kullanımı atlama tablosudur. Bir menüde her seçenek bir prosedürdür ve adresleri bir tabloda sırayla durur. Kullanıcının bastığı tuş tablo indisine çevrilir ve ilgili prosedür tek bir dolaylı CALL ile çağrılır.

MOV AH, 1
INT 21h           ; AL = '1'..'3'
SUB AL, '1'       ; 0..2
CMP AL, 2
JA gecersiz       ; aralık denetimi
MOV BL, AL
MOV BH, 0
SHL BX, 1         ; her adres 2 bayt
CALL islem[BX]
gecersiz: RET
islem DW topla, cikar, carp

Kod satır satır şöyle çalışır. INT 21h'nin 01h işlevi klavyeden bir karakter okur, ekrana yansıtır ve ASCII kodunu AL'ye koyar. SUB AL, '1' karakteri sayıya çevirir: '1' (31h) → 0, '2' → 1, '3' → 2. CMP AL, 2 ve JA gecersiz aralık denetimidir; değer 2'den büyükse çağrı yapılmaz. Sonraki üç komut 8 bitlik indisi 16 bitlik BX'e taşır ve 2 ile çarpar. Tablodaki adresler 2 bayt olduğu için indis i'nin bayt uzaklığı 2i'dir. Sola bir bit kaydırmak 2 ile çarpmanın en hızlı yoludur. Son olarak CALL islem[BX] seçilen prosedürü çağırır.

Aralık denetiminin bir inceliği vardır. JA işaretsiz karşılaştırma yapar. '0' tuşuna basılırsa SUB AL, '1' sonucu 30h − 31h = FFh olur. İşaretli bakışla bu −1'dir ve 2'den küçüktür. İşaretsiz bakışla ise 255'tir ve 2'den büyüktür, bu yüzden JA atlar ve '0' da reddedilir. Tek bir işaretsiz denetim hem alt hem üst sınırı kapatır. '9' tuşu 8 verir ve o da reddedilir.

Tablo 8.2.7: Tuşa göre BX ve çağrılan prosedür.
TuşAL (SUB'dan sonra)BXÇağrılan
'1'00h0000htopla
'2'01h0002hcikar
'3'02h0004hcarp
'0'FFh—hiçbiri (RET)
'9'08h—hiçbiri (RET)

Tablo 8.2.7'deki değerler, üç prosedür kısa gövdelerle eklenip emu8086'da beş tuşla çalıştırıldığında elde edilenlerle aynıdır. Aralık denetimi olmasaydı '9' tuşu BX = 16 verir ve CALL, tablonun 16 bayt ötesindeki iki baytı adres sanıp oraya giderdi. Orada program kodu ya da veri bulunur; işlemci anlamsız bir adrese atlar. Atlama tablosu kullanan her programda indis, tablo boyutuna göre denetlenmelidir.

Atlama tablosunun karşılığı, ardışık CMP ve JE komutlarıyla kurulan bir karar zinciridir. Seçenek sayısı arttıkça zincir uzar ve son seçeneğe ulaşmak için bütün karşılaştırmalar yürütülür. Tablo ise seçenek sayısından bağımsız olarak sabit sayıda komutla hedefe ulaşır. Yüksek düzeyli dillerdeki switch yapısını derleyiciler çoğu zaman bu yolla çevirir.

8.2.2.8CALL ve RET'in süresi

Prosedür çağırmanın bir zaman maliyeti vardır. 8086'nın komut süreleri saat turu (clock cycle) cinsinden Intel kılavuzunda verilir. Tablo 8.2.8 prosedürlerle ilgili komutların sürelerini toplar.

Tablo 8.2.8: Çağrıyla ilgili komutların 8086'daki süreleri (saat turu).
KomutSaat turuKomutSaat turu
CALL yakın19RET8
CALL yazmaç16RETF18
CALL FAR28RET n12
PUSH yazmaç11POP yazmaç8

Tablodaki değerler yazmaç ve doğrudan biçimler içindir. Bellek işlenenli biçimlere etkin adresin hesap süresi eklenir. Örneğin CALL tablo[SI] için kılavuz 21 + EA turu verir; EA, yer değiştirmeli indisli adreslemede 9 turdur. CALL'ın RET'ten pahalı olmasının nedeni, hem yığına yazması hem de hedef adresi hesaplamasıdır. İki komutta da komut kuyruğu boşaltılır ve yeniden doldurulur.

Örnek 8.2.2

İki yazmacı saklayan bir prosedürün, asıl işi dışındaki yükü ne kadardır? 5 MHz'lik bir 8086'da prosedür 1000 kez çağrılırsa bu yük ne kadar sürer?

  1. Komutlar. Her çağrıda bir CALL (19), bir RET (8), iki PUSH (2 × 11) ve iki POP (2 × 8) yürütülür.
  2. Toplam. 19 + 8 + 2 × (11 + 8) = 27 + 38 = 65 tur.
  3. Süre. 5 MHz'de bir tur 1 / 5 MHz = 200 ns'dir. 65 × 200 ns = 13 000 ns = 13 µs.
  4. 1000 çağrı. 1000 × 13 µs = 13 ms.

13 ms küçük görünür, ama prosedürün asıl işi birkaç komutluk bir işse yük işin kendisini geçebilir. Bu noktada makro bir seçenektir. Makro, assembler'ın her kullanım yerine gövdeyi kopyaladığı bir metin kalıbıdır. CALL ve RET olmadığı için çağrı yükü taşımaz, ama her kullanımda kod yeniden yer kaplar. Pratik kural şudur: kısa ve çok sık çalışan işler (tek karakter yazmak gibi) makroya, uzun ya da seyrek çağrılan işler prosedüre uygundur.

8.2.3Parametre geçirme

8.2.3.1Dört yol

Bir prosedür çoğu zaman bir girdiyle çalışır ve bir sonuç üretir. Ekrana yazan prosedür yazılacak metni, toplayan prosedür toplanacak sayıları bilmelidir. Çağıranın prosedüre verdiği bu değerlere parametre, değerlerin prosedüre ulaştırılmasına da parametre geçirme (parameter passing) denir. Yüksek düzeyli dillerde derleyici bu işi kendisi düzenler. Assembly dilinde yolu programcı seçer ve hem çağıranda hem prosedürde aynı kurala uyar.

çağıranprosedürAL, BL (yazmaç)number1 (bellek)SI → dizi (adres)PUSH → [BP+4] (yığın)
Şekil 8.2.10: Parametre prosedüre dört yoldan geçer: yazmaç, bellek değişkeni, adres (işaretçi) ve yığın.

Şekil 8.2.10'deki dört yolun her birinin bir yeri vardır.

  1. Yazmaçlarla. En kolay ve en hızlı yoldur; parametre zaten işlemcinin içindedir ve bellek erişimi gerekmez. Örneğin AL ve BL iki parametreyi, AX sonucu taşır. Yazmaç sayısı sınırlı olduğu için birkaç parametreden fazlasına yetmez.
  2. Bellek değişkenleriyle. Prosedür number1, result gibi ortak değişkenleri okur ve yazar. Kolaydır, ama prosedür o değişkenlere bağımlı kalır. Başka değişkenlerle çalışması için kodunun değişmesi gerekir.
  3. Adres (işaretçi) ile. Verinin kendisi değil, başlangıç adresi SI, DI ya da BX'te gönderilir. Uzun bir dizi ya da dizgi kopyalanmadan işlenir. Prosedür hangi diziye adresi verilirse onunla çalışır.
  4. Yığınla. Parametreler CALL'dan önce PUSH ile yığına itilir. Prosedür onları BP yazmacı üzerinden okur. Dönüşte RET n ya da çağıranda ADD SP, n ile yığından atılır. C ve Pascal gibi yüksek düzeyli dillerin derleyicileri bu yolu kullanır.

Hangi yol seçilirse seçilsin, prosedürün başına bir açıklama bloğu yazılır. Blokta prosedürün girdileri, çıktıları ve bozduğu yazmaçlar belirtilir. Assembly dilinde parametrelerin adı, türü ya da sayısı için derleyici denetimi yoktur. Bu açıklama, prosedür ile çağıran arasındaki sözleşmenin tek yazılı kaydıdır.

8.2.3.2Değerle mi, adresle mi?

Parametre geçirmenin yolundan bağımsız ikinci bir soru, prosedüre ne verildiğidir: verinin kopyası mı, yoksa verinin yeri mi?

Tanım 8.2.5

Değerle geçirmede (pass by value) prosedüre verinin bir kopyası verilir. Prosedür kopyayı değiştirse de bellekteki asıl değişken değişmez. Adresle geçirmede (pass by reference) prosedüre verinin adresi verilir. Prosedür bu adres üzerinden asıl değişkeni okur ve değiştirebilir.

MOV  AX, a         ; değer: AX'te a'nın kopyası
CALL iki_kat       ; AX iki katına çıkar, a aynı kalır
LEA  SI, a         ; adres: SI = OFFSET a
CALL iki_kat_m     ; [SI] iki katına çıkar, a değişir

İlk iki satırda a'nın içeriği AX'e kopyalanır. iki_kat AX'i iki katına çıkarır; sonuç AX'tedir ve a bellekte eski değeriyle durur. Son iki satırda a'nın adresi SI'ya yüklenir. iki_kat_m bellekteki [SI] sözcüğünü iki katına çıkarır; değişen, a'nın kendisidir.

Değerle geçirme tek sayı gibi küçük veriler için hızlı ve güvenlidir. Prosedür çağıranın değişkenini yanlışlıkla bozamaz. Adresle geçirme üç durumda gerekir: dizi ve dizgi gibi bir yazmaca sığmayan veriler, prosedürün değiştirmesi gereken değişkenler ve birden çok sonucun belleğe yazılması gereken durumlar.

LEA SI, a ile MOV SI, OFFSET a aynı sonucu verir: SI, a'nın bağıl konumunu taşır. emu8086 doğrudan adresli bu LEA'yı MOV ile aynı 3 baytlık koda (BE + 16 bitlik adres) çevirir. Bu notun adres hesaplarında LEA 3 bayt sayılmıştır. Gerçek 8086 assembler'ları bu biçimi 4 baytlık 8D 36 kodlamasıyla da üretebilir. MOV SI, a ise bambaşka bir komuttur: adresi değil, a'nın içindeki sözcüğü SI'ya yükler. Bu karışıklık adresle geçirmenin en sık hatasıdır.

8.2.3.3Adresle takas: kod izleme

Adresle geçirmenin tipik örneği iki değişkenin değerini değiştiren (takas eden) prosedürdür. Prosedür iki değeri değil, iki yeri alır. Değer alsaydı yalnız kendi kopyalarını değiştirebilirdi.

ORG 100h
LEA SI, a
LEA DI, b
CALL takas
RET
takas PROC        ; Girdi: SI, DI (adres)
PUSH AX
MOV AX, [SI]
XCHG AX, [DI]
MOV [SI], AX
POP AX
RET
takas ENDP
a DW 1234h
b DW 00FFh

Önce programın bellekteki yerleşimi çıkarılır. İki LEA 3'er bayttır (0100h ve 0103h). CALL takas 0106h'dedir ve 3 bayttır, dolayısıyla dönüş adresi 0109h'dir. Ana programın RET'i 0109h'yi, prosedür 010Ah'yi kaplar. Prosedürün komutları sırasıyla 1, 2, 2, 2, 1 ve 1 bayttır ve 0112h'deki RET ile biter. Veri hemen arkasından gelir: a = 0113h, b = 0115h.

Prosedür AX'i ara değişken olarak kullanır, ama AX onun girdisi ya da çıktısı değildir. Bu yüzden girişte PUSH AX ile saklanır ve çıkışta POP AX ile geri yüklenir. XCHG AX, [DI] takasın kalbidir: AX ile bellekteki sözcüğü tek komutta yer değiştirir. Üç komut (MOV, XCHG, MOV) takası tamamlar.

Tablo 8.2.9: Adresle takas izlemesi (her satır yürütüldükten sonraki durum).
SatırKomutSIDIAXSPa [0113h]b [0115h]
2–3LEA SI, a · LEA DI, b0113h0115h0000hFFFEh1234h00FFh
4CALL takas0113h0115h0000hFFFCh1234h00FFh
7PUSH AX0113h0115h0000hFFFAh1234h00FFh
8MOV AX, [SI]0113h0115h1234hFFFAh1234h00FFh
9XCHG AX, [DI]0113h0115h00FFhFFFAh1234h1234h
10MOV [SI], AX0113h0115h00FFhFFFAh00FFh1234h
11POP AX0113h0115h0000hFFFCh00FFh1234h
12RET0113h0115h0000hFFFEh00FFh1234h
SI0113hDI0115hAX00FFhSPFFFAhbellek1234ha (0113)1234hb (0115) SI0113hDI0115hAX0000hSPFFFEhbellek00FFha (0113)1234hb (0115)
Şekil 8.2.11: Solda XCHG AX, [DI]'den sonra: b = 1234h, AX = 00FFh. Sağda POP ve RET'ten sonra: AX eski değerine döndü, a ile b yer değiştirmiş olarak kaldı.

İzlemede (Tablo 8.2.9) dört noktaya bakılır. SI ve DI baştan sona değişmez; prosedüre gönderilen adreslerdir. AX önce a'nın değerini (1234h), XCHG'den sonra b'nin eski değerini (00FFh) taşır. Bellekte önce b, sonra a değişir. Son olarak POP AX, AX'i çağrıdan önceki değerine (0000h) döndürür. Çağıran için tek görünür etki, a ile b'nin yer değiştirmesidir.

XCHG'nin bellek işlenenli biçimi 8086'da bir okuma ve bir yazma yol çevrimi yapar. Aynı işi iki MOV ile yapmak bir yazmaç daha gerektirirdi. Okuma ile yazma arasında başka bir yol yöneticisinin araya girmemesi gereken durumlarda komutun önüne LOCK öneki konur. Önek, komut süresince 8086'nın LOCK pinini etkin tutar. Çok işlemcili sistemlerde semafor gibi paylaşılan değişkenler bu yüzden LOCK XCHG ile değiştirilir.

8.2.3.4Yazmaçlarla parametre: kod izleme

Yazmaçla parametre geçirmenin örneği olarak iki sayıyı çarpan m2 prosedürü kullanılır. AL ve BL iki parametreyi temsil eder; m2 bunları çarpar ve sonucu AX'e koyar.

ALBLm2: MUL BLAXgirdi yazmaçları → prosedür → çıktı yazmacı
Şekil 8.2.12: AL ve BL girdidir; m2 prosedürü MUL BL yapar, sonuç AX'tedir.

Prosedürün tek komutu MUL'dur. MUL işaretsiz çarpma yapar ve işlenen boyutuna göre iki biçimde çalışır (Tablo 8.2.10). Kaynak bir yazmaç ya da bellek işleneni olabilir, sabit olamaz: MUL 2 geçersizdir. Çarpımın üst yarısı (AH ya da DX) sıfır değilse CF = OF = 1 olur; bu bayraklar sonucun alt yarıya sığıp sığmadığını bildirir. SF, ZF, AF ve PF MUL'dan sonra tanımsızdır.

Tablo 8.2.10: MUL komutunun iki biçimi.
KaynakİşlemSonuç
8 bitAL × kaynakAX
16 bitAX × kaynakDX:AX
ORG 100h
MOV AL, 1      ; 1. parametre
MOV BL, 2      ; 2. parametre
CALL m2        ; 0104h: E8 04 00
CALL m2        ; 0107h: E8 01 00
RET            ; 010Ah: DOS'a dön
m2 PROC        ; 010Bh
MUL BL         ; AX = AL * BL
RET            ; çağırana dön
m2 ENDP
END

MOV AL, 1 ve MOV BL, 2 8 bitlik sabit yüklemeleridir ve 2'şer bayttır (B0 01, B3 02). İki CALL 0104h ve 0107h'dedir. Birincinin dönüş adresi 0107h, ikincininki 010Ah'dir. m2 010Bh'de başlar. İki CALL'ın makine kodları farklıdır (E8 04 00 ve E8 01 00), çünkü uzaklık her CALL'ın kendi konumuna göre hesaplanır. Hedef ikisinde de 010Bh'dir.

Tablo 8.2.11: m2 programının izlemesi (her satır yürütüldükten sonraki durum).
SatırKomutIP (sonraki)SPAXBLCF[FFFCh]
2MOV AL, 10102hFFFEh0001h00h0—
3MOV BL, 20104hFFFEh0001h02h0—
4CALL m2 (1.)010BhFFFCh0001h02h00107h
8MUL BL010DhFFFCh0002h02h00107h
9RET0107hFFFEh0002h02h0(geçersiz)
5CALL m2 (2.)010BhFFFCh0002h02h0010Ah
8MUL BL010DhFFFCh0004h02h0010Ah
9RET010AhFFFEh0004h02h0(geçersiz)
6RET (DOS'a)0000h0000h0004h02h0—

İzlemenin (Tablo 8.2.11) en öğretici yanı, aynı prosedürün iki farklı dönüş noktasına dönmesidir. Prosedürün RET'i iki çağrıda da aynı komuttur (010Dh). Birinci çağrıda yığında 0107h, ikincide 010Ah bulur ve oraya döner.

İkinci önemli nokta MUL'un AL'yi değiştirmesidir. Birinci çağrıda AX = 1 × 2 = 0002h olur. AL artık 02h'dir. İkinci çağrı AL'yi yeniden yüklemeden yapıldığı için birinci çağrının sonucu ikinci çağrının birinci parametresi olur: AX = 2 × 2 = 0004h. Program bu davranışı bilerek kullanır, ama aynı durum farkında olunmadan bir hataya da yol açabilir. Prosedür açıklamasında “Çıktı: AX” yazması, AL'nin de değiştiğini gösterir. Her iki çarpımda da AH = 0 olduğu için CF = OF = 0'dır.

8.2.3.5Sonucu bayrakla döndürmek: kod izleme

Bazı prosedürlerin sonucu bir sayı değil, bir evet/hayır cevabıdır: dosya açılabildi mi, karakter rakam mı, değer bulundu mu? Böyle bir cevap için bir yazmaç harcamak yerine bir bayrak kullanılır. Taşıma bayrağı (CF) bu iş için en uygun bayraktır. STC, CLC ve CMC komutlarıyla doğrudan kurulabilir, JC ve JNC ile sınanabilir. DOS hizmetleri de hatayı CF = 1 ile bildirir.

MOV AL, '7'
CALL rakam       ; CF = 0 → AL = değer
MOV BL, AL
MOV AL, 'x'
CALL rakam       ; CF = 1 → rakam değil
RET
rakam PROC       ; Girdi AL, Çıktı CF
SUB AL, '0'
CMP AL, 10       ; 0..9 → ödünç, CF = 1
CMC              ; rakamsa CF = 0
RET
rakam ENDP

rakam prosedürü AL'deki ASCII karakteri sayıya çevirir ve karakterin bir rakam olup olmadığını CF ile bildirir. SUB AL, '0' karakterden 30h çıkarır. Karakter '0'–'9' aralığındaysa sonuç 0–9'dur. CMP AL, 10 işaretsiz karşılaştırma yapar. AL < 10 ise çıkarma ödünç ister ve CF = 1 olur. AL ≥ 10 ise CF = 0 olur. Bu, istenen anlamın tersidir. CMC (complement carry) CF'yi tersine çevirir: rakam için CF = 0, rakam olmayan için CF = 1. '0'dan küçük bir karakterde SUB sonucu D0h–FFh aralığına sarar ve 10'dan büyük sayılır; o da reddedilir.

Tablo 8.2.12: Bayrakla dönüş izlemesi (her satır yürütüldükten sonraki durum).
SatırKomutALBLCFSPAçıklama
1MOV AL, '7'37h00h0FFFEh'7' = 37h
2, 8CALL rakam · SUB AL, '0'07h00h0FFFChdönüş 0105h; 37h − 30h = 7
9CMP AL, 1007h00h1FFFCh7 − 10 ödünç ister
10CMC07h00h0FFFCh“rakamdır”
11, 3RET · MOV BL, AL07h07h0FFFEhdeğer saklandı
4, 5, 8MOV AL, 'x' · CALL · SUB48h07h0FFFCh78h − 30h = 48h (72); dönüş 010Ch
9CMP AL, 1048h07h0FFFCh72 ≥ 10, ödünç yok
10CMC48h07h1FFFCh“rakam değil”
11RET48h07h1FFFEhsonuç CF'de
AL07hBL07hSPFFFEhCF0AL = 7sonuç: rakam AL48hBL07hSPFFFEhCF1AL = 72sonuç: rakam değil
Şekil 8.2.13: Solda birinci dönüşten sonra: CF = 0, BL = 7. Sağda ikinci dönüşten sonra: CF = 1, AL = 48h kullanılmamalıdır.

İzlemeden (Tablo 8.2.12) iki sonuç çıkar. Birincisi, RET bayraklara dokunmaz. Prosedürün içinde kurulan CF, dönüşten sonra çağıranda da geçerlidir. Çağıran JC hata ya da JNC tamam ile sonucu sınayabilir. İkincisi, CF = 1 durumunda AL bozulmuştur (48h). Prosedürün sözleşmesi şudur: AL'deki değer yalnız CF = 0 ise anlamlıdır. Çağıran önce bayrağa bakmalı, sonra yazmacı kullanmalıdır.

Sık yapılan hata

Sonucu CF ile döndüren bir prosedürde STC, CLC ya da CMC'den sonra bayrakları değiştiren bir komut yürütmek. Örneğin CMC'den sonra çıkışta ADD SP, 2 ya da CMP yapılırsa CF yeniden hesaplanır ve sonuç kaybolur. POP ve RET bayraklara dokunmaz; bayrağı kuran komut, RET'ten önceki son bayrak değiştiren komut olmalıdır.

8.2.3.6Birden çok sonuç döndürmek

Bir prosedür birden fazla sonuç üretebilir. Bölme işlemi hem bölüm hem kalan verir; arama hem “bulundu mu” cevabını hem de konumu verir. Bunun için üç yol vardır.

  • Yazmaç çifti. 32 bitlik sonuç DX:AX'te döner (16 bitlik MUL gibi). Bölüm ile kalan AL ve AH'de döner (8 bitlik DIV gibi).
  • Bayrak ve yazmaç. CF başarıyı, yazmaç değeri bildirir. Çağıran önce JC ile bayrağa bakar.
  • Bellek. Sonuçlar adresi verilen bir alana yazılır. Sonuç sayısı yazmaç sayısıyla sınırlı değildir.

Aşağıdaki bolkalan prosedürü ilk iki yolu birlikte kullanır. AX'i BL'ye böler, bölümü AL'de, kalanı AH'de döndürür. Bölen sıfırsa bölme yapmaz ve CF = 1 ile hata bildirir.

bolkalan PROC      ; Girdi: AX, BL (bölen)
CMP BL, 0          ; Çıktı: AL = bölüm
JE hata            ;        AH = kalan
DIV BL             ; CF = 1: bölen 0
CLC
RET
hata: STC
RET
bolkalan ENDP

8 bitlik DIV BL, AX'teki 16 bitlik sayıyı BL'ye böler. Bölümü AL'ye, kalanı AH'ye yazar. DIV'den sonra bütün bayraklar tanımsızdır. Bu yüzden başarı durumunda CF açıkça CLC ile sıfırlanır. Prosedürde iki RET vardır. Yığına hiçbir şey itilmediği için bu güvenlidir: iki RET de tepede dönüş adresini bulur.

Tablo 8.2.13: bolkalan prosedürünün üç girdiyle sonuçları.
GirdiSonuçAçıklama
AX = 100, BL = 7AL = 0Eh (14), AH = 02h, CF = 0100 = 7 × 14 + 2; AX = 020Eh
AX = 100, BL = 0CF = 1, AX = 0064h (değişmez)DIV hiç yürütülmez
AX = 1000, BL = 2bölme hatası (INT 0)bölüm 500, AL'ye sığmaz

Tablo 8.2.13'nin son satırı prosedürün bir açığını gösterir. Bölen sıfır olmadığı hâlde 1000 / 2 = 500 sonucu 8 bitlik AL'ye sığmaz (en büyük değer 255). 8086 bu durumda da tip 0 kesmesini (bölme hatası) üretir. Prosedür yalnız bölenin sıfır olmasını denetlediği için bu durumu yakalayamaz. Tam bir denetim için AH < BL koşulu da sınanmalıdır; bölüm ancak bu koşulda AL'ye sığar.

İki RET'li yapı yalnız yığına bir şey itilmemişse güvenlidir. PUSH yapan bir prosedürde bütün yollar, POP'ların bulunduğu tek bir çıkış etiketinde birleştirilmelidir. Bu kural, tek çıkış noktalı bir arama prosedürünün izlemesiyle gösterilir.

8.2.4Yazmaçların korunması

8.2.4.1Sorun ve belgeleme

8086'nın sekiz genel amaçlı yazmacı vardır ve prosedür ile çağıran bunları paylaşır. Prosedür, çağıranın kullandığı bir yazmacı habersizce değiştirirse çağıran kod bozulur. En tipik örnek döngü sayacıdır. Çağıran CX ile bir döngü kurar ve döngünün içinden bir prosedür çağırır. Prosedür de kendi döngüsü için CX kullanırsa, dönüşte CX'te çağıranın sayacı değil prosedürün bıraktığı değer bulunur.

Sorunun ilk çaresi belgelemedir. Her prosedürün başına girdisi, çıktısı ve bozduğu yazmaçlar yazılır:

; m2: AL * BL hesaplar
; Girdi : AL, BL (işaretsiz)
; Çıktı : AX = AL * BL
; Bozar : CF, OF

Bu açıklamayı okuyan biri, m2'yi çağırdıktan sonra AX'teki eski değerin kaybolacağını ve CF ile OF'nin değişeceğini bilir. Başka hiçbir yazmacın değişmediği de bu açıklamanın bir vaadidir. Vaat tutulmazsa, yani prosedür açıklamada yazmayan bir yazmacı değiştirirse, hata çoğu zaman prosedürde değil, onu çağıran kodda ortaya çıkar. Hatanın yeri ile nedeninin farklı olması onu bulmayı zorlaştırır.

Tanım 8.2.6

Yazmaçların korunması (register preservation), bir prosedürün kullandığı yazmaçları girişte yığına itmesi ve çıkışta ters sırayla geri almasıdır. Böylece bu yazmaçlar çağırana hiç değişmemiş görünür. Sonuç döndüren yazmaçlar korunmaz.

yaz PROC
    PUSH AX    ; kullanacaklarını
    PUSH SI    ; sakla
    ; ... AX ve SI kullanılır
    POP SI     ; TERS sırayla
    POP AX     ; geri yükle
    RET
yaz ENDP

Ters sıra yığının son giren ilk çıkar (LIFO) yapısından gelir. AX önce, SI sonra itildiği için yığının tepesinde SI'nın değeri durur. İlk POP o değeri alır ve SI'ya yazmalıdır. Sıra karıştırılırsa, yani önce POP AX yazılırsa, AX'e SI'nın, SI'ya AX'in eski değeri yüklenir. Yığın yine dengelidir ve RET doğru döner, ama iki yazmaç birbirinin değerini almıştır.

Tanımın son cümlesi de önemlidir. Sonucunu AX'te döndüren bir prosedür AX'i girişte itip çıkışta geri alırsa, hesapladığı sonucu kendi eliyle siler. Yığın dengeli olduğu için program çökmez; yalnız sonuç yanlış çıkar. Kural şudur: prosedür kullandığı yazmaçları korur, sonuç yazmaçlarını korumaz.

8.2.4.2PUSH CX ile sayaç koruma: kod izleme

Aşağıdaki program ekrana iki satır yazmak ister. Ana döngü CX = 2 ile döner ve her turda yildiz prosedürünü çağırır. Prosedür de üç # yazmak için CX'i iç döngü sayacı olarak kullanır. Prosedürün ilk ve son komutları çağıranın sayacını korur.

MOV CX, 2          ; ana döngü: 2 satır
satir: CALL yildiz
LOOP satir
RET
yildiz PROC
PUSH CX            ; çağıranın sayacı
MOV CX, 3          ; iç sayaç
MOV AX, 0E23h      ; AH = 0Eh, AL = '#'
ic: INT 10h
LOOP ic
POP CX             ; sayaç geri
RET
yildiz ENDP

MOV AX, 0E23h tek komutla iki yazmaç yarısını doldurur. AH = 0Eh, BIOS ekran hizmetinin (INT 10h) “teletype” işlevidir: AL'deki karakteri imlecin bulunduğu yere yazar ve imleci ilerletir. AL = 23h, '#' karakterinin ASCII kodudur. LOOP komutu önce CX'i 1 azaltır, sonra CX ≠ 0 ise etikete atlar. İç döngü bu yüzden tam üç tur döner.

Tablo 8.2.14: PUSH CX ile sayaç koruma izlemesi (her satır yürütüldükten sonraki durum).
SatırKomutCXSPYığın (tepeden)Ekran
1MOV CX, 20002hFFFEh0000h
2CALL yildiz0002hFFFCh0106h, 0000h
6PUSH CX0002hFFFAh0002h, 0106h, 0000h
7–8MOV CX, 3 · MOV AX, 0E23h0003hFFFAh0002h, 0106h, 0000h
9–10iç döngü, 3 tur0000hFFFAh0002h, 0106h, 0000h###
11POP CX0002hFFFCh0106h, 0000h###
12RET0002hFFFEh0000h###
3LOOP satir0001hFFFEh0000h###
2–12ikinci çağrı0001hFFFEh0000h######
3LOOP satir0000hFFFEh0000h######
CX0000hAX0E23hSPFFFAhyığınFFFA0002hSPFFFC0106hFFFE0000h###▌ CX0002hAX0E23hSPFFFChyığınFFFA····FFFC0106hSPFFFE0000h###▌
Şekil 8.2.14: Solda iç döngü bittiğinde: CX = 0, yığında çağıranın sayacı (2) ve dönüş adresi. Sağda POP CX'ten sonra: CX = 2, tepede dönüş adresi.

İzlemede (Tablo 8.2.14) CX'in yolculuğu açıkça görülür. Çağıranın sayacı (2) PUSH ile yığına gider. İç döngü CX'i 3'ten 0'a indirir. POP CX çağıranın değerini geri getirir. Ana LOOP bu değeri 1'e indirir ve ikinci satır için prosedürü yeniden çağırır. İkinci çağrıda yığına 1 itilir ve geri gelir. Son LOOP CX'i 0 yapar ve döngü biter. Ekranda 6 karakter vardır.

Prosedürün içinde yığının en derin anında (iç döngü sırasında) yığında iki sözcük vardır: dönüş adresi ve saklanan CX. SP = FFFAh'dir. Bu düzen, çıkıştaki RET'in neden doğru çalıştığını da açıklar: POP CX saklanan değeri aldıktan sonra tepede yine dönüş adresi durur.

Sık yapılan hata

CX'i kullanan bir prosedürü LOOP ile kurulmuş bir döngünün içinden, CX'i korumadan çağırmak. PUSH CX / POP CX olmasaydı prosedür CX = 0 ile dönerdi. Ana döngüdeki LOOP önce CX'i 1 azaltır: 0 − 1 = FFFFh. FFFFh sıfır olmadığı için atlar. Her çağrı CX'i yeniden 0 yapacağı için döngü bir daha bitmez ve ekrana durmadan karakter yazılır. Çözüm iki yerden birine konur: ya prosedürde PUSH CX / POP CX, ya da çağıranda PUSH CX / CALL yildiz / POP CX.

8.2.4.3Çağıran mı korur, çağrılan mı?

Yazmaçları saklama görevi iki yere verilebilir: prosedürün kendisine (çağrılan korur) ya da onu çağıran koda (çağıran korur). İki yaklaşımın güçlü ve zayıf yanları Tablo 8.2.15'dadır.

Tablo 8.2.15: Çağrılan korur ve çağıran korur yaklaşımlarının karşılaştırması.
ÖlçütÇağrılan korur (prosedürde PUSH/POP)Çağıran korur (CALL'ın çevresinde)
Kod nerede?prosedürde bir kezher çağrı noktasında
Gereksiz saklamaçağıran o yazmacı kullanmasa da saklanıryalnız gerçekten gereken yazmaçlar
Unutma riskidüşük: tek yerde yazılıryüksek: her çağrıda tekrar gerekir
Uygun olduğu durumkütüphane prosedürleriçıktı yazmacındaki eski değerin gerektiği yer

Çağıranın koruması gereken durumun örneği şöyledir. sayi_oku prosedürü klavyeden okuduğu sayıyı CX'te döndürür. CX onun çıktısıdır, prosedür onu koruyamaz. Çağıran ise CX'i döngü sayacı olarak kullanır. Bu durumda sayacı saklamak çağıranın işidir:

dongu: PUSH CX        ; çağıran: sayaç
       CALL sayi_oku  ; sonuç CX'te
       ADD toplam, CX
       POP CX         ; sayaç geri
       LOOP dongu

PUSH CX ile POP CX arasında CX iki farklı anlam taşır. Önce okunan sayıdır ve toplam'a eklenir; sonra yığından gelen döngü sayacıdır. Uygulamadaki kural iki yaklaşımı birleştirir: prosedür kullandığı yazmaçları korur, çıktı yazmaçlarını korumaz. Çıktı yazmacındaki eski değer gerekiyorsa onu çağıran saklar.

8.2.4.4Tek çıkış noktası: kod izleme

Yazmaç saklayan bir prosedürün birden çok dönüş yolu varsa bir tehlike doğar. Yollardan biri POP'ları atlayıp doğrudan RET'e giderse RET, yığının tepesinde dönüş adresi yerine saklanmış bir yazmaç değeri bulur. Bu yüzden PUSH yapan prosedürlerde bütün yollar, POP'ların bulunduğu tek bir çıkış etiketinde birleşir.

ORG 100h
LEA SI, dizi
MOV CX, 5
MOV AL, 7
CALL bul          ; CF = 1: bulundu
RET
bul PROC          ; Girdi: SI, CX, AL
PUSH CX
PUSH SI
XOR BX, BX        ; Çıktı: BX = indis
ara: CMP [SI], AL
JE buldu
INC SI
INC BX
LOOP ara
CLC               ; bulunamadı
JMP son
buldu: STC
son: POP SI       ; tek çıkış noktası
POP CX
RET
bul ENDP
dizi DB 3, 9, 7, 1, 4

bul prosedürü, SI'nın gösterdiği bayt dizisinde CX eleman içinde AL'deki değeri arar. Sonucu iki yolla döndürür: CF = 1 ise değer bulunmuştur ve BX onun indisidir. CF = 0 ise değer dizide yoktur. Prosedür arama sırasında SI'yı ve CX'i değiştirir. İkisi de girdidir ve çağıran onları bir sonraki çağrıda kullanmak isteyebilir, bu yüzden korunur. BX ise çıktıdır ve korunmaz. XOR BX, BX bir yazmacı sıfırlamanın kısa yoludur (2 bayt).

Programın yerleşimi şöyledir: LEA 3 bayt (0100h), MOV CX, 5 3 bayt (0103h), MOV AL, 7 2 bayt (0106h), CALL 3 bayt (0108h). Dönüş adresi 010Bh'dir. Prosedür 010Ch'de başlar ve 011Eh'deki RET ile biter. Dizi 011Fh'dedir; SI'nın başlangıç değeri budur.

Tablo 8.2.16: Tek çıkış noktası izlemesi (her satır grubu yürütüldükten sonraki durum).
SatırKomutSICXBXCFSPYığın (tepeden)
2–4LEA, MOV, MOV011Fh0005h0000h0FFFEh0000h
5CALL bul011Fh0005h0000h0FFFCh010Bh
8–10PUSH CX, PUSH SI, XOR011Fh0005h0000h0FFF8h011Fh, 0005h, 010Bh
11–15dizi[0] = 3 ≠ 70120h0004h0001h1FFF8h011Fh, 0005h, 010Bh
11–15dizi[1] = 9 ≠ 70121h0003h0002h0FFF8h011Fh, 0005h, 010Bh
11–12dizi[2] = 7: ZF = 1, JE atlar0121h0003h0002h0FFF8h011Fh, 0005h, 010Bh
18buldu: STC0121h0003h0002h1FFF8h011Fh, 0005h, 010Bh
19–20POP SI · POP CX011Fh0005h0002h1FFFCh010Bh
21RET011Fh0005h0002h1FFFEh0000h

Tablodaki CF sütununun ilk turlarda 1, sonra 0 olması dikkat çekebilir (Tablo 8.2.16). Bunun nedeni CMP'dir. CMP [SI], AL 3 − 7 hesabını yapar ve ödünç istediği için CF = 1 olur. İkinci turda 9 − 7 ödünç istemez, CF = 0 olur. LOOP ve INC CF'ye dokunmaz. Arama sırasındaki CF değerleri bir anlam taşımaz. Anlamlı olan, prosedürün çıkışta STC ya da CLC ile kurduğu değerdir.

SI0121hCX0003hBX0002hCF03091721344SIyığınFFF8011FhSPFFFA0005hFFFC010BhFFFE0000h SI011FhCX0005hBX0002hCF13091721344SIyığınFFF8····FFFA····FFFC····FFFE0000hSP
Şekil 8.2.15: Solda değer bulunduğu anda: döngü erken bitiyor, ama yığında hâlâ iki saklı değer var. Sağda RET'ten sonra: SI ve CX eski değerlerinde, CF = 1, BX = 2.

Kritik an, değerin bulunduğu üçüncü turdur. JE buldu'ya atlar ve döngü erken biter. O anda yığının tepesinde dönüş adresi değil, saklanmış SI (011Fh) durur (Şekil 8.2.15). buldu STC ile CF'yi kurar ve akış son etiketine geçer. Bulunamama yolu da (CLC ve JMP son) aynı etikete gelir. İki yol da POP SI ve POP CX'ten geçtiği için RET her durumda tepede 010Bh'yi bulur.

Programcı JE'nin hedefinde doğrudan bir RET kullanmış olsaydı, yani buldu: STC / RET yazsaydı, RET tepedeki 011Fh'yi dönüş adresi sanırdı. İşlemci, dizinin başladığı 011Fh adresindeki 03h, 09h, 07h baytlarını komut olarak yürütmeye çalışırdı. Sonuç öngörülemez olurdu. Program ancak arama başarısız olduğunda doğru çalışır ve hata yalnız bazı girdilerde ortaya çıkardı.

8.2.5Yığınla parametre ve yığın çerçevesi

8.2.5.1Yığınla parametre geçirme: kod izleme

Yazmaçla parametre geçirmenin iki sınırı vardır: yazmaç sayısı azdır ve her prosedür hangi yazmacın ne taşıdığını ayrıca belgelemelidir. Yığınla parametre geçirme ikisini de aşar. Çağıran parametreleri CALL'dan önce sırayla yığına iter. Prosedür onları yığındaki sabit yerlerinden okur. Parametre sayısı yığının büyüklüğüyle sınırlıdır ve kural her prosedür için aynıdır.

Prosedür parametreleri nasıl bulur? SP'yi kullanamaz: 8086'da SP bir adresleme yazmacı değildir, MOV AX, [SP+4] geçersizdir. Ayrıca SP, prosedür her PUSH ve POP yaptığında değişir; aynı parametrenin uzaklığı prosedürün farklı yerlerinde farklı olurdu. Çözüm BP yazmacıdır. BP'yi içeren adresler varsayılan olarak SS kesimini kullanır. Prosedür girişte BP'ye SP'nin değerini kopyalar ve böylece yığında sabit bir dayanak noktası kurar.

ORG 100h
MOV AX, 7
PUSH AX          ; 1. parametre
MOV AX, 3
PUSH AX          ; 2. parametre
CALL TOPLA
ADD SP, 4        ; parametreleri at
RET
TOPLA PROC
PUSH BP
MOV BP, SP
MOV AX, [BP+6]   ; 1. parametre
ADD AX, [BP+4]   ; 2. parametre
POP BP
RET
TOPLA ENDP

Programın yerleşimi izleme tablosundaki IP değerlerini açıklar. Ana program 0100h–010Eh'yi kaplar: MOV AX, 7 (3 bayt), PUSH AX (1), MOV AX, 3 (3), PUSH AX (1), CALL TOPLA (3, 0108h'de), ADD SP, 4 (3, 010Bh'de) ve RET (1). TOPLA 010Fh'de başlar. PUSH BP 1, MOV BP, SP 2, iki bellek işlenenli komut 3'er, POP BP ve RET 1'er bayttır.

Tablo 8.2.17: Yığınla parametre geçirme izlemesi (her satır yürütüldükten sonraki durum).
SatırKomutAXBPSPIP (sonraki)Yığına yazılan
2MOV AX, 70007h0000hFFFEh0103h
3PUSH AX0007h0000hFFFCh0104h[FFFCh] = 0007h
4MOV AX, 30003h0000hFFFCh0107h
5PUSH AX0003h0000hFFFAh0108h[FFFAh] = 0003h
6CALL TOPLA0003h0000hFFF8h010Fh[FFF8h] = 010Bh
10PUSH BP0003h0000hFFF6h0110h[FFF6h] = 0000h
11MOV BP, SP0003hFFF6hFFF6h0112h
12MOV AX, [BP+6]0007hFFF6hFFF6h0115h
13ADD AX, [BP+4]000AhFFF6hFFF6h0118h
14POP BP000Ah0000hFFF8h0119h
15RET000Ah0000hFFFAh010Bh
7ADD SP, 4000Ah0000hFFFEh010Eh
8RET (DOS'a)000Ah0000h0000h0000h

Tablo 8.2.17'deki ilk beş satır çağıranın hazırlığıdır. İki PUSH yığına 7 ve 3'ü yazar, CALL bunların üstüne dönüş adresini (010Bh) koyar. Prosedüre girildiğinde yığın, SP'den yukarı doğru şöyledir: [FFF8h] dönüş adresi, [FFFAh] ikinci parametre (3), [FFFCh] birinci parametre (7).

Prosedürün ilk iki komutu, PUSH BP ve MOV BP, SP, çerçeveyi kurar. Çağıranın BP'si yığına saklanır, çünkü çağıran da BP'yi kendi çerçevesi için kullanıyor olabilir. Sonra BP = SP = FFF6h olur. Bu andan sonra her parametrenin BP'ye göre uzaklığı sabittir:

  • [BP+0] = [FFF6h]: saklanan eski BP (0000h),
  • [BP+2] = [FFF8h]: dönüş adresi (010Bh),
  • [BP+4] = [FFFAh]: son itilen parametre (3),
  • [BP+6] = [FFFCh]: ilk itilen parametre (7).

Parametrelerin BP'ye göre uzaklığı, prosedürün içinde SP ne kadar değişirse değişsin aynı kalır. Prosedür arada başka yazmaçlar itse de [BP+6] hep birinci parametredir. MOV AX, [BP+6] fiziksel adresi SS × 16 + BP + 6 = 07000h + FFFCh = 16FFCh olarak hesaplar. Kesim öneki gerekmez, çünkü BP'li adresler varsayılan olarak SS'yi kullanır. Aynı adres DS ile hesaplansaydı tek kesimli .COM programında yine doğru çıkardı, ama DS ≠ SS olan .EXE programlarında yanlış olurdu.

····FFF40000hFFF6SP010BhFFF80003hFFFA0007hFFFC0000hFFFESS:bağıl konum · küçük adres yukarıda ····FFF40000hFFF6010BhFFF80003hFFFASP0007hFFFC0000hFFFESS:bağıl konum · küçük adres yukarıda
Şekil 8.2.16: Solda MOV AX, [BP+6] anında: BP = FFF6h, parametreler [BP+4] ve [BP+6]'da. Sağda prosedürün RET'inden sonra: SP = FFFAh, parametreler hâlâ yığında.

Çıkış girişin tersidir. POP BP çağıranın BP'sini geri getirir ve SP = FFF8h olur; tepede dönüş adresi vardır. RET 010Bh'ye döner ve SP = FFFAh olur. Bu noktada iki parametre hâlâ yığındadır (Şekil 8.2.16). Çağıranın ADD SP, 4 komutu onları atar: SP, PUSH'lardan önceki değerine, FFFEh'ye döner. Yığın dengelenmiştir ve ana programın RET'i doğru olarak 0000h'yi bulur. AX = 000Ah = 10 sonucu taşır.

8.2.5.2Parametreleri kim atar? RET n

Yığına itilen parametrelerin bir yerde atılması gerekir; yoksa her çağrı yığında birkaç sözcük bırakır. Bu işi ya çağıran ya da prosedür yapar.

Tanım 8.2.7

RET n, dönüş adresini yığından alıp IP'ye yükledikten sonra SP'ye ayrıca n ekleyen dönüş komutudur: IP ← [SP], SP ← SP + 2 + n. n, komutun içinde 16 bitlik bir sabit olarak saklanır (C2 n16, 3 bayt) ve parametre baytlarının sayısıdır.

TOPLA prosedüründe son komut RET 4 olsaydı çağıranın ADD SP, 4 yazmasına gerek kalmazdı. RET 4 çalışırken SP toplam 6 artar: dönüş adresi için 2, iki parametre için 4. SP, PUSH'lardan önceki değerine (FFFEh) tek komutta döner.

İki yöntemin farkı, kimin parametre sayısını bildiğidir. RET n'de sayı prosedürün içine sabitlenmiştir. Prosedür her zaman aynı sayıda parametre almalıdır. Pascal derleyicileri bu yöntemi kullanır. ADD SP, n'de ise sayıyı çağıran bilir. Aynı prosedür farklı çağrılarda farklı sayıda parametre alabilir. C dilinin değişken sayıda parametre alan printf gibi fonksiyonları bu yüzden çağıranın temizlediği yöntemle çalışır. Hangi yöntem seçilirse seçilsin, çağıran ile prosedür aynı kurala uymalıdır. İkisi birden temizlerse yığın 4 bayt fazla geri verilir; hiçbiri temizlemezse 4 bayt yığında kalır.

Örnek 8.2.3

Çağıran SP = FFFEh iken üç parametre itiyor ve yakın CALL yapıyor. Prosedür PUSH BP ve MOV BP, SP ile başlıyor. BP kaç olur, ilk itilen parametre nerededir, prosedür nasıl dönmelidir?

  1. İtmeler. Üç parametre 6 bayt, CALL 2 bayt, PUSH BP 2 bayt: FFFEh − 10 = FFF4h. BP = FFF4h olur.
  2. Uzaklıklar. [BP] eski BP, [BP+2] dönüş adresi, [BP+4] son itilen (üçüncü) parametre, [BP+6] ikinci, [BP+8] ilk itilen parametredir.
  3. Dönüş. POP BP'den sonra RET 6: SP = FFF6h + 2 + 6 = FFFEh. Yığın çağrıdan önceki hâline döner.

8.2.5.3Yığın çerçevesi ve yerel değişkenler

Prosedür çalışırken ara sonuçlarını saklayacak bir yere ihtiyaç duyabilir. Yazmaçlar yetmeyebilir. Ara sonuçları genel bellek değişkenlerinde tutmak ise prosedürü o değişkenlere bağlar ve özyinelemeli çağrıda her düzeyin aynı değişkeni bozmasına yol açar. Doğru yer yine yığındır.

Tanım 8.2.8

Yığın çerçevesi (stack frame), bir prosedür çağrısına ait yığın bölgesidir: parametreler, dönüş adresi, saklanan eski BP ve yerel değişkenler (local variables). Yerel değişken, yalnız prosedür çalışırken yığında yer alan değişkendir. BP çerçevenin sabit dayanak noktasıdır: parametreler BP'nin üstünde pozitif, yerel değişkenler altında negatif uzaklıktadır.

1. parametre (7)[BP+6]2. parametre (3)[BP+4]dönüş adresi[BP+2]eski BP[BP+0]yerel değişken 1[BP−2]yerel değişken 2[BP−4]BPSPküçük adres yukarıda · büyük adres aşağıda
Şekil 8.2.17: İki parametreli ve iki yerel değişkenli bir prosedürün yığın çerçevesi. Küçük adresler yukarıda, büyük adresler aşağıdadır.

Çerçeveyi kuran ve yıkan komutlar her prosedürde aynı kalıbı izler:

PUSH BP
MOV  BP, SP
SUB  SP, 4        ; yerel alan
MOV  [BP-2], AX   ; yerel 1
...
MOV  SP, BP       ; yerelleri at
POP  BP
RET  4            ; 2 parametreyi at

SUB SP, 4 yığında iki sözcüklük yer açar. Bu alana bir şey yazılmaz; yalnız SP aşağı çekilir. Böylece prosedürün sonraki PUSH'ları ve çağrıları yerel alanın üstüne yazmaz. İki yerel değişken [BP−2] ve [BP−4] adreslerindedir (Şekil 8.2.17). Açılan alanın ilk değeri, o adreste daha önce ne kaldıysa odur. Yerel değişkenler kullanılmadan önce bir değer almalıdır.

Çıkışta MOV SP, BP yerel alanı tek komutla geri verir. Kaç yerel değişken açıldığı önemli değildir; SP, BP'nin kurulduğu andaki değerine döner. Ardından POP BP ve RET n gelir. Bu sıra bozulamaz: yerel alan kapatılmadan yapılan POP BP, eski BP yerine bir yerel değişkeni BP'ye yükler.

Yerel değişkenin ömrü çağrının ömrüdür. Prosedür döndükten sonra o yığın sözcükleri bir sonraki PUSH ya da CALL ile ezilir. Bu yüzden bir prosedür, yerel değişkeninin adresini sonuç olarak döndürmemelidir. Aynı prosedür kendini çağırırsa her çağrı kendi çerçevesini, dolayısıyla kendi yerel değişkenlerini kurar. Düzeyler birbirinin değişkenine dokunmaz.

80186 ve sonraki işlemcilerde bu kalıbın iki komutluk kısaltması vardır: ENTER çerçeveyi kurar, LEAVE MOV SP, BP ile POP BP'yi birlikte yapar. 8086'da bu komutlar yoktur.

8.2.5.4Yerel değişkenli çerçeve: kod izleme

Aşağıdaki prosedür AX'teki x değerinden x·x + x'i hesaplar. MUL, AX'in üzerine yazdığı için x'in kopyası bir yerel değişkende saklanır.

MOV AX, 9
CALL kare_art       ; AX = x·x + x
RET
kare_art PROC
PUSH BP
MOV BP, SP
SUB SP, 2           ; yerel alan [BP-2]
MOV [BP-2], AX      ; yerel = x
MUL AX              ; AX = x·x
ADD AX, [BP-2]      ; + x
MOV SP, BP          ; yereli at
POP BP
RET
kare_art ENDP
Tablo 8.2.18: Yerel değişkenli çerçeve izlemesi (her satır yürütüldükten sonraki durum).
SatırKomutAXDXBPSPYığın
1MOV AX, 90009h0000h0000hFFFEh
2CALL kare_art0009h0000h0000hFFFCh[FFFCh] = 0106h
5PUSH BP0009h0000h0000hFFFAh[FFFAh] = 0000h
6MOV BP, SP0009h0000hFFFAhFFFAh
7SUB SP, 20009h0000hFFFAhFFF8h[FFF8h] = yerel alan
8MOV [BP-2], AX0009h0000hFFFAhFFF8h[FFF8h] = 0009h
9MUL AX0051h0000hFFFAhFFF8h
10ADD AX, [BP-2]005Ah0000hFFFAhFFF8h
11MOV SP, BP005Ah0000hFFFAhFFFAhyerel alan bırakıldı
12POP BP005Ah0000h0000hFFFCh
13RET005Ah0000h0000hFFFEhIP = 0106h

İzlemede (Tablo 8.2.18) SUB SP, 2'den sonra BP = FFFAh, SP = FFF8h'dir. Yerel değişken [BP−2] = [FFF8h]'dedir; SP onu gösterir ve sonraki bir PUSH onun altına yazar. MOV [BP-2], AX x = 9'u yerele kopyalar. 16 bitlik MUL AX DX:AX ← AX × AX hesaplar: 9 × 9 = 81 = 0051h, DX = 0000h. AX'teki x kaybolmuştur, ama yerelde durur. ADD AX, [BP-2] 81 + 9 = 90 = 005Ah verir.

AX0009hBPFFFAhSPFFF8hyığınFFF80009hyerel [BP−2]SPFFFA0000heski BP ← BPFFFC0106hdönüşFFFE0000h AX005AhBPFFFAhSPFFFAhyığınFFF8····FFFA0000heski BP ← BPSPFFFC0106hdönüşFFFE0000h
Şekil 8.2.18: Solda yerel değişkene x yazıldıktan sonra: [BP−2] = 9. Sağda MOV SP, BP'den sonra: SP = BP = FFFAh, yerel alan bırakıldı.

Çıkışta üç komut çerçeveyi ters sırayla yıkar. MOV SP, BP SP'yi FFFAh'ye çeker ve yerel alan bırakılır. [FFF8h]'de 0009h hâlâ yazılıdır, ama artık yığının dışında kalmıştır. POP BP çağıranın BP'sini (0000h) geri getirir. RET 0106h'ye döner. Sonuç AX = 90'dır.

Bu prosedürün belgelemesinde bir eksik vardır. 16 bitlik MUL her durumda DX'e de yazar: x = 9 için DX = 0000h olur. Çağıranın DX'te tuttuğu değer bu yüzden kaybolur. Bu izlemede DX zaten 0 olduğu için fark görünmez. x ≥ 256 olduğunda çarpım 16 bite sığmaz ve DX sıfırdan farklı bir değer alır. Açıklamaya “Bozar: DX” yazılmalı ya da DX prosedürde PUSH/POP ile korunmalıdır.

8.2.6Örnek programlar

8.2.6.1Merhaba Dünya: print_me

İlk örnek, sıfırla biten bir dizgiyi ekrana karakter karakter yazan print_me prosedürüdür. Parametre, dizginin adresidir ve SI ile geçirilir. Dizginin uzunluğu ayrıca verilmez; prosedür sıfır baytını görünce durur. C dilindeki dizgiler de bu düzende saklanır.

ORG 100h
LEA SI, msg       ; SI = msg adresi
CALL print_me
RET               ; DOS'a dön
print_me PROC
next_char:
CMP b.[SI], 0     ; sıfır mı?
JE stop           ; evet: bitir
MOV AL, [SI]      ; sıradaki ASCII karakter
MOV AH, 0Eh       ; teletype işlevi
INT 10h           ; AL'yi ekrana yaz
ADD SI, 1         ; sonraki karakter
JMP next_char     ; başa dön
stop:
RET               ; çağırana dön
print_me ENDP
msg DB 'Hello World!', 0
END

Ana program üç komuttur. LEA SI, msg dizginin adresini SI'ya yükler; prosedürün parametresi budur. CALL print_me prosedürü çağırır, RET programı bitirir. Programın yerleşiminde LEA 3 bayttır (BE 18 01), CALL 0103h'dedir ve dönüş adresi 0106h'dir. Prosedür 0107h'de başlar ve 0117h'deki RET ile biter. Dizgi 0118h'dedir: 12 karakter ve bir sıfır baytı.

Prosedürün ilk komutu CMP b.[SI], 0'dır. b., emu8086'nın BYTE PTR kısaltmasıdır. CMP [SI], 0 yazılsaydı assembler işlemin boyutunu bilemezdi: [SI] bir bayt da, bir sözcük de olabilir, 0 sabiti de boyut belirtmez. Boyut bu yüzden açıkça yazılır. MASM'da aynı satır CMP BYTE PTR [SI], 0 biçiminde yazılır.

Döngü her turda altı komut yürütür: karşılaştırma, koşullu atlama, karakteri AL'ye alma, AH'ye 0Eh yazma, INT 10h ve SI'yı ilerletme; sonra JMP başa döner. MOV AH, 0Eh döngünün dışına alınabilirdi, ama INT 10h'nin AH'yi koruyacağı kesin değilse içeride kalması güvenlidir.

Tablo 8.2.19: print_me izlemesi (seçilmiş anlar; msg = 0118h).
AnSIALZFSPEkran
LEA SI, msgmsg+0—0FFFEh
CALL print_memsg+0—0FFFCh
1. tur, CMP: 'H' ≠ 0msg+0—0FFFCh
1. tur, INT 10hmsg+048h0FFFChH
1. tur, ADD SI, 1msg+148h0FFFChH
2. tur sonumsg+265h0FFFChHe
5. tur sonumsg+56Fh0FFFChHello
11. tur sonumsg+1164h0FFFChHello World
12. tur sonumsg+1221h0FFFChHello World!
CMP: [SI] = 0, JE stopmsg+1221h1FFFChHello World!
prosedürün RET'imsg+1221h1FFFEhHello World!
ana programın RET'imsg+1221h10000hHello World!

Döngü 12 tur döner ve her turda bir karakter yazar (Tablo 8.2.19). Altıncı karakter boşluktur (20h); boşluk da bir karakterdir ve ekrana yazılır. On üçüncü kez yapılan karşılaştırmada [SI] = 0'dır, ZF = 1 olur ve JE stop'a atlar. Sıfır baytı ekrana yazılmaz. Bütün döngü boyunca SP = FFFCh'dir: prosedür yığına bir şey itmez, yığında yalnız dönüş adresi vardır.

Prosedür SI'yı değiştirir ve korumaz. Dönüşte SI, dizginin sonundaki sıfır baytını gösterir (msg + 12 = 0124h). Bu bir yan etkidir ve açıklamaya “Bozar: SI, AX” diye yazılmalıdır. Aynı prosedür başka bir metin için yalnız SI değiştirilerek çağrılabilir: LEA SI, baska_metin / CALL print_me.

8.2.6.2İki değişkeni toplamak

İkinci örnek, parametrelerini ve sonucunu bellek değişkenlerinde bulan bir prosedürdür.

ORG 100h
start:
CALL AddNumbers     ; prosedürü çağır
RET                 ; DOS'a dön
AddNumbers PROC
MOV AX, number1     ; AX = ilk sayı
ADD AX, number2     ; AX = AX + ikinci sayı
MOV result, AX      ; sonucu sakla
RET                 ; prosedürü bitir
AddNumbers ENDP
number1 DW 5        ; ilk sayı 5
number2 DW 3        ; ikinci sayı 3
result DW ?         ; sonucu tutar

Programın ilk komutu CALL'dır ve 0100h'dedir; dönüş adresi 0103h'dir. Prosedür 0104h'de başlar. Komutların uzunlukları birbirinden farklıdır. MOV AX, number1 AX'e özel kısa kodlamayla 3 bayttır (A1 + adres). ADD AX, number2 4 bayttır. MOV result, AX yine AX'e özel kodlamayla 3 bayttır. Değişkenler prosedürün arkasındadır: number1 = 010Fh, number2 = 0111h, result = 0113h.

Tablo 8.2.20: İki değişken toplama izlemesi (her satır yürütüldükten sonraki durum).
SatırKomutAXSPZFCFresult
3CALL AddNumbers0000hFFFCh00?
6MOV AX, number10005hFFFCh00?
7ADD AX, number20008hFFFCh00?
8MOV result, AX0008hFFFCh000008h
9RET0008hFFFEh000008h
4RET (DOS'a)0008h0000h000008h

Tablo 8.2.20'de ADD'in bayrakları da gösterilmiştir. 5 + 3 = 8 sıfır değildir (ZF = 0) ve 16 bitte elde üretmez (CF = 0). MOV bayraklara dokunmaz. result bellekte 08 00 olarak, little-endian düzende saklanır. DW ? başlangıç değerinin belirsiz olduğunu söyler. emu8086 bu alana 0 koyar, ama program bu değere güvenmemelidir.

Bu prosedür kısa ve anlaşılırdır, ama yalnız number1, number2 ve result ile çalışır. Başka iki sayıyı toplamak için ya önce o sayılar bu değişkenlere kopyalanır ya da yeni bir prosedür yazılır. Bellek değişkeniyle parametre geçirmenin zayıf yanı budur.

8.2.6.3Dizi toplamı prosedürü

Üçüncü örnek bir dizinin elemanlarını toplar. Parametre olarak iki şey alır: dizinin adresi (SI) ve eleman sayısı (CX). Sonucu AL'de döndürür. Prosedür hangi dizinin adresi verilirse onunla çalışır.

ORG 100h
LEA SI, vector     ; parametre 1: adres
MOV CX, 5          ; parametre 2: adet
CALL topla
MOV m, AL          ; dönüş değeri AL'de
RET
topla PROC         ; Girdi: SI, CX
MOV AL, 0          ; Çıktı: AL = toplam
dongu:
ADD AL, [SI]
INC SI
LOOP dongu
RET
topla ENDP
vector DB 5, 4, 5, 2, 1
m DB 0
Tablo 8.2.21: Dizi toplamı izlemesi (vector = 0115h, m = 011Ah).
SatırKomut / turSICXALSPm
2–3LEA SI, vector · MOV CX, 50115h0005h00hFFFEh00h
4CALL topla0115h0005h00hFFFCh00h
8MOV AL, 00115h0005h00hFFFCh00h
10–121. tur: +50116h0004h05hFFFCh00h
10–122. tur: +40117h0003h09hFFFCh00h
10–123. tur: +50118h0002h0EhFFFCh00h
10–124. tur: +20119h0001h10hFFFCh00h
10–125. tur: +1011Ah0000h11hFFFCh00h
13RET011Ah0000h11hFFFEh00h
5MOV m, AL011Ah0000h11hFFFEh11h

Toplam 5 + 4 + 5 + 2 + 1 = 17 = 11h'dir (Tablo 8.2.21). Son turdan sonra SI = 011Ah olur ve dizinin hemen arkasındaki m değişkenini gösterir. LOOP CX'i 0'a indirdiğinde atlamaz ve akış RET'e geçer. Çağıran AL'deki sonucu m'ye yazar.

Prosedürün iki sınırı açıklamasına yazılmalıdır. Birincisi, toplam 8 bitlik AL'de tutulur. Elemanların toplamı 255'i aşarsa sonuç taşar ve yalnız alt 8 biti kalır. İkincisi, prosedür SI ve CX'i değiştirir. Dönüşte SI dizinin sonunu, CX ise 0'ı gösterir. Aynı prosedür başka bir dizi için yalnız SI ve CX değiştirilerek çağrılabilir.

8.2.6.4Parametre bloğu

Prosedürün çok sayıda parametresi varsa yazmaçlar yetmez. Bir çözüm, parametreleri bellekte art arda duran bir bloğa yazmak ve prosedüre yalnız bloğun adresini vermektir. Tek bir yazmaç istenen sayıda parametreyi taşır.

LEA BX, blok
CALL hesap            ; sonuç = a × b + c
RET
hesap PROC            ; Girdi: BX = blok adresi
PUSH AX
PUSH DX               ; MUL DX'i de yazar
MOV AX, [BX]          ; a
MUL WORD PTR [BX+2]   ; × b
ADD AX, [BX+4]        ; + c
MOV [BX+6], AX        ; sonuç
POP DX
POP AX
RET
hesap ENDP
blok DW 12, 7, 5, ?
12[BX+0] a7[BX+2] b5[BX+4] c89[BX+6] sonuçBX → blok
Şekil 8.2.19: Parametre bloğu: [BX+0] a = 12, [BX+2] b = 7, [BX+4] c = 5, [BX+6] sonuç = 89.

Blok dört sözcükten oluşur (Şekil 8.2.19). Prosedür her parametreyi BX'e göre sabit bir uzaklıktan okur: a [BX], b [BX+2], c [BX+4]. Sonucu da bloğun dördüncü sözcüğüne, [BX+6]'ya yazar. MUL WORD PTR [BX+2] satırında boyut açıkça yazılır; MUL'un tek işleneni bellekte olduğu için assembler boyutu başka yoldan bilemez.

Hesap şöyle ilerler: AX = 12, MUL ile DX:AX = 12 × 7 = 84 = 0054h, ADD ile AX = 84 + 5 = 89 = 0059h. Sonuç [BX+6]'ya yazılır. emu8086'da çalıştırıldığında blok 0117h'de başlar ve sonuç 011Dh–011Eh baytlarına 59 00 olarak yazılır. Prosedür AX ve DX'i kullandığı hâlde ikisini de korur, çünkü sonucunu yazmaçta değil bellekte döndürür. Dönüşte AX ve DX çağrıdan önceki değerlerindedir.

Aynı prosedür başka bir blokla, yalnız BX değiştirilerek çağrılır. Blok düzeni, yani hangi uzaklıkta hangi parametrenin durduğu, prosedürün açıklamasında belgelenmelidir. DOS'un dosya ve program yükleme hizmetleri de bu yöntemi kullanır: çağıran bir parametre bloğu hazırlar ve adresini bir yazmaçta verir.

8.2.6.5Prosedürü test etmek

Bir prosedür yalnız olağan girdiyle denenirse gizli hataları ortaya çıkmaz. Sınama, sınır durumlarını da kapsamalıdır: ilk ve son eleman, bulunmayan değer, tek elemanlık ve boş girdi. Tek çıkış noktalı bul prosedürü (dizi: 3, 9, 7, 1, 4) bu durumlarla çalıştırıldığında Tablo 8.2.22'deki sonuçlar elde edilir.

Tablo 8.2.22: bul prosedürünün sınır durumlarıyla sınanması.
DurumALCXCFBXDeğerlendirme
ilk eleman3510doğru
son eleman4514doğru
dizide yok8505doğru; BX anlamsızdır, yalnız CF'ye bakılır
tek elemanlık arama9101doğru: 9 ikinci eleman, aranan aralığın dışında
boş dizi7012yanlış: CX = 0 iken LOOP 65 536 tur döner

Bütün durumlarda dönüşte CX ve SI girdideki değerlerindedir; koruma doğru çalışır. Hata yalnız boş dizide ortaya çıkar. Nedeni LOOP'un çalışma biçimidir: LOOP önce CX'i 1 azaltır, sonra sıfır olup olmadığına bakar. CX = 0 ile girilen döngüde ilk LOOP CX'i 0 − 1 = FFFFh yapar ve atlar. Döngü 65 536 tur dönebilir. Prosedür hiç eleman taramaması gerekirken dizinin baytlarını taramaya başlar ve üçüncü baytta 7'yi “bulur”.

Düzeltme, döngüye girmeden önce CX'i sınamaktır. JCXZ (jump if CX zero) komutu CX = 0 ise atlar ve bayraklara bakmaz:

bul PROC
PUSH CX
PUSH SI
XOR BX, BX
JCXZ yok          ; boş dizi: aramadan çık
ara: CMP [SI], AL
JE buldu
INC SI
INC BX
LOOP ara
yok: CLC          ; bulunamadı
JMP son
buldu: STC
son: POP SI
POP CX
RET
bul ENDP

Düzeltilmiş prosedür CX = 0 için CF = 0 ve BX = 0 döndürür. CX = 5 ile 7 arandığında önceki gibi CF = 1, BX = 2 verir. İki sonuç da emu8086'da çalıştırılarak elde edilmiştir. Bu örnek, sınamanın her prosedür için bir liste hâlinde hazırlanmasının nedenini gösterir: hata, olağan girdilerle yapılan hiçbir denemede görünmezdi.

8.2.6.6İç içe çağrılar

Bir prosedür başka bir prosedürü çağırabilir. Bu durumda yığında aynı anda birden çok dönüş adresi bulunur. Yığının LIFO yapısı, dönüşlerin doğru sırada yapılmasını kendiliğinden sağlar: son çağrılan ilk döner.

main0100h CALL A0103h …A0110h …0112h CALL B0115h RETB0120h …0122h RETIP = 0120hYığın (SS)FFF8hFFFAh0115hSPFFFCh0103hFFFEh0000hyukarı: düşük adresler
Şekil 8.2.20: main, A'yı; A da B'yi çağırmıştır. Yığında iki dönüş adresi vardır: tepede 0115h (A'ya dönüş), altında 0103h (main'e dönüş).
Tablo 8.2.23: İç içe çağrılarda yığın.
AnIPSPYığın (tepeden)
main, CALL A'dan önce0100hFFFEh0000h
CALL A (0100h)0110hFFFCh0103h, 0000h
A içinden CALL B (0112h)0120hFFFAh0115h, 0103h, 0000h
B'nin RET'i0115hFFFCh0103h, 0000h
A'nın RET'i0103hFFFEh0000h

Tabloda (Tablo 8.2.23) B'nin RET'i yığının tepesindeki 0115h'yi alır ve A'ya döner. A'nın işi bittiğinde onun RET'i bir alttaki 0103h'yi alır ve main'e döner. Her RET yalnız tepedeki adresi bilir; doğru adresin tepede olmasını yığının yapısı sağlar. Bu yüzden prosedürler, kaç düzey iç içe çağrıldıklarını bilmek zorunda değildir.

Aynı düzen daha somut bir programla da izlenebilir. main AX = 1 ile A'yı çağırır; A, AX'e 2 ekler, B'yi çağırır, sonra AX'e 4 ekler; B, AX'i iki katına çıkarır. Sıra 1 → 3 → 6 → 10'dur ve program AX = 000Ah ile biter. Yığında en çok iki dönüş adresi birikir.

8.2.6.7Yığın ihtiyacı hesabı

İç içe çağrılarda yığın, en derin çağrı zinciri boyunca biriken baytlar kadar büyür. Bir programın ne kadar yığına ihtiyaç duyduğunu bilmek, yığın için yeterli yer ayırmak ve yığının veriyle ya da kodla çakışmasını önlemek için gerekir.

Tanım 8.2.9

Bir programın yığın gereksinimi, çağrı ağacındaki en derin yol boyunca biriken baytların toplamıdır. Her yakın CALL 2 bayt dönüş adresi, her uzak CALL 4 bayt, her PUSH 2 bayt ve her yerel alan kendi boyutu kadar ekler.

mainAPUSH AX, SICPUSH AX, BX, CXBPUSH CX
Şekil 8.2.21: Çağrı ağacı: main, A ve C'yi; A da B'yi çağırır. A iki, C üç, B bir yazmaç saklar.
Tablo 8.2.24: Çağrı yollarının yığın ihtiyacı.
YolHesapBayt
main → A → B2 (CALL A) + 4 (PUSH AX, SI) + 2 (CALL B) + 2 (PUSH CX)10
main → C2 (CALL C) + 6 (PUSH AX, BX, CX)8

En derin yol main → A → B'dir ve 10 bayt gerektirir (Tablo 8.2.24). SP = FFFEh ile başlayan programda SP en çok FFFEh − 10 = FFF4h'ye iner. C üç yazmaç sakladığı için A'dan daha fazla PUSH yapar, ama derinliği daha azdır. A ve C hiçbir zaman aynı anda yığında değildir: C çağrıldığında A'nın baytları çoktan geri alınmıştır. Bu yüzden gereksinim yolların toplamı değil, en büyüğüdür.

Hesaba bir pay eklenmelidir. Donanım kesmeleri programın herhangi bir anında, yığının en derin anında da gelebilir. Her kesme yığına 6 bayt iter (FLAGS, CS ve IP) ve kesme hizmet yordamı kendi saklamalarını ekler. Hesaplanan değerin üstünde bu yüzden her zaman pay bırakılır. .COM programında yığın kesimin en üstünden aşağı büyür, kod ve veri en alttan yukarı. İkisi arasındaki boşluk bittiğinde yığın veriyi ezer. Buna yığın taşması (stack overflow) denir ve işlemci bunu bildirmez.

8.2.7Özyineleme

8.2.7.1Kendini çağıran prosedür

Bir prosedürün başka bir prosedürü çağırabildiği görüldü. Aynı düzenek, prosedürün kendini çağırmasına da izin verir. İşlemci için CALL'ın hedefinin hangi prosedür olduğu önemli değildir; dönüş adresini iter ve atlar.

Tanım 8.2.10

Özyineleme (recursion), bir prosedürün kendini doğrudan ya da başka bir prosedür üzerinden çağırmasıdır. Her çağrı yığına yeni bir dönüş adresi ve kendi sakladığı değerleri koyar; her düzeyin değerleri birbirinden ayrı kalır. Kendini yeniden çağırmadan dönülen duruma taban durum (durma koşulu) denir.

Özyineleme, problemin her adımda küçülerek kendine benzediği durumlarda doğal bir çözümdür. Bir sayıyı onluk basamaklarıyla yazmak buna iyi bir örnektir. 472'yi yazmak, önce 47'yi yazmak, sonra '2'yi yazmak demektir. 47'yi yazmak da önce 4'ü, sonra '7'yi yazmaktır. 4 tek basamaklıdır; onu yazmak için başka bir şey yazmaya gerek yoktur. Bu, taban durumdur.

yaz(472)sonra '2' yazaryaz(47)sonra '7' yazaryaz(4)sonra '4' yazartaban durum: bölüm 0 → yeni çağrı yok
Şekil 8.2.22: yaz(472), yaz(47)'yi; o da yaz(4)'ü çağırır. Dönüşte sırasıyla 4, 7, 2 yazılır.

Taban durum şarttır. Taban durumu olmayan bir prosedür kendini sonsuza kadar çağırır. Her çağrı yığına en az 2 bayt ekler. SP sürekli azalır, yığın programın verisinin ve kodunun üzerine kadar büyür ve program çöker. Sayı yazdırmanın yanında faktöriyel hesabı, ağaç dolaşma ve dizin dolaşma özyinelemenin tipik örnekleridir.

8.2.7.2Özyinelemeli sayı yazdırma: kod izleme

ORG 100h
MOV AX, 472
CALL yaz
RET
yaz PROC        ; Girdi: AX (işaretsiz)
XOR DX, DX
MOV BX, 10
DIV BX          ; AX = bölüm, DX = basamak
PUSH DX
OR AX, AX       ; başka basamak var mı?
JZ son
CALL yaz        ; önce üst basamaklar
son: POP AX
ADD AL, '0'
MOV AH, 0Eh
INT 10h
RET
yaz ENDP

Prosedür AX'teki işaretsiz sayıyı ekrana yazar. İlk üç komut sayıyı 10'a böler. 16 bitlik DIV BX, DX:AX'teki 32 bitlik sayıyı böler; bu yüzden önce XOR DX, DX ile DX sıfırlanır. Bölümden sonra AX = sayı / 10, DX = sayı mod 10 olur. Kalan, sayının son basamağıdır ve PUSH DX ile yığına saklanır. OR AX, AX AX'i değiştirmeden ZF'yi kurar: bölüm sıfırsa yazılacak başka basamak yoktur ve JZ taban duruma, son'a atlar. Bölüm sıfır değilse prosedür, bölümü yazmak için kendini çağırır. Dönüşte son'daki dört komut saklanan basamağı alır, ASCII'ye çevirir ve yazar.

Programın yerleşiminde CALL'lar iki yerdedir. Ana programın CALL yaz'ı 0103h'dedir ve dönüş adresi 0106h'dir. Prosedürün içindeki CALL yaz 0113h'dedir ve dönüş adresi 0116h'dir, yani son etiketi. Prosedür kendini çağırdığında bu CALL'ın makine kodu E8 F1 FF olur: 0107h − 0116h = −0Fh = FFF1h. Hedef geride olduğu için uzaklık negatiftir.

Tablo 8.2.25: Özyinelemeli sayı yazdırma izlemesi (her satır grubu yürütüldükten sonraki durum).
SatırOlayAXDXSPYığın (tepeden)Ekran
2–3AX = 472, CALL yaz01D8h0000hFFFCh0106h
6–91. düzey: 472 / 10, PUSH DX002Fh0002hFFFAh2, 0106h
10–12AX ≠ 0: CALL yaz002Fh0002hFFF8h0116h, 2, 0106h
6–122. düzey: 47 / 10, PUSH, CALL0004h0007hFFF4h0116h, 7, 0116h, 2, 0106h
6–113. düzey: 4 / 10, PUSH; AX = 0, JZ son0000h0004hFFF2h4, 0116h, 7, 0116h, 2, 0106h
13–16POP AX = 4, yaz0E34h0004hFFF4h0116h, 7, 0116h, 2, 0106h4
17, 13–16RET → 0116h; POP AX = 7, yaz0E37h0004hFFF8h0116h, 2, 0106h47
17, 13–16RET → 0116h; POP AX = 2, yaz0E32h0004hFFFCh0106h472
17son RET → 0106h0E32h0004hFFFEh0000h472

İzleme (Tablo 8.2.25) iki evreye ayrılır. İniş evresinde prosedür her düzeyde bir basamak ayırır ve kendini yeniden çağırır. Yığına her düzeyde iki sözcük eklenir: basamak ve dönüş adresi. Ama basamaklar sondan başa doğru ayrılır: önce 2, sonra 7, sonra 4. Üçüncü düzeyde bölüm 0 olur ve taban duruma ulaşılır. Bu anda SP en derin noktasındadır: FFF2h. Yığında üç basamak ve üç dönüş adresi vardır (Şekil 8.2.23).

AX0000hDX0004hSPFFF2hyığınFFF20004hSPFFF40116hFFF60007hFFF80116hFFFA0002hFFFC0106hFFFE0000hbasamakdönüşbasamakdönüşbasamakdönüşekran▌ AX0E37hDX0004hSPFFF8hyığınFFF2····FFF4····FFF6····FFF80116hSPFFFA0002hFFFC0106hFFFE0000hdönüşbasamakdönüşekran47▌
Şekil 8.2.23: Solda taban durumda: SP = FFF2h, yığında üç basamak ve üç dönüş adresi. Sağda ikinci düzeye dönüldükten sonra: '4' ve '7' yazıldı, SP = FFF8h.

Çıkış evresinde her düzey kendi basamağını yığından alır ve yazar. Son itilen basamak (4) ilk çıkar. Böylece basamaklar ayrıldıkları sıranın tersiyle, yani doğru sırada yazılır. Üçüncü düzeyin RET'i 0116h'ye, ikinci düzeyin son satırına döner. İkinci düzey 7'yi yazar ve onun RET'i de 0116h'ye, bu kez birinci düzeye döner. Aynı dönüş adresinin yığında iki kez bulunması doğaldır: iki ayrı çağrı aynı CALL komutundan yapılmıştır. Birinci düzey 2'yi yazar ve son RET 0106h'ye, ana programa döner. SP = FFFEh olur ve yığın dengelenmiştir.

İzlemenin son satırı, prosedürün en dıştaki RET'inden hemen sonraki durumdur: IP = 0106h, SP = FFFEh. Ardından ana programın RET'i 0000h'yi alır ve SP = 0000h ile program biter.

Her düzey kendi basamağını yığında sakladığı için ek bir değişken ya da dizi gerekmemiştir. Yığın, düzeylerin her birine ayrı bir saklama alanı sağlamıştır. Döngüyle yazılan bir çözüm de basamakları yığına itip sonra çekerek ters çevirir; özyineleme bu işi CALL ve RET'in doğal düzeniyle yapar.

Örnek 8.2.4

AX = 0 ile çağrıldığında prosedür ne yazar? SP en çok kaça iner?

  1. Bölme. 0 / 10: AX = 0, DX = 0. Basamak 0 yığına itilir.
  2. Taban durum. AX = 0, JZ son'a atlar; yeni çağrı yapılmaz.
  3. Yazma. POP AX = 0, ADD AL, '0' → 30h; ekrana '0' yazılır.
  4. Yığın. Ana programın CALL'ı (2 bayt) ve bir basamak (2 bayt): SP en çok FFFEh − 4 = FFFAh'ye iner.

Taban durum denetimi bölmeden sonra yapıldığı için 0 sayısı da ek kod gerektirmeden '0' olarak yazılır.

8.2.7.3Özyineleme mi, döngü mü?

Aynı iş bir döngüyle de yapılabilir: sayı art arda 10'a bölünür, basamaklar yığına itilir ve bir sayaçla sayılır; ikinci bir döngü basamakları çekip yazar. İki çözümün karşılaştırması Tablo 8.2.26'dadır.

Tablo 8.2.26: Özyinelemeli ve döngülü sayı yazdırmanın karşılaştırması.
ÖlçütÖzyineleme (yaz)Döngü (bölme + LOOP)
Kodkısa; problemin tanımına yakınsayaç ve ayrı bir yazdırma döngüsü gerekir
Yığınher düzeyde dönüş adresi + basamak: 4 baytbasamak başına 2 bayt
Süreher düzeyde CALL + RET (19 + 8 tur)çağrı yükü yok
Riskderinlik sınırsız büyürse yığın taşaryığın kullanımı sınırlı
Örnek 8.2.5

yaz prosedürü 16 bitlik en büyük işaretsiz sayı olan 65 535 ile çağrılırsa yığın en çok ne kadar derinleşir?

  1. Düzey sayısı. Düzey sayısı basamak sayısına eşittir: 65 535 beş basamaklıdır, beş düzey.
  2. Bayt. Her düzey bir dönüş adresi (2 bayt) ve bir basamak (2 bayt) ekler: 5 × 2 + 5 × 2 = 20 bayt.
  3. SP. FFFEh − 20 = FFFEh − 14h = FFEAh.
  4. Süre. Beş CALL ve beş RET: 5 × (19 + 8) = 135 tur, 5 MHz'de 27 µs çağrı yükü.

Bu prosedürün derinliği 5 ile sınırlıdır, çünkü AX en çok beş basamak taşır. emu8086'da 65 535 ile çalıştırıldığında SP'nin en küçük değeri FFEAh'dir.

Taban durumun yeri de önemlidir. Denetim yeni CALL'dan önce yapılmalıdır. yaz'da önce bölünür, sonra bölümün sıfır olup olmadığına bakılır ve ancak sıfır değilse CALL yapılır. Denetim CALL'dan sonraya konsaydı, CALL'a hiçbir koşul bağlanmamış olurdu ve prosedür kendini durmadan çağırırdı.

8.2.8Sık yapılan hatalar

8.2.8.1Dengesiz yığın: kod izleme

Prosedür hatalarının çoğunun kökü aynıdır: RET yürütüldüğünde yığının tepesinde dönüş adresi yoktur. Bunun en sık nedeni, bir PUSH'un POP'unun unutulmasıdır.

ORG 100h
MOV AX, 1234h     ; 0100h: B8 34 12
CALL hatali       ; 0103h: dönüş = 0106h
RET               ; 0106h
hatali PROC
PUSH AX           ; AX saklandı...
; ... işlemler ...
RET               ; POP AX unutuldu!
hatali ENDP
Tablo 8.2.27: Dengesiz yığın izlemesi (her satır yürütüldükten sonraki durum).
SatırKomutAXIP (sonraki)SPYığın (tepeden)
2MOV AX, 1234h1234h0103hFFFEh0000h
3CALL hatali1234h0107hFFFCh0106h, 0000h
6PUSH AX1234h0108hFFFAh1234h, 0106h, 0000h
8RET1234h1234hFFFCh0106h, 0000h

RET yığının tepesinde ne varsa onu dönüş adresi sayar (Tablo 8.2.27). Tepede AX'in saklanan değeri, 1234h, vardır. RET'ten sonra IP = 1234h olur. İşlemci CS:1234h adresindeki baytları komut olarak yürütmeye başlar. O adreste programın kodu yoktur; bellekte ne kaldıysa o yürütülür. Program çöker ya da öngörülemez davranır. Asıl dönüş adresi 0106h ise yığında, SP = FFFCh'nin gösterdiği yerde, hiç kullanılmadan kalır.

Kural şudur: prosedürden çıkarken SP, girişteki değerine eşit olmalıdır. Her PUSH'un ters sırada bir POP'u bulunmalıdır, üstelik prosedürün her yolunda. Hata, PUSH ile RET arasındaki kod uzadıkça ve dallar çoğaldıkça daha kolay yapılır. Tek çıkış noktası bu yüzden önerilir.

8.2.8.2Tür uyuşmazlığı: kod izleme

İkinci tipik hata, çağrının türü ile dönüşün türünün uyuşmamasıdır. Aşağıdaki programda uzak prosedürü RETF ile biter, ama yakın CALL ile çağrılır.

ORG 100h
MOV AX, 5
CALL uzak      ; yakın CALL: yalnız IP
ADD AX, 1
RET
uzak PROC
SHL AX, 1
RETF           ; IP ve CS çeker!
uzak ENDP
Tablo 8.2.28: Yakın CALL, uzak dönüş izlemesi (her satır yürütüldükten sonraki durum).
SatırKomutAXCS:IP (sonraki)SPYığın (tepeden)
2MOV AX, 50005h0700:0103FFFEh0000h
3CALL uzak0005h0700:010AFFFCh0106h, 0000h
7SHL AX, 1000Ah0700:010CFFFCh0106h, 0000h
8RETF000Ah0000:01060000hboş

Yakın CALL yığına yalnız IP'yi (0106h) iter. RETF ise iki sözcük çeker. İlki IP olur: 0106h, bu doğrudur. İkincisi CS olur. Yığında CS bulunmadığı için RETF bir alttaki sözcüğü, DOS'un .COM programı için ittiği 0000h'yi alır. SP 4 artar ve 0000h olur. İşlemci 0000:0106h adresine atlar (Tablo 8.2.28).

AX0005hIP010AhCS0700hSPFFFChyığınFFFC0106hSPFFFE0000h AX000AhIP0106hCS0000hSP0000hyığınFFFC····FFFE····
Şekil 8.2.24: Solda yakın CALL'dan sonra: yığında yalnız IP (2 bayt). Sağda RETF'ten sonra: CS = 0000h, IP = 0106h, SP = 0000h.

0000:0106h fiziksel adresi 00106h'dir. Bu adres kesme vektör tablosunun içindedir (00000h–003FFh); tip 41h'nin vektörü 00104h–00107h baytlarındadır. Orada program kodu değil, kesme yordamlarının adresleri vardır. İşlemci bu baytları komut sanıp yürütür ve program çöker. ADD AX, 1 hiç çalışmaz. Ters uyuşmazlıkta, yani CALL FAR ile çağrılan prosedür yakın RET ile bittiğinde, CS yığında kalır ve dönüş yine yanlış olur.

8.2.8.3Hata listesi

Prosedürlerde en sık yapılan hatalar Tablo 8.2.29'da toplanmıştır. Çoğu assembler'ın yakalayamadığı, yalnız çalışma anında ortaya çıkan hatalardır.

Tablo 8.2.29: Prosedürlerde sık yapılan hatalar.
HataNe olur?Önlem
RET'i unutmakİşlemci ENDP'yi görmez; prosedürden sonraki kod ya da veri yürütülür.Her prosedürün her yolu RET ile biter.
PUSH / POP dengesizliğiRET yanlış değeri IP'ye alır.Her PUSH'un ters sırada bir POP'u olur.
Prosedüre düşmekAna kodun ortasındaki prosedür CALL edilmeden yürütülür; sonundaki RET programı bitirir.Prosedürler ana programın RET'inden sonra yazılır ya da üzerinden JMP ile atlanır.
CALL yerine JMPJMP dönüş adresi itmez; RET yığındaki ilgisiz bir değere döner.Prosedüre yalnız CALL ile girilir.
Ad uyuşmazlığıp1 PROC … p2 ENDP: assembler hatası.Başta ve sonda aynı ad.
Erken RETPUSH'tan sonra koşullu bir RET, saklanan değeri dönüş adresi sanır.Bütün yollar tek çıkış etiketine gider.
LEA yerine MOVMOV SI, dizi adresi değil dizinin ilk sözcüğünü yükler; prosedür ilgisiz bir adresten okur.Adres için LEA ya da MOV SI, OFFSET dizi.
Gizlice bozulan DX16 bitlik MUL ve DIV DX'i de yazar; çağıranın DX'i silinir.Açıklamaya “Bozar: DX” ya da PUSH/POP DX.
Tür uyuşmazlığıYakın CALL + RETF ya da CALL FAR + RET: yığın 2 bayt kayar.Tür PROC satırında; dönüş için hep RET.
Bayrağı bozmakSTC/CLC'den sonra ADD, CMP gibi bir komut CF'yi değiştirir.Bayrağı kuran komut RET'ten önceki son bayrak değiştiren komut olur.
Sonuç yazmacını korumakPUSH AX / POP AX, AX'teki sonucu siler.Çıktı yazmaçları korunmaz.
Sonsuz özyinelemeDurma koşulu yoksa ya da CALL'dan sonra denetleniyorsa her çağrı 2 bayt yer, yığın taşar.Taban durum CALL'dan önce denetlenir.
Sık yapılan hata

Prosedür yerine JMP kullanmak. Aşağıdaki programda CALL ekle yerine JMP ekle yazılmıştır:

ORG 100h
MOV AX, 1
JMP ekle
MOV BX, 9      ; çalışır mı?
RET
ekle PROC
ADD AX, 5
RET
ekle ENDP

JMP yığına bir şey itmez. ekle'nin RET'i yığının tepesindeki DOS'un 0000h'sini alır ve program orada, AX = 6 iken sessizce biter. MOV BX, 9 hiç çalışmaz. Program çökmediği için hata fark edilmeyebilir. Yığında başka bir değer olsaydı RET rastgele bir adrese dönerdi.

8.2.9Alıştırmalar ve çözümleri

Sunumdaki altı alıştırma burada ara adımlarıyla çözülmüştür.

1. 0120h adresindeki CALL p komutu 0100h'daki p'yi çağırıyor. Makine kodu nedir? Yığına ne itilir?

CALL 3 bayttır; sonraki komut 0123h'dedir. Uzaklık = 0100h − 0123h = −23h. 16 bitlik ikiye tümleyende −23h = FFDDh'dir. Düşük bayt önce yazılır: E8 DD FF. Yığına dönüş adresi olarak 0123h itilir. Sağlama: 0123h + FFDDh = 10100h, 16 bite indirgenince 0100h.

2. m2 örneğinde ikinci CALL m2'den önce MOV AL, 3 eklenseydi son AX ne olurdu?

Birinci çağrı AX = 1 × 2 = 0002h verir. Eklenen komut AL'yi 3 yapar; AH = 00h kalır. İkinci çağrı AX = 3 × 2 = 0006h verir. Bu kez ikinci çağrının birinci parametresi birinci çağrının sonucuna bağlı değildir. Eklenen komut 2 bayt olduğu için ikinci CALL'ın adresi ve dönüş adresi 2 bayt kayar, ama sonuç değişmez.

3. SP = FFFEh iken art arda 3 CALL yapılıyor (hiç RET yok). SP kaç olur?

Her yakın CALL yığına 2 bayt iter: FFFEh − 3 × 2 = FFFEh − 6 = FFF8h. Yığında üç dönüş adresi vardır. Çağrılar uzak olsaydı her biri 4 bayt iterdi ve SP = FFF2h olurdu.

4. CX'i bozan bir prosedürü LOOP içinde çağırmak neden hatalıdır? İki çözüm önerin.

LOOP, sayacı CX'te tutar. Prosedür CX'i değiştirirse dönüşte LOOP çağıranın sayacı yerine prosedürün bıraktığı değeri azaltır. Döngü yanlış sayıda döner; prosedür CX = 0 bırakıyorsa LOOP onu FFFFh yapar ve döngü neredeyse hiç bitmez. Birinci çözüm, prosedürün girişte PUSH CX, çıkışta POP CX yapmasıdır. İkinci çözüm, çağıranın CALL'ın çevresinde PUSH CX / CALL / POP CX yazmasıdır. CX prosedürün çıktısıysa yalnız ikinci çözüm uygulanabilir.

5. AL'deki küçük harfi büyük harfe çeviren buyuk prosedürünü yazın (girdi/çıktı AL).

buyuk PROC        ; Girdi/Çıktı: AL
cmp al, 'a'
jb son            ; 'a'dan küçük: değiştirme
cmp al, 'z'
ja son            ; 'z'den büyük: değiştirme
sub al, 20h       ; 'a' (61h) → 'A' (41h)
son: ret
buyuk ENDP

ASCII tablosunda küçük ve büyük harfler arasında sabit 20h fark vardır. Prosedür yalnız 'a'–'z' aralığındaki karakterleri değiştirir; JB ve JA işaretsiz karşılaştırmadır. Başka yazmaç kullanmadığı için PUSH/POP gerekmez. 'a', 'Z', '{' ve '`' karakterleriyle denendiğinde sonuç 'A', 'Z', '{' ve '`' olur; aralığın iki yanındaki karakterler değişmez.

6. print_me'yi, yazdığı karakter sayısını CX'te döndürecek şekilde değiştirin.

Prosedürün başına MOV CX, 0, döngüde ADD SI, 1'in yanına INC CX eklenir. CX çıktı olduğu için korunmaz. 'Hello World!' için dönüşte CX = 12 olur. INC CX bayrakları değiştirir, ama döngünün başındaki CMP her turda ZF'yi yeniden kurduğu için sonuç etkilenmez.

8.2.10Özet

  • Prosedür, belirli bir görevi yapan, CALL ile çağrılıp RET ile çağrının arkasına dönen kod parçasıdır. Kod bir kez yazılır, birçok yerden çağrılır.
  • PROC / ENDP ve END direktiftir; makine kodu üretmez. CALL ve RET işlemci komutudur. Prosedürler ana programın RET'inden sonra yazılır; yoksa akış prosedüre düşer.
  • CALL sonraki komutun adresini yığına iter (SP − 2) ve IP'yi hedefe çevirir. Yakın doğrudan CALL hedefi bağıl uzaklık olarak saklar: rel16 = hedef − (CALL adresi + 3).
  • RET: IP ← [SP], SP + 2. .COM programında son RET, DOS'un ittiği 0000h'yi alır ve PSP'deki INT 20h programı bitirir.
  • Uzak CALL önce CS'yi, sonra IP'yi iter (4 bayt); RETF önce IP'yi, sonra CS'yi çeker. Çağrı ile dönüşün türü uyuşmalıdır. Dolaylı CALL hedefi yazmaçtan ya da bellekten alır; atlama tablolarının temelidir.
  • Çağrı yükü: CALL 19, RET 8, PUSH 11, POP 8 tur. İki yazmaç saklayan prosedürün yükü 65 turdur (5 MHz'de 13 µs). Çok kısa ve sık işler için makro daha uygundur.
  • Parametreler yazmaçla, bellek değişkeniyle, adresle (SI/DI/BX) ya da yığınla geçirilir. Değerle geçirmede kopya, adresle geçirmede yer verilir. Sonuç yazmaçla, CF ile ya da bellekle döner.
  • Yazmaçların korunması: prosedür kullandığı yazmaçları PUSH ile saklar ve ters sırayla POP eder; çıktı yazmaçları korunmaz. PUSH yapan prosedürün bütün yolları tek çıkış noktasında birleşir.
  • Yığın çerçevesi: PUSH BP · MOV BP, SP · SUB SP, n. Parametreler [BP+4], [BP+6]…, yerel değişkenler [BP−2], [BP−4]… adresindedir. Çıkış: MOV SP, BP · POP BP · RET n.
  • Özyineleme: prosedür kendini çağırır; her düzey yığında kendi dönüş adresini ve değerlerini tutar. Taban durum CALL'dan önce denetlenir. Yığın gereksinimi en derin çağrı yoluna göre hesaplanır ve kesmeler için pay bırakılır.
  • Hataların çoğunun kökü, RET anında yığının tepesinde dönüş adresinin olmamasıdır: unutulan POP, erken RET, CALL yerine JMP, tür uyuşmazlığı.

8.2.11Kavram listesi

Terimlerin İngilizce karşılıkları ve öteki bölümlerdeki kullanımları terim sözlüğünde bulunur.

8.2.12Kendini Dene

  1. 0150h adresindeki CALL p, 0180h'deki p'yi çağırıyor. Komutun baytları nedir, yığına hangi değer itilir?

    Cevap

    Sonraki komut 0153h'dedir. Uzaklık 0180h − 0153h = 002Dh: E8 2D 00. Yığına 0153h itilir.

  2. SP = FFFEh iken çağıran iki parametre itip yakın CALL yapıyor. Prosedür PUSH BP, MOV BP, SP, SUB SP, 4 ile başlıyor. BP ve SP kaçtır? Birinci itilen parametre ve iki yerel değişken hangi adreslerdedir?

    Cevap

    İki parametre (4 bayt), CALL (2) ve PUSH BP (2): BP = FFFEh − 8 = FFF6h. SUB SP, 4'ten sonra SP = FFF2h. Birinci parametre [BP+6] = FFFCh'de, ikinci [BP+4] = FFFAh'dedir. Yereller [BP−2] = FFF4h ve [BP−4] = FFF2h'dedir. MOV SP, BP, POP BP ve RET 4'ten sonra SP = FFFEh olur.

  3. AX = 1111h, BX = 2222h iken çağrılan bir prosedür PUSH AX, PUSH BX, MOV AX, 0, MOV BX, 0, POP AX, POP BX, RET yürütüyor. Dönüşte AX ve BX nedir? RET doğru adrese döner mi?

    Cevap

    Yığının tepesinde BX'in değeri (2222h) vardır; ilk POP onu AX'e alır. AX = 2222h, BX = 1111h olur: iki yazmaç yer değiştirmiştir. PUSH ve POP sayısı eşit olduğu için yığın dengelidir ve RET doğru döner. Hata yalnız yazmaç değerlerinde görülür.

  4. Sonucunu CF ile döndüren bir prosedür STC'den sonra OR AX, AX, POP CX ve RET yürütüyor. Çağıran CF'yi kaç bulur?

    Cevap

    CF = 0. OR mantık komutudur ve CF ile OF'yi her zaman sıfırlar. STC'nin kurduğu değer kaybolur. POP ve RET bayraklara dokunmaz; bayrağı kuran komut RET'ten önceki son bayrak değiştiren komut olmalıdır.

  5. 0700:0103 adresindeki CALL FAR PTR uz, 0700:0109'daki FAR prosedürü çağırıyor. Komut kaç bayttır? SP = FFFEh ise yığına ne yazılır? Prosedürün RET'i hangi kodla üretilir?

    Cevap

    Uzak doğrudan CALL 5 bayttır: 9A 09 01 00 07 (önce bağıl konum, sonra kesim). Dönüş adresi 0103h + 5 = 0108h'dir. Önce CS itilir: [FFFCh] = 0700h; sonra IP: [FFFAh] = 0108h. SP = FFFAh olur. FAR prosedürdeki RET, CB (RETF) olarak kodlanır ve dönüşte SP = FFFEh olur.

  6. main, P'yi çağırıyor; P, AX, BX ve CX'i itip Q'yu çağırıyor; Q, PUSH BP ve SUB SP, 4 yapıyor. Program SP = FFFEh ile başlıyorsa SP en çok kaça iner? Bir kesme için pay bırakılırsa en az kaça kadar yer ayrılmalıdır?

    Cevap

    2 (CALL P) + 6 (üç PUSH) + 2 (CALL Q) + 2 (PUSH BP) + 4 (yerel alan) = 16 bayt. SP en çok FFFEh − 10h = FFEEh'ye iner. Bir kesme 6 bayt daha iter: FFE8h. Kesme yordamının kendi saklamaları da hesaba eklenir.

  7. Özyinelemeli yaz prosedürü AX = 1000 ile çağrılıyor. Ekranda ne görünür? SP en çok kaça iner?

    Cevap

    Ekranda 1000 görünür; sıfırlar da birer basamaktır. 1000 dört basamaklıdır, dört düzey: 4 × (2 + 2) = 16 bayt. SP en çok FFFEh − 16 = FFEEh'ye iner.

  8. a değişkeni 0110h adresindedir ve 0005h değerini taşır. Bir programcı adresle geçirme için LEA SI, a yerine MOV SI, a yazıyor. SI kaç olur? Prosedür [SI]'dan okuduğunda nereye erişir?

    Cevap

    MOV SI, a değişkenin içeriğini yükler: SI = 0005h. Prosedür DS:0005h'den, yani .COM programında PSP'nin içinden okur. Okunan değer a ile ilgisizdir. Prosedür yazıyorsa PSP'yi bozar. Assembler bu hatayı yakalamaz, çünkü iki komut da geçerlidir.

  9. Yerel değişkenli kare_art prosedürü (x·x + x) AX = 300 ile çağrılıyor. Dönüşte AX ve DX nedir? Sonuç doğru mudur?

    Cevap

    300 × 300 = 90 000 = 15F90h. 16 bitlik MUL, DX = 0001h, AX = 5F90h verir; CF = OF = 1 olur. ADD AX, [BP−2]: 5F90h + 012Ch = 60BCh. Dönüşte AX = 60BCh, DX = 0001h'dir. Doğru sonuç 90 300 = 160BCh'dir ve 16 bite sığmaz. Prosedür yalnız AX'e baktığı için sonuç yanlıştır; üstelik çağıranın DX'i bozulmuştur.

8.2.13İleri okuma

  • Intel, The 8086 Family User's Manual (1979): CALL, RET, PUSH ve POP komutlarının işlem tanımları, makine kodları ve saat turu sayıları. Tablo 8.2.8'deki değerler bu kaynaktandır.
  • B. B. Brey, The Intel Microprocessors (8. baskı), “Procedures” kesimi: yakın ve uzak prosedürler, CALL ve RET'in biçimleri, yığınla parametre geçirme.
  • K. R. Irvine, Assembly Language for x86 Processors (7. baskı), “Procedures” bölümü: yığın çerçevesi, yerel değişkenler, özyineleme ve çağırma kuralları ayrıntılı örneklerle.
  • emu8086 belgeleri, 8086 Assembler Tutorial for Beginners, Part 8 “Procedures”: emu8086'da prosedür yazımı ve print_me örneği.

8.2.14Kaynaklar

  1. emu8086 belgeleri: 8086 Assembler Tutorial for Beginners (Part 8: Procedures).
  2. Intel, The 8086 Family User's Manual, 1979: CALL, RET ve yığın işlemleri.
  3. B. B. Brey, The Intel Microprocessors, 8. baskı, Pearson, 2009: Kesim 6.3 “Procedures”.
  4. K. R. Irvine, Assembly Language for x86 Processors, 7. baskı, Pearson, 2014: Bölüm 5 “Procedures”.

İlgili sayfalar: Bölüm 8.2 sunumu · Çalışma soruları · Terim sözlüğü · © Sercan Külcü. Tüm hakları saklıdır.