Mikroişlemciler · Ders Notu
Bölüm 8.1: Yığın
Bir program çalışırken pek çok değerin kısa bir süre için bir kenara konması gerekir. Bir yazmacın eski değeri, bir prosedürün dönüş adresi, kesilen programın bayrakları bunların başında gelir. 8086 bu işler için belleğin bir bölümünü yığın olarak kullanır. Yığına en son konan değer ilk geri alınır. Not, yığın kavramıyla ve son giren ilk çıkar düzeniyle başlar. Ardından yığının SS ve SP yazmaçlarıyla nasıl adreslendiğini, neden aşağı doğru büyüdüğünü ve 8086'nın yığına hangi yol çevrimleriyle eriştiğini anlatır. Sonra PUSH, POP, PUSHF ve POPF komutlarının yazmaçlara, bayraklara ve belleğe etkisini tek tek ele alır. Sunumdaki bütün örnekler yazmaçlar ve yığınla birlikte adım adım izlenir: değer saklama, takas, üç yazmacın döndürülmesi ve bir dizinin yığınla ters çevrilmesi. Son kesim dengesiz PUSH ve POP, yığın taşması ve alt taşma gibi hatalara ve bunlardan korunma yollarına ayrılmıştır.
Öğrenme çıktıları
- Yığın modelini açıklamak
- Yığının son giren ilk çıkar (LIFO) düzenini, 8086 yığınının SS:SP ile nasıl adreslendiğini ve neden aşağı doğru büyüdüğünü açıklamak; yığının üstünün fiziksel adresini hesaplamak.
- PUSH ve POP'u izlemek
- PUSH ve POP için geçerli işlenen türlerini bilmek, her komutta SP'nin ve yığın belleğinin nasıl değiştiğini bayt düzeyinde adım adım göstermek.
- Yazmaçları saklamak ve takas etmek
- Yığını bir yazmacın değerini korumak, geri yüklemek, iki yazmacı takas etmek ve bir diziyi ters çevirmek için doğru sırayla kullanmak.
- Bayrakları korumak
- PUSHF ve POPF ile bayrak yazmacını yığında saklamak, bir işlemin bozduğu bayrakları geri yüklemek ve bayrak sözcüğünü bit bit yorumlamak.
- Yığın hatalarını tanımak
- Dengesiz PUSH ve POP, yanlış sıralama, yığın taşması ve alt taşma hatalarının belirtilerini tanımak ve bunları önleyen yazım kurallarını uygulamak.
- emu8086 ile yığını izlemek
- Bir programı tek adım çalıştırırken yazmaç panelini, yığın penceresini ve bellek görünümünü yorumlamak.
8.1.1Yığın kavramı
8.1.1.1Yığın nedir?
8086'nın sekiz genel amaçlı yazmacı vardır. Bir program çoğu zaman bu yazmaçların hepsini kullanır ve bir noktada yer darlığı başlar. Örneğin AX bir toplamı tutarken AX'e başka bir iş için de gerek duyulur. Toplamı bir değişkene yazıp sonra geri okumak bir çözümdür. Ama her ara değer için ayrı bir değişken tanımlamak programı kalabalıklaştırır. Ayrıca kaç değerin saklanacağı her zaman önceden bilinmez. Bir prosedür başka bir prosedürü, o da bir başkasını çağırabilir. Her çağrının dönüş adresi bir yerde durmalıdır ve çağrı derinliği programın akışına bağlıdır.
Bu ihtiyaçların ortak bir özelliği vardır: en son saklanan değer ilk geri istenir. En içteki prosedür önce biter, dönüş adresi en son saklanmış olan adrestir. Bir yazmaç geçici olarak kullanıldığında da en son saklanan değer ilk geri yüklenir. 8086 bu düzene uyan bir bellek bölgesini donanım düzeyinde destekler.
Yığın (stack), geçici verileri saklamak için ayrılmış ve son giren ilk çıkar düzeninde kullanılan bir bellek alanıdır. 8086'da yığının yeri SS (yığın kesimi) ve SP (yığın işaretçisi) yazmaçlarıyla belirlenir. Yığına değer koymaya itme (push), yığından değer almaya çekme ya da alma (pop) denir.
Programcı yığına iki komutla erişir. PUSH 16 bitlik bir değeri yığına koyar. POP 16 bitlik bir değeri yığından alır ve bir hedefe yazar. Yığın yalnız programcının değil, işlemcinin de kullandığı bir alandır. CALL komutu bir prosedürü çağırırken dönüş adresini yığına koyar. Prosedürün sonundaki RET bu adresi yığından alır ve oraya döner. INT bir kesmeyi çağırdığında benzer bir iş yapılır. Bayrak yazmacı, kod kesimi (CS) ve komut işaretçisi (IP) yığına saklanır; IRET ile kesmeden dönülür ve üç değer geri yüklenir. İşletim sisteminin ve BIOS'un kesme yordamları da yazmaçları korumak için aynı yığını kullanır.
Yığının bu kadar çok kullanıcısı olması bir sorumluluk da getirir. Programcının yığına koyduğu her değer, işlemcinin dönüş adresleriyle aynı alanda durur. Programcı bir değeri yığında unutursa ya da fazladan bir değer alırsa, işlemcinin dönüş adresi kayar ve program yanlış bir adrese döner. Yığın güvenliği bu yüzden ayrı bir kesimde ele alınır.
8086'da yığının birimi sözcüktür. Yığına her zaman 2 bayt yazılır ve yığından her zaman 2 bayt okunur. Bu yüzden PUSH AL gibi 8 bitlik bir işlenen geçersizdir. Bir baytı saklamak gerekirse onu içeren 16 bitlik yazmaç itilir; PUSH AX hem AL'yi hem de AH'yi yığına koyar. Yığının bir yönü de vardır. Yığın yüksek adresten düşük adrese doğru, yani aşağı doğru büyür. Bu iki kural, PUSH ve POP komutlarının bütün ayrıntısını belirler.
| Kullanan | Komut | Yığına konan | Geri alan |
|---|---|---|---|
| programcı | PUSH, PUSHF | yazmaç, kesim yazmacı, bellek sözcüğü, bayraklar | POP, POPF |
| işlemci (prosedür) | CALL | dönüş adresi (yakın çağrıda IP, uzak çağrıda CS ve IP) | RET, RETF |
| işlemci (kesme) | INT n, donanım kesmesi | FLAGS, CS ve IP | IRET |
| işletim sistemi, BIOS | kesme yordamları | yordamın kullandığı yazmaçlar | yordamın sonundaki POP'lar |
Tablo 8.1.1 yığının dört kullanıcısını özetler. Bu notun konusu ilk satırdır: programcının PUSH, POP, PUSHF ve POPF ile yığını kullanması. CALL ve RET'in yığına etkisi yalnız yığının bakış açısından, kısa olarak anılır. Prosedürlerin tanımı, parametre geçirme ve yığın çerçevesi Prosedürler notunun konusudur.
8.1.1.2Son giren ilk çıkar (LIFO)
Yığın kelimesi günlük hayattaki anlamıyla da bu düzeni iyi anlatır. Masaya üst üste konan tabaklar düşünülebilir. Yeni bir tabak her zaman en üste konur. Bir tabak alınacağı zaman da en üstteki alınır. Ortadaki bir tabağa ulaşmak için önce üstündekilerin hepsi kaldırılmalıdır. En son konan tabak ilk alınır, ilk konan tabak en son alınır.
LIFO (last in, first out), “son giren ilk çıkar” düzenidir. Bir yapıya eklenen son öğe, yapıdan çıkan ilk öğe olur. Yığın LIFO düzeninde çalışır; yalnız en üstteki öğeye erişilir. En üstteki öğeye yığının üstü (top of stack) denir.
LIFO'nun tersi FIFO'dur (first in, first out): ilk giren ilk çıkar. Bir bankadaki sıra ya da klavye tamponu FIFO düzeninde çalışır. İki düzenin farkı en kolay bir sayı dizisiyle görülür. Yığına sırasıyla 1, 2, 3, 4 ve 5 konsun. İlk alınan değer 5 olur. Ardından 4, 3, 2 alınır ve en son 1 alınır. Aynı değerler bir FIFO kuyruğuna konsaydı 1, 2, 3, 4, 5 sırasıyla çıkardı.
Şekil 8.1.1 bu süreci üç anda gösterir. Şekildeki yığın kavramsal bir çizimdir; bellekteki adresler henüz yoktur. Önemli olan, her POP'un o anda en üstte duran değeri almasıdır. Üçüncü POP 3'ü alır, çünkü 5 ile 4 daha önce alınmıştır ve yığında 2 ile 1 kalır.
LIFO düzeninin programlamadaki iki sonucu vardır. Birincisi, yığın değerleri ters sırada geri verir. Bu özellik bilerek kullanılabilir: bir diziyi yığına sırayla itip sonra çekmek, diziyi ters çevirir. İkincisi, birkaç yazmaç saklanıp sonra eski yerlerine geri yüklenecekse POP sırası PUSH sırasının tersi olmalıdır. AX, BX ve CX bu sırayla itildiyse geri yükleme sırası CX, BX, AX'tir. Bu kurala ayna sırası denir. Kod alt alta yazıldığında PUSH'lar ile POP'lar bir aynadaki görüntü gibi durur.
Yazmaçları PUSH AX, PUSH BX sırasıyla itip POP AX, POP BX sırasıyla geri almak. Bu dizi yazmaçları korumaz, değerlerini takas eder: BX en son itildiği için ilk POP onu AX'e yazar. Hata derleme sırasında fark edilmez, çünkü dört komut da geçerlidir. Program yalnız yanlış sonuç üretir.
8.1.1.3Yığın kesimi: SS ve SP
8086'nın belleği kesimlere ayrılmıştır. Her bellek erişiminde 16 bitlik bir kesim değeri ile 16 bitlik bir bağıl konum birleştirilir. Kod için CS ve IP, veri için çoğu zaman DS ve bir adresleme ifadesi kullanılır. Yığın için de ayrılmış bir yazmaç çifti vardır.
SS (stack segment, yığın kesimi), yığının bulunduğu 64 KB'lık kesimin başlangıcını tutan kesim yazmacıdır. SP (stack pointer, yığın işaretçisi), yığının üstündeki sözcüğün bu kesim içindeki bağıl konumunu tutan 16 bitlik yazmaçtır. SS:SP her zaman yığının üstünü, yani en son konan sözcüğü gösterir. Yığının üstünün fiziksel adresi SS × 10h + SP'dir.
Fiziksel adres hesabı, kod ve veri erişimindeki hesapla aynıdır. Kesim değeri 16 ile çarpılır; onaltılık tabanda bu, sayının sağına bir 0 eklemek demektir. Sonra bağıl konum eklenir ve 20 bitlik adres bulunur.
emu8086 bir .COM programını yüklediğinde bütün kesim yazmaçlarını 0700h yapar ve SP'yi FFFEh'ye ayarlar. Yığının üstünün fiziksel adresi şöyle bulunur:
- Yığın kesimi: SS = 0700h.
- Kesim değeri 16 ile çarpılır: 0700h × 10h = 07000h.
- Bağıl konum eklenir: 07000h + FFFEh = 16FFEh.
Yığının üstü 16FFEh fiziksel adresindedir. Toplamada elde oluşur: 7000h + FFFEh = 16FFEh, yani sonuç 16 biti aşar. Adres yolu 20 bit olduğu için bu taşma kaybolmaz; 20 bitlik adreste bit 16 olarak yer alır.
SS ve SP'nin başlangıç değerlerini çoğu zaman programcı değil, programı belleğe yükleyen işletim sistemi belirler (Şekil 8.1.2). Bir .COM programında kod, veri ve yığın tek bir 64 KB'lık kesimi paylaşır. Yükleyici SS'yi CS ile aynı yapar ve SP'yi kesimin sonuna, FFFEh'ye ayarlar. Bir .EXE programında ise yığın ayrı bir kesimdir. Kesimin yeri ve SP'nin başlangıç değeri .EXE dosyasının başlığında yazılıdır; yükleyici bu değerleri SS ve SP'ye koyar. Kaynak kodda .STACK 100h gibi bir yönerge 256 baytlık bir yığın kesimi ayırır. emu8086'da .stack 100h, .data ve .code sırasıyla yazılmış bir .EXE programı SS = 0710h ve SP = 0100h ile başlar. Yığının üstü bu durumda 07100h + 0100h = 07200h fiziksel adresidir; ilk PUSH 071FEh'ye yazar.
Yığına yalnız SP ile erişilmez. BP (taban işaretçisi) de varsayılan olarak SS kesimini kullanır. MOV AX, [BP+4] gibi bir komut, kesim öneki yazılmadığı hâlde SS:BP+4 adresini okur. Bu yüzden BP yığının içindeki verilere, örneğin bir prosedüre yığından geçirilen parametrelere erişmek için uygun yazmaçtır. SP ise 8086'da adresleme ifadesinde kullanılamaz: MOV AX, [SP] geçersizdir. Yığının üstündeki değeri silmeden okumak için SP önce BX ya da BP'ye kopyalanır.
Programın yığını kendisi taşıması da mümkündür. Bunun için SS ve SP'ye yeni değerler yüklenir. Bu iki yükleme arasında bir kesme gelirse, işlemci FLAGS, CS ve IP'yi yarı eski, yarı yeni bir adrese yazar. 8086 bu tehlikeye karşı bir önlem içerir. Bir kesim yazmacına MOV ya da POP ile yazan komuttan sonra, bir sonraki komut bitene kadar kesme kabul edilmez. Bu yüzden yığın değiştirilirken önce SS, hemen ardından SP yüklenir:
MOV AX, 2000h
MOV SS, AX ; kesme bir komut ertelenir
MOV SP, 0100h ; SS:SP = 2000:0100, tutarlıİki satırın sırası değiştirilirse koruma işe yaramaz. Erteleme yalnız kesim yazmacına yazan komuttan sonraki komutu kapsar.
8.1.1.4Yığın aşağı doğru büyür
Bir .COM programında 64 KB'lık tek kesimin içinde üç bölge bulunur. Kesimin ilk 256 baytı (0000h–00FFh) program kesim önekidir (program segment prefix, PSP). DOS bu alanı programı yüklerken doldurur; komut satırı ve dönüş bilgileri buradadır. Program, ORG 100h yönergesinin istediği gibi 0100h adresinden başlar; kod ve veri yukarı doğru yerleşir. Yığın ise kesimin en üstünden, FFFEh'den başlar ve aşağı doğru büyür.
Bu yerleşimin mantığı Şekil 8.1.3'te açıkça görülür. Program ile yığın kesimin iki ucundan birbirine doğru büyür. Programın ne kadar yer kaplayacağı derleme anında bellidir. Yığının ne kadar derinleşeceği ise çalışma anında, çağrıların ve PUSH'ların sayısına göre belirlenir. İki bölgeyi karşılıklı uçlara koymak, aradaki boş alanın tamamını hangisi gerekirse onun kullanmasını sağlar. Yığın yukarı doğru büyüseydi, programın sonuna sabit bir boyut ayırmak gerekirdi ve bu boyut ya fazla ya da yetersiz kalırdı.
Yığının aşağı doğru büyümesinin komutlara yansıması basittir. Her PUSH, SP'yi 2 azaltır. Her POP, SP'yi 2 artırır. SP'nin değeri küçüldükçe yığın derinleşir. Yığındaki sözcük sayısı da SP'den hesaplanır. Örneğin başlangıçta SP = FFFEh, şu anda SP = FFF8h ise yığına (FFFEh − FFF8h) / 2 = 3 sözcük itilmiştir.
Bir .COM programında yığının ne kadar derinleşebileceği de hesaplanabilir. Yığınla diziyi ters çeviren örnek program 26 bayttır ve 0100h–0119h adreslerini kaplar. Programdan sonraki ilk boş bayt 011Ah'dir. Yığın FFFEh'den başlar; n PUSH'tan sonra SP = FFFEh − 2n olur. Yığının programın üzerine yazmaması için SP ≥ 011Ah kalmalıdır. Buradan n ≤ (FFFEh − 011Ah) / 2 = (65 534 − 282) / 2 = 32 626 bulunur. Bu programda yığına 32 626 sözcüğe kadar değer konabilir. Pratikte sınır bundan çok önce gelmez, ama sonsuz bir döngü ya da sonsuz özyineleme bu sınırı kısa sürede aşar.
“Yığının üstü” ifadesini “en yüksek adres” olarak anlamak. Yığının üstü, en son konan sözcüğün bulunduğu ve SP'nin gösterdiği yerdir. Yığın aşağı doğru büyüdüğü için bu yer, yığındaki öğelerin en düşük adreslisidir. Şekillerde küçük adresler yukarıya çizildiğinde üst kelimesi görsel olarak da doğru olur; bu notun yığın şekilleri bu düzeni kullanır.
8.1.1.5Yığın erişimi donanımda
PUSH ve POP, yazılım açısından tek bir komuttur. Donanım açısından ise 8086'nın iki birimi birlikte çalışır. Yürütme birimi (execution unit, EU) komutu çözer ve SP'yi kendi aritmetik-mantık biriminde 2 azaltır ya da artırır. Veri yolu arabirim birimi (bus interface unit, BIU) ise fiziksel adresi hesaplar ve belleğe erişir. BIU'daki adres toplayıcı SS'yi 4 bit sola kaydırıp SP'yi ekler. Bulunan 20 bitlik adres, bir bellek yazma (PUSH) ya da okuma (POP) yol çevrimiyle adres/veri hatlarına çıkarılır.
8086'nın veri yolu 16 bittir. Bir sözcük çift bir adreste duruyorsa tek yol çevrimiyle, yani dört saat turunda (T1–T4) taşınır. Bu çevrimde A0 = 0 ve BHE = 0 olur; düşük bayt D0–D7, yüksek bayt D8–D15 hatlarından geçer. SP çift olduğu sürece her PUSH tek bir yazma çevrimi, her POP tek bir okuma çevrimidir. SP tek bir değere düşerse sözcük iki ayrı çevrimle taşınır ve her erişim dört saat turu uzar. Yükleyiciler bu yüzden SP'yi hep çift bir değerle başlatır ve programlar SP'yi tek sayı kadar değiştirmez.
Yığın erişimini dış devreye bildiren bir ayrıntı daha vardır. 8086, her yol çevriminin T2–T4 aralığında A16/S3–A19/S6 hatlarına durum bitleri çıkarır. S4 ve S3 bitleri o çevrimde hangi kesim yazmacının kullanıldığını söyler. S4 = 0 ve S3 = 1 ise o çevrim SS kesimine, yani yığına yapılan bir erişimdir. Bir tasarımcı bu bitleri çözerek yığın erişimlerini ayrı bir bellek bankasına yönlendirebilir ve böylece 1 MB'tan geniş bir bellek alanı kullanabilir. Sıradan PC'lerde bu bitler kullanılmaz, ama mantık analizöründe bir programın yığına ne zaman eriştiğini görmek için işe yararlar.
| S4 | S3 | Kullanılan kesim | Örnek erişim |
|---|---|---|---|
| 0 | 0 | ES (ek kesim) | dizgi komutlarının hedefi, ES önekli erişim |
| 0 | 1 | SS (yığın kesimi) | PUSH, POP, CALL, RET, BP tabanlı erişim |
| 1 | 0 | CS ya da kesim yok | komut getirme, G/Ç ve kesme onayı çevrimleri |
| 1 | 1 | DS (veri kesimi) | sıradan değişken erişimi |
8088'de durum farklıdır. 8088'in dış veri yolu 8 bittir; her sözcük iki ayrı bayt çevrimiyle taşınır. Bu yüzden 8088'de her PUSH ve POP, SP çift olsa bile iki bellek çevrimi sürer. Programlama açısından iki işlemci aynıdır; yalnız süreler değişir. Bu notun bütün örnekleri 8086 içindir.
8.1.2PUSH ve POP
8.1.2.1PUSH komutu
PUSH kaynak komutu 16 bitlik kaynağın bir kopyasını yığının üstüne koyar. Kaynak değişmez; yığında onun bir kopyası oluşur. Kaynak dört türden olabilir: genel amaçlı ya da işaretçi yazmacı, kesim yazmacı, bellekteki bir sözcük ve 80186'dan itibaren anlık değer. Tablo 8.1.3 bu türleri örnekleriyle verir.
| Biçim | Geçerli işlenenler | Örnek |
|---|---|---|
| PUSH yazmaç | AX, BX, CX, DX, SI, DI, BP, SP | PUSH AX |
| PUSH kesim yazmacı | CS, DS, SS, ES | PUSH DS |
| PUSH bellek | [BX], [BX+SI+7], 16 bitlik değişken | PUSH [BX], PUSH sayi |
| PUSH anlık değer | 5, −24, 3Fh, 10001101b | PUSH 3Fh (8086'da yok) |
Tablonun son satırı bir uyarı taşır. Anlık değerle PUSH, 8086 ve 8088'in komut kümesinde yoktur; 80186 ile gelmiştir. emu8086 bu komutu kabul eder ve çalıştırır. Ama gerçek bir 8086 için yazılan programda bir sabit önce bir yazmaca yüklenir, sonra yazmaç itilir:
MOV AX, 5
PUSH AX ; 8086 için PUSH 5 yerineBellek işlenenli PUSH'ta işlenenin boyutu sözcük olmalıdır. sayi DW 0BEEFh olarak tanımlanmış bir değişken için PUSH sayi geçerlidir; değişkenin iki baytı yığına kopyalanır. DB ile tanımlanmış bir bayt değişkeni ise itilemez. Aynı kural yazmaçlar için de geçerlidir: PUSH AL ve PUSH byte_degisken derleme hatası verir. Bayrak yazmacı ise sıradan bir işlenen olarak yazılamaz; onun için ayrı bir komut, PUSHF vardır.
SP'nin kendisini itmek ilginç bir ayrıntı taşır. PUSH SP komutu SP'yi hem azaltır hem de yığına yazar; hangisinin önce olduğu sonucu değiştirir. 8086 önce SP'yi azaltır, sonra azaltılmış değeri yazar. SP = FFFEh iken PUSH SP yığına FFFCh koyar. 80286 ve sonraki işlemciler ise azaltmadan önceki değeri, FFFEh'yi yazar. Bu fark, eski programlarda işlemcinin 8086 mı yoksa 80286 mı olduğunu anlamak için kullanılmıştır. Taşınabilir bir programda PUSH SP'ye güvenilmez; SP'nin değeri gerekiyorsa önce başka bir yazmaca kopyalanır.
emu8086'da PUSH 5 çalıştı diye bu komutu 8086 komutu sanmak. emu8086 bazı 80186 komutlarını kabul eder ve yalnız uyarı verir. Aynı kod gerçek bir 8086 ya da 8088 üzerinde çalıştırıldığında işlemci bu baytları başka bir komut gibi yorumlar ve program beklenmedik biçimde davranır.
8.1.2.2POP komutu
POP hedef komutu yığının üstündeki sözcüğü okur, hedefe yazar ve yığının üstünü bir sözcük yukarı taşır. Hedef türleri PUSH'un kaynak türlerine benzer, ama iki eksik vardır (Tablo 8.1.4).
| Biçim | Geçerli işlenenler | Örnek |
|---|---|---|
| POP yazmaç | AX, BX, CX, DX, SI, DI, BP, SP | POP BX |
| POP kesim yazmacı | DS, SS, ES (CS hariç) | POP DS |
| POP bellek | [BX], [BX+SI+7], 16 bitlik değişken | POP sonuc |
Neden POP CS yok? CS, komutların getirildiği kesimi belirler. POP CS CS'yi değiştirip IP'yi olduğu gibi bıraksaydı, bir sonraki komut bambaşka bir kesimin aynı bağıl konumundan getirilirdi. Program akışı rastgele bir yere atlardı. Bu yüzden CS yalnız IP ile birlikte değişir: uzak JMP ve CALL, RETF, INT ve IRET CS'yi ve IP'yi aynı komutta yükler. Assembler'lar POP CS yazımını kabul etmez. İlk 8086'larda bu komutun kodu (0Fh) belgelenmemiş biçimde çalışıyordu; Intel bu kodu 80286'da iki baytlık yeni komutların öneki olarak kullandı.
Neden POP sabit yok? POP 5 anlamsızdır. POP yığından bir değer alır ve bu değerin yazılacağı bir yer gerekir. Bir sabit, değer yazılabilecek bir yer değildir. Yığının üstündeki değer gerekmiyorsa ve yalnız atılmak isteniyorsa iki yol vardır. Değer kullanılmayan bir yazmaca alınır ya da SP'ye doğrudan 2 eklenir (ADD SP, 2). İkinci yol bayrakları değiştirir; birincisi değiştirmez.
Bayraklar. PUSH ve POP bayraklara dokunmaz. POP AX ile AX'e 0000h gelse bile ZF değişmez; ZF'yi değiştiren bir aritmetik ya da mantık işlemidir, veri taşıma değil. Bu kuralın tek istisnası POPF'tir. POPF yığından aldığı sözcüğü bayrak yazmacının tamamına yükler.
Kesim yazmaçları arasında kopyalama. 8086'da bir kesim yazmacından diğerine doğrudan MOV yoktur; MOV DS, CS geçersizdir. Bu kopyalama iki yolla yapılır. Ya değer genel amaçlı bir yazmaç üzerinden aktarılır (MOV AX, CS ve MOV DS, AX) ya da yığın kullanılır:
PUSH CS ; CS'nin değeri yığına
POP DS ; DS ← CS
PUSH DS ; DS'nin değeri yığına
POP ES ; ES ← DSYığınla kopyalamanın üstünlüğü, hiçbir genel amaçlı yazmacı bozmamasıdır. AX'in değeri korunmak isteniyorsa ilk yol AX'i ayrıca saklamayı gerektirir; ikinci yol gerektirmez. PUSH CS ile POP DS deyimi, verisi kod kesiminde duran programlarda ve kesme yordamlarının başında sık görülür. emu8086'da DS = 1234h yapılıp bu dört komut çalıştırıldığında ES = 1234h ve DS = 0700h olur; SP yeniden FFFEh'ye döner.
8.1.2.3PUSH ve POP nasıl çalışır?
Her iki komut da iki adımdan oluşur. Adımların sırası, SP'nin her an neyi gösterdiğini belirler.
PUSH kaynak: (1) SP ← SP − 2; (2) [SS:SP] ← kaynak. POP hedef: (1) hedef ← [SS:SP]; (2) SP ← SP + 2. PUSH önce azaltır sonra yazar; POP önce okur sonra artırır. Böylece SP her an dolu olan en üst hücreyi gösterir.
Bu sıralamaya “dolu yığın” düzeni denir. SP'nin gösterdiği hücrede her zaman geçerli bir değer vardır. Bazı işlemciler ters düzeni kullanır: SP bir sonraki boş hücreyi gösterir, PUSH önce yazar sonra azaltır. Hangi düzen seçilirse seçilsin, PUSH ile POP birbirinin tam tersi olmalıdır. 8086'da PUSH “azalt, yaz”, POP ise “oku, artır” biçimindedir.
Şekil 8.1.4 iki komutu dört anda gösterir. Başlangıçta SP = FFFEh'dir ve AX = 1234h'dir. FFFEh adresinde DOS'un .COM programı için koyduğu 0000h sözcüğü durur; bu sözcüğün görevi program sonunda anlatılır.
- PUSH AX, adım 1. SP ← FFFEh − 2 = FFFCh. Yığının üstü bir sözcük aşağı iner. FFFCh'deki hücrede şimdilik eski, anlamsız bir değer vardır.
- PUSH AX, adım 2. [SS:FFFCh] ← 1234h. Yeni üst hücreye AX'in kopyası yazılır. AX değişmez.
- POP BX, adım 1. BX ← [SS:FFFCh] = 1234h. Değer yığının üstünden okunur.
- POP BX, adım 2. SP ← FFFCh + 2 = FFFEh. Yığının üstü bir sözcük yukarı çıkar.
Son adımın önemli bir sonucu vardır. POP, okuduğu değeri bellekten silmez. FFFCh'deki 1234h orada durmaya devam eder; yalnız artık SP'nin üstünde, yani yığının dışında kalmıştır. Şekilde bu hücre soluk çizilmiştir. Bir sonraki PUSH aynı hücrenin üzerine yazar. Bu yüzden yığından alınmış bir değere bellekten yeniden erişmeye çalışmak güvenilir değildir. Arada bir kesme gelirse, işlemci FLAGS, CS ve IP'yi tam o hücrelere yazar ve eski değer kaybolur.
POP'tan sonra değerin bellekten silindiğini, yerine 0 yazıldığını düşünmek. POP yalnız okur ve SP'yi artırır. emu8086'nın bellek penceresinde eski değerler görünmeye devam eder. Bu değerler “yığında” değildir; yığın yalnız SP'den FFFEh'ye kadar olan bölgedir.
8.1.2.4Yığında bayt sırası
Yığın sözcüklerle çalışır, bellek ise baytlarla adreslenir. Bir sözcük belleğe yazılırken iki baytına iki ayrı adres verilir. 8086 bu işte little-endian (little-endian) düzeni kullanır: sözcüğün düşük baytı düşük adrese, yüksek baytı bir sonraki adrese yazılır. Kural yığın için de aynıdır.
AX = 1234h iken PUSH AX çalıştığında SP = FFFCh olur. Sözcüğün düşük baytı 34h (AL) FFFCh adresine, yüksek baytı 12h (AH) FFFDh adresine yazılır (Şekil 8.1.5). SP her zaman sözcüğün düşük baytını, yani daha küçük adresi gösterir. emu8086'nın yığın penceresi sözcükleri gösterir ve bu satırda 1234h yazar. Bellek penceresi ise baytları sırayla gösterir: önce 34, sonra 12. İki pencere aynı veriyi farklı biçimde sunar. Bellek penceresinde değerin “ters” görünmesi bir hata değil, little-endian düzenin doğal sonucudur.
Bu düzen bir programla da sınanabilir. Yığının üstündeki baytları okumak için SP bir taban yazmacına kopyalanır, çünkü 8086'da [SP] adresleme ifadesi yoktur:
ORG 100h
MOV AX, 1234h
PUSH AX
MOV BX, SP ; BX = FFFCh
MOV CL, [BX] ; CL = 34h, düşük bayt
MOV CH, [BX+1] ; CH = 12h, yüksek bayt
POP DX ; DX = 1234hProgram şöyle çalışır. PUSH AX SP'yi FFFCh yapar ve 34h ile 12h baytlarını FFFCh ve FFFDh'ye yazar. MOV BX, SP BX'i FFFCh yapar. [BX] varsayılan olarak DS kesimini kullanır. .COM programında DS = SS olduğu için DS:FFFCh, yığının üstüyle aynı fiziksel adrestir: 07000h + FFFCh = 16FFCh. MOV CL, [BX] buradan 34h'yi, MOV CH, [BX+1] 16FFDh'den 12h'yi okur. CX = 1234h olur. Son olarak POP DX iki baytı yeniden sözcük olarak okur: DX = 1234h. Bir .EXE programında DS ile SS farklı olabilir. O zaman [BX] yığını değil veri kesimini okur; bu durumda SS:[BX] yazmak ya da BX yerine BP kullanmak gerekir.
SP'nin çift kalması da bayt düzeyindeki bu yapıyla ilgilidir. SP çift olduğu sürece her yığın sözcüğü çift bir adreste başlar ve 8086 onu tek yol çevrimiyle taşır. SP tek olsaydı sözcüğün düşük baytı tek, yüksek baytı çift adrese düşer ve iki ayrı çevrim gerekirdi. Program yine doğru çalışır, yalnız yavaşlar.
8.1.2.5Ne değişir, ne değişmez?
Yığını kullanan bütün komutlar SP'yi değiştirir. Belleğe yazıp yazmadıkları, bir hedefi değiştirip değiştirmedikleri ve bayraklara etkileri ise farklıdır. Tablo 8.1.5 bu etkileri yan yana verir.
| Komut | SP | Bellek | Hedef | Bayraklar |
|---|---|---|---|---|
PUSH kaynak | −2 | [SS:SP] ← kaynak | — | değişmez |
POP hedef | +2 | okunur, silinmez | hedef ← [SS:SP] | değişmez |
PUSHF | −2 | [SS:SP] ← FLAGS | — | değişmez |
POPF | +2 | okunur | FLAGS ← [SS:SP] | hepsi yüklenir |
CALL (yakın) | −2 | [SS:SP] ← dönüş IP'si | IP ← hedef | değişmez |
RET (yakın) | +2 | okunur | IP ← [SS:SP] | değişmez |
INT n | −6 | FLAGS, CS, IP | CS:IP ← vektör | IF = TF = 0 |
IRET | +6 | okunur | IP, CS, FLAGS | hepsi yüklenir |
Tablonun en önemli sütunu sonuncusudur. PUSH, POP ve PUSHF bayraklara dokunmaz. Bu sayede bir karşılaştırmanın sonucu, yani bayraklar, PUSH ve POP arasında güvenle taşınabilir. Aşağıdaki kodda CMP ZF'yi 1 yapar. Ardından AX yığına konur, değiştirilir ve geri alınır. Bu üç komut ZF'ye dokunmadığı için JE yine atlar:
MOV AX, 5
CMP AX, 5 ; ZF = 1
PUSH AX
MOV AX, 7 ; MOV bayrakları değiştirmez
POP AX ; AX = 5, ZF hâlâ 1
JE esit ; atlarArada bayrakları değiştiren bir komut, örneğin bir ADD olsaydı, karşılaştırmanın sonucu kaybolurdu. O durumda bayrakları saklamak için PUSHF ve POPF kullanılır. CALL ve RET'in yığın etkisi PUSH ve POP'unkine çok benzer: CALL dönüş adresini iter, RET onu çeker. Fark, hedefin bir yazmaç değil IP olmasıdır. INT ise üç sözcük, yani 6 bayt iter ve bu komutlar arasında bayrakları değiştiren tek itme komutudur: kesme yordamına girerken IF ve TF sıfırlanır.
8.1.2.6Makine kodları ve saat turları
Yığın komutları 8086'nın en kısa komutları arasındadır. Bir yazmacı itmek ya da çekmek tek bayt tutar. İşlem kodunun beş biti komutu, kalan üç biti yazmacı belirler. PUSH için kod 50h + r, POP için 58h + r biçimindedir. Yazmaç numaraları 8086'nın genel sırasını izler: AX = 0, CX = 1, DX = 2, BX = 3, SP = 4, BP = 5, SI = 6, DI = 7. Bu yüzden PUSH AX = 50h, PUSH BX = 53h, POP BX = 5Bh ve POP DI = 5Fh'dir.
Kesim yazmaçları da tek bayttır. İtme kodu 06h + 8 × s, çekme kodu 07h + 8 × s biçimindedir; s, ES = 0, CS = 1, SS = 2, DS = 3 numarasıdır. PUSH ES = 06h, PUSH CS = 0Eh, PUSH SS = 16h, PUSH DS = 1Eh; POP ES = 07h, POP SS = 17h, POP DS = 1Fh'dir. Bu düzende POP CS'nin kodu 0Fh olurdu; bu kod POP CS ile birlikte kullanım dışı kaldı. Bellek işlenenli biçimler ModR/M baytı kullanır: PUSH bellek = FFh /6, POP bellek = 8Fh /0. Örneğin 0109h adresindeki bir değişken için PUSH sayi = FF 36 09 01, dört bayttır. PUSHF = 9Ch ve POPF = 9Dh'dir.
| Komut | Kod | Bayt | Saat turu |
|---|---|---|---|
PUSH yazmaç | 50h + r | 1 | 11 |
PUSH kesim | 06h + 8s | 1 | 10 |
PUSH bellek | FFh /6 | 2–4 | 16 + EA |
POP yazmaç | 58h + r | 1 | 8 |
POP kesim | 07h + 8s | 1 | 8 |
POP bellek | 8Fh /0 | 2–4 | 17 + EA |
PUSHF | 9Ch | 1 | 10 |
POPF | 9Dh | 1 | 8 |
Tablo 8.1.6'daki süreler sözcüklerin çift adreste durduğu durum içindir. Bir PUSH 11 saat turu sürer; 5 MHz'lik bir 8086'da bir saat turu 200 ns olduğundan bu 2,2 µs eder. Bu sürenin dört turu bellek yazma çevrimidir; geri kalanı komutun çözülmesi ve SP'nin azaltılmasıdır. Bellek işlenenli biçimlerde iki bellek erişimi vardır: biri değişkene, biri yığına. Süre bu yüzden daha uzundur ve etkin adres hesabının süresi de eklenir. 8088'de her sözcük erişimine dört tur daha eklenir.
8.1.3Örnekler: kod izleme
Bu kesimdeki programların hepsi emu8086'da .COM programı olarak çalıştırılmıştır. Başlangıçta CS = DS = SS = ES = 0700h, IP = 0100h ve SP = FFFEh'dir; aksi belirtilmedikçe diğer yazmaçlar 0000h'dir. İzleme tablolarında her satır, o komut yürütüldükten sonraki durumu verir. O satırda değişen değerler vurgulanmıştır. Yığın sütunu yalnız SP'nin gösterdiği ve onun altındaki dolu hücreleri gösterir; FFFEh'deki 0000h, DOS'un .COM programı için koyduğu sözcüktür.
8.1.3.1İlk örnek: PUSH AX / POP BX
İlk program bir değeri AX'ten BX'e yığın üzerinden kopyalar. Bu iş bir MOV BX, AX ile de yapılabilirdi; amaç yığının işleyişini en sade biçimde görmektir.
ORG 100h
MOV AX, 42 ; AX'e 42 ata
PUSH AX ; AX'i yığına koy
POP BX ; yığından BX'e al
RET ; işletim sistemine dön
ENDProgram dört komuttan ve 6 bayttan oluşur: MOV AX, 42 3 bayt (B8 2A 00, 0100h–0102h), PUSH AX 1 bayt (50, 0103h), POP BX 1 bayt (5B, 0104h) ve RET 1 bayttır (C3, 0105h).
MOV AX, 42 satırında sayının sonunda h yoktur. Assembler onu onluk sayı olarak okur: 42 = 2 × 16 + 10 = 2Ah. AX = 002Ah olur, 0042h değil. Bu küçük ayrıntı emu8086'da yeni başlayanların en sık şaşırdığı noktalardan biridir. PUSH AX SP'yi FFFEh − 2 = FFFCh yapar ve [FFFCh] ← 002Ah yazar; bayt düzeyinde [FFFCh] = 2Ah, [FFFDh] = 00h olur. POP BX FFFCh'deki 002Ah'yi BX'e alır ve SP'yi FFFEh'ye geri getirir. Değer AX'ten BX'e yığın üzerinden kopyalanmıştır; AX de değerini korur.
| Komut | IP (sonraki) | AX | BX | SP | Yığın (üstten) |
|---|---|---|---|---|---|
| başlangıç | 0100h | 0000h | 0000h | FFFEh | 0000h |
MOV AX, 42 | 0103h | 002Ah | 0000h | FFFEh | 0000h |
PUSH AX | 0104h | 002Ah | 0000h | FFFCh | 002Ah, 0000h |
POP BX | 0105h | 002Ah | 002Ah | FFFEh | 0000h |
RET | 0000h | 002Ah | 002Ah | 0000h | (boş) |
Son satır yığının programın sonundaki rolünü gösterir (Tablo 8.1.7). RET yığının üstündeki sözcüğü dönüş adresi olarak alır. Yığında yalnız DOS'un koyduğu 0000h vardır; IP = 0000h ve SP = FFFEh + 2 = 0000h olur. SP 16 bitte sarar. İşlemci 0700:0000 adresine, yani PSP'nin başına gider. PSP'nin ilk iki baytı CD 20'dir; bu, programı sonlandıran INT 20h komutudur. Program böylece düzgün biter. Bu düzenek ancak yığın dengeliyse çalışır: RET'e gelindiğinde yığının üstünde DOS'un 0000h'si durmalıdır.
8.1.3.2LIFO sırası: 1, 2, 3 girer, 3, 2, 1 çıkar
İkinci program LIFO düzenini gerçek yazmaçlarla gösterir. AX'e sırayla 1, 2 ve 3 yüklenir ve her değer yığına itilir. Ardından üç kez POP BX yapılır.
MOV AX, 1 ; AX'e 1 ata
PUSH AX ; 1'i yığına koy
MOV AX, 2 ; AX'e 2 ata
PUSH AX ; 2'yi yığına koy
MOV AX, 3 ; AX'e 3 ata
PUSH AX ; 3'ü yığına koy
POP BX ; BX ← 3
POP BX ; BX ← 2
POP BX ; BX ← 1| Komut | AX | BX | SP | Yığın (üstten, DOS sözcüğü hariç) |
|---|---|---|---|---|
| başlangıç | 0000h | 0000h | FFFEh | (boş) |
MOV AX, 1 | 0001h | 0000h | FFFEh | (boş) |
PUSH AX | 0001h | 0000h | FFFCh | 0001h |
MOV AX, 2 | 0002h | 0000h | FFFCh | 0001h |
PUSH AX | 0002h | 0000h | FFFAh | 0002h, 0001h |
MOV AX, 3 | 0003h | 0000h | FFFAh | 0002h, 0001h |
PUSH AX | 0003h | 0000h | FFF8h | 0003h, 0002h, 0001h |
POP BX | 0003h | 0003h | FFFAh | 0002h, 0001h |
POP BX | 0003h | 0002h | FFFCh | 0001h |
POP BX | 0003h | 0001h | FFFEh | (boş) |
İzlemede iki nokta dikkat çeker (Tablo 8.1.8). Birincisi, AX'e yeni bir değer yüklendiğinde eski değer kaybolmaz; bir önceki PUSH onu yığına kopyalamıştır. AX = 0002h olduğunda 0001h yığında güvendedir. İkincisi, BX'e gelen değerler giriş sırasının tersidir: önce 3, sonra 2, sonra 1. Üç PUSH ve üç POP yapıldığı için SP başladığı yere, FFFEh'ye döner. Programın sonunda AX = 0003h kalır, çünkü AX'e son yüklenen değer 3'tür ve POP'lar yalnız BX'i değiştirir.
8.1.3.3Bir yazmacın değerini saklamak
Yığının en yaygın kullanımı bir yazmacı geçici olarak başka bir iş için kullanıp sonra eski değerine döndürmektir. Program önce AX'e 1234h yükler. Sonra AX'i saklar, AX'e başka bir değer yazar ve saklanan değeri geri yükler.
ORG 100h
MOV AX, 1234h
PUSH AX ; AX'in değerini sakla
MOV AX, 5678h ; AX'i değiştir
POP AX ; eski değeri geri yükle
RET
END| Komut | AX | SP | [FFFCh] | Açıklama |
|---|---|---|---|---|
MOV AX, 1234h | 1234h | FFFEh | — | korunacak değer |
PUSH AX | 1234h | FFFCh | 1234h | AX'in kopyası yığında |
MOV AX, 5678h | 5678h | FFFCh | 1234h | AX başka bir iş için kullanılıyor |
POP AX | 1234h | FFFEh | 1234h | eski değer geri geldi; hücre silinmedi |
RET | 1234h | 0000h | 1234h | IP ← 0000h, PSP'deki INT 20h |
Program 9 bayttır: iki üç baytlık MOV, birer baytlık PUSH, POP ve RET. Tablo 8.1.9'un dördüncü sütunu POP'un belleği silmediğini bir kez daha gösterir: POP'tan sonra da [FFFCh] = 1234h'dir. Bu desen, prosedürlerin girişinde kullandıkları yazmaçları PUSH ve çıkışta POP etmesinin aynısıdır. Bir prosedür AX'i bozacaksa girişte PUSH AX, çıkışta POP AX yazar. Prosedürü çağıran program AX'in değiştiğini fark etmez. Bu yönteme yazmaçların korunması denir.
AX'te bir döngünün sayacı, BX'te bir dizinin adresi duruyor. Döngünün ortasında ekrana bir karakter yazmak için AH = 02h ve DL = karakter yüklenip INT 21h çağrılacaktır. INT 21h'nin 02h işlevi AL'yi de değiştirir: işlev bittiğinde AL, yazılan karakterdir. AX'in iki yarısı da bozulacağı için yazdırma şöyle sarılır:
PUSH AX ; sayacı sakla
PUSH DX ; DX de bozulacak
MOV AH, 2
MOV DL, '*'
INT 21h ; AL = '*' olur
POP DX ; ayna sırası: önce DX
POP AX ; sayaç geri geldiPUSH sırası AX, DX olduğu için POP sırası DX, AX'tir. BX'e dokunulmadığı için saklanmaz. Saklanan her yazmaç iki komut ve 19 saat turu (11 + 8) tutar; yalnız gerçekten bozulan yazmaçlar saklanır.
8.1.3.4Yığınla iki yazmacı takas etmek
Ayna sırasının tersi, yani aynı sırayla POP, değerleri takas eder. Bu bazen bir hata, bazen de bilerek kullanılan bir yöntemdir.
ORG 100h
MOV AX, 1212h ; AX ← 1212h
MOV BX, 3434h ; BX ← 3434h
PUSH AX ; AX'i yığına koy
PUSH BX ; BX'i yığına koy
POP AX ; AX ← BX'in eski değeri
POP BX ; BX ← AX'in eski değeri
RET
ENDemu8086 bu programı yüklediğinde CX'te 000Bh görülür. Bu değer programın bayt sayısıdır: iki MOV 3'er bayt, dört yığın komutu ve RET birer bayt, toplam 3 + 3 + 1 + 1 + 1 + 1 + 1 = 11 bayt. emu8086 yükleme sonrasında CX'e .COM programının boyutunu yazar; DOS'un DEBUG programı da yüklediği dosyanın uzunluğunu BX:CX'e yazar. Bu bir gelenektir, bir kural değildir. Program CX'in başlangıç değerine güvenmemeli, CX'i kullanmadan önce kendisi yüklemelidir. Bu notun diğer izlemelerinde CX'in başlangıç değeri 0000h alınmıştır; takas programı CX'i kullanmadığı için sonuç değişmez.
| Komut | AX | BX | SP | Yığın (üstten) |
|---|---|---|---|---|
| başlangıç | 0000h | 0000h | FFFEh | 0000h |
MOV AX, 1212h | 1212h | 0000h | FFFEh | 0000h |
MOV BX, 3434h | 1212h | 3434h | FFFEh | 0000h |
PUSH AX | 1212h | 3434h | FFFCh | 1212h, 0000h |
PUSH BX | 1212h | 3434h | FFFAh | 3434h, 1212h, 0000h |
POP AX | 3434h | 3434h | FFFCh | 1212h, 0000h |
POP BX | 3434h | 1212h | FFFEh | 0000h |
RET | 3434h | 1212h | 0000h | (boş); IP = 0000h |
Takasın sırrı LIFO'dur (Tablo 8.1.10). BX en son itildiği için yığının üstündedir ve ilk POP AX onu alır. Ardından POP BX sıradaki değeri, yani AX'in eski değeri 1212h'yi alır. Bir an için AX ile BX'in ikisi de 3434h olur; yığın bu sırada 1212h'yi güvenle tutar. Aynı sırayla POP edilseydi, yani önce POP BX sonra POP AX yazılsaydı, değerler yerinde kalırdı.
8.1.3.5Üç yazmaç: aynı sırayla POP
Üç yazmaçla aynı deney yapıldığında sonuç daha ilginçtir. Bu program sunumda bir .BIN dosyası olarak yüklenmiştir. emu8086 .BIN dosyasını CS = SS = 0100h ve IP = 0000h ile başlatır; SP yine FFFEh'dir. Kesim değeri farklı olduğu için fiziksel adresler değişir, ama bağıl konumlar ve SP değerleri .COM programıyla aynıdır.
mov ax, 2
mov bx, 4
mov cx, 8
push ax
push bx
push cx
pop ax
pop bx
pop cx| Komut | AX | BX | CX | SP | Yığın (üstten) |
|---|---|---|---|---|---|
mov ax, 2 | 0002h | 0000h | 0000h | FFFEh | (boş) |
mov bx, 4 | 0002h | 0004h | 0000h | FFFEh | (boş) |
mov cx, 8 | 0002h | 0004h | 0008h | FFFEh | (boş) |
push ax | 0002h | 0004h | 0008h | FFFCh | 0002h |
push bx | 0002h | 0004h | 0008h | FFFAh | 0004h, 0002h |
push cx | 0002h | 0004h | 0008h | FFF8h | 0008h, 0004h, 0002h |
pop ax | 0008h | 0004h | 0008h | FFFAh | 0004h, 0002h |
pop bx | 0008h | 0004h | 0008h | FFFCh | 0002h |
pop cx | 0008h | 0004h | 0002h | FFFEh | (boş) |
Sonuçta AX = 0008h, BX = 0004h ve CX = 0002h olur (Tablo 8.1.11). AX ile CX yer değiştirmiş, BX yerinde kalmıştır. Bunun nedeni, ortadaki değerin hem itme hem de çekme sırasında ortada olmasıdır. Genel olarak n yazmaç aynı sırayla itilip aynı sırayla çekilirse yazmaç dizisi ters çevrilir: birinci yazmaç sonuncunun değerini, sonuncu birincinin değerini alır. Üç yazmaçta ortadaki, sabit noktadır. pop bx satırında BX'e yine 0004h yazılır; değer değişmediği için tabloda vurgulanmamıştır, ama komut yürütülmüş ve SP artmıştır.
8.1.3.6Yığınla diziyi ters çevirmek
LIFO'nun ters çevirme özelliği bir dizgiye de uygulanabilir. Program 'ABC' dizgisinin karakterlerini sırayla yığına iter, sonra yığından çekerek ekrana yazar. Ekranda CBA görünür.
ORG 100h
MOV SI, OFFSET metin
MOV CX, 3
koy: MOV AL, [SI]
PUSH AX ; karakteri sakla
INC SI
LOOP koy
MOV CX, 3
yaz: POP DX ; DL = karakter
MOV AH, 2
INT 21h ; DL'yi ekrana yaz
LOOP yaz
RET
metin DB 'ABC' ; 0117hÖnce programın bellekteki yerleşimi çıkarılır, çünkü metin değişkeninin adresi koddan sonra gelir. Tablo 8.1.12 her komutun adresini ve makine kodunu verir. Kod 0100h'den 0116h'ye kadar 23 bayt tutar. Ardından gelen metin 0117h–0119h adreslerindedir; 41h, 42h ve 43h baytları 'A', 'B' ve 'C' karakterlerinin ASCII kodlarıdır. Program toplam 26 bayttır.
| Satır | Adres | Kod | Komut |
|---|---|---|---|
| 2 | 0100h | BE 17 01 | MOV SI, OFFSET metin |
| 3 | 0103h | B9 03 00 | MOV CX, 3 |
| 4 | 0106h | 8A 04 | koy: MOV AL, [SI] |
| 5 | 0108h | 50 | PUSH AX |
| 6 | 0109h | 46 | INC SI |
| 7 | 010Ah | E2 FA | LOOP koy |
| 8 | 010Ch | B9 03 00 | MOV CX, 3 |
| 9 | 010Fh | 5A | yaz: POP DX |
| 10 | 0110h | B4 02 | MOV AH, 2 |
| 11 | 0112h | CD 21 | INT 21h |
| 12 | 0114h | E2 F9 | LOOP yaz |
| 13 | 0116h | C3 | RET |
| 14 | 0117h | 41 42 43 | metin DB 'ABC' |
LOOP komutlarının ikinci baytı bağıl atlama uzaklığıdır. LOOP koy 010Ah'dedir ve iki bayttır; sonraki komut 010Ch'dedir. 0106h'ye dönmek için uzaklık 0106h − 010Ch = −6 olmalıdır; −6'nın 8 bitlik ikiye tümleyeni FAh'dir. LOOP yaz için uzaklık 010Fh − 0116h = −7 = F9h'dir.
Birinci döngü: karakterleri yığına itmek. SI dizgiyi, CX döngü sayacını tutar. Her turda MOV AL, [SI] bir karakteri AL'ye okur. Yığına bayt itilemediği için PUSH AX AL'yi AH ile birlikte sözcük olarak iter. Programın başında AH = 00h olduğu için yığına 0041h, 0042h ve 0043h gider. INC SI sonraki karakteri gösterir; LOOP koy CX'i 1 azaltır ve CX ≠ 0 ise başa döner. Üç tur sonunda SI = 011Ah ve SP = FFF8h olur. Yığının üstünde son itilen 'C' vardır.
| Satır | Komut | AX | CX | SI | SP | Yığın (üstten, DOS sözcüğü hariç) |
|---|---|---|---|---|---|---|
| 2 | MOV SI, OFFSET metin | 0000h | 0000h | 0117h | FFFEh | (boş) |
| 3 | MOV CX, 3 | 0000h | 0003h | 0117h | FFFEh | (boş) |
| 4 | MOV AL, [SI] | 0041h | 0003h | 0117h | FFFEh | (boş) |
| 5 | PUSH AX | 0041h | 0003h | 0117h | FFFCh | 0041h |
| 6 | INC SI | 0041h | 0003h | 0118h | FFFCh | 0041h |
| 7 | LOOP koy | 0041h | 0002h | 0118h | FFFCh | 0041h |
| 4 | MOV AL, [SI] | 0042h | 0002h | 0118h | FFFCh | 0041h |
| 5 | PUSH AX | 0042h | 0002h | 0118h | FFFAh | 0042h, 0041h |
| 6 | INC SI | 0042h | 0002h | 0119h | FFFAh | 0042h, 0041h |
| 7 | LOOP koy | 0042h | 0001h | 0119h | FFFAh | 0042h, 0041h |
| 4 | MOV AL, [SI] | 0043h | 0001h | 0119h | FFFAh | 0042h, 0041h |
| 5 | PUSH AX | 0043h | 0001h | 0119h | FFF8h | 0043h, 0042h, 0041h |
| 6 | INC SI | 0043h | 0001h | 011Ah | FFF8h | 0043h, 0042h, 0041h |
| 7 | LOOP koy | 0043h | 0000h | 011Ah | FFF8h | 0043h, 0042h, 0041h |
Birinci döngüde bayraklarla ilgili küçük bir ayrıntı da vardır. INC SI CF dışındaki aritmetik bayrakları günceller. SI = 0118h olduğunda düşük bayt 18h = 0001 1000b iki tane 1 biti içerir; PF = 1 olur. SI = 0119h ve 011Ah'de düşük baytlardaki 1 sayısı tektir ve PF = 0'dır. Bu bayraklar programın sonucunu etkilemez, çünkü LOOP bayraklara bakmaz; yalnız CX'i sınar.
İkinci döngü: yığından çekip yazmak. CX yeniden 3 yapılır. Her turda POP DX yığının üstündeki sözcüğü DX'e alır; DL karakteri, DH ise 00h'dir. MOV AH, 2 DOS'un karakter yazma işlevini seçer ve INT 21h DL'deki karakteri ekrana yazar (Şekil 8.1.6). INT 21h'nin 02h işlevi bir yan etki taşır: işlev bittiğinde AL, yazılan karakterdir. Bu yüzden AX her INT 21h'den sonra değişir. İlk turda AL zaten 43h olduğu için fark görünmez, sonraki turlarda AL = 42h ve AL = 41h olur. Yığından ilk çıkan 'C', sonra 'B', en son 'A''dır.
| Satır | Komut | AX | CX | DX | SP | Ekran |
|---|---|---|---|---|---|---|
| 8 | MOV CX, 3 | 0043h | 0003h | 0000h | FFF8h | |
| 9 | POP DX | 0043h | 0003h | 0043h | FFFAh | |
| 10 | MOV AH, 2 | 0243h | 0003h | 0043h | FFFAh | |
| 11 | INT 21h | 0243h | 0003h | 0043h | FFFAh | C |
| 12 | LOOP yaz | 0243h | 0002h | 0043h | FFFAh | C |
| 9 | POP DX | 0243h | 0002h | 0042h | FFFCh | C |
| 10 | MOV AH, 2 | 0243h | 0002h | 0042h | FFFCh | C |
| 11 | INT 21h | 0242h | 0002h | 0042h | FFFCh | CB |
| 12 | LOOP yaz | 0242h | 0001h | 0042h | FFFCh | CB |
| 9 | POP DX | 0242h | 0001h | 0041h | FFFEh | CB |
| 10 | MOV AH, 2 | 0242h | 0001h | 0041h | FFFEh | CB |
| 11 | INT 21h | 0241h | 0001h | 0041h | FFFEh | CBA |
| 12 | LOOP yaz | 0241h | 0000h | 0041h | FFFEh | CBA |
| 13 | RET | 0241h | 0000h | 0041h | 0000h | CBA |
Tablo 8.1.14'ün son satırında SP = 0000h ve IP = 0000h olur; RET, DOS'un 0000h sözcüğünü almıştır. Program toplam 28 komut yürütür ve PSP'deki INT 20h ile biter. İki döngü aynı sayıda tur döndüğü için üç PUSH'a üç POP karşılık gelir ve yığın dengelidir. Döngü sayıları farklı olsaydı, örneğin ikinci döngü yanlışlıkla CX = 2 ile başlasaydı, 'A' yığında kalır ve RET onu dönüş adresi sanardı: IP = 0041h. Program PSP'nin ortasına atlar ve düzgün sonlanmazdı.
Aynı yöntem dizgiyi ekrana yazmadan, bellekte yerinde ters çevirmek için de kullanılabilir. İkinci döngüde karakter POP AX ile alınır ve MOV [SI], AL ile dizgiye baştan itibaren yeniden yazılır; bunun için SI önce dizginin başına geri getirilir. Yığın burada bir ara bellek görevi görür. Ayrı bir tampon dizi tanımlamak gerekmez, ama her karakter yığında 2 bayt yer kaplar.
INT 21h çağrısından sonra AX'in değişmediğini varsaymak. AH = 02h işlevi AL'yi yazılan karaktere eşitler; AH = 09h işlevi AL'ye 24h ('$') bırakır. Döngü sayacı ya da ara sonuç AL'de tutuluyorsa çağrıdan önce PUSH AX, sonra POP AX yazılır.
8.1.3.7Yazmaçlar ne olur?
Sunumdaki tahmin sorusu, PUSH ve POP'ların karışık bir sırayla kullanıldığı bir koddur. Başlangıçta SP = FFFEh'dir.
MOV AX, 1111h
MOV BX, 2222h
MOV CX, 3333h
PUSH AX
PUSH BX
PUSH CX
POP AX
POP CX
PUSH AX
POP BX
POP BXÇözüm yığının içeriğini adım adım yazarak bulunur. Üç PUSH'tan sonra yığın üstten alta 3333h, 2222h, 1111h'dir ve SP = FFF8h'dir. POP AX üstteki 3333h'yi alır: AX = 3333h. POP CX sıradaki 2222h'yi alır: CX = 2222h, SP = FFFCh. Yığında yalnız 1111h kalmıştır. PUSH AX 3333h'yi yeniden iter ve POP BX onu hemen alır: BX = 3333h. Son POP BX yığındaki 1111h'yi alır ve BX'in az önceki değerinin üzerine yazar: BX = 1111h.
Sonuç AX = 3333h, BX = 1111h, CX = 2222h ve SP = FFFEh'dir. Dört PUSH ve dört POP yapıldığı için yığın dengelidir. Değerler yazmaçlar arasında bir tur dönmüştür: AX, CX'in; CX, BX'in; BX de AX'in eski değerini almıştır. Bu tür sorularda en güvenilir yöntem, yığını bir kâğıt üzerinde sütun olarak çizmek ve her komutta üstteki hücreyi işaretlemektir.
8.1.3.8Takas yöntemlerinin karşılaştırması
İki yazmacı takas etmenin üç yolu vardır. Yığınla takas dört komut tutar: PUSH AX, PUSH BX, POP AX, POP BX. Geçici bir yazmaçla takas üç MOV tutar: MOV CX, AX, MOV AX, BX, MOV BX, CX. Üçüncü yol 8086'nın takas komutudur: XCHG AX, BX.
| Yöntem | Bayt | Bellek erişimi | Saat turu | Yan etki ve kapsam |
|---|---|---|---|---|
| yığınla | 4 | 4 | 2 × 11 + 2 × 8 = 38 | hiçbir yazmaç bozulmaz; kesim yazmaçları için de çalışır |
| geçici yazmaçla | 6 | 0 | 3 × 2 = 6 | CX'in değeri bozulur |
XCHG AX, BX | 1 (93h) | 0 | 3 | en kısa ve en hızlı; kesim yazmaçlarıyla kullanılamaz |
Tablo 8.1.15 açık bir sonuç verir: iki genel amaçlı yazmacı takas etmenin en iyi yolu XCHG'dir. Komut tek bayttır. AX ile başka bir 16 bitlik yazmacı takas eden XCHG için 90h + r biçiminde kısa kodlar vardır; BX'in numarası 3 olduğundan kod 93h'dir. 5 MHz'de yığınla takas 38 × 200 ns = 7,6 µs, XCHG ise 0,6 µs sürer. Yığın yönteminin asıl değeri takas değil, saklamadır. Yazmaçları bir prosedür ya da kesme süresince korumak, ara sonuçları geçici olarak tutmak ve kesim yazmaçlarını kopyalamak (PUSH DS, POP ES) yığının asıl görevleridir. Kesim yazmaçları XCHG ile takas edilemediği için onların takası da yığınla yapılır: PUSH DS, PUSH ES, POP DS, POP ES.
8.1.3.9Etkileşimli yığın simülatörü
Sunumdaki etkileşimli simülatör AX, BX, CX ve SP yazmaçlarını ve SS:SP çevresindeki yığın hücrelerini gösterir. Yazmaç değerleri onaltılık olarak düzenlenebilir. Altı düğme PUSH AX, PUSH BX, PUSH CX, POP AX, POP BX ve POP CX komutlarını çalıştırır ve çalıştırılan komutlar bir listede birikir. Simülatörle bu notun bütün sıralama örnekleri yeniden denenebilir: ayna sırası, takas, üç yazmacın döndürülmesi.
Simülatörün gösterdiği önemli bir durum boş yığından POP'tur. Gerçek 8086 bunu engellemez. Yığın boşken POP yapılırsa SP yine 2 artar ve kesimde o adreste ne varsa okunur. İşlemcinin yığının boş ya da dolu olduğunu bilmesinin bir yolu yoktur; SP yalnız bir sayıdır. Yığının sınırlarını izlemek tamamen programcının işidir.
8.1.4Bayrakları saklamak: PUSHF ve POPF
8.1.4.1Bayrak sözcüğü
8086'nın bayrak yazmacı (FLAGS) 16 bitlik bir yazmaçtır, ama bitlerinin yalnız dokuzu kullanılır. Altı durum bayrağı son aritmetik ya da mantık işleminin sonucunu anlatır: CF (elde, bit 0), PF (eşlik, bit 2), AF (yardımcı elde, bit 4), ZF (sıfır, bit 6), SF (işaret, bit 7) ve OF (taşma, bit 11). Üç denetim bayrağı işlemcinin davranışını belirler: TF (tuzak, bit 8), IF (kesme, bit 9) ve DF (yön, bit 10). Kalan bitler kullanılmaz. 8086'da bit 1 ve bit 12–15 her zaman 1 okunur; bit 3 ve bit 5 ise 0'dır.
PUSHF, bayrak yazmacının 16 bitlik sözcüğünü yığına iter: SP ← SP − 2, [SS:SP] ← FLAGS. POPF, yığının üstündeki sözcüğü bayrak yazmacına yükler: FLAGS ← [SS:SP], SP ← SP + 2. PUSHF bayrakları değiştirmez; POPF ise bütün bayrakları birden değiştirebilir.
Bayraklar programda tek tek okunur: JC CF'ye, JZ ZF'ye bakar. Bayrak yazmacının tamamına doğrudan erişen bir MOV ise yoktur. LAHF yalnız düşük baytı (SF, ZF, AF, PF, CF) AH'ye kopyalar; OF, DF, IF ve TF'ye ulaşmaz. Bayrak yazmacının tamamını saklamanın ve geri yüklemenin tek yolu yığındır. Bayrak sözcüğünü bir yazmaca almak için de yığın kullanılır: PUSHF ve ardından POP AX. Bir .COM programının başında bu iki komut AX'e F202h getirir. F000h dört sabit 1 bitidir, 0200h IF = 1'dir, 0002h ise sabit 1 olan bit 1'dir. STC'den sonra aynı iki komut F203h verir; fark CF'nin bit 0'ıdır.
8.1.4.2PUSHF ve POPF: kod izleme
Aşağıdaki program bir toplamanın bayraklarını saklar, araya bayrakları bozan başka bir toplama koyar ve saklanan bayrakları geri yükler.
ORG 100h
MOV AX, 0FFFFh
ADD AX, 1 ; CF=1, ZF=1
PUSHF ; bayrakları sakla
MOV BX, 5
ADD BX, 3 ; CF=0, ZF=0
POPF ; bayraklar geri
RETİlk satırdaki sayı 0FFFFh biçiminde yazılmıştır. Onaltılık sayı bir harfle başlıyorsa assembler onu bir ad sanmasın diye başına 0 eklenir; FFFFh yazmak hata verir. ADD AX, 1 FFFFh + 1 = 1 0000h hesaplar. Sonuç 17 bittir; AX'e düşük 16 bit, 0000h yazılır ve dışarı taşan 1 CF'ye gider. Sonuç sıfır olduğu için ZF = 1'dir. 0000h'nin düşük baytında 1 biti yoktur, yani çift sayıda 1 vardır ve PF = 1 olur. Düşük dörtlüde F + 1 = 10h elde ürettiği için AF = 1'dir. SF = 0 ve OF = 0'dır: −1 + 1 = 0 işaretli olarak da doğrudur.
PUSHF bu anda bayrak sözcüğünü iter. Sözcüğün değeri bitlerden hesaplanır: sabit bitler F000h + 0002h, IF için 0200h, ZF için 0040h, AF için 0010h, PF için 0004h ve CF için 0001h. Toplam F257h'dir. [FFFCh] = 57h ve [FFFDh] = F2h olur. MOV BX, 5 bayrakları değiştirmez. ADD BX, 3 5 + 3 = 8 hesaplar: elde yok, sonuç sıfır değil. CF, ZF, PF ve AF'nin dördü de 0 olur. 8 = 0000 1000b'de tek bir 1 biti vardır, bu yüzden PF = 0'dır. İlk toplamanın bayrakları kaybolmuştur. POPF F257h'yi bayrak yazmacına yükler ve CF = 1, ZF = 1, PF = 1, AF = 1 geri gelir. AX ve BX'e dokunulmaz; AX = 0000h, BX = 0008h kalır.
| Komut | AX | BX | SP | Bayraklar | Yığın (üstten) |
|---|---|---|---|---|---|
MOV AX, 0FFFFh | FFFFh | 0000h | FFFEh | odItszapc | 0000h |
ADD AX, 1 | 0000h | 0000h | FFFEh | odItsZAPC | 0000h |
PUSHF | 0000h | 0000h | FFFCh | odItsZAPC | F257h, 0000h |
MOV BX, 5 | 0000h | 0005h | FFFCh | odItsZAPC | F257h, 0000h |
ADD BX, 3 | 0000h | 0008h | FFFCh | odItszapc | F257h, 0000h |
POPF | 0000h | 0008h | FFFEh | odItsZAPC | 0000h |
RET | 0000h | 0008h | 0000h | odItsZAPC | (boş); IP = 0000h |
Programın son durumu Tablo 8.1.16'da görülür. POPF yerine POP BX yazılsaydı ne olurdu? Yığından yine F257h alınırdı, ama bu kez BX'e yazılırdı: BX = F257h. Bayraklar ikinci toplamadaki gibi CF = 0 ve ZF = 0 kalırdı. Bu deney POPF ile POP arasındaki farkı açıkça gösterir. İki komut yığından aynı sözcüğü alır; yalnız hedefleri farklıdır.
Program 15 bayttır. ADD BX, 3 üç baytla kodlanır (83 C3 03): 83h işlem kodu, 8 bitlik anlık değerin işaret genişletilerek 16 bite tamamlandığını söyler. ADD AX, 1 ise AX'e özel kısa biçimle 05 01 00 olarak kodlanır. PUSHF (9Ch) ve POPF (9Dh) birer bayttır.
PUSHF ile saklanan bayrakları geri yüklemeden önce yığına başka bir değer itip POPF ile o değeri bayrak yazmacına almak. PUSHF, PUSH AX, …, POPF, POP AX dizisinde POPF, AX'in değerini bayrak olarak yükler ve AX'e eski bayrak sözcüğü gelir. Bayraklar ve yazmaçlar da ayna sırasına uyar: PUSH AX, PUSHF, …, POPF, POP AX.
8.1.4.3Bayrak sözcüğünü değiştirmek
CF, DF ve IF için doğrudan komutlar vardır: STC/CLC/CMC, STD/CLD, STI/CLI. TF'yi değiştiren bir komut ise yoktur. TF'yi 1 yapmanın tek yolu yığından geçer. Bayrak sözcüğü yığına itilir, bir yazmaca alınır, bit 8 değiştirilir, sözcük yeniden itilir ve POPF ile bayrak yazmacına yüklenir:
PUSHF
POP AX
OR AX, 0100h ; bit 8 = TF
PUSH AX
POPF ; TF = 1Hata ayıklayıcılar programı tek adım çalıştırmak için bu kalıbı kullanır. TF = 1 olduğunda işlemci her komuttan sonra bir tip 1 kesmesi üretir. Aynı yöntemle OF de değiştirilebilir; OF'yi doğrudan kuran bir komut da yoktur. Bu kalıpta AX bozulur; AX korunacaksa kalıp PUSH AX ile POP AX arasına alınır.
Bayrak sözcüğünü yığında değiştirmenin bir başka kullanımı da kesme yordamlarıdır. Bir kesme yordamı çağıran programa CF ile bir hata bilgisi döndürmek isterse, yığındaki FLAGS kopyasının bit 0'ını değiştirir. IRET bu kopyayı geri yüklediğinde çağıran program CF'yi görür. DOS işlevlerinin çoğu hatayı bu yolla, CF = 1 olarak bildirir.
8.1.4.4Yığını otomatik kullanan komutlar
PUSHF ve POPF'in yaptığını bazı komutlar kendiliğinden yapar. INT n önce bayrak sözcüğünü, sonra CS'yi ve IP'yi iter; ardından IF ve TF'yi sıfırlar. IRET IP'yi, CS'yi ve bayrak sözcüğünü geri yükler. Kesme yordamları bu yüzden bayrakları kendileri saklamaz; INT bu işi otomatik yapar. Normal kodda ise bir sonucu korumak için PUSHF ve POPF gerekir, çünkü CALL bayrakları saklamaz.
CALL ve RET de yığını otomatik kullanır. Yakın bir CALL, kendisinden sonraki komutun bağıl konumunu, yani dönüş adresini iter ve SP'yi 2 azaltır. RET bu adresi IP'ye çeker ve SP'yi 2 artırır. Uzak bir CALL önce CS'yi, sonra IP'yi iter; RETF ikisini ters sırada geri alır. Bu komutlar, yığın açısından bakıldığında, hedefi IP olan PUSH ve POP'lardır. Prosedürün içindeki her PUSH'a bir POP karşılık gelmezse RET dönüş adresini değil, unutulan değeri alır. Yığın güvenliği kurallarının hepsi bu basit gözleme dayanır.
| Komut | İttiği (sırayla) | Çeken komut | SP farkı |
|---|---|---|---|
CALL (yakın) | IP | RET | 2 bayt |
CALL (uzak) | CS, IP | RETF | 4 bayt |
INT n, donanım kesmesi | FLAGS, CS, IP | IRET | 6 bayt |
8.1.5Yığın güvenliği
8.1.5.1Denge kuralı ve .COM programının sonu
PUSH ve POP komutları programın çalışması boyunca geçici verileri saklar. Yığının bütünlüğü, eşit sayıda PUSH ve POP ile korunur. Bu kural programın yalnız bir yolunda değil, her yolunda, her dalında sağlanmalıdır. Eşit sayıda PUSH ve POP yapılmazsa yığın bozulur ve işletim sistemine geri dönülemeyebilir.
Bir kod parçası, girişindeki SP değeri ile çıkışındaki SP değeri aynıysa yığın açısından dengelidir. Dengeli bir parçada her PUSH'a (ve PUSHF'e) bir POP (ya da POPF) karşılık gelir. Parçanın birden çok çıkışı varsa denge her çıkış için ayrı ayrı sağlanmalıdır.
Dengenin neden bu kadar önemli olduğu .COM programının sonunda görülür. RET yığının üstünde bir dönüş adresi bekler. DOS bir .COM programını başlatırken yığına 0000h koyar. Program dengeliyse son RET bu sözcüğü alır ve CS:0000h'ye, yani PSP'nin başına gider. Orada duran INT 20h programı sonlandırır. Program dengeli değilse RET başka bir sözcüğü alır.
Dengesizliğin üç biçimi vardır ve üçü de farklı belirtiler verir (Şekil 8.1.8). Fazla PUSH'ta RET, dönüş adresi yerine son konan veriyi alır ve rastgele bir adrese atlar. Fazla POP'ta dönüş adresi bir yazmaca “yenir”; RET ondan sonraki sözcüğü alır ve yine yanlış yere döner. Yanlış sırada ise yığın dengelidir ve program doğru yere döner, ama değerler karışır. Program sessizce yanlış çalışır. Üçüncüsü bulunması en zor olanıdır, çünkü program çökmez.
8.1.5.2Hata örneği: dengesiz PUSH
Aşağıdaki programda AX yığına konur, ama hiç geri alınmaz.
ORG 100h
MOV AX, 1234h
PUSH AX ; POP unutuldu!
RET ; IP ← ?| Komut | IP (sonraki) | AX | SP | Yığın (üstten) |
|---|---|---|---|---|
| başlangıç | 0100h | 0000h | FFFEh | 0000h |
MOV AX, 1234h | 0103h | 1234h | FFFEh | 0000h |
PUSH AX | 0104h | 1234h | FFFCh | 1234h, 0000h |
RET | 1234h | 1234h | FFFEh | 0000h |
RET yığının üstündeki sözcüğü dönüş adresi sanar (Tablo 8.1.18). IP = 1234h olur ve SP FFFEh'ye çıkar. İşlemci 0700:1234h adresindeki baytları komut olarak çalıştırmaya başlar. Bu program yalnız 5 bayttır; 1234h adresinde program yoktur, belleğin o bölgesinde ne kaldıysa o vardır. Program çöker ya da donar. Garip olan, DOS'un 0000h sözcüğünün hâlâ yığında durmasıdır. Dönüş için gereken bilgi kaybolmamış, yalnız bir sözcük altta kalmıştır.
Düzeltme kolaydır: RET'ten önce POP AX eklenir ya da PUSH hiç yazılmaz. Asıl zorluk, dengesizliğin çoğu zaman bir dalda gizlenmesidir. Aşağıdaki kodda AX iki dalda da itilir, ama yalnız bir dalda geri alınır:
PUSH AX
CMP BX, 0
JE bitti ; bu dalda POP yok!
; … AX'i kullanan işlem …
POP AX
bitti: RETBX ≠ 0 olduğu sürece program doğru çalışır. BX = 0 olduğu ilk anda RET, AX'in değerine atlar. Bu tür hatalar testte kolay yakalanmaz. Kural şudur: her dalın (JMP, Jcc) PUSH ve POP sayısı ayrı ayrı sayılır. En güvenli çözüm, POP'u tek bir çıkış etiketinin altına koyup bütün dalları oraya yönlendirmektir. Örnekte JE bitti yerine POP'tan önceki bir etikete atlanır.
8.1.5.3Fazla POP ve yanlış sıra
Fazla POP'un etkisi de bir izlemeyle görülebilir:
ORG 100h
MOV AX, 5
PUSH AX
POP BX
POP CX ; fazladan POP
RETPUSH AX ile POP BX dengelidir; BX = 0005h ve SP = FFFEh olur. Fazladan POP CX, DOS'un 0000h sözcüğünü CX'e alır ve SP'yi FFFEh + 2 = 0000h yapar. SP 16 bitte sarmıştır. RET şimdi SS:0000h'deki sözcüğü okur. Bu adres PSP'nin başıdır ve ilk iki baytı CD 20'dir. Little-endian düzende bu sözcük 20CDh'dir. IP = 20CDh olur ve işlemci programla ilgisi olmayan bir adrese atlar. Dönüş adresi, yani 0000h, CX'e “yenmiştir”.
Yanlış sıra ise farklı bir belirti verir. PUSH AX, PUSH BX, …, POP AX, POP BX dizisi dengelidir; RET doğru yere döner. Ama AX ile BX'in değerleri takas olmuştur. Prosedürü çağıran program, AX'te kendi bıraktığı değeri bulacağını sanar ve yanlış bir değerle devam eder. Hata, ancak bu değer kullanıldığında ve çoğu zaman prosedürden çok uzakta ortaya çıkar.
8.1.5.4Yığın taşması ve alt taşma
Dengesizlik bir kez değil de sürekli tekrarlanırsa yığın sınırlarını aşar. 8086 bu sınırları denetlemez; ortada bir hata kesmesi yoktur, yalnız bozulma vardır.
Yığın taşması (stack overflow), yığına ayrılan alandan fazla değer itilmesi ve SP'nin yığın alanının altına inmesidir. Yığın alt taşması (stack underflow), yığına itilmemiş bir değerin çekilmesi ve SP'nin yığın alanının üstüne çıkmasıdır.
Taşmanın tipik nedenleri sonsuz özyineleme ve döngü içinde POP'suz PUSH'tur. SP küçüldükçe yığın aşağı iner ve .COM programında aynı kesimdeki veriyi, sonra kodu ezer (Şekil 8.1.9). Kod ezildikten sonra işlemci bir sonraki turda yığın verisini komut diye çalıştırır. Belirti, çoğu zaman taşmanın başladığı yerden çok uzakta görülür.
Döngüde POP'suz PUSH'un küçük bir örneği şöyledir:
ORG 100h
MOV CX, 3
MOV AX, 0100h
tekrar: PUSH AX
INC AX
LOOP tekrar
RETDöngü üç tur döner ve yığına 0100h, 0101h ve 0102h'yi iter. SP = FFF8h olur. RET yığının üstündeki 0102h'yi alır ve IP = 0102h olur. Bu adres programın içindedir, ama bir komutun başı değildir: MOV CX, 3 0100h–0102h'yi kaplar ve 0102h onun son baytıdır. İşlemci 00h baytını yeni bir komutun başı sayar ve anlamsız bir komut dizisini yürütmeye başlar. Üç tur yerine CX = 0 ile başlayan bir döngü 65 536 tur döner. Bu durumda yığın 128 KB kadar değer iter. 64 KB'lık kesim bu kadarını tutamaz: SP sarar ve yığın programın kodunun üzerine yazar.
Alt taşma PUSH edilmemiş bir değeri POP etmektir. SP = FFFEh iken POP yapılırsa SP = 0000h olur; 16 bitlik toplama sarar. Okunan değer çöptür ya da .COM programında DOS'un 0000h'sidir. Bundan sonraki PUSH, SP'yi FFFEh'ye indirir ve DOS'un sözcüğünün üzerine yazar. Program bu durumda RET ile düzgün sonlanamaz, çünkü dönüş için gereken 0000h ezilmiştir.
Taşmayı önlemenin yolu yığın alanını yeterli büyüklükte seçmektir. Yığın alanının boyutu programın en derin çağrı zincirine ve en çok PUSH sayısına göre seçilir. Her CALL 2 bayt (uzak çağrıda 4 bayt), her PUSH 2 bayt ve her kesme 6 bayt tutar. Kesmeler programın herhangi bir anında gelebilir; donanım kesmesi yordamları da aynı yığını kullanır ve kendi kaydettikleri yazmaçlar için yer ister. .EXE programlarında .STACK 100h gibi bir yönergeyle ayrı ve yeterli bir yığın kesimi ayrılır. Ayrı kesimin bir üstünlüğü de vardır: yığın taşsa bile kodun üzerine yazmaz, yalnız kendi kesiminin altındaki belleği bozar.
Bir program en çok dört düzey iç içe yakın CALL yapar. Her prosedür girişte üç yazmacı iter. Programın her anında bir donanım kesmesi gelebilir ve kesme yordamı dört yazmaç iter. Yığının en çok kaç bayt derinleşeceği şöyle bulunur:
- Çağrılar: 4 düzey × (2 bayt dönüş adresi + 3 × 2 bayt yazmaç) = 4 × 8 = 32 bayt.
- Kesme: 6 bayt (FLAGS, CS, IP) + 4 × 2 bayt yazmaç = 14 bayt.
- Toplam: 32 + 14 = 46 bayt.
Yığın en çok 46 bayt derinleşir. .STACK 100h ile ayrılan 256 bayt bu program için fazlasıyla yeterlidir; aradaki pay sonradan eklenecek prosedürlere ve BIOS yordamlarının kendi kullanımına kalır.
8.1.5.5İyi uygulamalar
Yığın hatalarının çoğu birkaç yazım alışkanlığıyla önlenir. Sunumdaki altı kural şunlardır:
- Ayna sırası.
PUSH AX,PUSH BX,PUSH CXsırasının tersiPOP CX,POP BX,POP AX'tir. Kod alt alta yazıldığında ayna görüntüsü gibi görünmelidir. - Tek çıkış. Prosedürde birden çok RET yerine tek bir çıkış etiketi kullanılır. POP'lar bir kez yazılır ve bütün dallar oradan geçer.
- SP'ye dokunmamak. SP'yi doğrudan değiştirmek yalnız iki iş için yapılır: yığından geçirilen parametreleri temizlemek (
ADD SP, n) ve yerel değişkenlere yer ayırmak (SUB SP, n). n her zaman çift olmalıdır. - BP çerçevesi. Yığındaki verilere
[SP+n]ile erişilemez, çünkü 8086'da SP taban yazmacı değildir.MOV BP, SPyapılır ve[BP+n]kullanılır; BP varsayılan olarak SS kesimini adresler. - Kesimleri korumak. DS ya da ES'yi değiştiren yordamlar onları da PUSH ve POP etmelidir. Çağıran program kendi veri kesiminin değiştiğini bilemez.
- emu8086 ile denetlemek. Yığın penceresi açılır ve RET'ten önce SP'nin girişteki değerine döndüğü denetlenir.
Şekil 8.1.10 ayna sırasını görsel olarak verir. PUSH'lar ile POP'lar arasındaki çizgiler iç içe geçmiş yaylar oluşturur. Çizgiler birbirini kesiyorsa sıra yanlıştır. Bu görüntü, açılış ve kapanış parantezlerinin iç içe dizilişine benzer: en son açılan parantez ilk kapanır.
Aşağıdaki iskelet bu kuralların hepsini bir arada gösterir:
yordam PROC
PUSH AX ; kullanılan yazmaçlar
PUSH BX
PUSH DS ; kesim de değişecek
; … iş …
JC cikis ; hata dalı da çıkıştan geçer
; … iş …
cikis: POP DS ; ayna sırası
POP BX
POP AX
RET
yordam ENDP8.1.5.6emu8086 ile yığını izlemek
Bu notun izleme tablolarının hepsi emu8086'da adım adım elde edilebilir. Yöntem dört adımdan oluşur.
- Derle ve yükle. Program compile ile derlenir, emulate ile öykünücüye yüklenir. Yazmaç panelinde CS, IP, SS ve SP'nin başlangıç değerleri görülür: .COM programı için 0700h, 0100h, 0700h ve FFFEh.
- Tek adım. single step düğmesi her basışta bir komut çalıştırır ve değişen yazmaçlar vurgulanır. step back son adımı geri alır; bir PUSH'un etkisini tekrar görmek için kullanışlıdır.
- Yığın penceresi. Alttaki stack düğmesi SS:SP çevresindeki sözcükleri gösteren pencereyi açar. SP'nin gösterdiği satır işaretlidir. Her PUSH'ta işaret bir satır yukarı (daha küçük adrese), her POP'ta bir satır aşağı kayar.
- Bellek görünümü. Öykünücünün ortasındaki liste baytları onaltılık, onluk ve ASCII olarak gösterir.
B8 12 12baytlarıMOV AX, 1212hkomutudur. Little-endian düzen burada görülür:MOV AX, 1234hB8 34 12 olarak kodlanır.
Yüklemeden sonra CX programın bayt sayısını gösterir; takas örneğinde bu değer 000Bh, yani 11 bayttır. Bir program bu değere güvenmemeli, CX'i kullanmadan önce kendisi yüklemelidir. emu8086'nın yığın penceresinde FFFEh satırında görülen 0000h da bir rastlantı değildir; program sonundaki RET'in dönüş adresidir. Programın her RET'inden önce bu satıra bakmak, yığın dengesini denetlemenin en kısa yoludur.
8.1.6Alıştırmalar ve çözümleri
Sunumdaki beş alıştırma burada ara adımlarıyla çözülmüştür.
1. SS = 2000h, SP = 0100h iken PUSH AX sonrası yazılan baytların fiziksel adresleri nedir?
PUSH önce SP'yi azaltır: SP = 0100h − 2 = 00FEh. Yığının yeni üstü 2000h × 10h + 00FEh = 20000h + 00FEh = 200FEh'dir. Little-endian düzende AL 200FEh'ye, AH 200FFh'ye yazılır. AX = 1234h olsaydı [200FEh] = 34h ve [200FFh] = 12h olurdu.
2. MOV AX,1 / MOV BX,2 / MOV CX,3 / PUSH AX / PUSH BX / PUSH CX / POP BX / POP CX / POP AX sonunda AX, BX, CX nedir?
Üç PUSH'tan sonra yığın üstten alta 3, 2, 1'dir. POP BX 3'ü, POP CX 2'yi, POP AX 1'i alır. Sonuç AX = 1, BX = 3, CX = 2'dir. AX yerinde kalır, BX ile CX takas edilir. AX hem ilk itilen hem de son çekilen yazmaç olduğu için sabit kalmıştır.
3. Yalnız PUSH ve POP ile DS'nin değerini ES'ye kopyalayın.
PUSH DS ve ardından POP ES. İki komut iki bayttır (1Eh, 07h) ve hiçbir genel amaçlı yazmacı bozmaz. Yığın dengeli kalır.
4. SP = FFFEh iken PUSH AX / PUSH BX / PUSHF / POP CX yürütülüyor. SP kaç olur, CX'e ne gelir?
Üç itme SP'yi 6 azaltır, bir çekme 2 artırır: FFFEh − 6 + 2 = FFFAh. CX'e en son itilen sözcük, yani PUSHF anındaki bayrak sözcüğü gelir. Bir .COM programının başında bu değer F202h'dir. Yığında AX ve BX'in kopyaları kalır; program bu hâliyle RET ile bitirilirse BX'in değerine döner.
5. AX'i ve bayrakları bir işlem boyunca koruyan PUSH/POP dizisini yazın.
PUSH AX, PUSHF, … işlem …, POPF, POP AX. Bayraklar en son itildiği için ilk çekilir; dizi ayna sırasına uyar. Örneğin AX = 0000h, CF = ZF = 1 iken sarmalın içinde MOV AX, 7 ve ADD AX, 1 çalıştırılsın. Sarmalın sonunda AX = 0000h, CF = 1, ZF = 1 ve SP = FFFEh olur.
8.1.7Özet
- Yığın, geçici verileri saklamak için ayrılmış ve son giren ilk çıkar (LIFO) düzeninde çalışan bir bellek alanıdır. Programcı (PUSH, POP), işlemci (CALL, INT) ve işletim sistemi aynı yığını kullanır.
- Yığının üstü SS:SP'dir; fiziksel adresi SS × 10h + SP'dir. .COM programında SS = CS ve SP = FFFEh, .EXE programında başlıktaki değerler kullanılır. BP de varsayılan olarak SS kesimini adresler; SP adresleme ifadesinde kullanılamaz.
- Yığın aşağı doğru büyür: PUSH SP'yi 2 azaltır, POP 2 artırır. Birim her zaman sözcüktür;
PUSH ALgeçersizdir. .COM programında kod yukarı, yığın aşağı büyür ve aradaki boş alanı paylaşır. - PUSH: önce SP ← SP − 2, sonra [SS:SP] ← kaynak. POP: önce hedef ← [SS:SP], sonra SP ← SP + 2. POP belleği silmez. SP her an dolu olan en üst hücreyi gösterir.
- PUSH yazmaç, kesim yazmacı ve bellek sözcüğü alır; anlık değerli PUSH yalnız 80186 ve sonrasında vardır. POP CS ve POP sabit yoktur. 8086'da
PUSH SPazaltılmış değeri yazar. - Sözcük yığına little-endian yazılır: düşük bayt SP'nin gösterdiği adrese, yüksek bayt bir üstüne. SP çift tutulur; tek SP her erişimi iki yol çevrimine çıkarır.
- PUSH, POP ve PUSHF bayrakları değiştirmez; POPF bütün bayrakları yükler. Bir CMP sonucu PUSH ve POP arasında güvenle taşınır.
- Yazmaç saklarken POP sırası PUSH sırasının tersidir (ayna sırası); aynı sırayla POP değerleri takas eder ya da ters çevirir. Yığınla bir dizi ters çevrilebilir. İki yazmacın takası için en iyi yol XCHG'dir; yığının asıl görevi saklamadır.
- PUSHF/POPF bayrak yazmacının tamamını saklar ve geri yükler. Bayrak sözcüğü bir yazmaca PUSHF ve POP ile alınır; TF bu yolla değiştirilir. INT bayrakları kendiliğinden iter, CALL itmez.
- Her yolda ve her dalda eşit sayıda PUSH ve POP yapılır. .COM programında son RET, DOS'un koyduğu 0000h'yi alır ve PSP'deki INT 20h programı bitirir. Fazla PUSH'ta RET veriye, fazla POP'ta yığının ötesine döner.
- 8086 taşmayı ve alt taşmayı denetlemez: SP 16 bitte sarar ve yığın kodu ezebilir. Yığın alanı en derin çağrı zinciri, en çok PUSH ve kesmeler hesaba katılarak seçilir.
8.1.8Kavram listesi
Terimlerin İngilizce karşılıkları ve öteki bölümlerdeki kullanımları terim sözlüğünde bulunur.
- yığın stack
- LIFO (son giren ilk çıkar) last in, first out
- yığına itme / yığından alma (PUSH / POP) push / pop
- yığın kesimi (SS) stack segment
- işaretçi yazmacı (SP, BP) pointer register
- taban işaretçisi (BP) base pointer
- bayrak yazmacı (FLAGS) flags register
- yığın taşması / alt taşması stack overflow / underflow
- little-endian / big-endian
- kesim:bağıl konum (mantıksal adres) segment:offset
- fiziksel adres physical address
- varsayılan kesim default segment
- dönüş adresi return address
- yazmaçların korunması register preservation
- veri taşıma komutu data transfer instruction
8.1.9Kendini Dene
SS = 3000h ve SP = 0000h iken
PUSH AXçalışıyor. SP kaç olur? AX'in iki baytı hangi fiziksel adreslere yazılır?Cevap
SP = 0000h − 2 = FFFEh; 16 bitlik çıkarma sarar. Yığının üstü 30000h + FFFEh = 3FFFEh'dir. AL 3FFFEh'ye, AH 3FFFFh'ye yazılır. SP = 0000h başlangıç değeri, yığının kesimin en üstünden başlaması için kullanılabilir; ilk PUSH kesimin son sözcüğüne yazar.
MOV AX, 1A2Bh,PUSH AX,MOV BX, SP,MOV DL, [BX+1],MOV DH, [BX],POP AXdizisinden sonra (.COM programı, SP = FFFEh ile başlıyor) BX ve DX nedir?Cevap
BX = FFFCh. [FFFCh] = 2Bh (düşük bayt), [FFFDh] = 1Ah (yüksek bayt). DL = 1Ah, DH = 2Bh; DX = 2B1Ah olur. Baytlar ters yerlere okunduğu için DX, AX'in baytları yer değiştirmiş hâlidir.
AX = AAAAh, BX = BBBBh, CX = CCCCh iken
PUSH AX,PUSH BX,PUSH CX,POP BX,POP AX,POP CXçalışıyor. Sonuçta üç yazmaç nedir?Cevap
Yığın üstten alta CCCCh, BBBBh, AAAAh'dir. BX = CCCCh, AX = BBBBh, CX = AAAAh olur. Her yazmaç bir başkasının değerini almıştır; yığın dengelidir.
STC,PUSHF,CLC,POPFdizisinden sonra CF kaçtır?POPFyerinePOP AXyazılsaydı CF ve AX ne olurdu (programın başında IF = 1, diğer bayraklar 0)?Cevap
POPF, STC sonrası saklanan sözcüğü geri yükler: CF = 1. POP AX ile CF, CLC'nin bıraktığı gibi 0 kalır ve AX = F203h olur (sabit bitler F002h, IF 0200h, CF 0001h).
Bir .COM programı
MOV AX, 5,PUSH AX,PUSH AX,POP BX,RETile bitiyor. RET'ten sonra IP kaçtır? Program düzgün sonlanır mı?Cevap
İki PUSH'a bir POP karşılık gelir; yığının üstünde ikinci 0005h kalır. RET bu değeri alır: IP = 0005h. İşlemci PSP'nin içindeki bir adrese atlar; program INT 20h'ye ulaşmaz ve düzgün sonlanmaz.
Yığınla diziyi ters çeviren 26 baytlık program 0100h–0119h'yi kaplıyor. SP = FFFEh'den başlayan yığına, programın üzerine yazmadan en çok kaç sözcük itilebilir?
Cevap
n itmeden sonra SP = FFFEh − 2n olur ve SP ≥ 011Ah kalmalıdır. n ≤ (FFFEh − 011Ah) / 2 = (65 534 − 282) / 2 = 32 626. En çok 32 626 sözcük itilebilir.
Bir öğrenci
CMP AX, BXileJEarasınaPUSH CX,INC CX,POP CXkoyuyor. JE karşılaştırmanın sonucuna göre mi atlar?Cevap
Hayır. PUSH ve POP bayrakları değiştirmez, ama
INC CXZF'yi CX'in yeni değerine göre yeniden kurar. JE artık CMP'nin değil INC'in sonucuna bakar. Çözüm, araya PUSHF/POPF koymak ya da INC'i karşılaştırmadan önceye almaktır.8086'da yığınla takas (iki PUSH, iki POP) ile
XCHG AX, BXkaç saat turu sürer? 5 MHz'de fark kaç µs'dir?Cevap
Yığınla: 2 × 11 + 2 × 8 = 38 tur. XCHG AX, BX: 3 tur. Fark 35 tur; 5 MHz'de bir tur 200 ns olduğundan 35 × 200 ns = 7 µs'dir.
8.1.10İleri okuma
- Intel, The 8086 Family User's Manual (1979): PUSH, POP, PUSHF, POPF, CALL, RET, INT ve IRET'in kesin tanımları, kodlamaları ve saat turu sayıları; S3–S6 durum bitlerinin anlamı. Tablo 8.1.2 ve 8.1.6'daki değerler bu kaynaktandır.
- B. B. Brey, The Intel Microprocessors (8. baskı), Bölüm 4: veri taşıma komutları arasında yığın komutları, yığının başlatılması ve PUSH/POP'un ayrıntılı örnekleri.
- K. R. Irvine, Assembly Language for x86 Processors (7. baskı), yığın işlemleri bölümü: çalışma anı yığını, yazmaçların korunması ve yığınla dizgi ters çevirme örneği.
8.1.11Kaynaklar
- Intel, The 8086 Family User's Manual, 1979 — PUSH, POP, CALL, RET, INT, IRET komut tanımları.
- B. B. Brey, The Intel Microprocessors, 8. baskı, Pearson, 2009 — Bölüm 4 ve 6.
- K. R. Irvine, Assembly Language for x86 Processors, 7. baskı, Pearson, 2014 — yığın işlemleri ve prosedürler.
- emu8086 yardım belgeleri: 8086 Assembler Tutorial, Part 9 (The Stack).
İlgili sayfalar: Bölüm 8.1 sunumu · Çalışma soruları · Terim sözlüğü · © Sercan Külcü. Tüm hakları saklıdır.