Sebuah siklus tanpa akhir yang bukan: kisah serangga Cawan Suci

Sekali waktu ada permainan untuk GBA disebut Hello Kitty Collection: Miracle Fashion Maker. Itu adalah game imut berdasarkan franchise Sanrio Hello Kitty yang terkenal dan dikembangkan oleh Imagineer. Tapi dengan kedok nama yang tampaknya tidak bersalah adalah masalah berbahaya. Untuk beberapa alasan, gim sederhana ini tidak berjalan pada emulator GBA apa pun. Tetapi ini saja tidak akan cukup untuk menyebut masalah itu sebagai bug dari Cawan Suci. Seperti semua bug dari Holy Grail, bug ini sendiri sangat membingungkan. Penjelasannya sederhana: pada suatu titik dalam urutan memulai permainan, itu jatuh ke dalam siklus dari mana ia tidak pernah keluar , menunggu nilai tertentu untuk dibaca dari memori yang tidak ada . Meskipun ada bug serupa di banyak game, misalnya, di intro populerThe Legend of Zelda: The Minish Cap , mereka mengandalkan perilaku khusus yang disebabkan oleh membaca alamat memori yang tidak valid. Namun siklus ini sepertinya melanggar perilaku semacam itu. Meskipun demikian, game ini bekerja dengan peralatan nyata. Selain itu, bug yang sama persis terjadi ketika memuat save ke Sonic Pinball Party setelah reboot dingin. Mungkinkah harapan alamat memori yang tidak valid ini menjadi salah? Tetapi jika demikian, bagaimana?


Tapi ini ilegal, kan?


Tunggu sebentar - jika Anda mencoba mengakses memori yang tidak valid, maka gim tersebut hanya perlu macet, bukan? Operasi yang tidak terselesaikan, segfault, atau beberapa kesalahan lain harus terjadi . Baik?

Yah itu lebih seperti Ya. Tapi tidak juga. Setidaknya tidak pada GBA.

Dalam arsitektur prosesor ARM yang digunakan dalam GBA, keadaan salah ini disebut pembatalan data dan terjadi hanya ketika Anda mencoba mengakses memori yang manajer memori belum menetapkan izin baca 1 . Ketika data dibatalkan, prosesor menyelesaikan apa yang dilakukan dan pergi ke vektor pengecualianditugaskan untuk pengecualian batalkan data. Kemudian sistem operasi dapat memilih salah satu solusi: matikan proses saat ini, tetapkan memori kesalahan halaman , biarkan proses mengatasi situasi, karena beberapa emulator JIT melakukannya dengan "fastmem", atau melakukan beberapa tindakan lain.

Bagaimana GBA menangani pembatalan data? Entri vektor pengecualian untuk pembatalan data terletak di ROM boot dari konsol GBA (atau, demikian juga disebut, di BIOS). Jika GBA menemukan data batal, maka ia mencoba untuk pergi ke penangan DACS 2jika ada, jika tidak terjadi pemblokiran. Tidak ada game komersial yang memiliki penangan DACS. Jadi mengapa game ini tidak kedinginan? Semuanya sangat sederhana - GBA tidak pernah menghasilkan pembatalan data. Itu tidak memiliki manajer memori (MMU) (atau bahkan unit perlindungan memori, seperti di DS), jadi itu hanya terus bekerja dan membaca memori yang tidak valid.

Bus memori memasuki lokasi.



Apakah memori yang tidak valid secara umum? Bagaimana penampilannya? Ini adalah halangan utama. Ini adalah situasi yang sulit: apa yang dibaca kode sangat bergantung pada apa yang baru-baru ini dilakukan CPU, atau, lebih tepatnya, apa yang dilakukan bus memori baru-baru ini . Singkatnya, ketika mengakses memori yang tidak valid, CPU membaca apa yang terakhir di bus memori. Untuk memahami apa yang mengikuti dari ini, Anda perlu belajar sedikit tentang bus memori dan cara kerjanya.

Bus memori adalah bagian dari sirkuit elektronik yang menghubungkan CPU ke semua komponen memori platform. Pada GBA, beberapa perangkat terhubung ke bus memori: RAM yang berfungsi, memori video, dan bus kartrid. Ketika CPU mencoba mengakses memori, ia memberi tahu bus memori ke alamat mana ia perlu akses, dan kemudian komponen yang sesuai dengan alamat itu diaktifkan. Kemudian komponen menempatkan nilai di alamat ini di bus, yang mungkin memakan waktu beberapa 3 siklus , dan kemudian CPU akhirnya bisa membaca nilai dari bus. Dalam hal GBA, jika tidak ada peralatan yang dikaitkan dengan alamat, maka tidak ada nilai yang ditulis ke bus, dan CPU membaca nilai apa pun yang ditempatkan terakhir di bus. Situasi dapat bervariasi dalam berbagai cara, misalnya, jika pembacaannya 16-bit, dan CPU mencoba melakukan pembacaan 32-bit, tetapi secara umum itu akan selalu menjadi nilai dari bus. Pengembang menyebut fitur ini "bus terbuka". Sebelumnya, saya menulis bagaimana ini memengaruhi game lain .

Ya, sepertinya semuanya tidak terlihat buruk ... Benar?


Jadi Anda bisa men-cache akses memori terakhir? Dan kemudian membawanya kembali? Dalam kasus umum, pendekatan ini akan berhasil, tetapi ada kesulitan tertentu. Pertama, Anda harus memastikan bahwa semua operasi akses memori dalam urutan yang benar. Ini lebih rumit daripada kedengarannya, karena CPU mengakses memori dengan setiap instruksi untuk mendapatkan instruksi berikutnya dalam pipa. Dan pada kenyataannya, dalam kasus umum *, memori yang tertahan di dalam bus adalah instruksi terakhir yang diterima. Ini menyederhanakan proses, karena Anda hanya perlu mendapatkan nilai terakhir yang sudah dipilih sebelumnya ini. Tetapi karena nilai yang dipilih sebelumnya tergantung hanya pada tempat kita mengeksekusi dari memori, itu harus selalu sama. Bahkan jika alamat yang diterima berubah saat tidak valid,Anda akan selalu mendapatkan memori yang sama.

Uh ... Berhenti. Tapi siklus ini ada, dan tidak bisa keluar jika nilai ini dipilih sebelumnya. Jadi apa yang terjadi? Jika dia terus-menerus menerima instruksi berikut, lalu apa yang terjadi antara operasi-operasi ini? Saya mencoba menjalankan loop tanpa akhir seperti itu pada test ROM untuk memeriksa apakah, misalnya, nilainya bisa memburuk. Ini pasti dapat terjadi jika nilai belum diperbarui baru-baru ini, tetapi nilai diperbarui dalam setiap instruksi, sehingga tidak punya waktu untuk rusak. Tes saya tidak pernah meninggalkan loop. Saya melakukan sesuatu yang berbeda dari pada game-game ini, meskipun saya membuat ulang siklusnya dengan tepat. Apa kesalahan yang telah aku perbuat?

Pokémon Emerald dan ACE, hanya terjadi pada besi


Maju cepat dalam waktu, pada Januari 2020. Laporan bug di Sonic Pinball Party pada waktu itu berusia sekitar tiga setengah tahun. Di emulator lain, ia dikenal selama bertahun-tahun. Saya sudah kehabisan teori kerja. Pada akhir bulan ini, seorang pengguna dengan julukan merrpbergabung dengan komunitas Discord dari emulator mGBA dan mengatakan bahwa Pokémon Emerald memiliki kesalahan eksekusi kode arbitrer (ACE) baru yang hanya berfungsi pada perangkat keras. Selain itu, kesalahan ini kemungkinan besar akan digunakan oleh speedrunners, yang mungkin ingin berlatih emulator. Jelas, bug ini telah menjadi target yang menarik untuk memperbaiki kesalahan, meskipun akan lebih baik jika saya mengetahuinya sebelum versi 0.8.0. Saya mulai meneliti kesalahan dan mengkonfirmasi pengamatan merrp bahwa itu hanya bekerja pada perangkat keras. Di semua emulator yang saya coba, permainan digantung dengan layar hitam. Tetapi merrp memberi tahu saya bahwa itu tergantung pada pembacaan dari memori yang tidak valid dalam satu lingkaran, dan saya menyadari bahwa kemungkinan besar saya tidak dapat memperbaiki kesalahan dalam waktu dekat. Lagi - lagi ini adalah bug yang sama .

Kali ini, belajar tentang fungsi perulangan memberi saya keunggulan. Berkat proyek dekompilasi pokeemerald, saya dapat dengan mudah melakukan perubahan yang ditargetkan pada fungsi untuk mencoba mencari tahu bagaimana dia berhasil keluar dari loop. Versi sederhana dari loop ini terlihat seperti ini:

uint16_t type = /* ... */;
for (int32_t i = 0; table[type][i] != 0xFFFF; ++i) {
	uint16_t value = table[type][i] & 0xFE00;
	if (value > 0x7E00) {
		break;
	}
	/* ... */
}

Loop melakukan tugas yang cukup sederhana. Ada tabel nilai dua dimensi. Pada setiap baris tabel kolom ini, typeloop pertama kali mencoba menentukan apakah nilainya adalah nilai sentinel tertentu. Jika demikian, loop berakhir. Kalau tidak, itu berlaku masker untuk nilai dan memeriksa apakah itu lebih besar dari nilai yang diperiksa. Kalau tidak, itu akan turun siklus. Dalam kasus kesalahan tertentu, nilainya typemelampaui batas-batas tabel, yang mengarah ke tampilan pointer yang tidak valid. Ini berarti ketika Anda mencoba mengaksesiUntuk elemen kolom yang tidak ada ini, kami akan selalu mengakses memori yang tidak valid. Meskipun offset meja meningkat dengan setiap iterasi loop sebelum kembali ke memori aktual, mungkin membutuhkan ratusan juta pengulangan. Karena itu, jelas bahwa dia tidak. Jadi bagaimana sebuah program keluar dari loop?

Untuk menyelidiki ini, saya mengubah siklus dan melihat apa yang akan terjadi jika saya baru saja keluar dari siklus. Semuanya ternyata sangat sederhana: saat ini ACE bekerja pada perangkat keras dan emulator, dan tidak ada yang menggantung. Jadi sebagai gantinya, saya mencoba untuk mengatur warna layar ke nilai yang dibaca program ketika keluar dari loop dan membeku sehingga warnanya tidak berubah. Saya mengkompilasi ulang kode dan menjalankannya pada GBA nyata. Setelah beberapa detik membeku di layar hitam, itu menjadi rona biru yang cantik.


SANGAT BIRU

Tetapi emulator masih menggantung di layar hitam. Nilai apa yang akan dia baca jika dia membaca nilai yang diterima sebelumnya? Sebaliknya, itu menjadi pirus hitam.


Fu.

Artinya, program, sebelum berhasil keluar dari siklus, pasti melewati setidaknya sekali. Ternyata waktu yang dibutuhkan untuk keluar dari siklus besi bervariasi. Ini biasanya memakan waktu 2 hingga 30 detik. Apa yang sedang terjadi?

Teori kerja baru


Lalu saya perhatikan perbedaan antara test ROM saya dan Pokémon Emerald saat digantung. Pokemon memainkan musik. Sonic Pinball Party juga memainkan musik. Hello Kitty tidak memainkan musik, tetapi itu memberi saya ide. Apa yang terjadi jika interupsi terjadi antara pengambilan awal dan pemuatan data? Apakah program mulai mengambil vektor interupsi sebelum mengakses memori yang tidak valid? Saya dengan cepat membuat tata letak untuk situasi ini di mGBA, menyalakan interupsi di ROM tes, dan tentu saja itu keluar dari loop. Lalu saya mencoba test ROM yang sama pada perangkat keras dan ... tidak keluar dari loop. Maka teori pun muncul. Pada akhirnya, saya menyadari sesuatu. Saya yakin Anda melihat tanda bintang di atas, jadi ya, ada satu peristiwa antara pengambilan awal dan mengakses memori,tetapi hanya jika, antara prefetch dan akses ke memori yang tidak valid, bus memori mengirimkan permintaan tidak ke CPU, tetapi ke sesuatu yang lain.

Saya mengatakan bahwa bus memori dikendalikan oleh CPU. Untuk sebagian besar, ini benar, tetapi ada peralatan penting lainnya yang juga memiliki akses ke bus memori melewati prosesor. Proses ini disebut akses memori langsung . Saya berbicara tentang DMA di artikel sebelumnya , jadi sekarang saya tidak akan membahas prinsip kerjanya. Jika Anda membaca kembali artikel tersebut, Anda mungkin memperhatikan bahwa saya mengatakan bahwa CPU utama berhenti sementara DMA sedang berjalan. Ini berarti bahwa ketika DMA berjalan, nilai pada bus sekarang akan menjadi akses terakhir ke memori DMA. Ini terutama penting jika DMA melampaui memori aktual ke wilayah yang tidak valid; Namun, itu menggandakan nilai bagus terakhir.

Sudah lama diketahui bahwa jika Anda memuat memori yang tidak valid ke DMA, Anda akan mendapatkan nilai DMA terakhir, tapi saya menerapkannya dalam mGBA untuk waktu yang lama dan sudah lupa tentang itu. Ketika saya melihat ini di kode akses untuk memori tidak valid ketika mempelajari bug, sesuatu diklik di kepala saya. Bagaimana jika nilai DMA bertahan di bus untuk satu instruksi? Jika instruksi pertama setelah DMA selesai memuat memori yang tidak valid sebelum mendapatkan nilai berikutnya, maka secara teori ini harus mengarah untuk memuat ulang nilai DMA. Selain itu, memutar musik di GBA biasanya menggunakan DMA untuk mengirimkan output audio. Untuk implementasi yang benar dari ini, diperlukan emulator taktik-akurat, yang dapat memblokir CPU di tengah-tengah pelaksanaan instruksi, antara awal instruksi dan akses memori, dan emulasi konsol GBA di emulator mGBA tidak akurat taktik.Dan ini sesuatu bagiku.kenang . Untungnya, saya berhasil mengatasi masalah ini. Solusinya tidak sempurna, tetapi sekarang saya dapat membandingkan alamat CPU yang diharapkan untuk instruksi setelah DMA dengan alamat CPU saat ini pada beban yang tidak valid dan menggunakan satu alamat alih-alih nilai yang dipilih sebelumnya untuk nilai DMA ini.

Keputusan yang ditunggu-tunggu


Saya menyalakan operasi DMA untuk H-blank dalam ROM tes dan menyinkronkannya dengan V-blank sehingga timingnya stabil, menjalankannya pada perangkat keras, dan ... kali ini berhasil! ROM tes terus-menerus keluar dari loop setelah jumlah iterasi yang sama ketika nilai DMA dibaca dari bus. Saya benar! Untuk implementasi yang benar dalam mGBA ini, beberapa upaya diperlukan, tetapi sekarang program keluar dari siklus dengan hasil yang sama seperti pada perangkat keras. Saya akhirnya mendapat warna biru pada mGBA. Hello Kitty telah di-boot. Menyimpan di Sonic Pinball Party telah menghasilkan.

Saya melakukannya.

Ini mungkin waktu terlama yang saya habiskan untuk satu bug. Selama tiga tahun, saya menginvestasikan begitu banyak waktu dalam debugging sehingga saya kehilangan hitungan, dan saya yakin bahwa pengembang lain juga menghadapi situasi serupa di emulator mereka. Tanpa wawasan ini, mungkin diperlukan saya satu tahun lagi, atau bahkan lebih, tetapi layar hitam, yang tidak ada yang terjadi selain bermain musik, menjadi ubin domino yang menyebabkan runtuhnya seluruh masalah.

Sekarang solusinya ditemukan, itu dapat diimplementasikan dalam emulator GBA lainnya, mengakhiri bug ini. Bug ini akan diperbaiki di mGBA 0.9.0, yang, saya harap, akan dirilis tahun ini, dan telah diperbaiki dalam uji coba build. Anda akhirnya dapat memainkan Hello Kitty Collection: Miracle Fashion Maker. Kecuali, tentu saja, Anda berharap, bukan bagi saya untuk menghakimi Anda.

gambar

  1. Jika Anda mencoba mengeksekusi memori yang tidak memiliki izin eksekusi, ini disebut prefetch abort.
  2. DACS (kependekan dari Debugging dan Sistem Komunikasi) adalah bagian dari kit pengembangan GBA.
  3. Siklus-siklus idle saat membaca dari bus ini kadang-kadang disebut status tunggu.

All Articles