plazma - amatör bilgisayar kültürü

Vice ile Debug

Bilgem 'Nightlord' Çakır

1. Giriş

Merhaba arkadaşlar. Bu yazıda genel bazı hata arama ve bulma tekniklerinden bahsedip, bu tekniklerin özellikle C64 programları geliştirirken nasıl uygulandıklarını göstereceğiz. Bunun için en önemli araçlardan biri olan Vice emülatörünü kullanacağız. Vice emülatörü hem Windows hem Linux'ta çalışan bir araç olduğu için iki platformda Vice'ın farklı olduğu noktaları da belirteceğiz.

Bu yazıda aşağıdaki konular anlatılacak:

  • Debuggerlara Genel Bakış

    • Breakpointler

    • Kod'u adım adım çalıştırma

    • Registerları inceleme

    • Belleği inceleme

  • Vice Monitörü

    • Örnek kod

    • Monitöre giriş ve çıkış

    • Breakpointler

    • Adım adım ilerlerken registerları takip etmek

  • C64 Programlarında Sık yapılan hatalar

  • Daha ileri teknikler

    • Bellekteki değişmeleri yakalamak.

    • Karmaşık breakpointler

    • Başkalarının programlarını incelemek.

2. Debuggerlara Genel Bakış

Her platformda debugger deyince aklımıza gelen bazı ortak noktalar vardır. Genelde yazılan bir programın içinde herhangi bir anda belleğin ve CPU registerlerinin durumunu sorgulayarak o programın içindeki hataları bulmaya ve gidermeye çalışırız. Debuggerlar çeşitli GUI'ler veya komut konsolu arayüzlerine sahip olabilir. Fakat bazı temel özellikler bütün debuggerlarda aynıdır.

2.1. - Breakpointler

Bellekte çalışan kodun, herhangi bir noktasına gelince programın duraklamasını isteyebiliriz. Breakpointler debuggerda, bellekteki kodun bir satırına (veya makine dili komutuna) yerleştirebildiğimiz bir araçtır. CPU kodu işletirken, breakpoint ile karşılaşırsa o anda programı dondurup kontrolu bize ve debugger'a verir. Böylece biz debugger'ın çeşitli komutlarını kullanarak registerlerın ve belleğin tam o anda sahip oldukları değerleri inceleyebiliriz.

Genelde breakpointleri kullanırken aklımızda iki şey vardır:

Birincisi CPU oradan önceki komutları başarıyla işletip bu noktaya gelebiliyor mu, bunu görmek isteyebiliriz. Programımızı yazıp çalıştırdığımız zaman ilk karşımıza çıkan ve yanlış olan belirti, genelde bize nereye bakmak istediğimiz ile ilgili fikir verebilir. Örneğin, programın bir yerde ekrana bir mesaj çıkarmasını bekliyorsak ve bu mesaj çıkmıyorsa, mesajı yazdıran fonksiyonu çağıran yerlere breakpoint koyabiliriz. Programımız o fonksiyonu düzgünce çağırıyor mu? Bazen bu şekilde breakpoint koyup programımızı çalıştırınca CPUnun durmadığını görür ve anlarız ki program oraya hiç gelmiyor. Ya bir yerlerdeki kontrollü dallanma (if else / je / bne ...) bizim beklediğimiz gibi çalışmıyor, ya da program daha önce bir yerde bir sonsuz döngüde kalıyor.

İkincisi ise eğer program başarıyla breakpointe ulaşıp duruyorsa breakpointten sonraki o kritik algoritmamızı yavaş yavaş inceleyebilme isteği. Çoğu zaman bir breakpointten sonra programımızı adım adım çalıştırarak yaptığımız pekçok hatayı buluruz.

Breakpointleri öğrendiğiniz anda hata ayıklama sanatının %50'sini öğrendiniz demektir.

2.2. - Kod'u adım adım çalıştırma

Genelde bir algoritmanın tam olarak doğru çalışıp çalışmadığını anlamak için debugger'ın adımlama özelliklerinden faydalanırız. Bütün debuggerlarda Step-In, Step-Over ve Return-from-Function denilen üç adım çeşidi her zaman vardır.

Step-In bir seferde bir komut ilerlemenizi sağlar. Bu komut eğer bir alt fonksiyona atlama komutu ise, Step-In yaptığınızda gidilen alt fonksiyonun ilk komutunda bulursunuz kendinizi.

Step-Over da bir seferde bir komut ilerlemenizi sağlar. Fakat Step-In'den farklı olarak eğer bir sonraki komut alt rutine atlama komutuysa, Step-Over ile bütün alt fonksiyonu bir seferde çalıştırıp o alt rutin dondukten sonraki ilk komutta bulursunuz kendinizi. Dolayısıyla kodu adım adım ilerletirken detaylı incelemek istemediğiniz bir alt rutin çağrısı ile karşılaşırsanız Step-Over ile vakit kaybetmeden ilerleyebilirsiniz.

Son olarak Return-From-Function komutu ile de o an bulunduğunuz fonksiyon veya rutinin sonuna kadar ilerleyip, o rutini çağıran yere dönebilirsiniz. Bu da genelde bir alt rutini incelemek için Step-In ile girdikten sonra aradığınız sonucun o rutinde olmadığını görünce kullanabileceğiniz bir komuttur.

2.3. - Registerları ve Belleği inceleme

Genelde kodda adım adım ilerlerken her komuttan sonra CPU registerlerindeki değerleri inceleyerek komutların ve hesaplamaların beklediğiniz sonuçları ortaya çıkarıp çıkarmadığını takip edebilirsiniz. Her debugger CPU registerlerindeki değerleri ve belleğin durumunu size gösterecek komutlarla donatılmıştır.

3. Vice Monitörü

Bu kadar genel tanıtımdan sonra yukarıda bahsettiğimiz komutları Vice'da nasıl vereceğimizi inceleyelim. Bu bölümde örnek olarak hatalı bir kod ile başlayacağız. Daha sonra vice debugger'ını kullanarak hatayı nasıl bulabileceğimizi göreceğiz.

3.1. - Örnek kod

Bir bahar akşamı cevval coder adayı Psychadelic/Granite yeni introsunu (Devil is My Friend adlı şaheser olacaktı) kodlamaya başladı. Tabii ki ilk olarak her introsunda olduğu gibi ekrana bir tane raster çizgisi koyarak başlayacaktı. Hemen aşağıdaki kodu yazdı. Birkaç ACME uyarısını atlatıp sonunda kodu derledi. Vice ile açtı. sys 49152... Ve hic bir şey olmadı.

;; UYARI: BU KOD BİLİNÇLİ OLARAK 
;; HATALAR İÇERMEKTEDİR

	!to "devil.prg", cbm
	*=$c000
;; ------------------------------------
Baslangic:
	jsr RasterIRQHazirla
Son:
	jmp Son
;; ------------------------------------
RasterIRQHazirla:
	sei
	
	lda #$7f
	sta $dc0d
	
	lda $d01a
	ora #$01
	sta $d01a
	
	lda $d011
	and #$7f
	sta $d011
	
	lda #$20
	sta $d012

	lda $00			
	sta $0314
	lda $c1
	sta $0315

	cli
	rts
;; ------------------------------------
	*=$c100
IRQRutini:
	inc $d019

	ldx #$2
IRQ_gecikme_dongusu:	
	dex
	bne IRQ_gecikme_dongusu
	
	lda $d020
	pha
	lda #$01
	sta $d020

	ldx $0a  		
IRQ_gecikme_dongusu2:
	dex
	bne IRQ_gecikme_dongusu2
	
	lda #$0e		
	sta $d020

	jmp $ea81
;; ------------------------------------

3.2. - Monitöre giriş ve çıkış

Bunun üzerine Psychadelic hemen Vice'ı resetleyip bu sefer sys49152 demeden önce, Alt-M ile Monitore girdi (Linux Alt-H). Artık Debugger konsolu karşısında onun komutlarını bekliyordu.

Vice Debugger Konsolu

Şekil 1. Vice Debugger Konsolu

3.3. - Breakpointler

Hemen kendine sordu. "Kurduğum interrupt çalışıyor mu? Program interrupt rutinine ulaşıyor mu?" Bunu anlamak için $c100'deki interrupt rutininin başına bir breakpoint koymaya karar verdi. Bunun için konsola şu komutu yazdı

break c100

Ardından kodu çalıstırmak için debuggerdan çıkmaya gerek duymadı ve direk başlangıç adresine atlama komutu verdi.

g c000

Program breakpoıntte durmadı ve çalışmadı. Psychadelic anladı ki interrupt rutinini doğru kurmuyor. Bunun üzerine programın başına breakpoint koyup interrupt rutinini hazırladığı bölümü tek tek incelemeye karar verdi.

break c000
g c000

İkinci komutu vermesiyle beraber program calışmaya başlamak uzere c000a atladı ve oradaki breakpointe yakalandı

3.4. - Adım adım ilerlerken registerları takip etmek

z komutunu kullanarak adım adım komutları işletmeye başladı. Vice kullanıcı konsol penceresinde Enter'a her bastığında son debugger komutunu işletiyordu. Bu sayede ilk seferde z komutunu verdikten sonra her enter'a basışında yeni bir Step-In komutu vermiş oluyordu. Bu komutlar sayesinde program satır satır ilerledi ve her defasında bir sonraki assembly komutunun işletildiğini gördü.

Sonsuz döngü komutuna kadar böyle gelince, buradaki komutların hepsine uğrandığına dair emin olduktan sonra daha detaylı incelemeye karar verdi. tekrar g c000 yaparak başa döndü.

Bu sefer debugger'ın registers penceresini de açtı. Bütün komutları tek tek işletirken bir yandan da her komuttan sonra registerlerın değerlerine bakıyordu. (Vice'ın linux versiyonunda malesef ayrı bir register penceresi yoktur. Debugger konsolunda registers komutu vererek registerlar listelenebilir. Dolayısıyla satır satır ilerleyip her satırdan sonra registerlara bakma için sırayla bir Step-In bir registers komutu vermek gerekiyor. Neyseki ilk defa bu komutları kullandıktan sonra, yukarı ok tuşunu kullanarak son kullanılan komutlara erişmek mümkün)

Debugger Registers Penceresi

Şekil 2. Debugger Registers Penceresi

Sonunda tam sta $0314 komutuna geldiğinde A registerinde garip bir değer gördü. Daha bir önceki satırda 0 yüklenmesine rağmen şimdi A registerinde $2f değeri vardı. bir önceki satıra tekrar baktı:

lda $00

Birden kafasında şimşek çaktı. Bu komut akümülatöre 0 yüklemiyordu. Aksine 0. adresten okuduğu değeri yüklüyordu. Yani adresleme modu yanlıştı. Immediate adresleme modu yerine sıfırıncı sayfa adresleme modu çalışıyordu. Hemen komutu düzeltti.

lda #$00

Zaten bir altındaki satırda da aynı hatayı yaptığını farketti ve onuda düzeltti. Hemen acme ile yeniden derleyip vice ile çalıştırdı.

Sonuç hüsrandı. Yine boş ekran ve ready yazısına dönmüştü makine. Hemen çıkıp bir daha derledi ve çalıştırdı sonuç aynıydı. Yalnız bu sefer ekranda ready mesajından önce küçük beyaz bir flaş olduğunu farketti. Bu iyiye işaretti. Acaba bir şekilde IRQ rutinindeki beyaz çizgi koduna varıyor muydu sistem.

Hemen vice monitörüne girip yine IRQ rutinine breakpoint koydu ve programı çalıştırdı.

break c100
g c000

Debugger breakpointte durdu. Yani sistem başarıyla interrupta gelebilmişti. Sistem interruptı düzgün kurduktan sonra her ekran taramasında yeniden c100'deki breakpointe yakalanmalıydı. Daha detaylı bakmadan önce tekrar yakalanacak mı diye kontrol etmek için programı devam ettirmeye karar verdi. Bunun için debuggerdan çıkış komutunu verdi:

exit

bu komutla beraber program devam etti ve kendisini ready yazısında buldu. Demek ki IRQ rutinine sadece bir kere giriliyor sonra IRQ rutininde yaşanan bir problemden dolayı bir daha girilemiyordu. Bunun üzerine IRQ'dan çıkışı kontrol etmeye karar verdi. Alt-M ile tekrar monitöre girip kernelde atladığı IRQ çıkış rutinine breakpoint koydu.

break $ea81
g c000

Program önce daha önceden koyduğu c100'deki breakpointte durdu.

exit

Bu sefer program ea81 adresinde durdu. Burada art arda z komutuyla ilerlerken karşısına RTI komutu çıktı. Bu komutla CPU IRQ işlemekten çıkacak ve normal rutine yani bu programda ihtimalen c003 adresindeki sonsuz döngüye dönecekti.

Fakat z demesiyle beraber kendini alakasız bir adreste buldu. Karşısına BRK komutu çıkmıştı. Yani program IRQ dan düzgün şekilde dönmüyordu.

İnterrupttan dönerken olanları düşündü. RTI komutuyla işlemci stackten döneceği adresi alıyor ve oraya atlıyordu. Bu program yanlış yere atladığına göre stackten yanlış adresi alıyor olmalıydı. Ama stack nasıl bozulabilirdi. Hemen IRQ rutini için yazdığı koda bir daha baktı. Ve orada PHA komutunu gördü.

"Aaaah ben d020'deki değeri stack'e atıp beyaz çizgiden sonra geri stackten okuyacaktım." Evet başta böyle tasarlamıştı. Ama sonra bunu unutup d020 yi eski rengine döndürmek için PLA ile stackten değeri okuyacağına LDA ile direk yüklemeye kalkmıştı. Bunun sonucu olarak, IRQya girilirken stackteki en üst iki bayt dönüş adresi oluyor fakat çıkış esnasında aradaki PHA ile gonderilen bayt hiç PLA ile geri çekilmediği için en üst iki bayt dönüş adresi baytlarından biri ile d020'nin PHA'lanmış rengi oluyordu. Bu da yanlış adres demekti. Hemen gidip lda #$0e satırını sildi ve yerine pla komutunu koydu:

pla
sta $d020

Hemen derleyip yeniden çalıştırdı. Ve bu sefer karşısına beyaz bir çizgi çıktı. İşte artık IRQ çalışıyordu. Fakat çizgi çok kalındı. Gecikme değeri olarak sadece $0a koymuştu oysaki. Ama artık içi rahattı. Bu raster çizgisinin çıkmamasına göre çok daha küçük bir hataydı.Elbet bulunurdu.

Hemen gecikme döngüsünü tekrar inceledi. çok geçmeden hatayı gördü. Yine # karakterini unutmuş ve yanlış adresleme modunu kullanmıştı.

lda #$0a

ve tekrar derlediğinde artık programı çalışıyordu.

3.5. Vice komutlarının özeti

  • break c000 : $c000 adresine breakpoint koy

  • g c000 : $c000adresinden kodu çalıştırmaya başla

  • z : (breakpointe gelip durduğunda) Step-In

  • n : Step-Over

  • ret : Fonksiyondan geri dön

  • exit : debuggerdan çıkıp sonraki breakpointe rastlayana kadar normal çalışmaya devam et

  • registers: registerların o anki değerini göster

  • m c000 : c000 adresinden başlayarak bellekteki baytları göster

  • d c000 : c000 adresinden başlayarak assembly kodunu listele

Disassembly Penceresi

Şekil 3. Disassembly Penceresi

4. Sık Yapılan Hatalar

Bazı basit hataları nedense en tecrübeli programcılar bile sıklıkla tekrar tekrar yaparlar. Bunlardan bazıları aşağıda. Eğer programınız çalışmıyorsa ilk arayacağınız hatalar bunlar olmalı. Yaptığınız hataların en az %50'si bunlar olacaktır.

  • Unutulan #: Bunu örneğimizde de gördük

  • Stack hataları: Dengesiz push ve pop komutları. Eğer bir IRQ rutininde veya alt rutinde pha ve pla komutlarının sayısı eşit değil ise, yani başka bir deyişle stack'e PHA ile koyduğunuz her değeri geri almıyor veya fazladan değerler alıyorsanız. Programınız RTS veya RTI komutuna rastladığında yanlış yere atlayacaktır. Bunun sebebi de daha önce de belirttiğimiz gibi dönüş adresinin stackte tutulması.

  • İndex hataları: İndexli adresleme kullanırken (lda $c800,x gibi) iki şeyi kontrol ettiğinizden emin olun. Bu komutlar taban adresinin yanlış olduğu durumlarda veya X registerinin beklenmedik bir değer aldığı durumlarda yanlış çalışırlar. Genelde okunan tablo ebadı gözden kaçabilir. Örneğin c800 adresinden başlayan 32 baytlık bir tablonuz varsa X registeri de 0 ile 31 arasında olmalıdır. Eğer X registerindeki değer başka bir matematiksel işlemle hesaplanıyorsa bu işlemin sonucunun 0-31 arasında kalıp kalmadığını kontrol etmelisiniz

  • Karşılaştırma hataları: Özellikle C64 ile ilk programlamaya başladığınız zamanlarda koşullu dallanma komutlarını birbiriyle karıştırabilirsiniz. BNE, BEQ en çok kullandıklarınız olduğu için fazla karışmaz fakat, BPL ile BCCyi karıştırabilirsiniz bu komutların anlamlarını tam olarak anladığınızdan emin olun

  • Unutulan inc $d019: VIC IRQlarını kullanırken, IRQ rutini içinde inc d019 ile VIC'e IRQ isteminin işlendiğini bildirmezseniz IRQ rutininiz sadece bir kere çalışır

  • Kendini değiştiren programlar yazarken yanlış baytı değiştirme: Genelde C64de hız optimizasyonu olarak kendini değiştiren kodlar yazmak sık kullanılan bir tekniktir. Bunların debug edilmesi genelde daha zordur. Eğer programınız beklemediğiniz şekillerde kilitleniyorsa kodun kendi kendini değiştirdiği noktaların yakınlarına breakpointler koyarak oraları adımlamalısınız. Örneğin aşağıdaki koda bakın

    ldx value
    lda jmpTable_lo, x
    sta jumper
    lda jmpTable_hi, x
    sta jumper + 1
    
    jumper:
    jmp $0000
    

    Bu kod val değerine bakarak bir adrese atlamaya karar veriyor. Yani değişik val değerlerine göre değişik yerlere atlayacak. Fakat bunu denerseniz çalışmayacak ve debug ederseniz, jmp komutunun bozulduğunu göreceksiniz. Bunun sebebi jumper adresindeki ilk baytın JMP komutunun kendisi olması. Değişmesi gereken baytlar aslında jumper ve jumper+1 adreslerinde değil, jumper+1 ve jumper+2 adreslerinde yer alıyor.

  • 16 bitlik değerlerin üst 8 biti ile ilgili hatalar: Bazen 16 bitlik değerlerle uğraşırken hatalar yapabilirsiniz. İlk önce her zaman iki baytı aynı şekilde anlamlandırdığınızdan emin olun (küçük bayt önce büyük bayt sonra yani little endian). Bunun dışında 16 bitlik aritmetik işlemlerde carry bitlerini doğru kullandığınızdan emin olun

5. Daha ileri teknikler

Önceden de belirttiğimiz gibi en çok yapılan hatalar ve temel breakpoint koyma ve kodu adımlama ile ilgili bilgileriniz zaten debug işlemlerinizin %80'ini oluşturacaktır. Bunun yanında elbette bazı daha ileri teknikler de mevcut. Bunların kullanımı yer yer daha karmaşık olabilir fakat zaman zaman ihtiyaç duyabilirsiniz

5.1. - Bellek erişimlerini yakalamak.

Bunun için breakpointlere benzeyen başka bir aracı kullanıyoruz. Vice'da bunlara "watchpoint" deniyor. Bellekte bir adrese veya adres aralığına watchpoint koyarak, oranın okunduğu veya yazıldığı anda programın bir breakpointe gelmiş gibi durmasını sağlayabiliriz. Bu özellikle bellekte bir bölgeye koyduğunuz kod veya data siz istemeden üzerine yazılıyorsa ve siz bunu yapanın kim olduğunu bulmak istiyorsanız çok işinize yarayacaktır.

watch store $0314 

Bu komut 0314'e bir değer yazılınca durulmasını sağlar.

watch load $1000 

bu komut 1000 adresinden okuma yapınca durulmasını sağlar.

5.2. - Trace

Bu da breakpoint ve watchpointlere benzer şekilde "tracepoint" adı verilen bir aracın kullanılmasıyla olur. Yine istediğiniz adreslere tracepointler koyarsınız. Ardından programı çalıştırdığınızda CPU tracepointlere rastladıkça adresini yazar. Böylece bu yazılan adreslere bakarak programın çalışırken nerelerden geçtiğini görmüş olursunuz. Yani programın yürüdüğü yolu takip edebilirsiniz. Bu da genelde kilitlenmelerde işinize yarar. Kilitlenmeden önce programınızın hangi noktalardan geçtiğini öğrenebilirsiniz. Bunun için trace komutunu kullanmanız yeterlidir

trace c000

Bu komut c000 adresine bir tracepoint koyar

5.3. - Karmaşık breakpointler

Bir diğer konu breakpointlerinizi koşullu yapabilirsiniz. Bazen büyük döngülerin içinde her defasında uğramak istemediğiniz yerlere breakpoint koymanız gerekir. Bu zamanlarda breakpoint'in sadece belli koşullarda durmasını sağlayabilirsiniz. Bunun için condition komutu kullanılır. Önce normal şekilde breakpoint konulur. Bu noktada oluşturulan breakpointin numarasına bakılır. Ardından aşağıdaki şekilde koşul tanımlanır:

condition 2 if .X==7

Bu örnek 2 numaralı breakpointin yalnızca X registerinde 7 değeri varken programı durdurmasını sağlar

5.4. Başkalarının programlarını incelemek.

Son olarak, çok faydalı bir öğrenme yolu olan başkalarının kodunu dissassemble etmekten kısaca bahsedeceğiz. Genelde C64 te hoşunuza giden ilginç bir demo efekti veya eklentiler yapmak istediğiniz toollar olabilir. Bunları disassemblerda inceleyip anlamak sabır ve tecrübe ister ama bir yerlerden başlamak gerekir. İşte başlangıç için bir kaç ipucu

  • h (Hunt) komutu: Bu komut ile bir adres aralığında istediğiniz uzunlukta bir bayt dizisini arayabilirsiniz. Örneğin

    h 0800 d000 78 a9
    

    komutuyla 0800 d000 adresleri arasında 78 a9 baytlarının arka arkaya geldiği yerleri ararsınız. Neden 78 a9 örneği verdik. Çünkü 78 sei komutudur a9 ise lda# komutudur. Bu komutla bellekte bir demo rutininin IRQ hazırlığını yaptığı bölümü bulabilirsiniz. Ardından çıkan adresteki kodu disassemble ederek takibe başlayabilirsiniz. Böyle başka aramak isteyebileceğiniz komutlar sta $0314, sta $d012 olabilir. Aynı şekilde ilginizi çeken bir rutin bulduktan sonra o rutinin adresine nereden jsr veya jmp yapıldığını da arayabilirsiniz.

  • m d000 komutu: Bu komut daha önce anlattığımız m komutu yani bellek adreslerine bakmak için. Tabi çoğu zaman d000 adresinde IO bölgesi aktif olduğu ve VIC çipinin registerleri orada olduğu için m d000 komutu çok faydalıdır. Bize VIC çipinin o anki durumunu verir. Mesela d015 adresine bakarak spriteların kulllanılıp kullanılmadığını, d018 adresine bakarak video matris ve karakter bankları için hangi adreslerin kullanıldığını öğrenebilirsiniz.

  • sta d011 aramak: Demin bahsettiğimiz h komutuyla d011 erişimlerini arayabilirsiniz. Neredeyse bütün VIC efektlerinde d011 erişimleri kullanıldığı için bu şekilde kolaylıkla IRQ rutinini bulabilir ve Crossbow'un cycle artırma tekniklerini öğrenebilirsiniz.

  • defter ve "çağırma grafiği": Son olarak eğer bir programı tamamen anlamak istiyorsanız, detaylı bir şekilde hangi adresteki hangi rutin kimi çağırıyor oturup bir deftere çizelge olarak çizmeniz gerekir. Malesef genel olarak VIC efektleri dışında komplike araçları oyunları veya matematiksel demo efektlerini bu şekilde anlamak hayli zordur. Ama tecrübeniz arttıkça şansınız artacaktır.

6. Sonuç

Bu yazıda oldukça geniş bir konu olan hata bulma tekniklerine değinmeye çalıştık. Artık C64 üzerinde kod geliştirirken daha donanımlı olacağınızı ve elinizdeki araçlardan daha fazla verim alacağınızı umut ediyoruz.

Bugsız günler

nightlord (at) nightnetwork (nokta) org

7. Yararlanilan Kaynaklar

  • Vice Online Documentation: http://www.viceteam.org/vice_toc.html

plazma - 2008