Donkey Kong Country 2 और Open Bus
(jsgroth.dev)- Donkey Kong Country 2 के कुछ rotating barrel सेक्शन में पुराने SNES emulator ZSNES पर ऐसा bug है कि direction key छोड़ने के बाद भी barrel घूमता रहता है, और इसका कारण open bus behavior का emulation न होना है
- असली SNES में unmapped address पढ़ने पर data bus पर मौजूद आखिरी value फिर से पढ़ी जाती है, और DKC2 bank $B3 में $2000/$2001 read के दौरान इसी विशेषता पर निर्भर करता है
- समस्या वाला routine barrel की पिछली direction और नई direction को XOR करने के बाद
and $2000चलाता है, और असली hardware पर यह 16-bit open bus read 0x2020 लौटाता है जिससे direction boundary पार होने का पता चलता है - ZSNES की तरह अगर open bus read 0 लौटाए, तो AND result हमेशा 0 बनता है और rotation रोकने वाली branch कभी execute नहीं होती, इसलिए barrel तब तक घूमता रहता है जब तक उलटी direction न दबाई जाए
- संभव है कि यह instruction वास्तव में
and #$2000होना चाहिए था, और एक ROM revision में $33EDAC पर opcode को0x2Dसे0x29बदलने पर open bus के बिना भी यह सही काम करता है
ZSNES में rotating barrel का न रुकने वाला व्यवहार
- Donkey Kong Country 2 में कुछ stages के rotating barrel ZSNES पर सही तरह काम नहीं करने वाला एक पुराना bug मौजूद है
- सामान्य behavior में player barrel के अंदर left/right direction key दबाए रखने के दौरान ही rotation को control कर सकता है
- ZSNES में left या right एक बार दबाने पर barrel उसी दिशा में लगातार घूमता रहता है, और उलटी दिशा दबाने पर दूसरी ओर लगातार घूमने लगता है
- बाद के हिस्सों में barrel spikes या दूसरे hazards के ऊपर रखे गए हैं, इसलिए यह bug stage की difficulty को developers की मंशा से काफी ज्यादा बढ़ा देता है
- यही समस्या लगभग 20 साल पहले Anomie ने खोजी थी और लगता है कि Snes9x में इसे ठीक किया गया था, लेकिन उस समय का fix पूरे open bus emulation के बजाय game जिन specific address values पर निर्भर था उन्हें hardcode करने का था
- ZSNES में यह bug आखिर तक ठीक नहीं हुआ, और project की आखिरी release 2007 में आई थी
SNES का open bus और 65816 addressing
- SNES में गलत memory address पढ़ने पर भी आमतौर पर program crash नहीं होता
- unmapped address पढ़ने पर CPU data bus पर आखिरी बार मौजूद value को फिर से पढ़ता है; इसे open bus behavior कहा जाता है
- SNES का main CPU 65C816 है, यानी 65816, जो Ricoh 5A22 S-CPU package के अंदर शामिल है
- 65816, 6502 का 16-bit extended CPU है, और SNES में 24-bit address bus इस्तेमाल होती है, लेकिन ज्यादातर addresses 8-bit bank और 16-bit offset के combination से बनते हैं
- कई instructions अंदरूनी तौर पर 16-bit address इस्तेमाल करते हैं, और instruction fetch में program bank register, जबकि data access में data bank register शामिल होता है
- DKC2 के rotating barrel state में bank $B3 से $2000 और $2001 पढ़े जाते हैं, और ये addresses bank $B3 में कहीं mapped नहीं हैं, इसलिए यहाँ open bus read होता है
समस्या वाले routine की memory access
- rotating barrel के अंदर left/right direction छोड़ने पर हर frame चलने वाला routine open bus read करता है
- शुरुआती state में data bank register
$B3, direct page$0000, और M/X flags दोनों clear होते हैं, इसलिए registers और memory access 16-bit हैं - यह routine
$0000-$2001range के कई addresses इस्तेमाल करता है$0EE6: मौजूदा barrel direction$0E0A: प्रति frame rotation amount$0032: संभवतः एक temporary variable
- bank $00-$3F और $80-$BF में
$0000-$1FFFconsole की 128KB WRAM के पहले 8KB पर mapped होते हैं $2000-$20FFunmapped area है, इसलिएand $2000instruction एक open bus read बन जाता है
0x2020 का rotation रोकने के निर्णय में उपयोग
- routine current direction में rotation amount जोड़कर नई direction बनाता है और उसे temporary variable में store करता है
- इसके बाद पिछली direction और नई direction को XOR किया जाता है, फिर result पर
and $2000लगाया जाता है और देखा जाता है कि result 0 है या नहीं - असली SNES hardware पर 16-bit open bus read
and $2000हमेशा 0x2020 लौटाता हैand $2000का machine code2D 00 20है- 65816 8-bit data bus इस्तेमाल करता है, इसलिए 16-bit read दो 8-bit reads में किया जाता है
- इस मामले में आखिरी पढ़ा गया byte address का high byte
0x20होता है, इसलिए दोनों बार0x20लौटता है
- इसलिए वास्तविक behavior कार्यात्मक रूप से
and #$2020जैसा है - अगर AND result 0 हो, तो नई direction को ज्यों का त्यों store किया जाता है और अगले frame में भी rotation जारी रहती है
- अगर 0 न हो, तो rotation amount को 0 कर दिया जाता है, नई direction में
0x1000जोड़ा जाता है, और फिर0xE000से mask करके उसे नज़दीकी0x2000multiple direction पर align किया जाता है
ZSNES में यह लगातार क्यों घूमता रहता है
- barrel direction value का scale ऐसा लगता है कि
0x0000नीचे की दिशा और0x4000बाईं दिशा को दिखाता है - विश्लेषित barrel में clockwise rotation amount
0x0300और counterclockwise rotation amount0xFD00है, और 360 degree rotation में 85 frames से थोड़ा ज्यादा समय लगता है - 60fps के हिसाब से यह 1.5 सेकंड से थोड़ा कम है
0x2020के साथ AND में वास्तव में मायने रखने वाला बदलाव bit 13 यानी 0x2000 है- direction value में
0x2000का बदलाव किसी base direction या diagonal direction के एक step बदलने के बराबर है - सही hardware पर direction key छोड़ने के बाद barrel अगली base/diagonal direction तक घूमता है और फिर ठीक उसी दिशा की ओर रुक जाता है
- अगर open bus read हमेशा 0 लौटाए, तो AND result भी हमेशा 0 होगा और rotation रोकने वाला code कभी execute नहीं होगा
टाइपो की संभावना और ROM patch का परिणाम
and $2000एक absolute addressing instruction है, और तर्क के हिसाब से संभव है कि यहand #$2000immediate addressing होना चाहिए था- असली hardware पर open bus
0x2020लौटाता है, इसलिएand $2000संयोग से intended behavior के मुताबिक काम करता है - यह संयोग इस शर्त पर निर्भर है कि प्रति frame rotation amount के निचले 6 bits हमेशा 0 हों, जिससे
0x2020कार्यात्मक रूप से0x2000के बराबर हो जाता है - गलत opcode bank
$B3offset$EDACपर execute होता है, और विश्लेषित revision में game के 4MB ROM के अंदर यह$33EDACपर mapped है - इस byte को
0x2Dसे0x29बदलने पर, भले ही open bus read हमेशा 0 लौटाए, rotating barrel सही तरह काम करता है - game की दूसरी revisions में सटीक ROM location अलग हो सकती है
- पुराने ZSNES को छोड़कर लगभग सभी SNES emulators में game सही काम करता है, इसलिए यह मामला व्यावहारिक patch से ज्यादा hardware-dependent behavior को trace करने वाला analysis है
1 टिप्पणियां
Hacker News टिप्पणियां
6502 assembly लिखते समय immediate value से पहले
#लगाना भूलकर memory access कर बैठने जैसी गलती में मैंने अपनी ज़िंदगी का बहुत समय गंवाया हैइस उदाहरण की तरह, कई बार यह संयोग से कुछ परिस्थितियों में काम भी कर जाता है। इससे भी बुरा मामला uninitialized RAM पर निर्भर होना है; DRAM की प्रकृति के कारण यह मेरी मशीन या emulator पर हमेशा ठीक चल सकता है, लेकिन किसी और की मशीन में अलग DRAM chip होने पर टूट सकता है। आम तौर पर यह बात demoparty में presentation से 15 मिनट पहले party machine पर न चलने पर पता चलती है
LDA #2को “A में संख्या 2 load करो” औरLDA 2को “memory location 2 में मौजूद value को A में load करो” की तरह अलग-अलग सोचता थाtitle में Open Bus capital letters में लिखा था, इसलिए शुरू में पढ़ना मैंने यह सोचकर शुरू किया कि यह कोई पुराना bus protocol या standard का proper noun होगा जिसके बारे में मैंने नहीं सुना
पढ़ने पर पता चला कि address line decoder specified address
$2000पर किसी भी memory device को activate नहीं करता, इसलिए bus किसी चीज़ से connected न होकर “open” state में है। immediate addressing वाले#को भूल जाने की समस्या, पुराने emulator द्वारा actual hardware की तरह memory read handle न करने पर ही सामने आई—यह काफी मज़ेदार है। fix में absolute addressing की जगह immediate addressing इस्तेमाल करने से memory read नहीं होगा, इसलिए execution time भी तेज़ होगा। उस code chunk में लगभग 2µs की तेजी आ सकती है, लेकिन शायद यह bare metal पर ही मायने रखेगा और emulator में वैसे भी पूरी timing accuracy होने की संभावना कम हैDonkey Kong 64 में memory leak था, जिससे उस समय के हिसाब से अव्यावहारिक रूप से लंबे continuous play time, यानी 8–9 घंटे बाद game crash हो जाता था। development के दौरान यह पकड़ में नहीं आया, लेकिन emulator save state इस्तेमाल करके in-game save feature की जगह लगातार आगे बढ़ते रहने पर यह समय जल्दी जमा हो जाता है। हालांकि history थोड़ी अस्पष्ट है। कुछ sources दावा करते हैं कि Memory Pak bundle करना crash time को 8–9 घंटे से 13–20 घंटे तक धकेलकर bug छिपाने के लिए last-minute कदम था, लेकिन recent research के हिसाब से यह संयोग था और ऐसा नहीं लगता कि Rare या Nintendo ने bug जानते हुए release किया था
[0] https://arstechnica.com/gaming/2021/06/how-snes-emulators-go...
RetroArch के RunAhead feature पर काम करते हुए, जब मैं save states के mismatch होने वाले points देख रहा था, तब मैंने SNES Puyo Puyo को PPU open bus इस्तेमाल करते देखा था
state load करने के बाद PPU Open Bus से पढ़ी गई value बदल गई, इसलिए CPU execution trace logs match नहीं हुए
6502 family वाली गलतियां मैं हमेशा नहीं करता, लेकिन जब करता हूं तो आम तौर पर immediate value की जगह memory address लिख देने की गलती होती है
यह बहुत आम और आसानी से हो जाने वाली गलती है, और मेरा मानना है कि Chuck Peddle खुद भी immediate value में
#$1234की तरह#लगाने वाली syntax पर गहरा पछताते होंगे। IDE में#को bright red दिखाने लगा तो थोड़ी मदद हुई। Rare के assembly देवता भी इसी problem में फंस गएintel_syntax noprefixmode में मुझे ऐसी ही problem हुई थीउन instructions में जो immediate value और memory address दोनों ले सकते हैं, आगे reference किए जाने वाले named constant immediate value को unknown symbol reference के रूप में interpret कर सकने वाली syntax ambiguity थी। नतीजा यह हुआ कि expected immediate value के बजाय instruction ऐसे placeholder memory address के साथ assemble हो गया, जिसे link time पर symbol के relocation address से भरा जाना था। debugging बेहद कष्टदायक थी
ऐसे लेख मुझे पसंद हैं। assembly code को मैं हमेशा लगभग 60% ही follow कर पाता हूं, इसलिए साथ में दी गई prose explanation सच में मदद करती है
classic software के अंदर ऐसी bugs की कहानियां सुनना भी मज़ेदार है जिन्हें शायद किसी ने समझा नहीं था, या शायद अब तक notice भी नहीं किया था
ऐसे checks जो network से connect हो सकने वाले systems के लिए जरूरी हैं, और अब इतने सस्ते हो गए हैं कि पूरी तरह isolated embedded architectures में भी डाले जा सकते हैं। original NES में कई reads और writes असल में कहीं की lines पर voltage toggle करने भर थे, और उसके बाद जो होना था वह हो जाता था। desired effect CRT blanking interval दिखाने वाले signal के साथ बिल्कुल align करके voltage को बहुत controlled तरीके से toggle करने से मिलता था। Super Mario Bros 3 की कुछ animations RAM multiplexer को toggle करके कई sprite data banks में से एक चुनवाती थीं, और graphics hardware जब sprite fetch करता था तो वह थोड़ा अलग दिखने वाले पूरी तरह अलग chip से पढ़ता था। TV timing महत्वपूर्ण थी, इसलिए NTSC और PAL TV regions के लिए software भी अलग release करना पड़ता था; दोनों में refresh rate अलग था और वही refresh rate rendering logic को drive करने वाली clock थी। सचमुच बहुत rough दौर था
जहां तक मुझे पता है, open bus सिर्फ शुरुआती systems में दिखता है जो simple synchronous bus इस्तेमाल करते थे
बाकी ज्यादातर systems में non-existent address access करने पर केवल 0s से भरी या 1s से भरी constant value मिलती है। क्योंकि bus protocol में handshake होता है जिससे master जान सकता है कि कोई response नहीं है, और PCI terminology में इसे “master abort” कहा जाता है
Open bus का मतलब शाब्दिक रूप से डेटा बस लाइन का open circuit होना है
CPU ने address bus पर ऐसा address रखा जो mapped नहीं था या write-only था, और bus पर किसी भी hardware ने जवाब नहीं दिया, इसलिए bus lines drive नहीं हुईं और floating state में रह गईं। नाममात्र तौर पर यह hardware level पर undefined behavior है
असल में क्या होता है, यह समझने के लिए data bus की physical structure को थोड़ा और देखना होगा। motherboard और cartridge के आसपास signals ले जाने वाले लंबे conductors होते हैं, और वे पतली insulating substrate layer के बीच से ground plane से अलग रहते हैं। यह capacitor जैसा दिखता है, और सच में engineers इसे parasitic capacitance के रूप में समझाते और model करते हैं। यह effect bus की maximum data transfer speed को limit करता है, इसलिए इसे minimize करने की कोशिश की जाती है। लेकिन इसी effect की वजह से, जब bus drive नहीं हो रही होती, तो वह आख़िरी बार drive किए गए voltage पर बने रहने की प्रवृत्ति रखती है। यह छोटी DRAM cell की तरह काम करती है, जिससे लेख में बताया गया “open bus read, bus से आख़िरी बार गुज़रे value को return करता है” वाला effect पैदा होता है
DKC2 की तरह, games का गलती से open bus effect पर निर्भर हो जाना दुर्लभ नहीं है। NES में controller connection के लिए serial port register सिर्फ lower bits drive करता है और upper bits open bus रहते हैं। कुछ games
LDA $4016instruction से controller input पढ़कर$40या$41value की उम्मीद भी करते हैं। यहाँ 4 open bus की वजह से बची हुई value हैmemory corruption या arbitrary code execution exploits के हिस्से के रूप में open bus behavior पर निर्भर करने वाली speedrun strategies भी हैं। उदाहरण के लिए Super Mario World credits warp program counter को unmapped memory में भेजकर उसे काफी देर तक आगे बढ़ने देता है, फिर अंत में RAM तक पहुँचाता है, और enemy positions को बहुत बारीकी से manipulate करके बनाए गए payload को execute करता है [1]
हालांकि आम तौर पर predictable open bus behavior में भी exceptions होते हैं। non-standard cartridges unmapped memory पर default value return कर सकते हैं, या pull-up/pull-down resistors शामिल करके open bus behavior को प्रभावित कर सकते हैं। DMA के साथ भी दिलचस्प interaction है। SNES HDMA नाम की feature support करता है, जिससे application frame के बीच में data upload करने या settings बदलने के लिए CPU से graphics hardware तक data को exact timing पर transfer करने के लिए schedule कर सकती है [2]। यह DMA transfer bus का उपयोग करके transfer करने के लिए CPU को थोड़ी देर रोकता है, और अगर DMA transfer instruction execution के बीच में, यानी target address पढ़ने के बाद लेकिन वास्तविक open bus read से पहले आ जाए, तो open bus read behavior बदल सकता है
यह बेहद खास edge case Super Metroid speedrun exploit [3] पर बड़ा असर डालता है। यह exploit out-of-bounds
memcpyकराकर open bus से RAM में data का बड़ा block ले जाने की कोशिश करता है। open bus read लगभग हमेशा 0 return करता है, क्योंकि संबंधित load instruction का आख़िरी byte 0 होता है। लेकिन जिन खास rooms में HDMA graphics effects बहुत होते हैं, वहाँ DMA transfer के उन reads में से किसी एक को प्रभावित करने की संभावना काफी होती है, और तब non-zero byte किसी critical location में घुसकर exploit को ठीक से काम करने से रोकता है और crash करा देता है। इसकी वजह से community में हल्की बहस हुई। कुछ routes और strategies केवल emulators और non-standard firmware पर ही stable हैं। original hardware या बहुत accurate emulator इस्तेमाल करने वाले players को crash का सामना करने की संभावना ज़्यादा होती है, लेकिन Nintendo के सभी official re-releases समेत ज़्यादातर emulators instruction के बीच HDMA transfer द्वारा open bus read value बदलने वाले इस खास edge case को emulate नहीं करतेसाथ ही, वर्तमान में सबसे तेज़ Super Metroid TAS clear [4] इसी HDMA interaction पर निर्भर करता है। open bus execute करने की कोशिश में crash होने वाली स्थिति मिली थी, लेकिन सामान्यतः उसे उपयोगी तरीके से control नहीं किया जा सकता था। room में enemies को manipulate करके CPU timing को प्रभावित किया गया, और HDMA से सही timing पर bus पर उपयोगी instructions रखे जा सके। अंत में console controller input को code के रूप में execute कराने में सफल हुआ और पूरी arbitrary code execution हासिल हुई
[1]: https://youtu.be/vAHXK2wut_I
[2]: https://youtu.be/K7gWmdgXPgk
[3]: https://youtu.be/CnThmKhtfOs
[4]: https://tasvideos.org/8214S
6502 से breadboard computer बनाने वाली उनकी video series की वजह से लेख की बात और hardware problem की explanation सच में समझ आती है। बेशक, यह उनके basic bus example को commercial machine तक extend करके सोचने जैसा है। वरना मुझे लगभग कुछ भी पता नहीं होता
https://eater.net
Parallax Propeller chip को program करते समय भी कुछ हद तक मिलती-जुलती समस्या होती है
specified memory location पर jump करने के लिए
JMP #addressइस्तेमाल करना चाहिए, लेकिन मैं हमेशाJMP addressलिख देता हूँ, जो specified memory location से पढ़े गए address पर jump करता है। शायद 6502 assembler अब भी muscle memory में बसा हुआ हैPropeller:
JMP #address6502:
JMP addressPropeller:
JMP address6502:
JMP (address)सबसे बुरी बात यह है कि इस लेख की तरह buggy Propeller code कभी-कभी चल भी जाता है। फिर किसी पल रुक जाता है, और क्यों हुआ यह पता लगाने में घंटों लग जाते हैं
DKC 1 के SGI pre-rendered 3D graphics उस समय cutting-edge थे। Genesis के Vector Man ने भी कुछ ऐसा ही किया था, लेकिन उसे कम attention मिला
जो देख रहा था उस पर विश्वास नहीं हो रहा था। release के आसपास game को teaser करने और development की behind-the-scenes चीज़ें दिखाने वाली एक videotape थी, शायद cereal box जैसी किसी जगह से apply करके मिली promotional चीज़ याद आती है। मैंने वह tape बहुत बार देखी। DKC मेरे पास खुद नहीं था, लेकिन दोस्त के घर पर खेल सकता था
उस समय magazines इस विषय पर काफी vague तरीके से लिखती थीं, और ऐसा इशारा करती थीं जैसे SNES की performance characters वगैरह को real-time में render कर रही हो। जबकि असल में वह मूल रूप से flipbook animation के ज्यादा करीब था
emulation पर गेम खेलते-खेलते अगर कहीं अटक जाऊँ, तो आखिर में यह सोचने लगता हूँ कि कहीं यह emulator bug तो नहीं है
इस खास समस्या के मामले में, मैं यही सोचता कि गेम को मूल रूप से ऐसे ही design किया गया है और यह बस कठिन है। यह बहुत सीधे तौर पर संबंधित नहीं है, लेकिन जब कोई गेम सच में बहुत कठिन लगता है, तब भी मैं कुछ ऐसा ही सोचता हूँ: “क्या यह emulation delay की वजह से है?” इस समस्या में गहराई तक जाते-जाते आखिरकार मैंने खुद एक MiSTer FPGA बना लिया
याद है कि एक जगह चूहे को पकड़ने के बाद चार keys को एक साथ input करना पड़ता था। लेकिन USB input एक बार में सिर्फ 3 ही भेजता था, इसलिए आगे बढ़ने के लिए चारों keys को बेतहाशा दबाना पड़ता था ताकि बहुत छोटे समय के भीतर आखिरकार सभी register हो जाएँ। कई बार कोशिश करनी पड़ी और यह बहुत झुंझलाहट भरा था
जैसा कहा गया है, मैं बस यही सोचता था कि barrel से launch करने की timing मिलाकर सही angle बनाना intended game design है। यह जानकर सच में हैरानी हुई कि यह bug था
2000 के दशक की शुरुआत में emulator पर चलाकर देखा तो यह मेरी याद से कहीं ज्यादा कठिन था। बाद में पता चला कि bases को blast करने के बाद भी enemies गायब नहीं होते थे—यह emulation bug था, और Ladd फिर भी freeze रहता था। इसलिए level clear करने के लिए लगभग 2 extra health bars चाहिए थीं। यह देखने के लिए कि क्या यह संभव है, मैंने एक बार वैसे clear किया, लेकिन फिर कभी नहीं किया