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.
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.
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
ENDBurada 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.
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 ENDPCALL 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.
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.
| Öğe | Türü | Görevi |
|---|---|---|
PROC / ENDP | assembler direktifi | Prosedürün başlangıç adresini ve türünü (NEAR/FAR) assembler'a bildirir. Makine koduna çevrilmez. |
CALL | işlemci komutu | Dönüş adresini yığına iter, prosedürün adresine atlar. Yakın doğrudan biçimin kodu E8'dir. |
RET | işlemci komutu | Dö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. |
END | assembler direktifi | Kaynak dosyanın bittiğini bildirir. |
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 ENDPPUBLIC 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.
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
ENDAçı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.
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.
| Adım | İşlem | IP | SP | Yığın |
|---|---|---|---|---|
| 0 | başlangıç, sıradaki komut CALL m1 | 0100h | FFFEh | [FFFEh] = 0000h |
| 1 | CALL çözüldü: IP sonraki komutu gösterir | 0103h | FFFEh | değişmedi |
| 2 | SP ← SP − 2, [SP] ← IP | 0103h | FFFCh | [FFFCh] = 0103h |
| 3 | IP ← 0103h + 0004h | 0107h | FFFCh | değişmedi |
| 4 | MOV BX, 5 yürütüldü | 010Ah | FFFCh | değişmedi |
| 5 | RET: IP ← [SP], SP ← SP + 2 | 0103h | FFFEh | [FFFCh]'deki 0103h artık geçersiz |
| 6 | MOV AX, 2 yürütüldü | 0106h | FFFEh | [FFFEh] = 0000h |
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.
| Satır | Komut | IP (sonraki) | SP | AX | BX | Yığın |
|---|---|---|---|---|---|---|
| — | başlangıç | 0100h | FFFEh | 0000h | 0000h | [FFFEh] = 0000h |
| 2 | CALL m1 | 0107h | FFFCh | 0000h | 0000h | [FFFCh] = 0103h |
| 6 | MOV BX, 5 | 010Ah | FFFCh | 0000h | 0005h | [FFFCh] = 0103h |
| 7 | RET | 0103h | FFFEh | 0000h | 0005h | [FFFEh] = 0000h |
| 3 | MOV AX, 2 | 0106h | FFFEh | 0002h | 0005h | [FFFEh] = 0000h |
| 4 | RET (DOS'a) | 0000h | 0000h | 0002h | 0005h | boş |
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.
Ş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.
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.
İ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.
- Sonraki komut. (a) 0107h + 3 = 010Ah. (b) 0120h + 3 = 0123h. Bu adresler aynı zamanda yığına itilecek dönüş adresleridir.
- Fark. (a) 010Bh − 010Ah = 0001h. (b) 0100h − 0123h = −23h. 16 bitlik ikiye tümleyende −23h = 10000h − 23h = FFDDh.
- Baytlar. Düşük bayt önce yazılır. (a)
E8 01 00. (b)E8 DD FF. - 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ı)?
| Komut | Kod | Bayt | Yığına / yığından | Açıklama |
|---|---|---|---|---|
| CALL yakın | E8 rel16 | 3 | IP | Aynı kesimde doğrudan çağrı (varsayılan). |
| CALL yazmaç/bellek | FF /2 | 2–4 | IP | Dolaylı yakın çağrı: CALL BX, CALL [tablo+SI]. |
| CALL FAR | 9A ofs seg | 5 | CS, sonra IP | Başka kesimdeki prosedür; CS:IP birlikte değişir. |
| RET | C3 | 1 | IP | Yakın dönüş. |
| RETF | CB | 1 | IP, sonra CS | Uzak dönüş (FAR prosedür). |
| RET n | C2 n16 | 3 | IP; SP += n | Dö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.
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.
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 ENDPKodun 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.
| Satır | Komut | AX | CS:IP (sonraki) | SP | Yığın (tepeden) |
|---|---|---|---|---|---|
| 2 | MOV AX, 5 | 0005h | 0700:0103 | FFFEh | 0000h |
| 3 | PUSH CS | 0005h | 0700:0104 | FFFCh | [FFFCh] = 0700h, 0000h |
| 4 | CALL uzak | 0005h | 0700:010B | FFFAh | [FFFAh] = 0107h, 0700h, 0000h |
| 8 | SHL AX, 1 | 000Ah | 0700:010D | FFFAh | 0107h, 0700h, 0000h |
| 9 | RETF | 000Ah | 0700:0107 | FFFEh | 0000h |
| 5 | ADD AX, 1 | 000Bh | 0700:010A | FFFEh | 0000h |
İ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.
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
RETtablo, 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.
| Satır | Komut | AX | BX | IP (sonraki) | SP | [FFFCh] |
|---|---|---|---|---|---|---|
| 1 | MOV AX, 6 | 0006h | 0000h | 0103h | FFFEh | — |
| 2 | MOV SI, 2 | 0006h | 0000h | 0106h | FFFEh | — |
| 3 | CALL tablo[SI] | 0006h | 0000h | 0116h | FFFCh | 010Ah |
| 10 | iki_kat: SHL AX, 1 | 000Ch | 0000h | 0118h | FFFCh | 010Ah |
| 11 | RET | 000Ch | 0000h | 010Ah | FFFEh | (geçersiz) |
| 4 | MOV BX, OFFSET bir_ekle | 000Ch | 0114h | 010Dh | FFFEh | — |
| 5 | CALL BX | 000Ch | 0114h | 0114h | FFFCh | 010Fh |
| 8 | bir_ekle: INC AX | 000Dh | 0114h | 0115h | FFFCh | 010Fh |
| 9 | RET | 000Dh | 0114h | 010Fh | FFFEh | (geçersiz) |
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, carpKod 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'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.
| Komut | Saat turu | Komut | Saat turu |
|---|---|---|---|
| CALL yakın | 19 | RET | 8 |
| CALL yazmaç | 16 | RETF | 18 |
| CALL FAR | 28 | RET n | 12 |
| PUSH yazmaç | 11 | POP 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.
İ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?
- 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.
- Toplam. 19 + 8 + 2 × (11 + 8) = 27 + 38 = 65 tur.
- Süre. 5 MHz'de bir tur 1 / 5 MHz = 200 ns'dir. 65 × 200 ns = 13 000 ns = 13 µs.
- 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.
Şekil 8.2.10'deki dört yolun her birinin bir yeri vardır.
- 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.
- Bellek değişkenleriyle. Prosedür
number1,resultgibi 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. - 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.
- Yığınla. Parametreler CALL'dan önce PUSH ile yığına itilir. Prosedür onları BP yazmacı üzerinden okur. Dönüşte
RET nya da çağırandaADD SP, nile 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?
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.
| Satır | Komut | SI | DI | AX | SP | a [0113h] | b [0115h] |
|---|---|---|---|---|---|---|---|
| 2–3 | LEA SI, a · LEA DI, b | 0113h | 0115h | 0000h | FFFEh | 1234h | 00FFh |
| 4 | CALL takas | 0113h | 0115h | 0000h | FFFCh | 1234h | 00FFh |
| 7 | PUSH AX | 0113h | 0115h | 0000h | FFFAh | 1234h | 00FFh |
| 8 | MOV AX, [SI] | 0113h | 0115h | 1234h | FFFAh | 1234h | 00FFh |
| 9 | XCHG AX, [DI] | 0113h | 0115h | 00FFh | FFFAh | 1234h | 1234h |
| 10 | MOV [SI], AX | 0113h | 0115h | 00FFh | FFFAh | 00FFh | 1234h |
| 11 | POP AX | 0113h | 0115h | 0000h | FFFCh | 00FFh | 1234h |
| 12 | RET | 0113h | 0115h | 0000h | FFFEh | 00FFh | 1234h |
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.
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.
| Kaynak | İşlem | Sonuç |
|---|---|---|
| 8 bit | AL × kaynak | AX |
| 16 bit | AX × kaynak | DX: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
ENDMOV 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.
| Satır | Komut | IP (sonraki) | SP | AX | BL | CF | [FFFCh] |
|---|---|---|---|---|---|---|---|
| 2 | MOV AL, 1 | 0102h | FFFEh | 0001h | 00h | 0 | — |
| 3 | MOV BL, 2 | 0104h | FFFEh | 0001h | 02h | 0 | — |
| 4 | CALL m2 (1.) | 010Bh | FFFCh | 0001h | 02h | 0 | 0107h |
| 8 | MUL BL | 010Dh | FFFCh | 0002h | 02h | 0 | 0107h |
| 9 | RET | 0107h | FFFEh | 0002h | 02h | 0 | (geçersiz) |
| 5 | CALL m2 (2.) | 010Bh | FFFCh | 0002h | 02h | 0 | 010Ah |
| 8 | MUL BL | 010Dh | FFFCh | 0004h | 02h | 0 | 010Ah |
| 9 | RET | 010Ah | FFFEh | 0004h | 02h | 0 | (geçersiz) |
| 6 | RET (DOS'a) | 0000h | 0000h | 0004h | 02h | 0 | — |
İ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 ENDPrakam 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.
| Satır | Komut | AL | BL | CF | SP | Açıklama |
|---|---|---|---|---|---|---|
| 1 | MOV AL, '7' | 37h | 00h | 0 | FFFEh | '7' = 37h |
| 2, 8 | CALL rakam · SUB AL, '0' | 07h | 00h | 0 | FFFCh | dönüş 0105h; 37h − 30h = 7 |
| 9 | CMP AL, 10 | 07h | 00h | 1 | FFFCh | 7 − 10 ödünç ister |
| 10 | CMC | 07h | 00h | 0 | FFFCh | “rakamdır” |
| 11, 3 | RET · MOV BL, AL | 07h | 07h | 0 | FFFEh | değer saklandı |
| 4, 5, 8 | MOV AL, 'x' · CALL · SUB | 48h | 07h | 0 | FFFCh | 78h − 30h = 48h (72); dönüş 010Ch |
| 9 | CMP AL, 10 | 48h | 07h | 0 | FFFCh | 72 ≥ 10, ödünç yok |
| 10 | CMC | 48h | 07h | 1 | FFFCh | “rakam değil” |
| 11 | RET | 48h | 07h | 1 | FFFEh | sonuç CF'de |
İ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.
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 ENDP8 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.
| Girdi | Sonuç | Açıklama |
|---|---|---|
| AX = 100, BL = 7 | AL = 0Eh (14), AH = 02h, CF = 0 | 100 = 7 × 14 + 2; AX = 020Eh |
| AX = 100, BL = 0 | CF = 1, AX = 0064h (değişmez) | DIV hiç yürütülmez |
| AX = 1000, BL = 2 | bö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, OFBu 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.
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 ENDPTers 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 ENDPMOV 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.
| Satır | Komut | CX | SP | Yığın (tepeden) | Ekran |
|---|---|---|---|---|---|
| 1 | MOV CX, 2 | 0002h | FFFEh | 0000h | |
| 2 | CALL yildiz | 0002h | FFFCh | 0106h, 0000h | |
| 6 | PUSH CX | 0002h | FFFAh | 0002h, 0106h, 0000h | |
| 7–8 | MOV CX, 3 · MOV AX, 0E23h | 0003h | FFFAh | 0002h, 0106h, 0000h | |
| 9–10 | iç döngü, 3 tur | 0000h | FFFAh | 0002h, 0106h, 0000h | ### |
| 11 | POP CX | 0002h | FFFCh | 0106h, 0000h | ### |
| 12 | RET | 0002h | FFFEh | 0000h | ### |
| 3 | LOOP satir | 0001h | FFFEh | 0000h | ### |
| 2–12 | ikinci çağrı | 0001h | FFFEh | 0000h | ###### |
| 3 | LOOP satir | 0000h | FFFEh | 0000h | ###### |
İ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.
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.
| Ölçüt | Çağrılan korur (prosedürde PUSH/POP) | Çağıran korur (CALL'ın çevresinde) |
|---|---|---|
| Kod nerede? | prosedürde bir kez | her çağrı noktasında |
| Gereksiz saklama | çağıran o yazmacı kullanmasa da saklanır | yalnız gerçekten gereken yazmaçlar |
| Unutma riski | düşük: tek yerde yazılır | yüksek: her çağrıda tekrar gerekir |
| Uygun olduğu durum | kü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 donguPUSH 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, 4bul 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.
| Satır | Komut | SI | CX | BX | CF | SP | Yığın (tepeden) |
|---|---|---|---|---|---|---|---|
| 2–4 | LEA, MOV, MOV | 011Fh | 0005h | 0000h | 0 | FFFEh | 0000h |
| 5 | CALL bul | 011Fh | 0005h | 0000h | 0 | FFFCh | 010Bh |
| 8–10 | PUSH CX, PUSH SI, XOR | 011Fh | 0005h | 0000h | 0 | FFF8h | 011Fh, 0005h, 010Bh |
| 11–15 | dizi[0] = 3 ≠ 7 | 0120h | 0004h | 0001h | 1 | FFF8h | 011Fh, 0005h, 010Bh |
| 11–15 | dizi[1] = 9 ≠ 7 | 0121h | 0003h | 0002h | 0 | FFF8h | 011Fh, 0005h, 010Bh |
| 11–12 | dizi[2] = 7: ZF = 1, JE atlar | 0121h | 0003h | 0002h | 0 | FFF8h | 011Fh, 0005h, 010Bh |
| 18 | buldu: STC | 0121h | 0003h | 0002h | 1 | FFF8h | 011Fh, 0005h, 010Bh |
| 19–20 | POP SI · POP CX | 011Fh | 0005h | 0002h | 1 | FFFCh | 010Bh |
| 21 | RET | 011Fh | 0005h | 0002h | 1 | FFFEh | 0000h |
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.
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 ENDPProgramı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.
| Satır | Komut | AX | BP | SP | IP (sonraki) | Yığına yazılan |
|---|---|---|---|---|---|---|
| 2 | MOV AX, 7 | 0007h | 0000h | FFFEh | 0103h | |
| 3 | PUSH AX | 0007h | 0000h | FFFCh | 0104h | [FFFCh] = 0007h |
| 4 | MOV AX, 3 | 0003h | 0000h | FFFCh | 0107h | |
| 5 | PUSH AX | 0003h | 0000h | FFFAh | 0108h | [FFFAh] = 0003h |
| 6 | CALL TOPLA | 0003h | 0000h | FFF8h | 010Fh | [FFF8h] = 010Bh |
| 10 | PUSH BP | 0003h | 0000h | FFF6h | 0110h | [FFF6h] = 0000h |
| 11 | MOV BP, SP | 0003h | FFF6h | FFF6h | 0112h | |
| 12 | MOV AX, [BP+6] | 0007h | FFF6h | FFF6h | 0115h | |
| 13 | ADD AX, [BP+4] | 000Ah | FFF6h | FFF6h | 0118h | |
| 14 | POP BP | 000Ah | 0000h | FFF8h | 0119h | |
| 15 | RET | 000Ah | 0000h | FFFAh | 010Bh | |
| 7 | ADD SP, 4 | 000Ah | 0000h | FFFEh | 010Eh | |
| 8 | RET (DOS'a) | 000Ah | 0000h | 0000h | 0000h |
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.
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.
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.
Ç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?
- İtmeler. Üç parametre 6 bayt, CALL 2 bayt, PUSH BP 2 bayt: FFFEh − 10 = FFF4h. BP = FFF4h olur.
- Uzaklıklar. [BP] eski BP, [BP+2] dönüş adresi, [BP+4] son itilen (üçüncü) parametre, [BP+6] ikinci, [BP+8] ilk itilen parametredir.
- 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.
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.
Ç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 atSUB 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| Satır | Komut | AX | DX | BP | SP | Yığın |
|---|---|---|---|---|---|---|
| 1 | MOV AX, 9 | 0009h | 0000h | 0000h | FFFEh | |
| 2 | CALL kare_art | 0009h | 0000h | 0000h | FFFCh | [FFFCh] = 0106h |
| 5 | PUSH BP | 0009h | 0000h | 0000h | FFFAh | [FFFAh] = 0000h |
| 6 | MOV BP, SP | 0009h | 0000h | FFFAh | FFFAh | |
| 7 | SUB SP, 2 | 0009h | 0000h | FFFAh | FFF8h | [FFF8h] = yerel alan |
| 8 | MOV [BP-2], AX | 0009h | 0000h | FFFAh | FFF8h | [FFF8h] = 0009h |
| 9 | MUL AX | 0051h | 0000h | FFFAh | FFF8h | |
| 10 | ADD AX, [BP-2] | 005Ah | 0000h | FFFAh | FFF8h | |
| 11 | MOV SP, BP | 005Ah | 0000h | FFFAh | FFFAh | yerel alan bırakıldı |
| 12 | POP BP | 005Ah | 0000h | 0000h | FFFCh | |
| 13 | RET | 005Ah | 0000h | 0000h | FFFEh | IP = 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.
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
ENDAna 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.
| An | SI | AL | ZF | SP | Ekran |
|---|---|---|---|---|---|
LEA SI, msg | msg+0 | — | 0 | FFFEh | |
CALL print_me | msg+0 | — | 0 | FFFCh | |
| 1. tur, CMP: 'H' ≠ 0 | msg+0 | — | 0 | FFFCh | |
| 1. tur, INT 10h | msg+0 | 48h | 0 | FFFCh | H |
| 1. tur, ADD SI, 1 | msg+1 | 48h | 0 | FFFCh | H |
| 2. tur sonu | msg+2 | 65h | 0 | FFFCh | He |
| 5. tur sonu | msg+5 | 6Fh | 0 | FFFCh | Hello |
| 11. tur sonu | msg+11 | 64h | 0 | FFFCh | Hello World |
| 12. tur sonu | msg+12 | 21h | 0 | FFFCh | Hello World! |
| CMP: [SI] = 0, JE stop | msg+12 | 21h | 1 | FFFCh | Hello World! |
prosedürün RET'i | msg+12 | 21h | 1 | FFFEh | Hello World! |
ana programın RET'i | msg+12 | 21h | 1 | 0000h | Hello 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 tutarProgramı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.
| Satır | Komut | AX | SP | ZF | CF | result |
|---|---|---|---|---|---|---|
| 3 | CALL AddNumbers | 0000h | FFFCh | 0 | 0 | ? |
| 6 | MOV AX, number1 | 0005h | FFFCh | 0 | 0 | ? |
| 7 | ADD AX, number2 | 0008h | FFFCh | 0 | 0 | ? |
| 8 | MOV result, AX | 0008h | FFFCh | 0 | 0 | 0008h |
| 9 | RET | 0008h | FFFEh | 0 | 0 | 0008h |
| 4 | RET (DOS'a) | 0008h | 0000h | 0 | 0 | 0008h |
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| Satır | Komut / tur | SI | CX | AL | SP | m |
|---|---|---|---|---|---|---|
| 2–3 | LEA SI, vector · MOV CX, 5 | 0115h | 0005h | 00h | FFFEh | 00h |
| 4 | CALL topla | 0115h | 0005h | 00h | FFFCh | 00h |
| 8 | MOV AL, 0 | 0115h | 0005h | 00h | FFFCh | 00h |
| 10–12 | 1. tur: +5 | 0116h | 0004h | 05h | FFFCh | 00h |
| 10–12 | 2. tur: +4 | 0117h | 0003h | 09h | FFFCh | 00h |
| 10–12 | 3. tur: +5 | 0118h | 0002h | 0Eh | FFFCh | 00h |
| 10–12 | 4. tur: +2 | 0119h | 0001h | 10h | FFFCh | 00h |
| 10–12 | 5. tur: +1 | 011Ah | 0000h | 11h | FFFCh | 00h |
| 13 | RET | 011Ah | 0000h | 11h | FFFEh | 00h |
| 5 | MOV m, AL | 011Ah | 0000h | 11h | FFFEh | 11h |
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, ?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.
| Durum | AL | CX | CF | BX | Değerlendirme |
|---|---|---|---|---|---|
| ilk eleman | 3 | 5 | 1 | 0 | doğru |
| son eleman | 4 | 5 | 1 | 4 | doğru |
| dizide yok | 8 | 5 | 0 | 5 | doğru; BX anlamsızdır, yalnız CF'ye bakılır |
| tek elemanlık arama | 9 | 1 | 0 | 1 | doğru: 9 ikinci eleman, aranan aralığın dışında |
| boş dizi | 7 | 0 | 1 | 2 | yanlış: 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 ENDPDü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.
| An | IP | SP | Yığın (tepeden) |
|---|---|---|---|
| main, CALL A'dan önce | 0100h | FFFEh | 0000h |
CALL A (0100h) | 0110h | FFFCh | 0103h, 0000h |
A içinden CALL B (0112h) | 0120h | FFFAh | 0115h, 0103h, 0000h |
B'nin RET'i | 0115h | FFFCh | 0103h, 0000h |
A'nın RET'i | 0103h | FFFEh | 0000h |
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.
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.
| Yol | Hesap | Bayt |
|---|---|---|
| main → A → B | 2 (CALL A) + 4 (PUSH AX, SI) + 2 (CALL B) + 2 (PUSH CX) | 10 |
| main → C | 2 (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.
Ö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.
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 ENDPProsedü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.
| Satır | Olay | AX | DX | SP | Yığın (tepeden) | Ekran |
|---|---|---|---|---|---|---|
| 2–3 | AX = 472, CALL yaz | 01D8h | 0000h | FFFCh | 0106h | |
| 6–9 | 1. düzey: 472 / 10, PUSH DX | 002Fh | 0002h | FFFAh | 2, 0106h | |
| 10–12 | AX ≠ 0: CALL yaz | 002Fh | 0002h | FFF8h | 0116h, 2, 0106h | |
| 6–12 | 2. düzey: 47 / 10, PUSH, CALL | 0004h | 0007h | FFF4h | 0116h, 7, 0116h, 2, 0106h | |
| 6–11 | 3. düzey: 4 / 10, PUSH; AX = 0, JZ son | 0000h | 0004h | FFF2h | 4, 0116h, 7, 0116h, 2, 0106h | |
| 13–16 | POP AX = 4, yaz | 0E34h | 0004h | FFF4h | 0116h, 7, 0116h, 2, 0106h | 4 |
| 17, 13–16 | RET → 0116h; POP AX = 7, yaz | 0E37h | 0004h | FFF8h | 0116h, 2, 0106h | 47 |
| 17, 13–16 | RET → 0116h; POP AX = 2, yaz | 0E32h | 0004h | FFFCh | 0106h | 472 |
| 17 | son RET → 0106h | 0E32h | 0004h | FFFEh | 0000h | 472 |
İ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).
Çı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.
AX = 0 ile çağrıldığında prosedür ne yazar? SP en çok kaça iner?
- Bölme. 0 / 10: AX = 0, DX = 0. Basamak 0 yığına itilir.
- Taban durum. AX = 0, JZ
son'a atlar; yeni çağrı yapılmaz. - Yazma. POP AX = 0, ADD AL, '0' → 30h; ekrana '0' yazılır.
- 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.
| Ölçüt | Özyineleme (yaz) | Döngü (bölme + LOOP) |
|---|---|---|
| Kod | kısa; problemin tanımına yakın | sayaç ve ayrı bir yazdırma döngüsü gerekir |
| Yığın | her düzeyde dönüş adresi + basamak: 4 bayt | basamak başına 2 bayt |
| Süre | her düzeyde CALL + RET (19 + 8 tur) | çağrı yükü yok |
| Risk | derinlik sınırsız büyürse yığın taşar | yığın kullanımı sınırlı |
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?
- Düzey sayısı. Düzey sayısı basamak sayısına eşittir: 65 535 beş basamaklıdır, beş düzey.
- Bayt. Her düzey bir dönüş adresi (2 bayt) ve bir basamak (2 bayt) ekler: 5 × 2 + 5 × 2 = 20 bayt.
- SP. FFFEh − 20 = FFFEh − 14h = FFEAh.
- 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| Satır | Komut | AX | IP (sonraki) | SP | Yığın (tepeden) |
|---|---|---|---|---|---|
| 2 | MOV AX, 1234h | 1234h | 0103h | FFFEh | 0000h |
| 3 | CALL hatali | 1234h | 0107h | FFFCh | 0106h, 0000h |
| 6 | PUSH AX | 1234h | 0108h | FFFAh | 1234h, 0106h, 0000h |
| 8 | RET | 1234h | 1234h | FFFCh | 0106h, 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| Satır | Komut | AX | CS:IP (sonraki) | SP | Yığın (tepeden) |
|---|---|---|---|---|---|
| 2 | MOV AX, 5 | 0005h | 0700:0103 | FFFEh | 0000h |
| 3 | CALL uzak | 0005h | 0700:010A | FFFCh | 0106h, 0000h |
| 7 | SHL AX, 1 | 000Ah | 0700:010C | FFFCh | 0106h, 0000h |
| 8 | RETF | 000Ah | 0000:0106 | 0000h | boş |
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).
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.
| Hata | Ne 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ği | RET yanlış değeri IP'ye alır. | Her PUSH'un ters sırada bir POP'u olur. |
| Prosedüre düşmek | Ana 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 JMP | JMP 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 RET | PUSH'tan sonra koşullu bir RET, saklanan değeri dönüş adresi sanır. | Bütün yollar tek çıkış etiketine gider. |
| LEA yerine MOV | MOV 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 DX | 16 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ğı bozmak | STC/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ı korumak | PUSH AX / POP AX, AX'teki sonucu siler. | Çıktı yazmaçları korunmaz. |
| Sonsuz özyineleme | Durma 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. |
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 ENDPJMP 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 ENDPASCII 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.
- prosedür procedure (subroutine)
- prosedür çağrısı procedure call
- dönüş adresi return address
- direktif directive
- etiket label
- yığın stack
- yığın işaretçisi (SP) stack pointer
- taban işaretçisi (BP) base pointer
- yığına itme / yığından alma (PUSH / POP) push / pop
- LIFO (son giren ilk çıkar) last in, first out
- parametre geçirme parameter passing
- değerle / adresle parametre geçirme pass by value / by reference
- yazmaçların korunması register preservation
- yığın çerçevesi stack frame
- yerel değişken local variable
- özyineleme recursion
- yığın taşması / alt taşması stack overflow / underflow
- bağıl konum offset
- kesim segment
- bayrak flag
- little-endian little-endian
8.2.12Kendini Dene
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.SP = FFFEh iken çağıran iki parametre itip yakın CALL yapıyor. Prosedür
PUSH BP,MOV BP, SP,SUB SP, 4ile 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 BPveRET 4'ten sonra SP = FFFEh olur.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,RETyü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.
Sonucunu CF ile döndüren bir prosedür
STC'den sonraOR AX, AX,POP CXveRETyü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.
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.main, P'yi çağırıyor; P, AX, BX ve CX'i itip Q'yu çağırıyor; Q,
PUSH BPveSUB SP, 4yapı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.
Özyinelemeli
yazprosedü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.
adeğişkeni 0110h adresindedir ve 0005h değerini taşır. Bir programcı adresle geçirme içinLEA SI, ayerineMOV SI, ayazıyor. SI kaç olur? Prosedür[SI]'dan okuduğunda nereye erişir?Cevap
MOV SI, adeğ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ğeraile ilgisizdir. Prosedür yazıyorsa PSP'yi bozar. Assembler bu hatayı yakalamaz, çünkü iki komut da geçerlidir.Yerel değişkenli
kare_artprosedü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
- emu8086 belgeleri: 8086 Assembler Tutorial for Beginners (Part 8: Procedures).
- Intel, The 8086 Family User's Manual, 1979: CALL, RET ve yığın işlemleri.
- B. B. Brey, The Intel Microprocessors, 8. baskı, Pearson, 2009: Kesim 6.3 “Procedures”.
- 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.