18 Temmuz 2012 Çarşamba

Peki Neden Trap Flag?

Aslında yazının tam başlığı "Peki Neden Trap Flag? veya FLAGS Yazmacı ve Debuggerlara Genel Bir Bakış" olmalıydı. Son birkaç yazıyı donanımla ilgili yazmışken biraz da yazılımla ilgili birşeyler yazmak aklımdaydı. Bu seferki yazı yazılımla ilgili olacak ağırlıklı olarak.

Önce neden trap flag sorusuna cevap vereyim: Çünkü programlamada en çok kullanılan zero flag adı blogspot'ta alınmıştı. Ayrıca bundan 6-7 sene öncesinde kendi debugger'ımı yazmak aklımdaydı. Hatta bence en zaman alıcı kısım olan disassembler rutinleri hemen hemen hazırdı. Haliyle bir debugger'ın en çok kullanması gereken trap flag aynı zamanda blog adı oldu.

http://css.csail.mit.edu/6.858/2012/readings/i386/s02_03.htm
Neden böyle düşündüğümü bilmiyorum ama FLAGS yazmacı bence x86'ların en eğlenceli yazmacı. 80286'larla birlikte Virtual 86 modunu desteklemek için bir kaç bit eklendi ve 32 bitlik 80386'larla FLAGS yazmacı da 32 bite çıkarıldı ve adı EFLAGS oldu. Benim değineceğim daha çok ilk 16 bitlik FLAGS kısmı. Zaten assembly'e elini bulaştıran herkes az çok Zero Flag (ZF) ile yada Carry Flag (CF) ile uğraşmıştır.


  • Carry Flag (CF): İşlem sonucunda bir elde yada bir taşma varsa set edilir. 8086'da iki 32 bitlik sayıyı toplarken önce düşük anlamlı 16 bit ADD ile toplanır, sonraki yüksek anlamlı 16 bit ADC (add with carry) ile toplanır. ADC eğer ilk ADD işlemi carry oluşturduysa sonuca bir daha ekler. Carry aynı zamanda kaydırma (shift) ve döndürme (rotate) işlemlerinde de kullanılır. Bir tamsayı tek mi çift mi anlamak için sayı sağa kaydırılır. Carry flag set edildiyse tektir, edilmediyse çifttir. Set durumunda CY (Carry), reset durumunda NC (No Carry) adını alır. İçeriği doğrudan CLC, STC ve CMC komutlarıyla değiştirilebilen ender flag'lardandır.
  • Parity Flag (PF): İşlem sonucunda sonuçta tek sayıda bit varsa set edilir, çift sayıda bit varsa resetlenir. Yoksa tam tersi miydi? Hatırlayamadım. Pek kullandığım bir bit değil. Çift bit için PE (Parity Even), tek bit için PO (Parity Odd) adını alır.
  • Auxiliary Carry (AF): CF'ın bir nibble'da (4bit) oluşan eldeler için olanı gibi düşünülebilir. Doğrudan kullanılmasa da AAA, DAM gibi komutlar bu bit yardımıyla BCD düzenlemesi yaparlar. Set durumunda AC (Auxiliary Carry), reset durumunda NA (No Auxiliary Carry) adını alır.
  • Zero Flag (ZF): Yapılan işlemin sonucu sıfırsa bu set edilir. Bu aslında doğrudan kullanmaya kalktığımızda çok da bir işe yaramayacağı açık. CMP ve TEST komutları sırasıyla mikroişlemcide sonucun operandları değil sadece FLAGS'i etkilediği bir çıkarma ve VE (AND) işlemi gerçekleştirir. CMP AL, 23h çalıştığında sonucunda FLAGS'ın değeri SUB AL, 23h çalıştırmakla aynı olur ancak AL'nin içeriğine dokunulmaz. Bu da eğer AL'nin değeri 23h ise ZF set edilir. Bundan sonra JZ yada JNZ komutlarıyla AL'nin içeriğine göre dallanma gerçekleştirilebilir. Set durumunda ZR (Zero) reset durumunda NZ (Non-Zero) adını alır.
  • Sign Flag (SF): Yapılan işlemin en yüksek anlamlı biti olan işaret biti bu bitin içeriğine kopyalanır. Çıkarma işleminin sonucunun negatif olup olmadığını kontrol edebileceğiniz gibi CMP işleminden sonra kullanıldığında sayının büyük yada küçük olup olmadığı kontrol edilebilir. JG ve JL komutları bu flag'ın içeriğine bakar. Reset edildiğinde PL (Plus), set edildiğinde NG (Negative) olur. 
  • Trap Flag (TF): Dediğim gibi bir debugger için en önemli flag. Eğer set edilmişse işlemci her bir komutun çalıştırılması biter bitmez Single Step diye bilinen 1 numaralı kesmeyi çağırır. Ekrana bir numaralı kesmenin vektörünü yazan bir program yazıp bunu hem debugger'da hem de komut satırında çalıştırın. Farkı görün. Debugger'lar bu kesmeye asılırlar. Set edildiğinde EI (Enable Interrupt); reset edildiğinde DI (Disable Interrupt) değerindedir. 
  • Interrupt Flag (IF): IRQ kesmelerini kapatır yada açar. Eğer bir kesme vektörünü değiştirmek, kesme denetleyicisini programlamak vb. kritik bir iş yapıyorsanız bunu resetlemeniz tavsiye edilir ki tam siz vektörü değiştirirken kullanıcı klavyeden bir tuşa basıp bir siz daha tam işinizi bitirirememişken bir kesme oluşturmasın. NMI (Non Maskable Interrupt) haricindeki bütün kesmeler iptal olur. Bunu reset etmek için CLI set etmek için de STI komutları bulunur. Resetleyince DI (Disabled Interrupts), set edince EI (Enabled Interrupts) değerini alır.
  • Direction Flag (DF): MOVS, STOS, LODS gibi komutlar çalıştırıldıktan sonra indis yazmaçlarının (SI ve DI) değerlerinin arttırılacağı yada azaltılacağı seçilir. Yani bellek bloğu işlemleri bellekte ileri doğru yada geriye doğru yapılır. Hangisi reset'ti hangisi set'ti unuttum ama bir değeri UP diğer değeri de DN'dir (Down). STD ve CLD komutlarıyla değeri değiştirilebilir.
  • Overflow Flag (OF): Yapılan işlemin sonucunda bir overflow varsa set edilir yoksa reset'lenir. Hakkında tek bildiğim işaretsiz sayılarda yapılan karşılaştırmalarla ilgili olduğuydu. Set edildiği zaman değeri OV (Overflow), reset edildiği zaman ne oluyordu onu da unuttum.

Buradan sonra asıl görevini açıklayacağım trap flag.


- DOS'un debug.exe'si benzeri basit bir debugger nasıl yazılır? 

Öncelikle eğer .exe dosyaları debugger'da açmak istiyorsanız nasıl belleğe yüklendiklerini bilmek gerekiyor, ki bunu ben de çok az biliyorum. .com dosyaları ele alalım. Onların belleğe yüklenmesi kolay. Herhangi bir segmentin 0100h adresine yükleniyorlar. Segmentte 0000h ile 0100h adresleri arasında .com dosyanın içerisinde bulunmayan, DOS'un exec() fonksiyonunun ürettiği bir kısım var, adı program segment prefix (PSP). Bu kısmı ayrıntısıyla hatırlamak zor ancak 0000h adresinde 0CDh ve 20h byte'ları var. Bu çalıştırılabilir bir kod ve assembly karşılığı INT 20h. DOS'un eski usül (CP/M zamanlarından kalma) kodu sonlandırma fonksiyonu. Diğer DOS kodunu sonlandırma yolları AH'nin değeri 00h yada 4Ch iken INT 21h'yı çağırmak. İlk iki yolu DOS'un ilk versiyonu bile destekliyor. Son yol ise DOS2.0 ile birlikte destekleniyor ve DOS'a bir geri dönüş değeri (Errorlevel) bildirebilmek için tek yol. PSP'nin 80h ile 0FFh byte'ları arasında programın çağırıldığı komut yer alıyor. Komut karakterlerinin nasıl tutulduğunu anlamak için örnek resme bakmak yeterli.


PSP'nin CP/M zamanlarında kalma olduğunu söylemiştim. JMP 0000h gibi bir komut .com dosyayı sonlandırmak için yeterli aslında. Program ilk defa belleğe yüklendiğinde aslında stack'da da bazı değerler oluyor. Bunları tam bilmemekle birlikte bunlardan ilki her zaman 0000h değeri. Dolayısıyla RET komutu da .com dosyayı pekala sonlandırıyor. Bunun nedeninden tam olarak emin olmamakla birlikte bir seçenek yine CP/M'e uyumluluk için bırakılmış olabileceği. Diğer uzak bir olasılıksa UNIX uyumlulularında her program aslında çekirdeğin çağırdığı bir fonksiyon olarak ele alınıyor. POSIX standardı bile olabilir. Bu nedenle DOS'u bilmem ama UNIX için C kodu yazarken main()'i mutlaka int olarak tanımlamak gerekiyor. void tanımlamak DOS'da sorun çıkarmazken UNIX'de sorun çıkarabilir. Herneyse eğer program bir fonksiyonsa onu sonlandırmak için RET çağırmaktan daha doğal birşey olamaz.

Bu arada bir program çalıştığında DOS ona dallanmadan önceki son CS:IP değerlerini INT 22h'nin vektörüne yazıyor ancak programı INT 22h'yi çağırarak sonlandırmak, programın çalıştığı bellek bloklarının işletim sistemine geri verilememesine açtığı dosya ve sistemden aldığı diğer kaynakların açık kalarak sisteme iade edilememesine neden olacaktır. Eğer exec()'i işletim sistemi değil başka bir program çağırmışsa o zaman kesme vektörü o programın CS:IP'sini gösterecektir. INT 23h CTRL+C yada CTRL+Break tuş kombinasyonu basıldığında aktif hale gelir. Bir tuşa basılır basılmaz klavye IRQ'sunun kesme hizmet programı basılan tuşu (yada ASCII kodunu) klavye belleğine (keyboard buffer) kopyalar ve çalışmayı o anda ödünç aldığı (kestiği) programa geri döner. DOS kesme hizmet programını değiştirip geliştirebilir. Komut satırındayken CTRL+C için eğer basılan tuş buysa klavye belleğine kopyalamayıp yeni bir komut satırı açar yada o anda bir program çalışıyorsa o programı sonlandırabilir. Bir program da yazılma amacına göre bu kesmeyi değiştirip (hatta yeri geldiğinde klavye kesme hizmet programını da değiştirebilir) CTRL+C'ye basıldığında başka birşeylerin yapılmasını sağlayabilmelidir. Tam ayrıntısını bilmemekle birlikte bir program çalışırken CTRL+C'ye basıldığında DOS, kesme hizmet programından sonra INT23h'ü çağırır. (Sanırım geri dönüş değerlerinin DOS çekirdeği mi yoksa ayrı bir program mı olduğunu kontrol ediyor.) Eğer program INT23h'ü değiştirmişse kendi istediklerini yapabilir. Son olarak herhangi önemli bir DOS hatası oluşursa yine CTRL+C'de olana benzer biçimde INT24h çağrısı yapılır. Bütün bu kesmelerin dökümantasyonu dünyaca ünlü Ralf Brown's Interrupt List'de bulunabilir. Ben bile bunları yazarken arada bu listeye bakıyorum.

Debugger'da .com dosya için uyumluluk için bile olsa uygun PSP oluşturmak gerekli. Bu INT 21h'in büyük olasılıkla AH=26h altfonksiyonunu (yada belki AH=4Bh ama bence 26h'dır kesin) çağırarak halledilebilir. Kodu belleğe yükledikten sonra kullanıcıdan debug.exe gibi komutlarını alıyoruz. Eğer g (Go) vermişse direk çalıştırıyoruz yani kodu yüklediğimiz segmentin 0100h offsetine dallanıyoruz. Eğer p (Proceed yani Step/Trace Over) yada t (Trace yani Step/Trace Into) çağırıldıysa işte o zaman trap flag'i set edip kodun son kaldığı yere dallanmak gerekiyor. Bu durumda program çalışırken bütün yazmaçların değerlerini yedeklemeli ve komut verildiğinde dallanmadan önce bu değerleri geri yüklemeli. Bu arada Intel'in Pentium'lara eklediği LOADALL adında dökümante etmediği bir komutu vardı sanırım, POPA'ya benzeyen. Trap flag en son set edilmeli yada trap flag'i set eden bir komut çalıştırılacak kodun başına eklenip o kodun uzunluğu kadar geriye de dallanılabilir. Tabi uygun bir INT 01h yordamı da yazıp vektörünü girmek gerekiyor. Bütün işleri yapan tek bir INT 01h kullanılacaksa program başlar başlamaz set edilebilir ama t için ayrı p için ayrı INT 01h yazmak bana daha tercih edilebilir geliyor. debug.exe'de INT 01h'in hizmet programı, komutun çalışması sonlanınca yazmaçların değerlerini ve sıradaki kodu hem makina kodu olarak hem de disassemble edilmiş karşılığını ekrana yazıyor. Debugger'ın disassembler rutinleri bu aşamada gerekli (bir de u (Unassemble) komutu verildiğinde).

Bir de debugger'in asıl daha önemli bir kesmesi var ki o da INT 3. Bütün debugger'lar bu kesmeye de asılırlar (hook). INT 3'ün önemli özelliği makina kodu olarak hem 0CDh 03h ile hem de 0CCh ile çağırılabilir olması. Tabii ki tercih edilen yol 0CCh ile çağırmak. Tek byte opkod programcıya tek byte'ı değiştirerek oraya kırılma noktası (break point) eklenebilmesini sağlıyor. Aslında kesmenin başka da önemli bir özelliği yok çünkü aynı işi iki byte'da da yapabilmek olanaklı ve debugger olmadıkça INT 3'ün içeriği bir IRET'den ibaret (en azından öyle olması gerekir ama ne yapacağı işletim sistemine kalmış). INT 01h'ya benzer şekilde bütün işi INT 3'ün vektörünü yazmak olan bir programımız olsaydı, bu da debugger'da farklı kendi kendine farklı sonuçlar verecekti.

Debugger'lar bu kadarla bitmiyor, örneğin derleyiciyle çalıştırılabilir kodun içerisinde sembolleri saklayacak biçimde derledikten sonra bunları okuyabilir olması gerek. Sanırım debug.exe'de bu özellik bulunmuyor. Bundan başka breakpoint'lere değindim ama bunların belli koşul gerçekleştiğinde çalışanları, "conditional breakpoint"'ler de güzel bir özellik. debug.exe de bundan da yok. Assembler ile kodun içerisinde çalışırken kodları değiştirebilme gibi özellikler zaten olmazsa olmaz. Sadece trap flag ile iş bitmiyor. Bu yazıyı genel bir fikir vermesi için yazdım. İş debugger'i kodlamaya geldiğinde bu kadar basit olmayacağı açıktır.

Hiç yorum yok:

Yorum Gönder