1 poin oleh GN⁺ 2025-07-02 | 1 komentar | Bagikan ke WhatsApp
  • 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

 
GN⁺ 2025-07-02
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 jalan

    • Saya 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 #2 sebagai “muat angka 2 ke register A”. Sebaliknya, LDA 2 terasa seperti “muat nilai dari lokasi memori 2”, jadi perbedaan ini membuat saya sejak awal berusaha menghindari kesalahan seperti itu

    • Dalam 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 identik

    • Ada 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 #$1234 memang 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 ini

    • Sudah 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 menyiksa

    • Instruction 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 #address dan JMP address, kemungkinan besar karena muscle memory dari assembler 6502. Pada Propeller, JMP #address berarti lompat ke alamat tersebut, sedangkan JMP address berarti 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 macet

  • Open 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 $4016 menghasilkan $40 atau $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 Metroid

    • Seri video komputer 6502 breadboard dari Ben Eater sangat membantu saya memahami bagaimana perilaku hardware seperti ini bekerja. Itu membuat saya lebih bisa merasakan bagaimana perilaku bus semacam ini berkembang pada perangkat komersial situs Ben Eater
  • Saya 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

    • Sistem pada masa itu makin menarik karena belum memiliki sebagian besar mekanisme pengecekan yang sekarang dianggap penting di sistem embedded modern, entah karena kemungkinan koneksi jaringan atau bukan. Pada era NES, banyak operasi read/write pada dasarnya hanyalah men-toggle tegangan pada jalur, dan apa yang benar-benar terjadi baru bisa diketahui pada saat itu juga. Mereka men-toggle tegangan dengan timing yang sinkron tepat terhadap sinyal blanking CRT untuk mendapatkan efek tertentu, dan di Super Mario Bros. 3, mereka bahkan memainkan multiplexer RAM untuk mengganti bank sprite pada setiap timing refresh layar. Karena perbedaan TV regional NTSC/PAL menjadikan laju pemindaian sebagai semacam clock bagi logika rendering, software yang berbeda memang harus dirilis untuk masing-masing jenis TV. Benar-benar era yang liar
  • 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