解构神奇宝贝故障?

发布于 2024-11-26 22:02:33 字数 1118 浏览 1 评论 0原文

(如果这是错误的地方,我很抱歉。我认为这肯定与编程有关,但如果这属于其他网站,请告诉我)

我从小就玩神奇宝贝红和蓝,这些游戏非常有趣,但有点因存在大量可利用的故障而臭名昭著(例如,请参阅游戏中这种荒谬的快速运行使用内存损坏将项目屏幕变成十六进制编辑器)。

最近,我发现了游戏中一个有趣的速通游戏,它使用了一个名为“ZZAZZ 故障”的故障来破坏重要的内存位置,并让玩家几乎立即赢得游戏。根据作者对speedrun的描述,ZZAZZ故障的工作原理如下:

要开始训练师战斗,游戏需要加载大量数据,例如[...]如果失败他将承认的金钱。当它加载金钱时,事情就会变得非常糟糕。由于我无法理解的原因,金钱以完全不同的方式存储,游戏使用三个字节的数据结构,而不是将值转换为二进制,而是以“人类”表示形式存储。例如,$123456 将存储为 0x123456,而不是 0x01E240,这是正确的转换。

[训练师表中的一些无效条目]指向具有无效金钱数据的位置。当游戏尝试使用上述结构中的这些数据执行算术时,它会发疯并开始覆盖 RAM 的大部分。更具体地说,对于每个三个字节的块,其中两个将包含 0x9999(训练师可以给出的最大金额)。这种模式通过 RAM 重复多次。为了更好地看到这一点,我建议在面对 ZZAZZ 训练器后暂停模拟器上的视频,并将 VBA 的内存查看器设置为 0xD070。

这种分析是有道理的,但作为一名程序员,我忍不住想知道程序员到底是如何编写使这成为可能的代码的。如果输入不是有效的十六进制编码的十进制数,我能想到的编写将十六进制编码的十进制数转换为十进制数的函数的方法都不会开始用 0x9999 填充随机内存块。

我的问题是 - 如果没有专门设计以这种方式失败的算法,是否有一种从十六进制编码的十进制到十进制的转换的直接实现,当输入无效值时可能会导致这种内存损坏?

,如果这是题外话,我很抱歉。我的想法是,这个网站上的其他程序员可能也是玩这个游戏长大的,这听起来像是逆向工程中的一个有趣的练习,试图找出这样的故障是如何发生的。

(I apologize if this is the wrong place to ask this. I think it's definitely programming related, though if this belongs on some other site please let me know)

I grew up playing Pokémon Red and Blue, games that were great fun but are somewhat notorious for having numerous exploitable glitches (for example, see this ridiculous speedrun of the game that uses memory corruption to turn the item screen into a hex editor).

Recently, I found an interesting speedrun of the game that uses a glitch called the "ZZAZZ glitch" to corrupt important memory locations and allow the player to almost immediately win the game. According to the author's description of the speedrun, the ZZAZZ glitch works as follows:

To start a Trainer battle, the game needs to load a lot of data, such as [...] the money he'll concede if defeated. When it loads the money is where things can get really ugly. For reasons that are beyond me, money is stored in a completely different manner, the game uses a data structure of three bytes and instead of converting the value to binary, it stores it in "human" representation. For example, $123456 would be stored as 0x123456 instead of 0x01E240, the proper conversion.

[Some invalid entries in the Trainer table] point to location with invalid money data. When the game tries to perform arithmetic with these data in said structure, it goes nuts and starts overwriting huge portions of RAM. More specifically, for every block of three bytes, two of them will contain 0x9999 (the maximum amount of money a trainer could give). This pattern repeats itself many times through RAM. To see this better, I recommend pausing the video on the emulator after the ZZAZZ trainer is faced and set VBA's memory viewer to 0xD070.

This analysis makes sense, but as I programmer I can't help but wonder how on earth the programmers wrote the code that would make this possible. No approach I can think of for writing a function that converts a hexadecimal-encoded decimal number to decimal would ever start filling random blocks of memory with 0x9999 if the input wasn't a valid hexadecimal-encoded decimal number.

My question is - without specifically designing the algorithm to fail this way, is there a straightforward implementation of a conversion from hexadecimal-coded decimal to decimal that could result in this sort of memory corruption when fed in an invalid value?

Again, if this is off-topic, my apologies. My thoughts are that other programmers on this site may have also grown up playing this game, and it sounds like an interesting exercise in reverse-engineering to try to figure out how a glitch like this could be possible.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

暗地喜欢 2024-12-03 22:02:33

老实说,我的猜测是,这只是某人在编写他们的第一个目标游戏时犯的一个愚蠢而令人讨厌的故障。 《口袋妖怪红/蓝》是该系列中的第一个,并且有很多其他故障,任天堂通常会排除批次测试,我想知道它是如何通过的。滚动屏幕移动问题是让我困扰的一个问题。无论如何,谁知道他们在想什么。也许这个区域是通过脚本写入的,因此存储的内容不同。也许位模式 0x0101 用于显示内存已释放,并且该代码意外地在奇怪的地方变得疯狂。我可以翻阅 Z80 代码并在该平台上重温我自己的游戏开发时间,但是呃。试图解密他们的想法需要太多的工作。

不过它确实赚了很多钱...

编辑 1:

好吧,你赏金了它。我花了更多的时间整理我的记忆,为你找到了一个花絮。 GBC/DMG 有一个称为DAA 的操作码。小数调整累加器 (A)。其作用是将累加器中的值转换为 BCD 格式。您看到的内存区域已经采用 BCD 格式:http:// /en.wikipedia.org/wiki/Binary-coded_decimal

现在我可以告诉你,在我手工编写游戏 Z80 汇编程序的 4 年左右的时间里,我从来没有需要过这个操作码,并且只在我们制作的棒球比赛中见过它用于显示一些分数。虽然它是 1 周期算术指令,但我在正常编码中从未真正找到它的良好用途。唔。实际上我还保留着任天堂的 DMG 技术文档。想想看;)无论如何,除了它以时髦的方式与许多标志混淆之外,它也没有什么令人兴奋的地方。

我的猜测是该表被假定为 BCD 格式。将其更改为该格式之外的内容会导致内部数学变得极其混乱 - 在不应该设置的情况下设置进位和零标志。这会导致从一列溢出到下一列,从而导致计算非常大的数字。如果不直接查看读取该区域的相关操作码,我不能肯定地说,但我猜测这里有一个包罗万象的检查,表明在 BCD 完成后是否仍设置进位> 数学,设置最大值而不是存储负值或越界值。该指令或 DAA 指令在接收垃圾数据时返回 0x99 作为其返回值,尽管我对此不太确定。

希望这有帮助...

Honestly, my guess is that this is just a stupid, nasty glitch by someone writing one of their first games on target. Pokemon Red/Blue were the first of the series and had so many other glitches that Nintendo would normally kick out of lot-testing, that I wonder how it got through. The scroll screen shift issue is the one that gets me. Anyways, who knows what they were thinking. Perhaps this area was written to via script and therefore stored things differently. Perhaps the bit pattern 0x0101 was used to show memory was freed and that code accidentally goes bonkers in weird places. I could pour over the Z80 code and relive my own game development time on that platform, but meh. Too much work to try and decrypt what the blazes they were thinking.

It sure made a ton of money though...

Edit 1:

Ok, you bountied it. I spent a bit more time pouring over my memory and found a tidbit for you. The GBC/DMG has an opcode called DAA. Decimal Adjust Accumulator (A). What this does is convert a value in the accumulator into BCD format. The areas in memory you are seeing are already in BCD format: http://en.wikipedia.org/wiki/Binary-coded_decimal

Now I can tell you, in the 4 years or so when I was hand coding Z80 assembler for games, I never once had a need for this opcode, and only seen it used once in a baseball game we made for displaying some scores. While it is a 1 cycle arithmetic instruction, I could never really find a good use for it in normal coding. Hmm. I actually still have the DMG tech docs from Nintendo. Go figure ;) Anyways, nothing exciting there about it either except that it messes with a number of flags in funky ways.

My guess is that table is assumed to be in BCD format. Changing it to something outside of that format causes the internal math to go extremely haywire - Carry and Zero flags set when they aren't supposed to be. This causes overflow from one column to the next, causing very large numbers to be calculated. Without looking directly at the opcodes in question that read this area, I can't say for certain, but my guess is there is a catch all check here that says if carry is still set upon completion of the BCD math, set a max value instead of storing a negative or out of bounds value. That or the DAA instruction, when receiving garbage data is returning 0x99 for it return value, though I'm less sure about that.

Hope this helps...

╰◇生如夏花灿烂 2024-12-03 22:02:33

可以想到一个算法(尽管我为可能编写它的人感到遗憾):

  • 假设输入是十六进制表示法的32位十进制数字,小尾数(例如0x56 0x34 0x12 0x00)。

  • 现在循环遍历每个字节,虽然还没有达到零字节。 (如果 0x999999 确实保证是最大值,那么这种情况永远不会发生……但可惜,事实并非如此。)

  • 在每个循环中,计算实际值并写入数据回到整数(或进入其他缓冲区,您可以在其中执行“loop-while”而不是“for i = 0 to 4”之类的操作)。


如果您的值末尾没有 0x00(即 32 位“十进制”整数大于 0x999999),您可以看到如何出现故障。

当然,这是一种相当晦涩的计算值的方法,但我认为很可能有人为此执行了 while/do-while 循环而不是有界 for 循环。

编辑1:

起初我认为这有一个“优点”,允许直接向用户显示字符串(因为它将以空终止),但当然这是行不通的与小端。他们可以用大端字节序做类似的事情,但这需要一个向后循环才能溢出,我发现这对某人来说不太可能犯错误。

编辑2:

也许这是由于未定义的行为(程序员没有意识到,例如无效的指针转换或别名问题)而进行的编译器优化?

I can think of an algorithm (although I feel sorry for whoever might have written it):

  • Assume the input is a 32-bit decimal digit in hex notation, little endian (e.g. 0x56 0x34 0x12 0x00).

  • Now loop through every byte, while you haven't reached a zero byte. (This should never happen, if 0x999999 is indeed guaranteed to be the max... but alas, it's not.)

  • On every loop, calculate the actual value and write the data back into the integer (or into some other buffer, where you do a "loop-while" rather than something like "for i = 0 to 4").

You can see how you can get a glitch, if your value doesn't have 0x00 at the end (i.e. the 32-bit "decimal" integer is larger than 0x999999).

Of course, this is a rather obscure way of calculating the value, but I think it's quite possible that someone did a while/do-while loop rather than a bounded for loop for this.

Edit 1:

At first I thought this would have the "advantage" of allowing the string to be shown directly to the user (since it would be null-terminated), but of course that doesn't work with little endian. They could've done something similar with big endian, but that would require a backwards loop to overflow, which I find to be a less likely mistake for someone to make.

Edit 2:

Perhaps it was a compiler optimization due to undefined behavior (which the programmer was unaware of, like an invalid pointer cast or an aliasing issue)?

你是暖光i 2024-12-03 22:02:33

谜团解开了!看起来用户 TheZZAZZGlitch 找出了导致此问题的原因

当游戏尝试计算一个极大的整数时,就会触发故障。在内部,游戏有一个例程,反复添加值来模拟乘法。它似乎在写入字节时移动输出写入位置。该代码旨在截断任何超过 0x009999 的值,以便玩家从训练师战斗中获得的收入不会超过 9999 美元(这些值以十六进制编码的十进制存储)。然而,当这种情况发生时,游戏会忘记重置输出指针,因此,如果生成了非常大的数字,游戏将通过移动写入指针并将 0x99 写入每三个字节中的两个来在 RAM 上重复写入模式 0x009999。

希望这有帮助!

Mystery solved! It looks like user TheZZAZZGlitch figured out what causes this.

The glitch is triggered when the game tries to compute an extremely large integer. Internally, the game has a routine that repeatedly adds values to simulate a multiplication. It seems to write bytes as it goes, shifting over an output write position. The code is designed to cut off any value that exceeds 0x009999 so that the player doesn't earn more than $9999 from a trainer battle (the values are stored in hexadecimally-coded decimal). However, the game forgets to reset the output pointer when this occurs, so if an extremely large number is generated, the game will repeatedly write the pattern 0x009999 across RAM by shifting the write pointer over and writing 0x99 to two out of every three bytes.

Hope this helps!

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文