Donkey Kong Country 2 dan Open Bus
(jsgroth.dev)- Bug barrel berputar di Donkey Kong Country 2 terjadi pada emulator ZSNES
- ZSNES tidak mengemulasikan perilaku open bus dengan benar, sehingga barrel terus berputar secara permanen
- Berbeda dari perangkat keras asli, saat terjadi akses memori yang salah di ZSNES, 0 selalu dikembalikan, sehingga memicu bug
- Pada perilaku yang benar, barrel memiliki logika untuk berhenti berputar pada arah yang tepat (8 arah)
- Masalah ini diduga berasal dari kesalahan kecil dalam penulisan kode (yakni memakai absolute addressing alih-alih immediate addressing)
Bug barrel di Donkey Kong Country 2 dan emulator ZSNES
Donkey Kong Country 2 memiliki bug terkenal di mana barrel (tong) berputar pada beberapa stage tidak bekerja dengan benar di emulator SNES lawas bernama ZSNES
Saat masuk ke barrel, seharusnya barrel hanya berputar selama tombol arah kiri/kanan ditekan, tetapi di ZSNES, bahkan jika kiri/kanan ditekan sebentar, barrel akan terus berputar selamanya ke arah tersebut
Karena bug ini, khususnya pada stage-stage akhir, bagian barrel berputar yang muncul di atas semak duri atau rintangan menjadi jauh lebih sulit daripada yang dimaksudkan pengembang
Masalah ini dulu sempat terdokumentasi sampai batas tertentu di forum ZSNES, tetapi karena forum tersebut kini sudah hilang, materi terkait sulit ditemukan
Penyebab bug - Emulasi Open Bus
Penyebab mendasar bug ini adalah ZSNES tidak mengemulasikan perilaku open bus
- open bus adalah perilaku yang terjadi pada platform lama seperti SNES saat membaca alamat memori yang tidak valid
- Pada perangkat keras asli, nilai terakhir yang ditempatkan di bus akan dikembalikan
- CPU utama SNES adalah 65C816 (65816)
- 65816 adalah versi 16-bit dari 6502, memiliki bus alamat 24-bit, dan menggunakan metode memory banking
Dalam kode barrel berputar di DKC2, saat mengakses alamat tidak valid (Bank $B3 pada $2000, $2001), perangkat keras mengembalikan nilai 0x2020 melalui open bus
Karena fitur ini tidak ada di ZSNES, 0 selalu dikembalikan, sehingga bug pun muncul
Cara kerja kode game
Rutin game yang berkaitan dengan barrel berputar memiliki alur kerja sebagai berikut
- Menjumlahkan arah barrel saat ini dan jumlah putarannya (kecepatan), lalu menyimpannya ke variabel sementara
- Mengukur perubahan arah dengan operasi XOR, lalu melakukan operasi AND pada hasilnya dengan nilai yang dibaca dari open bus
- Jika hasil AND adalah 0 maka putaran berlanjut, jika bukan 0 maka putaran berhenti, lalu arah disejajarkan dengan pembulatan ke salah satu dari 8 arah
Pada perangkat keras asli, nilai open bus adalah 0x2020, tetapi jika yang dikembalikan adalah 0, maka putaran akan berlanjut tanpa batas
Diduga logika ini seharusnya melakukan operasi AND dengan nilai immediate (address #$2000), tetapi karena kesalahan, yang digunakan adalah absolute address (address $2000)
Namun, karena karakteristik open bus pada perangkat keras, dalam praktiknya kedua cara tersebut sama-sama bekerja normal
Solusi dan kesimpulan
Emulator SNES lain seperti Snes9x memperbaiki bug ini dengan pendekatan hardcoded, sedangkan ZSNES sudah berhenti dikembangkan sehingga tidak pernah ditambal
Jika opcode instruksi AND dalam rutin tersebut diubah dari 0x2D menjadi 0x29 (AND #$2000), barrel berputar akan berfungsi normal bahkan tanpa perilaku open bus
Masalah ini tidak terjadi pada perangkat keras asli maupun emulator modern
Pada akhirnya, bug ini adalah contoh yang muncul ketika ketiadaan dukungan emulasi open bus bertemu dengan kesalahan penulisan kode
Latar belakang tambahan: struktur 65816 dan peta memori SNES
CPU 65816 memiliki bus alamat 24-bit, tetapi umumnya menggunakan kombinasi bank 8-bit dan offset 16-bit
- Program counter (PC) berukuran 16-bit, dan alamat penuh dibentuk dengan register program bank (PBR, K)
- Data bank (DBR, B) digunakan untuk memilih bank bagi operasi data
- Hardware stack dan direct page selalu berada di bank $00
Peta memori SNES juga dirancang berdasarkan 65816, sehingga lebih efisien memandang alamat sebagai kombinasi bank 8-bit + offset 16-bit
Penutup
Kasus ini menunjukkan bahwa karakteristik perangkat keras legacy (seperti open bus) dapat menyebabkan bug tak terduga dalam emulasi
Pengembang seharusnya menggunakan immediate addressing, tetapi ini menjadi contoh di mana absolute address kebetulan tetap bekerja normal
Di era modern, hal ini menunjukkan bahwa mengemulasikan hingga perilaku open bus pun sangat penting untuk reproduksi akurat perangkat lunak lama
1 komentar
Komentar Hacker News
Sebagai programmer assembly 6502, saya pernah berkali-kali membuang banyak waktu karena lupa menulis simbol
#lalu tanpa sengaja melakukan akses memori alih-alih memakai nilai immediate; masalah seperti ini makin merepotkan karena kadang, kalau beruntung, tetap terlihat berjalan dengan baik. Tapi kasus yang bahkan lebih buruk daripada masalah floating bus di contoh ini adalah kode yang bergantung pada RAM yang belum diinisialisasi; karena setiap DRAM bisa punya nilai awal berbeda, program itu selalu jalan mulus di komputer atau emulator milik sendiri, tetapi gagal di komputer lain yang memakai DRAM berbeda. Biasanya masalah seperti ini baru ketahuan ketika harus menjalankan kode di hardware orang lain di demo party, dengan waktu kurang dari 15 menit tersisa dan kodenya mendadak tidak jalanSaya jadi penasaran apakah pernah ada arsitektur berbasis 6502 yang benar-benar memakai memori dinamis. Sepanjang pengalaman saya, platform seperti itu selalu memakai SRAM saja
6502 adalah bahasa assembly pertama saya, dan saya memaknai
LDA #2sebagai “muat angka 2 ke register A”. Sebaliknya,LDA 2terasa seperti “muat nilai dari lokasi memori 2”, jadi perbedaan ini membuat saya sejak awal berusaha menghindari kesalahan seperti ituDalam situasi seperti ini, justru bisa berguna untuk melewatkan kode ke LLM. LLM cukup kuat dalam menemukan typo atau titik rawan kesalahan yang dampaknya besar seperti ini
Saat melihat istilah Open Bus ditulis dengan huruf kapital, saya sempat mengira itu nama protokol atau standar bus lama, lalu membaca artikelnya dengan asumsi itu. Ternyata itu hanya berarti bus sedang tidak terhubung ke mana pun, karena pada alamat yang dipilih address decoder (
$2000) tidak ada perangkat memori yang diaktifkan. Jadi, karena lupa mode immediate (#), program akhirnya mencoba membaca dari memori dan tidak mendapatkan apa pun; hal ini ditemukan karena emulator lama berperilaku berbeda dari hardware asli. Solusinya adalah mengubah direktif ke mode pengalamatan immediate, sehingga tidak lagi melakukan pembacaan memori dan kodenya menjadi sekitar 2us lebih cepat. Tapi selisih performa sekecil ini rasanya tidak terlalu berarti kecuali di hardware asli, terutama pada emulator yang timing-nya tidak benar-benar identikAda penjelasan bahwa (sebagian) emulator SNES saat ini nyaris mencapai kesempurnaan berbasis timing. Meski begitu, selisih 2us benar-benar hanya berdampak dalam kasus yang sangat luar biasa. Artikel terkait: How SNES emulators got a few pixels from complete perfection
Ada beberapa game dengan bug tersembunyi yang baru terungkap jauh setelah rilis berkat arsitektur baru, seperti kasus Rare. Di Donkey Kong 64 ada memory leak fatal yang muncul setelah bermain terus selama 8–9 jam, tetapi berkat fitur save state di emulator, akumulasi waktu itu bisa terjadi seketika sehingga bug-nya jauh lebih mudah terekspos. Konon Memory Pak yang dibundel saat rilis dipakai untuk menyamarkan bug itu, tetapi riset terbaru menunjukkan bahwa baik Rare maupun Nintendo tampaknya tidak mengetahui bug tersebut saat itu
Saya pernah menemui fenomena open bus pada PPU di SNES Puyo Puyo. Itu terjadi saat menelusuri alasan state save tidak konsisten ketika mengerjakan fitur RunAhead di RetroArch; nilai yang dibaca dari PPU open bus berubah setelah state dimuat ulang, sehingga log trace eksekusi CPU tidak cocok. Kasus yang sangat spesifik
Pada 6502 atau kode serupa, saya sering keliru membedakan alamat memori dan nilai immediate. Saya rasa notasi seperti
#$1234memang memancing kesalahan, dan saya bahkan pernah mendengar bahwa Chuck Peddle sendiri sangat menyesali sintaks itu. Di IDE, saya bisa sedikit mencegahnya dengan menyorot#dengan warna merah. Bahkan developer Rare pun ternyata tidak luput dari kesalahan seperti iniSudah cukup lama, saya pernah mengalami masalah serupa di GNU assembler dalam mode
intel_syntax noprefix; di sana ada ambiguitas sintaks saat mereferensikan konstanta bernama sebagai nilai immediate di depan, karena bisa ditafsirkan sebagai alamat memori atau simbol. Akibatnya, alih-alih perilaku yang saya harapkan, assembler membuat alamat memori sementara yang menunggu sampai timing linking simbol, dan pengalaman mencari bug-nya benar-benar menyiksaInstruction set seperti ARM yang mewajibkan instruksi terpisah untuk menangani memori pada dasarnya mencegah kesalahan membingungkan seperti ini
Setahu saya, fenomena open bus hanya muncul pada sistem bus sinkron sederhana awal. Sebagian besar sistem lain mengembalikan nilai tetap seperti semua 0 atau semua 1 saat alamat yang tidak ada diakses; ini ditangani lewat handshaking sehingga master bisa mendeteksi tidak adanya respons dalam protokol bus, seperti master abort pada PCI
Saat memprogram chip Parallax Propeller, saya juga berulang kali mengalami kesalahan serupa. Saya sering tertukar antara
JMP #addressdanJMP address, kemungkinan besar karena muscle memory dari assembler 6502. Pada Propeller,JMP #addressberarti lompat ke alamat tersebut, sedangkanJMP addressberarti lompat ke nilai yang dibaca dari alamat itu. Masalahnya, bug seperti ini kadang tetap berjalan, sehingga saya bisa menghabiskan berjam-jam mencari penyebabnya sampai akhirnya eksekusi macetOpen bus berarti jalur data bus benar-benar terbuka sehingga rangkaiannya tidak didorong oleh apa pun. Ketika CPU menaruh alamat yang tidak dipetakan atau write-only ke bus, tidak ada hardware yang merespons, sehingga jalur bus tetap mengambang — dengan kata lain, undefined behavior di level hardware. Untuk memahami apa yang benar-benar terjadi, kita harus melihat struktur fisik data bus. Bus adalah konduktor panjang yang membawa sinyal antara motherboard dan cartridge, dipisahkan dari ground plane oleh substrat isolator tipis. Struktur ini bertindak seperti kapasitor, sehingga pada akhirnya mampu “menahan” tegangan sinyal terakhir selama beberapa saat. Karena itu, pada open bus, efeknya adalah nilai terakhir yang lewat bisa terbaca lagi. Game seperti DKC2 tanpa sadar bisa bergantung pada sifat open bus ini, dan port serial kontroler NES juga hanya memberi sinyal pada bit rendah sementara bit tinggi dibiarkan open bus, sehingga game tertentu mengharapkan
LDA $4016menghasilkan$40atau$41. Fenomena open bus bahkan dimanfaatkan dalam strategi speedrun seperti credit warp di Super Mario World, baik untuk memory corruption maupun arbitrary code execution. Tentu ada pengecualian, misalnya cartridge non-standar, penggunaan resistor pull-up/pull-down, atau interaksi tidak lazim dengan DMA seperti Horizontal DMA. Sebagai contoh, jika transfer HDMA di SNES terjadi di tengah instruksi, timing pembacaan open bus bisa terpengaruh; dalam exploit speedrun Super Metroid, ini bisa menyisipkan nilai abnormal di antara blok memori yang ingin disalin, sehingga exploit-nya rusak. Karena itu, pada hardware asli atau emulator yang sangat presisi, game bisa crash, sedangkan pada kebanyakan emulator atau rilis ulang resmi, perilaku khusus seperti ini tidak diimplementasikan dengan sempurna sehingga strateginya justru berjalan normal. Penyelesaian world record TAS Super Metroid juga bergantung pada perilaku HDMA ini. Dengan memanipulasi posisi musuh untuk mengubah timing CPU, HDMA bisa dipaksa menaruh nilai yang diinginkan ke open bus, yang pada akhirnya mengeksekusi input kontroler sebagai kode dan memungkinkan arbitrary code execution. Video Super Mario World credits warp, video pemanfaatan HDMA, video exploit DMA Super Metroid, rekor TAS Super MetroidSaya suka konten analisis bug menarik seperti ini; saya mungkin cuma bisa mengikuti sekitar 60% kode assembly-nya, tetapi penjelasan tulisan yang menyertainya sangat membantu pemahaman. Dan cerita tentang bug yang tidak diketahui selama bertahun-tahun lalu akhirnya terungkap dalam software legendaris seperti ini sangat menyenangkan
Saat bermain game di emulator lalu progres saya macet, saya selalu curiga, “jangan-jangan ini bug emulator?”. Dalam kasus ini juga, saya rasa saya mungkin hanya akan menganggap desain gamenya memang dibuat sesulit itu. Dan ketika tingkat kesulitan game terasa sangat tinggi, saya juga sering curiga, “apa ini gara-gara latensi emulator?”, sampai akhirnya saya membuat dan memakai mister FPGA sendiri
Di Chrono Trigger ada bagian yang mengharuskan menekan empat tombol sekaligus, tetapi input USB hanya bisa mengirim tiga sekaligus, jadi dari empat percobaan hanya satu yang terdaftar; saya ingat itu sangat sulit dan membuat frustrasi
Karena saya dulu hanya memainkan DKC di ZSNES, sampai membaca artikel ini saya sama sekali tidak tahu bahwa itu adalah bug emulator. Saya benar-benar mengira desain gamenya memang seperti itu, dan setelah tahu itu bug, saya sangat kaget
Saya banyak memainkan Bionic Commando waktu kecil, lalu ketika memainkannya lagi di emulator rasanya jauh lebih sulit. Belakangan saya tahu penyebabnya adalah bug emulator yang membuat musuh tidak menghilang, sehingga nyawa yang dibutuhkan praktis jadi dua kali lipat. Saya memang pernah menamatkannya dengan kondisi begitu, tapi saya tidak ingin mengulanginya lagi
Grafik 3D pre-render berbasis SGI pada DKC 1 adalah teknologi mutakhir pada masanya. Vector Man di Mega Drive juga memakai teknik serupa, tetapi tidak mendapat perhatian sebesar DKC
Pada 1995 saya berada tepat di usia target utama DKC, 11 tahun, dan grafis game ini benar-benar mengejutkan. Saya juga pernah menerima video promosi sekitar waktu rilis, dan saya memutar kaset berisi cuplikan behind-the-scenes itu berkali-kali. Saya sendiri tidak pernah memiliki gamenya, tetapi sempat beberapa kali memainkannya di rumah teman
Waktu kecil, saya merasa grafis DKC itu entah bagaimana terasa “palsu”. Majalah-majalah saat itu sering memberi penjelasan yang dibuat-buat seolah SNES merender karakter 3D secara real time, tetapi saya samar-samar menyadari bahwa sebenarnya itu lebih mirip animasi flipbook