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.
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.
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.
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.
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.
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.
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 ;; ------------------------------------
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.
Şekil 1. Vice Debugger Konsolu
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ı
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)
Ş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.
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
Şekil 3. Disassembly Penceresi
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
Ö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
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.
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
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
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.
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
Vice Online Documentation: http://www.viceteam.org/vice_toc.html