Vice ile Debug

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:

Debugger'lara Genel Bakış * Breakpoint'ler Kod'u adım adım çalıştırma Register'ları inceleme * Belleği inceleme Vice Monitörü Örnek kod Monitöre giriş ve çıkış Breakpoint'ler Adım adım ilerlerken register'ları takip etmek C64 Programlarında Sık yapılan hatalar Daha ileri teknikler

Bellekteki değişmeleri yakalamak.

Karmaşık breakpoint'ler

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

Debugger'lara 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 register'larının durumunu sorgulayarak o programın içindeki hataları bulmaya ve gidermeye çalışırız. Debugger'lar çeşitli GUI'ler veya komut konsolu arayüzlerine sahip olabilir. Fakat bazı temel özellikler bütün debugger'larda aynıdır.

- Breakpoint'ler

Bellekte çalışan kodun, herhangi bir noktasına gelince programın duraklamasını isteyebiliriz. Breakpoint'ler debugger'da, 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 kontrolü bize ve debugger'a verir. Böylece biz debugger'ın çeşitli komutlarını kullanarak register'ların ve belleğin tam o anda sahip oldukları değerleri inceleyebiliriz.

Genelde breakpoint'leri 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 CPU'nun 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 breakpoint'e ulaşıp duruyorsa breakpoint'ten sonraki o kritik algoritmamızı yavaş yavaş inceleyebilme isteği. Çoğu zaman bir breakpoint'ten sonra programımızı adım adım çalıştırarak yaptığımız pek çok hatayı buluruz.

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

- 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 debugger'larda 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, 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 döndükten 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 içine girdikten sonra aradığınız sonucun o rutinde olmadığını görünce kullanabileceğiniz bir komuttur.

- Register'ları

ve belleği inceleme

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

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.

- Ö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 hiçbir şey olmadı.

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

”
a
!to "devil.prg", cbm
*—-$c000
A
Baslangic:
jsr RasterIROHazirla
Son:
sei
lda
sta
#$7E£
$dc0d
lda
ora
sta
$d0la
#$O1
$d0la
lda
and
sta
#$7E
lda
sta
#$20
$d012
lda
sta
lda
sta
$00
$0314
$el
$0315
eli
*—-$c100
IRORutini:
inc $d019
ldx #$2
IRO gecikme dongusu:
dex
bne IRO gecikme dongusu
lda $d020
pha
lda #$01
sta $d020
ldx $0a
IRO gecikme dongusu2:
dex
bne IRO gecikme dongusu2
lda #$0e
sta $d020
jmp

- Monitöre

giriş ve çıkış

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

Şekil 1. Vice Debugger Konsolu

- Breakpoint'ler

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ıştırmak için debugger'dan çıkmaya gerek duymadı ve direkt başlangıç adresine atlama komutu verdi.

g c000

Program breakpoint'te 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 çalışmaya başlamak üzere c000'a atladı ve oradaki breakpoint'e yakalandı.

- 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 registerların değerlerine bakıyordu. (Vice'ın Linux versiyonunda maalesef 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. Neyse ki ilk defa bu komutları kullandıktan sonra, yukarı ok tuşunu kullanarak son kullanılan komutlara erişmek mümkün)

Ş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ı fark etti ve onu da 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 fark etti. 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 breakpoint'te durdu. Yani sistem başarıyla interrupt'a gelebilmişti. Sistem interrupt'ı düzgün kurduktan sonra her ekran taramasında yeniden c100'deki breakpoint'e yakalanmalıydı. Daha detaylı bakmadan önce tekrar yakalanacak mı diye kontrol etmek için programı devam ettirmeye karar verdi. Bunun için debugger'dan çı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 kernel'de atladığı IRQ çıkış rutinine breakpoint koydu.

break $ea8l
g c000

Program önce daha önceden koyduğu c100'deki breakpoint'te 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.

Interrupt'tan dönerken olanları düşündü. RTI komutuyla işlemci stack'ten döneceği adresi alıyor ve oraya atlıyordu. Bu program yanlış yere atladığına göre stack'ten 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 stack'ten okuyacaktım." Evet başta böyle tasarlamıştı. Ama sonra bunu unutup d020'yi eski rengine döndürmek için PLA ile stack'ten değeri okuyacağına LDA ile direkt yüklemeye kalkmıştı. Bunun sonucu olarak, IRQ'ya girilirken stack'teki en üst iki bayt dönüş adresi oluyor fakat çıkış esnasında aradaki PHA ile gönderilen 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.

Vice komutlarının özeti

«break c000 : $c000 adresine breakpoint koy : $c000 adresinden kodu çalıştırmaya başla * Z: (breakpointe gelip durduğunda) Step-In . Step-Over * Tet: 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 göster

1: c000 adresinden başlayarak bellekteki baytları

.« d c000 : c000 adresinden başlayarak kodunu listele

Şekil 3. Disassembly Penceresi

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 stack'te 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, en çok kullandıklarınız olduğu için fazla karışmaz fakat, BPL ile BCC'yi karıştırabilirsiniz bu komutların anlamlarını tam olarak anladığınızdan emin olun

Unutulan inc $d019: VIC IRQ'ları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 C64'te 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, ve 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

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 temelini 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

- Bellek erişimlerini yakalamak.

Bunun için breakpoint'lere 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 breakpoint'e 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.

- Trace

Bu da breakpoint ve watchpoint'lere benzer şekilde "tracepoint" adı verilen bir aracın kullanılmasıyla olur. Yine istediğiniz adreslere tracepoint'ler koyarsınız. Ardından programı çalıştırdığınızda CPU tracepoint'lere 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.

- Karmaşık breakpoint'ler

Bir diğer konu breakpoint'lerinizi 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 breakpoint'in 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ı breakpoint'in yalnızca X register'ında 7 değeri varken programı durdurmasını sağlar.

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

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

(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 I/O bölgesi aktif olduğu ve VIC çipinin register'ları orada olduğu için m d000 komutu çok faydalıdır. Bize VIC çipinin o anki durumunu verir. Mesela d015 adresine bakarak sprite'ların kullanı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. Maalesef genel olarak 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.

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.

Bug'sız günler nightlord (at) nightnetwork (nokta) org

Yararlanılan Kaynaklar

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