溢出关卡吧 关注:246贴子:3,564
  • 5回复贴,共1

【bug研究】“敌人被顶成龟壳”的原理及修复过程

取消只看楼主收藏回复

好久没来了今年在这里只发了两个新帖子:

嗯,包括这个帖子在内,正好4个月一帖
好了说正事吧。
在SMB中,有一个很有趣的bug:如果一个“行走类敌人”(板栗仔、乌龟、刺猬)位于屏幕左侧的特定位置,那么它被从下面顶起时,会变身成龟壳,可能变成绿色,也可能变成红色,但它们起身之后都跟绿乌龟的表现一样(遇到悬崖会掉下去)。
其实在很早的时候,就已经有人分析过这个bug的原理了,就是当敌人被顶时,会执行一个变身程序,如果敌人ID在特定范围,就会被变成00或01(分别是绿乌龟和“会跳崖”的红乌龟的ID;说到这里顺便吐槽一句,为什么SMB要搞两种红乌龟……);本来“行走类敌人”的ID都不在这个范围,应该不会变身,但是执行到变身程序时,却误将“敌人在屏幕中的相对横坐标”当作敌人ID使用了,于是特定位置的敌人被顶之后,就会变成龟壳。
后来有一天,我忽然想尝试修复这个bug,于是在数日的代码分析和研究之后,终于成功修复了这个bug,而且还是连同另外一个bug一起修复了
然后,你们就看到了这个记录当时过程的帖子。


IP属地:上海1楼2019-12-15 12:16回复
    下面开始正文。
    为了深入研究这个bug的原理,自然要分析相关的程序代码。这里就不贴代码了,说一下大概意思:
    首先,根据敌人ID确定是否为“行走类敌人”,只有$12(刺猬)、$2E(蘑菇)、$00~06范围内(各种红绿乌龟、硬壳龟,以及板栗仔,但不包括锤子龟05)的敌人归于这一类;然后,对敌人进行“脚下检测”,获取敌人脚下的地形块ID,如果这个ID=$23(即“被顶起的砖块”),则将这个地形块改成00,再根据敌人ID的不同,分为如下几种情况执行程序:
    ——如果ID>=$15(虽然用了个范围,但是只有蘑菇在这个范围),则直接跳到$E01B的位置继续执行(当然,这个跳转用的是分支跳转指令);
    ——如果ID=$06(板栗仔),则先调用$E18E处的程序,把板栗仔顶死,然后执行下面“其他敌人”分支的程序;
    ——如果ID为其他数值,则令A=1,调用$DA11处的程序(得分程序),然后继续执行$E01B处的程序。
    简单总结一下,就是:如果是蘑菇,就不执行得分程序(即顶蘑菇不会得分),而顶其他敌人都会得到100分;如果顶的是板栗仔,板栗仔会死,而其他敌人都不会死。
    那么,$E01B处的程序又做什么了呢?它就是“敌人变身”的程序了然而,它并没有再次读取敌人ID,而是直接对A的值进行判断了……也就是说,这个程序应该是先将敌人ID读取到寄存器A中,然后才能正确执行的;但是,从上面的过程我们可以看到,除了蘑菇以外,其他的“行走类敌人”都会先执行一个“得分程序”,这个程序利用寄存器A写了一系列内存,最后读取到A的值是来自内存$03AE的,它正是敌人在屏幕中的相对横坐标!这就是这个bug产生的原因。
    除了变身程序以外,这段程序后面还有一系列操作,比如把敌人变成“龟壳”状态,给敌人一个向上的速度,根据Mario与敌人的横坐标关系确定敌人的横向飞出的速度方向,等等,懒得详细说了


    IP属地:上海2楼2019-12-15 12:18
    回复
      那么,既然“行走类敌人”被顶时都不会变身,那么直接删掉这个变身程序不就把这个bug修复了么?然而问题没那么简单
      让我们继续看一下,把板栗仔顶死的$E18E处的程序又是如何执行的,看完这个,你就明白为什么不能删掉这个变身程序了。
      首先看一下内存表,我们会惊讶地发现,$E18E的程序位于“锤子龟与地形的碰撞检测”程序当中,是其中的一段……嗯,顶死板栗仔,顶死锤子龟,原来执行的是同一段程序,也难怪只有这两种敌人能被顶死
      然后,这个程序是这样执行的:先调用$D795处的程序(根据内存表的说法,当敌人被子弹打死、被龟壳撞死,或者被顶死时,执行的都是这段程序;通过实际分析代码,得知敌人被无敌星状态的Mario撞死也是执行的这段程序),然后将$FC这个数值(即十进制的-4)写入“敌人纵向速度”内存(负数表示向上),就搞定了~而$D795处的“各种搞死敌人的方法的共同程序”,则是这样执行的:先读取敌人ID,如果ID=$0D(食人花),就先额外执行一个“坐标下移”的程序;然后重点来了,调用$E01B处的程序!这之后,再把敌人的状态变成“死的”,根据敌人种类不同分别给A不同的值来调用得分程序,再配上打死敌人的音效,这些都无所谓了。
      既然用各种方法搞死敌人都要执行$E01B的这段程序,那么这个变身程序是否有存在的必要呢?如果打死的是带翅膀的乌龟,那么它们需要变成龟壳的形态,而只有不带翅膀的乌龟才有龟壳形态(这个要验证很简单,进入特定的溢出世界的城堡关,打死Boss,就可以看到死亡状态的飞龟,只是把贴图上下颠倒了而已;说到这里了,联动两个帖子,我会尝试在楼中楼贴链接),这样就需要执行变身程序了。所以,这个程序还是不能随便删的……
      说了这么多,变身程序到底怎么执行的还没提呢过程如下:判断A的值(正常来说应该是敌人ID,但是会有特殊情况,除了这个bug的情况以外,还有一种情况,下文详细说明)的范围,如果在$09~10之间(再排除掉$0A~0C这三个数),也就是各种飞龟以及食人花($0D),则取A的值的最后一位,作为变身之后的敌人ID,即双数ID变绿乌龟,单数ID变红乌龟。不过等等……食人花也在会变身的范围内?可是明明食人花被打死之后不会变身啊?这就是刚才说的另一种特殊情况了。还记得刚才说过的打死食人花之后会执行什么程序么?对了,“坐标下移”!而这个程序是通过寄存器A来读写内存的,也就是说,对于食人花,执行变身程序时,是用它的纵坐标代替ID来判断的这个纵坐标的范围是多少呢?食人花被打死之前,纵坐标可以在$00~D0范围内取值,下移时则会加上$19,因此死食人花纵坐标的最小值也是$19,不在可变身的数值范围内,所以食人花就不会变身了……不过,就算有这样“巧妙”的写法,程序写出bug了仍然是事实
      (说到变身程序了,踩踏敌人也会执行变身程序,不过不是这段,而是另一段不同的程序,这两段程序对应的可变身敌人ID范围也不同,所以会出现踩鱼变乌龟的情况——这里说的是会游的那种鱼,不是飞鱼;这也算是个bug吧,不过这里暂时不提它……)


      IP属地:上海本楼含有高级字体3楼2019-12-15 12:24
      收起回复
        现在代码就算分析清楚了,那么这个bug要怎么修呢?
        这个问题花了我好几天的时间……先后有过好几种思路,但是多数思路都必须额外写代码,然而这段代码写得如此紧凑(虽然有点乱),根本就没有额外的空间可以加代码……
        以下是我设想的几种修改思路:
        ——在执行变身程序之前再读取一次敌人ID,或者前面某个代码把敌人ID暂存到堆栈之后这里再读取,都需要额外2个字节;
        ——对于顶敌人的情况,执行完得分程序之后直接跳过变身程序执行后面的程序,需要额外2~3个字节(如果可以达成某个“必然满足的条件”,才能用只需要2个字节的分支跳转指令,否则就必须写3个字节的JMP);另外,我在完全分析明白这段代码之前,曾经以为“各种方法搞死敌人”的情况下只要执行变身程序就行了,这样加1个字节的RTS就可以,然而事实是:首先,这里连1个字节的空余都抠不出来;其次,执行完变身程序之后,后面的程序也是有用的,不能直接RTS……
        ——删掉“判断蘑菇”的程序,让顶蘑菇也得分,这样就有加指令的空间了,不过这样就修改原本的特性了;
        ——修改可变身的敌人ID范围,删掉几个判断条件,同样会修改原本的特性;(其实要是无视掉bug敌人$09“静止绿飞龟”的感受,这种改法倒是完全可行
        ——调整程序执行顺序,这个虽然改动量比较大,但是如果能解决问题,也不失为一种方法,刺球运动特性的修复就是通过调整程序顺序来实现的;这也是我正式开始尝试的第一种方案,然而……
        我先试着把得分程序调整到变身程序之后的某个程序(有读取敌人ID的指令)后面,然后发现,踩死快乐云怎么只得100分了?难道说……
        于是回头继续分析代码,发现踩踏敌人的程序也要用到这段程序,是从$E02F处开始执行的,也就是变身程序后面的各种程序;如果敌人属于可以直接踩死的种类,那么会根据敌人ID的不同,用不同的A值调用得分程序,然后再调用$E02F处的程序,因此当我把得分程序调整到$E02F的后面时,这个得分程序会覆盖掉前一个的效果,最后就变成只得100分了……
        那就再交换一下这两个得分程序的顺序?交换之后……怎么又变成得200分了?看来$E02F的程序还会改变寄存器Y的值啊(踩死敌人的得分程序就是用Y来读取不同的得分设定值的)
        看来,这些思路都行不通啊……
        (注:码字的时候,重新研究了一下“无视敌人09”的方案,发现即使不无视这个敌人,也完全可以把原来的4个判断条件压缩成3个,直接拿到了4个字节的空间,用“再次读取敌人ID”的加代码方法,甚至还可以再写个2字节的指令让“静止绿飞龟”被打死之后不变红作为单独修改这一个bug的方案,可以说是完美了~于是把这种方案也改了出来。)


        IP属地:上海4楼2019-12-15 12:44
        回复
          就在反复思索无果的时候,有一天,我又盯着“把地形块$23变成00”的那几句代码看……忽然,灵光一闪:
          为什么要把$23变成00啊?用完一次就不需要了?这样会造成什么后果?
          嗯……对了!如果同时顶到两个敌人,只会顶翻一个,另一个会掉下来!另外还有“顶蘑菇上穿墙”这种操作!
          这很明显也是个bug啊
          对了,顺便看看,如果顶的是锤子龟,程序又是什么操作。果然,$E185的程序中,就没有把地形块$23变成00的代码!
          也就是说,这6个字节的代码就是多余的,应该删掉!嗯,6个字节啊
          于是,一种同时修复两个bug的修改方案就这样出来了。
          补丁文件已上传至我的网盘,网盘链接见置顶帖,文件在IPS文件夹中。楼上提到的“另一种可行方案”的补丁也一并分享,请各位自取。(另外还有最近新做的一批补丁,可能今后我会再对这些补丁发个分析帖


          IP属地:上海5楼2019-12-15 12:45
          收起回复
            好了,本帖到此结束。


            IP属地:上海6楼2019-12-15 12:46
            回复