溢出关卡吧 关注:249贴子:3,572

6502程序员笔记:个人制作的补丁代码解析

只看楼主收藏回复

大家好,2020元旦快乐
半个月前,我在发布了某个bug修复帖的同时,也上传了一批19年新制作的补丁,当时就说过要发帖一并分析一下。现在,我来兑现诺言了
先来看看本人网盘里已有的补丁文件:

嗯,我没打算一天就把这些补丁全部分析完,所以算是先开个坑吧


IP属地:上海1楼2020-01-01 09:44回复
    元旦快乐啊


    IP属地:新疆来自iPhone客户端2楼2020-01-01 10:34
    收起回复
      以下将按照补丁制作时间顺序依次介绍,基本上是上面列表从下到上的顺序,但有些补丁由于是同时上传的,上传之后打乱了原有的顺序,因此并不完全是列表中的顺序。
      SMB 0-1修复补丁
      修改指令量:1条(2字节)
      实际修改大小:1字节
      修改前指令:
      CPY #$30
      修改后指令:
      CPY #$60
      指令相关代码:
      LDX #$07
      LDA #$00
      STA $06 ;将A的值(0)写入内存$06
      STX $07 ;将X的值写入内存$07(第一次写入的值为7)
      CPX #$01 ;如果X≠1
      BNE #$04 ;则跳过下面2条指令
      CPY #$60 ;如果Y>=$60
      BCS #$02 ;则跳过下面1条指令
      STA ($06),Y ;将内存$06和$07指向的内存初始化为0
      DEY ;Y值减1
      CPY #$FF ;如果Y≠$FF
      BNE #$F1 ;则跳回到CPX #$01的位置继续循环
      DEX ;X值减1
      BPL #$EC ;如果X不为负数,则跳回到STX $07的位置继续循环
      RTS
      解析:这是一段初始化内存的代码,将内存$0000~$07D6的范围全部初始化成0,除了堆栈区的栈底部分($0160~$01FF)。是的,初始化范围只到$07D6,这是因为调用这段程序之前,有一条LDY #$D6的指令,这样,就保留了一部分不会变的内存,如二周目状态($07FC)、续关世界编号($07FD)等。说到$07FD,我就想到热插拔卡带进溢出关卡
      补丁原发布帖:【代码分析】盗版SMB1的0-1问题成因分析


      IP属地:上海3楼2020-01-01 11:03
      收起回复
        修复两位生命显示
        修改指令量:8条(17字节)
        修改数据量:1字节
        实际修改大小:16字节
        修改前指令:
        LDA $075A ;读取生命数到寄存器A
        CLC
        ADC #$01 ;生命数的存储值比实际显示的值小1,因此要加1
        CMP #$0A ;如果小于10
        BCC #$07 ;则跳过以下指令
        SBC #$0A ;减去10(正常来说这里应该先执行SEC,但前面的判断之后,这里C必然为1,否则就会被跳过)
        LDY #$9F ;读取一个特殊符号(皇冠的图形)
        STY $0308 ;显示到个位数前面
        修改后指令:
        LDY $075A ;读取生命数到寄存器Y
        INY ;加1
        TYA ;转存到A
        CMP #$0A ;如果小于10
        BCC #$08 ;则跳过以下指令
        INX ;X值加1
        STX $0308 ;显示到十位数的位置
        SBC #$0A ;将A值减10
        BCS #$F4 ;如果够减,就返回CMP #$0A的位置继续循环
        修改的数据:ROM偏移$7A6处的$BA改为$AA,这1个字节是4个16x16图块的调色板数据,其中包括了“皇冠图形”所在的位置,因此皇冠会显示成金色;修改之后,两位数显示的颜色就一致了。
        解析:这是我早年的得意作品,巧妙利用了各个寄存器的使用状况,堪堪抢出若干个字节的空间,在这段有限的指令空间内成功完成了修复的任务。虽然当时已经有同样功能的补丁了,但是出于练手的目的,我还是自己坚持写出了这段补丁,随后得到的反馈也相当不错(用过都说好)
        我们知道,原版这段程序只能正常显示一位数的生命,如果加命到了两位数,十位会显示成一个皇冠,真不知道制作人员当初怎么想的……
        要修改成能够正常显示两位数,就需要把十六进制的内存值正确转换成十进制,如果6502有除法运算,那么这个代码写起来会简单很多,但是……事实是没有除法所以我们只能写一个减法循环,让一个寄存器减10之后,另一个寄存器加1,从而达到分成两位数的目的。(想要更多位数?抱歉,在不扩容的情况下做不到……)减10的寄存器只能是A,而加1的寄存器,X或Y都可以。
        写循环之前,先看看前面这部分代码能不能精简一点,万一循环需要的代码容量不够呢……原先的写法需要6个字节,只用到了寄存器A,那我如果借用一个别的寄存器来实现加1呢?修改之后,果然省出了一个字节那么具体用哪个寄存器呢?原来的代码用到了Y,说明Y是可以用的,那就用Y好了。
        然后就该写循环了。要写这个循环,正常来说,应该先把存十位数的寄存器先清零,但是,根本挤不出这样的空间……所以只能看看上下文,看有没有寄存器在这里正好是0的。这段代码前面有这样两句:DEX / BNE #$23,意思就是,先让X值减1,之后如果X不为0,则跳过这段程序。漂亮!这说明执行这段程序的时候,X一定是0!(实际上,如果看得再远些,就会发现,Y在这里也一定是0,所以用谁都可以,不过这就是后话了。)
        这样写下来,3个寄存器都用到了,而且代码空间刚好够用,所以我对这个小作品还是相当满意的


        IP属地:上海4楼2020-01-01 16:09
        回复
          碰敌人不死
          修改指令量:1条(3字节)
          实际修改大小:3字节
          修改前指令:
          LDA $079E ;读取“隐身状态倒计时”,准备进行后续判断(如果为0,则执行受伤程序)
          修改后指令:
          JMP $D955 ;跳过“受伤程序”
          隐身无敌
          修改指令量:2条(6字节)
          实际修改大小:4字节
          修改前指令:
          (第一条与上述相同,改法也相同)
          (第二条指令)LDA $079E ;这是另一处的读取指令,后续是用来显示人物贴图的
          修改后指令:
          (第二条指令)INC $079E ;使“隐身状态倒计时”加1
          解析:这两个补丁是应某位吧友的要求改的,时间仓促,只求改出效果,没有多深究。
          “碰敌人不死”的修改,上面的指令注释已经说明了修改效果,也没必要多做解释;
          “隐身无敌”的修改,其实是上了个“双保险”,随便修改一处指令使$079E在大多数时间都不为0,这样在显示上就有了闪烁效果,然后为了避免$079E=0时的一瞬间碰敌人受伤(有个改$079F的金身无敌改版就会有这样的非无敌瞬间),再附加上“碰敌人不死”的修改,把受伤的可能性封死,这样就保证不会出问题了。


          IP属地:上海5楼2020-01-01 19:42
          回复
            旧作品到此就分析完了,其余的补丁都是19年的作品了
            跳关区特性修复
            修改指令量:4条(10字节)
            实际修改大小:10字节
            修改前指令:
            AND $B5
            BNE #$F4
            STA $0723
            INC $06D6
            修改后指令:
            BEQ #$05 ;如果(内存$CE的值)为0,则跳到最后一句指令
            LDA $B5 ;读取内存$B5的值
            BEQ #$01 ;如果为0,则跳过RTS指令
            RTS ;如果两个内存都不为0,则什么都不做,退出本段程序
            STA $0723 ;不管哪个内存是0,总之都是读取到寄存器A的,把A的值0写入内存$0723
            相关帖子:对某“负一关”揭秘帖的“吐槽”(当时只提到了制作补丁的事情,没有将成品发布)
            解析:……本来想直接略掉的,不过还是从那个帖子里摘抄过来吧;另外,当时也没有提到修改后的代码是怎么写的,这里就一并解释一下。
            以下是原文引用:
            「这段程序“解除停止滚屏”的判断条件到底是什么呢?这里涉及到了Mario的纵坐标,可以看到用来表示纵坐标的内存共有2个,具体含义分别为:低字节就是Mario在屏幕中的实际纵坐标(以像素为单位),高字节则表示Mario在“哪块屏幕”中,比如玩家可见的区域为“1号屏幕”,即00B5这个内存值为1;如果Mario站在天花板上,则内存00CE的值刚好为0,即这就是“1号屏幕”的最高点,此时再跳起来(哪怕刚跳离地面仍然看得见),那么Mario就进入“0号屏幕”了。所以,我们可以先得出一个结论:表示“Mario在天花板上方”的条件应该是“内存00CE的值为0,或内存00B5的值为0”。
            现在,再回到上面这段程序,它的条件又是什么呢?是“内存00CE与00B5的值进行按位与运算后的结果为0”……这句话怎么看怎么别扭;事实上,从程序的运行结果我们也可以知道,这个条件跟“两个数有一个是0”肯定不是一回事。这个条件到底如何解释呢?如果纵坐标高字节(00B5)是0,那么没什么问题(0与任何数AND运算的结果都是0),但如果它是1(只要Mario保持在屏幕内,这个数不是0就肯定是1),即换算成二进制只有最末位是1,其余位都是0,那么低字节只要末位不是1(即它是偶数),两个数AND的结果就是0了……这相当于有一半的机会可以使“跳关区”敌人生效,把停止滚屏效果解除,而且这个检测只看位置,不看状态的(即不管是在空中还是落地),我之前说的“落地解除停止滚屏”,还是有点想当然了。」
            关于修改之后的代码,其实就是按照正确的思路改写了程序,但是这样指令空间并不够,需要额外2个字节……不过,本着修bug的思想,决定删掉那句会导致“负关bug”的INC $06D6,于是反而多出来了1个字节的空间,干脆写了个RTS,不用别人的了


            IP属地:上海6楼2020-01-02 17:26
            收起回复
              人物大小切换程序修改
              修改指令量:1条(3字节)
              实际修改大小:1字节
              修改前指令:
              LDA $0754
              修改后指令:
              LDA $0756
              指令相关代码:
              LDA $0754 ;读取“人物大小”(修改后读取的则是“人物状态”)
              EOR #$01 ;将最后一位反转
              STA $0754 ;存入“人物大小”内存
              受伤不直接变小
              修改指令量:2条(6字节)
              实际修改大小:2字节
              修改前指令:
              (第一条与上述相同,改法也相同)
              (第二条指令)STA $0756 ;将A值存入“人物状态”内存(执行到这里时A=0)
              修改后指令:
              (第二条指令)DEC $0756 ;将“人物状态”内存值减1


              IP属地:上海7楼2020-01-03 15:46
              回复
                (接7楼)
                解析:这两个补丁算是质量比较低的作品,修改量小,达成的效果也是“半吊子”,比如“受伤不直接变小”,当火力花状态受伤时,仍然会出现缩小的动画,但随后还会恢复成大个子,看上去比较滑稽
                其实,当初做这两个补丁时的初衷是,希望能够修复“大个子踩斧后受伤状态反转”这个bug,于是啃了半天可能相关的代码,无果……遂放弃,但同时产生了一个想法:如果能修复这个bug相关的后续bug(小个子发子弹),那也是好的啊
                “大个子踩斧后受伤状态反转”这个bug的大致原理是这样的:人物受伤时,当前帧会执行将$0756变为0的程序,然后会向$000E写入一个值($0A),表示下一帧执行“将$0754的值反转”的程序;但是在踩斧通关的状态下,$000E会被写入另外一个值,于是“将$0754的值反转”这一程序就不会被执行了。从根本上说,这个bug及其后续的“小个子发子弹”bug之所以会产生,就是因为SMB用了两个不同的内存来分别表示“人物大小”和“人物状态”,如果都用同一个,应该就不会产生bug了。当然,如果真想把整个SMB程序这样改,那可是大工程……不过,至少可以用某种手段把这两个内存绑定起来吧?


                IP属地:上海8楼2020-01-03 15:54
                回复
                  (接8楼)


                  IP属地:上海9楼2020-01-03 15:58
                  收起回复
                    吃花直接变火力形态
                    修改指令量:7条(14字节)
                    调整位置指令量:11条(25字节)
                    实际修改大小:37字节
                    修改前指令相关代码:
                    LDA $39 ;读取“道具类型”
                    CMP #$02 ;如果小于2,即蘑菇(0)或花(1)
                    BCC #$0E ;跳转到相应位置
                    CMP #$03 ;如果是绿蘑菇
                    BEQ #$24 ;跳转到相应位置
                    ;如果没有跳转,那就是无敌星
                    LDA #$23
                    STA $079F ;设置“金身无敌”倒计时为$23
                    LDA #$40
                    STA $FB ;更改音乐
                    RTS ;退出
                    ;蘑菇或花
                    LDA $0756 ;读取“人物状态”
                    BEQ #$1B ;如果为0(小个子),则跳转到“变大”程序
                    CMP #$01 ;如果不等于1(那么就是2,即火力花状态)
                    BNE #$23 ;跳转到RTS退出程序
                    LDX $08 ;读取“当前敌人编号”
                    LDA #$02
                    STA $0756 ;变身火力花状态
                    JSR $85F1 ;调用“写调色板”程序,使人物变色
                    LDX $08
                    LDA #$0C ;令A=$0C
                    JMP $D847 ;跳转到“变大”和“变火力花状态”的公共程序
                    ;绿蘑菇
                    LDA #$0B
                    STA $0110,X ;将得分改写为1UP
                    RTS ;退出
                    ;变大
                    LDA #$01
                    STA $0756 ;变成大个子
                    LDA #$09 ;令A=$09
                    ;变大和变火力花状态的公共程序
                    LDY #$00 ;令Y=0
                    JSR $D948 ;调用“受伤程序”的后半段,将A写入内存$000E,Y写入内存$001D
                    RTS ;结束
                    修改后代码:
                    LDA $39
                    BEQ #$30 ;如果是蘑菇(0),则跳转到“变大”程序
                    CMP #$01 ;如果是花
                    BEQ #$0E ;跳转到“变火力花状态”程序
                    CMP #$03 ;绿蘑菇
                    BEQ #$22 ;跳转到相应程序
                    ;无敌星的程序,略(只是调整了位置)
                    ;变火力花状态
                    LDA $0756
                    CMP #$02 ;如果是火力花状态
                    BEQ #$23 ;跳转到RTS退出程序
                    LSR A ;将A右移一位($0756不是2那么就是0或1,右移之后必为0)
                    STA $0754 ;存入“人物大小”内存(变成大个子)
                    LDA #$02
                    STA $0756
                    JSR $85F1
                    LDA #$0C ;从这句起就没有修改了
                    JMP $D847
                    ;绿蘑菇、变大等的程序略


                    IP属地:上海10楼2020-01-04 10:04
                    回复
                      (接10楼)
                      解析:做完了“受伤不直接变小”的补丁,自然还想再移植一个来自SMB后期版本的特性,比如吃个花就直接变成可以发子弹的状态
                      说起来简单,看过代码之后发现,这个修改可不像上次那样改2个字节就完事了(虽然改2个字节达成的效果也并不理想),牵涉太多,肯定要大改。
                      原始程序对蘑菇和花的处理方式是“一视同仁”的,即不管到底是蘑菇还是花,只看人物当前的状态:小个子变大,大个子变火力花状态,火力花状态什么都不做。现在要想修改这个逻辑,就必须把蘑菇和花分开判断,把“吃蘑菇变大”和“吃花变火力花状态”这两个逻辑给对应起来。
                      那就这样分开呗,先尝试着小小修改了一下,然后一测试发现——小个子吃到花之后确实可以发子弹了,但还是小个子!上次刚修了这个bug,结果这次又直接把这个bug给写出来了
                      那再把$0756和$0754这两个内存绑定起来?也不行,“变火力花状态”的程序并没有触发“反转$0754”的程序,改那里也行不通……
                      看来,只能在“变火力花状态”程序里加个令$0754=0的指令了。但是……刚才的修改只是调整了部分代码的顺序,把一个判断(比较指令)移到了另一个地方,并没有删掉指令,也就没有地方可以加指令了。这可怎么办……
                      于是再仔细看代码,找找有没有多余的指令。反复观察之后,注意到这样一条指令:LDX $08(而且出现了两次),它是干什么用的呢?根据分析,这应该是从内存$0008读取“敌人编号”的指令,当处理屏幕上的敌人时,需要用到一个偏移值(比如LDA $16,X这样的指令),这个偏移值X就是敌人编号,在处理敌人的程序中,这个编号是暂时保存在内存$0008当中的,这样,如果某段程序需要临时使用寄存器X,就可以放心使用,用完之后再从$0008把X的值读取回来就行了。
                      那么,这段程序改变过X的值么?然而并没有……事实上,整段程序除了这两句LDX $08,就没有别的指令动过X——除了那几个调用子程序的JSR指令;实际上,$85F1处的程序确实改变了X的值,所以后面似乎有必要写一个LDX $08来恢复X的值。但是,一定要写在这里么?再看看$D948处的程序,我们会发现,这段程序的最后正好有一句LDX $08!嗯,这么一来,两句LDX $08就都可以删掉了,腾出了4个字节的空间。
                      不过,只有4个字节么……够用么?先看看各个寄存器当前的取值吧:
                      A——这里刚刚读取过$0756,用来判断吃花之后是否需要有动作,有动作的$0756取值为0或1;
                      X——道具类敌人的编号必然是5;
                      Y——这段程序到目前为止也没用过Y,向前回溯可以知道,最近一次读取到Y的值是敌人ID,道具类敌人的ID=$2E。
                      好吧,没有一个寄存器正好是0……但是,如果按照常规的写法:LDA #$00 / STA $0754,这样需要5个字节……
                      等等!有没有别的办法把A变成0的?除以2?(可是6502不是没有除法么)嗯,是没有除法,但是有移位运算啊。就决定是你了,LSR!正好只需要1个字节!
                      于是,这就成为了我近年的得意作品


                      IP属地:上海11楼2020-01-04 10:10
                      收起回复
                        还剩下3个补丁,都是最新发布的,并且都有发布帖子,所以只贴代码,不贴解析了
                        顶敌人相关的2个bug修复
                        修改指令量:4条(9字节)
                        调整位置指令量:7条(15字节)
                        实际修改大小:24字节
                        修改前指令:
                        LDY $02 ;读取“被顶砖块在屏幕中的位置”
                        LDA #$00
                        STA ($06),Y ;将对应内存改写为0
                        LDA $16,X ;读取敌人ID
                        CMP #$15 ;如果ID>=$15(其实只有蘑菇)
                        BCS #$0C ;跳出本段指令
                        CMP #$06 ;板栗仔
                        BNE #$03 ;其他敌人跳过下一条指令
                        JSR $E18E ;调用“顶死”程序
                        LDA #$01 ;令A=1
                        JSR $DA11 ;设置得分为100分
                        修改后指令:
                        LDA $16,X
                        CMP #$15
                        BCS #$26 ;其实这里可以不改(我也没把它算到“修改的指令”中),
                        ;但是反正要跳到后面,不如干脆一次性跳过去
                        CMP #$06
                        BNE #$03
                        JMP $E18E ;跳转到“顶死”程序(反正它还要回来调用这里后面的程序)
                        LDA #$01
                        JSR $DA11
                        JMP $E02F ;跳过“变身”程序
                        NOP ;三个“无操作”指令其实这里写什么都可以了,已经变成3个字节的“可用空间”了
                        NOP
                        NOP


                        IP属地:上海12楼2020-01-04 16:20
                        回复
                          顶敌人变龟壳bug修复,更改敌人09特性
                          修改指令量:8条(16字节)
                          实际修改大小:12字节
                          修改前指令:
                          CMP #$09 ;如果A<$09
                          BCC #$10 ;跳过“变身”程序
                          CMP #$11 ;如果A>=$11
                          BCS #$0C ;跳过“变身”程序
                          CMP #$0A ;如果A<$0A(执行到这里,只能是$09了)
                          BCC #$04 ;跳到“变身”程序执行
                          CMP #$0D ;如果A<$0D(再吐槽一次,如果这里用食人花的ID来判断,是会变身的)
                          BCC #$04 ;跳过“变身”程序
                          修改后指令:
                          LDA $16,X ;读取敌人ID
                          CMP #$09 ;静止绿飞龟
                          BNE #$02 ;其他敌人跳过下一条指令
                          LDA #$0E ;令A=$0E,把“静止绿飞龟”变成“绿跳龟”的ID
                          CMP #$0E ;如果A<$0E
                          BCC #$08 ;跳过“变身”程序
                          CMP #$11 ;如果A>=$11
                          BCS #$04 ;跳过“变身”程序


                          IP属地:上海13楼2020-01-04 16:38
                          回复
                            踩鱼变乌龟bug修复
                            修改指令量:6条(12字节)
                            调整位置指令量:8条(16字节)(包括2对交换位置的指令)
                            实际修改大小:22字节
                            修改前指令相关代码:
                            LDY $16,X ;读取敌人ID存入寄存器Y
                            CPY #$2E ;道具类敌人
                            BNE #$03 ;其他敌人跳过下一条指令继续执行
                            JMP $D800 ;跳转到“吃掉道具”的程序(也就是8楼的代码)
                            ;一些其他指令,略
                            CPY #$12 ;刺猬
                            BEQ #$4E ;跳转到“可以踩”的程序
                            ;火球、食人花的判断代码略
                            CPY #$33 ;炮台炮弹
                            BEQ #$42 ;跳转到“可以踩”的程序
                            ;无修改区域,略
                            LDA $16,X ;读取敌人ID存入寄存器A
                            CMP #$12 ;刺猬
                            BEQ #$BD ;向上跳转到“受伤”程序
                            LDA #$04
                            STA $FF ;播放“踩敌人”音效
                            LDA $16,X ;再次读取敌人ID
                            LDY #$00 ;令Y=0(用Y控制读取不同的得分设置值)
                            CMP #$14 ;飞鱼
                            BEQ #$1B ;跳转到“踩死”程序
                            CMP #$08 ;炮弹
                            BEQ #$17 ;跳转到“踩死”程序
                            CMP #$33 ;炮台炮弹
                            BEQ #$13 ;跳转到“踩死”程序
                            CMP #$0C ;火球(嗯,不可能的)
                            BEQ #$0F ;跳转到“踩死”程序
                            修改后指令相关代码:
                            LDY $16,X
                            ;道具类敌人和其他无关指令略
                            CPY #$33 ;交换“刺猬”和“炮台炮弹”的判断位置
                            BEQ #$4E
                            ;火球、食人花的代码仍然略
                            CPY #$12 ;刺猬
                            BEQ #$75 ;跳转到“受伤”程序
                            ;无修改区域,略
                            LDA #$04
                            STA $FF
                            LDA $16,X
                            LDY #$00
                            CMP #$09 ;静止绿飞龟
                            BNE #$02 ;其他敌人跳过下一条指令
                            LDA #$0E ;熟悉的修改套路
                            CMP #$14
                            BEQ #$1F
                            CMP #$33 ;交换“炮弹”和“炮台炮弹”的判断位置
                            BEQ #$17
                            CMP #$08 ;如果ID<$08
                            BCC #$04 ;跳过下面2条指令
                            CMP #$0C ;如果ID<$0C(这个判断终于有意义了)
                            BCC #$0F ;跳转到“踩死”程序


                            IP属地:上海14楼2020-01-04 18:34
                            回复
                              啊,那个,嗯,这个,记得HL去年有个顶金币踩龟壳,敌人变成半身库巴的bug,要不,也试着修复下


                              IP属地:上海来自Android客户端15楼2020-02-14 12:07
                              收起回复