Fizzbuzz Mario World:学习汇编语言和乐趣

2021-03-07 11:42:27

我希望长时间学习汇编语言(ASM)编程。我终于找到了这个完美的项目来做:黑客超级马里奥世界(SMW)。这是很有趣,所以我以为我' d记录了这个过程。

SMW ROM黑客社区充满活力。它'这是一个令人印象深刻的有才华和创意社区,它具有许多具有自定义图形,音乐,级设计和游戏物理学的令人敬畏的游戏。在Youtube上绊倒这些黑客开始了我这个兔子洞。

TLDR:可以在Github上找到此帖子中提到的代码和资源列表。

FizzBu​​zz是一个常见的问题,初学者程序员为实践解决。目标是循环从1到100:

该项目的目标是通过编写可以修补或插入的自定义ASM来解决类似于FIZZBUZZ的问题,或插入SMW代码。 SMW中FizzBu​​zz的完美背景是硬币计数。在任何时候,玩家都可以在0到99个硬币之间。此外,马里奥可以拥有4个电源状态的1个:小,大,斗篷和火灾。这里的行为我们的SMW版本和#39;

几分钟后搜索网络,我发现了大多数信息I' LL正在分享:SWM Central。您可以在本文末尾找到完整的资源列表,但我发现最有用的指南是eRsanio' snes和super Mario World的汇编组装。前者假设没有对ASM的了解。后者从前者刷新了ASM信息,然后进入如何应用类似于我们的简单补丁; LL正在写作。两者都是快速的阅读,我向任何程序员推荐给想要知道ASM编程的感觉。

ASM是一种低级语言,您可以直接处理单个字节的内存。 SNES游戏由只读存储器(ROM)和随机存取存储器(RAM)组成。您可以将ROM视为游戏盒本身。对于现有目的,ROM而不是实际的盒式磁带,而不是实际的盒式磁带,而是可以在SNES仿真器上播放的计算机文件。 ROM是如何存储游戏工作原理的所有代码的位置。这个内存,顾名思义,通常只读取。我们的目标是通过覆盖其中的一小部分来破解这个rom,从而改变游戏的行为方式。

RAM在SNES本身上,它' s在播放游戏时将更改变的值。硬币计数器价值需要生活在RAM中,因为它经常变化。同样适用于玩家'上电状态。

在两个ROM和RAM中,内存是可以存储值的长长列表。这些地址以十六进制(十六进制)表示。出于我们的目的,我们需要弄清楚三件事的内存地址:硬币计数的值,上电状态的值,以及插入某些自定义代码的位置。

幸运的是,人们完全拆解了SMW并映射了RAM和ROM,所以找到我们所需要的是一个简单的网络搜索。 RAM地图可以在SMW Central上找到。 RAM以7E0000美元的价格开始于7E0000美元,以7亿美元的价格结束。 $表示十六进制。通过搜索一些相关的关键字,我发现硬币计数存储在地址7e0019,电源状态存储在Address $ 7E0DBF。上电状态的条目还指示此地址的4个可能的值以及它们的意思:1为Big Mario,2个用于Cape Mario,3为火星Mario,以及小型Mario。

这为我们提供了一个基本思想,对伪代码中需要做的亚军代码需要做的事情:

每次硬币计数增加:如果在RAM地址为$ 7E0DBF(上电状态)在RAM地址为$ 7E0DBF(上电状态)其他值,则获得RAM地址$ 7E0019(硬币计数)的值。 RAM地址$ 7e0dbf如果该值在RAM地址中的3 store 1中可分开,则RAM地址$ 7e0dbf else store 0 $ 7e0dbf

现在我们需要弄清楚在哪里插入我们的代码。我希望这段代码在玩家获得硬币时运行,所以我搜索了"硬币计数"在ROM地图中,发现$ 008f1d,一个30字节的代码,它与#34;处理实际上增加玩家的手柄' s硬币计数并从100个硬币中留下生命。"这是一个很好的开始,但我们可以' t只插入到30个字节的rom,而不看到它的作用。如果我们&#39,我们会打破一切。不幸的是,SWM Central上的ROM地图不会在此地址存储实际代码。但后来我找到了All.log ++:使用广泛的评论和标签,在ASM中完成SMW源代码的完整拆卸。

在这个硬币计数代码地址处检查拆卸的SMW源代码,我注意到硬币计数实际上增加的地址是008F25。也就是说,这是存储字面图的指令的ROM的存储器地址,这些指令在玩家接收1硬币的每一次时每次单一时间添加1到硬币计数。但是,不仅仅是增加硬币计数1,我想修补ROM,以便在执行此地址的代码时,SNES也运行我的FIZZBUZZ代码。

现在我们拥有我们需要的所有相关的内存地址,我们都有伪代码,我们需要在硬币计数增加ROM地址时运行。现在我们组装。

我们只需将COIN计数和上电状态设置为某些标签所需的RAM内存地址,因此它们更容易在代码中引用。请注意,我们从两个地址丢弃了7e。我们不需要它。这些行Don' t实际上被修补到ROM中。我们将写入的代码中的所有标签都会替换为汇编程序的地址。汇编程序是将采用我们的代码并将其插入ROM的东西。

我们知道我们要在哪里插入我们的代码:COIN计数增加的ROM地址。但我们可以' t将所有代码插入此地址,我们' ll覆盖了很多东西并打破游戏。我们只能插入几个字节,我们必须确保我们覆盖的字节由我们在自己的代码中执行,以便原始代码应该仍然发生的所有内容。那么我们' ll do do在coin count增加的地址上插入一个指令,告诉处理器跳转到我们的其余部分。然后我们' ll告诉汇编程序将我们的代码插入一些可用空间,因此我们不会覆盖任何东西。这看起来像:

ORG $ 008F25指示汇编程序将以下指令插入JSL FIZZBUZZ,进入COIN计数的ROM地址。 JSL FizzBu​​zz意味着:跳转到标记为FizzBu​​zz的子程序代码。大多数ASM操作代码或操作码是菜单。 j和s是跳跃和子程序。我们可以忽略L,它' s超出了这个范围。

什么' s与nops?事实证明,我们选择覆盖的代码,硬币计数增加指令,长期3个字节(我们知道这一点,因为All.log ++告诉我们)。我们插入的代码JSL FIZZBUZZ,是4个字节。因此,我们尝试覆盖1 3字节长指令,我们将覆盖第二条指令的第一个字节。

解决此问题是,我们需要记住执行我们在编写代码中覆盖的两个指令。我们覆盖的第二个指令,如第一个,是3个字节长,总共6个字节。但是,我们插入的代码只有4个字节:仍然存在2个悬空字节,这是第二个指令的一部分。那个'不好。代码中的随机,部分覆盖的字节将打破游戏。所以,我们包括2个N​​OP指令。这些都没有操作。我们对2个字节做的任何都不做,填补我们插入前4个字节的代码未填写的空间。要回顾,我们覆盖了6个字节的2个指令,其中6个字节的我们自己的指令跳转到我们的自定义代码。

FreeCode只是意味着在ROM中找到一些可用空间来放置我们的其余自定义ASM代码。

第一行是标签。第二个,Inc!coIncount,意味着将硬币计数的内存地址中的值增加1.这是我们覆盖的代码的第一个指令应该做到,所以我们在这里做到这一点。

在ASM中,最重要的事情之一是累加器。基本上,累加器(a)是微处理器存储其由数学和逻辑操作的结果的存储器地址。我们还可以将内容存储在数学运营中。 LDA#$ 0f是这样的。它进入累加器的值15. 0f为十进制值15的十六进制,并且#表示我们希望值15本身,而不是存储在存储器地址15上的'

然后我们运行STA $ 00,这将累加器的值存储到内存地址$ 00中。这个内存地址是"划痕"除了存储临时值的位置之外,内存没有分配的目的。我们无法将一个值直接存放在$ 00中,而是一个两步的进程:将值加载到A中,然后将值存储为$ 00。接下来,LDA!CoIncount将硬币计数的值加载到A中。

所以现在我们有两个存储在内存中的值:硬币计数,存储在a和15中,存储为$ 00。要检查某些东西是否已被3和5可分开,我们可以将其分开15并检查是否存在余数。许多编程语言都有一个模数运算符,为您提供了剩余的1个数字除以另一个数字。例如,47 Modulo 15是2. JSR MOD表示跳转到标记为MOD的子例程。 65C816微处理器的ASM并不是一个模数运营商,所以我们必须自己写作。它的解释比其他人更复杂,我不想让它分散我们的注意力,所以你可以在帖子的尽头找到它。 JSR Mod将发现剩余的硬币计数除以00,15美元的价值。重要的是,它将存储在A中的其余部分。然后我们执行以下代码:

BNE TestMoD5表示TestMod5的分支如果不等于。它检查a是否等于0,如果它是n' t,它分支为testmod5,从而跳过该代码片段中的其他三个指令。什么时候平等0?当硬币计数除以15作为剩余部分时。如果a是0,则代码赢得' t分支。相反,LDA#$ 03将值3加载到A中,然后将STA!PowerupStatus将值存储到电源状态存储器地址中。 3是火马里奥的价值。

我们刚刚编码了逻辑,使播放器获得硬币,硬币计数已被3&amp可分开; 5,我们将马里奥设置为火马里奥。如果它已被3&amp所划散; 5,我们'完成了!所以我们执行胸罩返回,这意味着分支返回。我们' ll看看后来做了什么。如果A不等于0,那么硬币计数不可分割3和5,所以我们没有将马里奥塞到火星。这意味着我们需要测试下一个案例,无论不是硬币计数是否可分解5.以下代码是在上面的分支的位置,如果MOD 15未被' t等于0:

我把它留给了读者,以逐步解决这里发生的事情。没有什么新的。我们只是重复我们上面的工作,但是为5.如果硬币计数mod 5不是0,我们再次分支到测试3:

在这里有更多相同的3.但是当硬币计数mod 3并不等于0,我们分支到SetSmall。此代码STZ!PowerupStatus在上电状态地址中存储零。如果硬币计数没有,我们只能达到该代码,其中包括3,5或15,所以我们将马里奥设置为小。如果其中任何一个案例设置了上电状态,则它将' vere一直跳转到返回子程序:

这些说明做了两件事。首先将硬币计数加载到A中。这是我们插入代码覆盖的第二条指令。回想一开始,我们将跳转到我们的代码插入到COIN计数增加的内存位置。我们覆盖了两条指令和LDA!CoIncount是第二条指令,所以在我们返回我们插入代码之前,我们在这里做到这一点。然后,我们的RTL意味着返回到我们的jtleve的任何地方。

ASM,至少对于65C816,并不有许多操作码。它甚至没有乘法和划分,更不用说模数运营商。以任何其他语言编写模数子程序很简单。如果您想知道x modulo y是什么,从x中减去y,然后检查x是否小于0.如果它是,则将y添加回x并且'您的剩余部分。如果它是' t,重复减法并再次询问。喜欢:

18 Modulo 718 - 7 = 1111 - 7 = 44 - 7 = -3-3小于0,因此余数为-3 + 7,4

理解这一点的关键是了解如何称为携带标志的工作原理。不可否认,我知道携带标志的携带旗帜才能完成这项工作,所以我的解释并不完整。但是,我所知道的是,如果携带标志设置为1,则从产生否定结果的累加器中减去一个数字,然后将带标志设置为0.这是好的,我们的模数算法要求我们知道我们何时到达负数。

我们做的第一件事,Sec,设置了携带标志(设置它等于1)。然后,我们将携带00美元的价值从A中携带。记住上面的代码,当我们保持00美元和3,5或15美元时,以便将硬币计数置于00。这就是为什么。

现在,我们' ve从a中减去了00的价值,我们执行了bcs mod。如果携带设置为mod,则这意味着分支。如果我们确实导致了正数,那么携带标志仍然设置,所以我们再次回去并再做一次。如果携带标志ISN' T Set,那么我们知道A现在是负数,所以我们继续。我们所做的下一件事是ADC $ 00,这意味着携带到A.我们将值以$ 00添加到A.现在这意味着现在是累计器的值是最初的剩余部分(硬币计数)Modulo $ 00(15,5或3)。最后,我们从子程序开始,或者从子程序转回,然后回到我们跳起来的地方。我们去了。在超级马里奥世界的组装中的模态。

这个项目很有趣,这是我一直想要的ASM介绍,甚至没有知道它。 我不知道我是否知道如果我' ll有时间继续黑客SMW,但我可能会试图找到其他途径来探索ASM,比如嵌入式编程。 事实证明,我们修补了SMW的这种行为的这种行为实际上是很有趣! 不幸的是,我可以'如果你喜欢玩它,请拍我一封电子邮件:[email protected],或者只是关于如何处理smw rom的谷歌 编写的代码;写了。