TA的每日心情 | 慵懒 2015-3-19 14:35 |
|---|
签到天数: 4 天 连续签到: 1 天 [LV.2]偶尔看看I
龙骑伍长
 
- 积分
- 323
|
第三节:这条路不对,我要绕过去
本章附件:68000指令参考手册
68000.pdf (822.2 KB) 68000.pdf (822.2 KB)
下载次数: 24
2007-9-20 13:55
我们经常会碰到走到岔路路的情形,生活中如此,RPG游戏中也如此,在看过攻略或撞过墙以后,我们肯定会知道到底该往哪条分支路线走。这些决策都是在我们脑中完成的,那怎样让68K CPU完成类似的操作,让我们的程序流程按着一定的条件走我们想走的分支路线呢?简单的概括就是一个比较和跳转的过程。
在这一节中,我会穿插强化一下上一节讲的内容,所以请务必看熟上两小节内容,至少是大部分内容。
MOVE指令的各种形式:
MOVE指令有很多其他形式,比如MOVEA, MOVEM, MOVEP和MOVEQ,它们对于数据的传送方式各有不同,编译后生成的二进制机器码也不同,一般在编译时,编译器会选择合适的形式,但有些时候你必须在写代码的时候就按情况手工指定(编译器毕竟不如人那么万能)。。
===
MOVEA是复制一个地址值到地址寄存器:
语法:movea address,a0-a7
举例:
movea #$FF0000,a0 ;执行后,a0里存储的就是$FF0000,以后用(a0)就可以读出FF0000地址里的数据
movea (a0)+,a5 ;执行后,把FF0000里存的一个数复制到a5,然后a0自增
movea d0,a3 ;把d0里的值复制到a3
注意我之前讲过的,不要去用a7(并不是不能用,只是在你了解它用途前最好先不要动它,我之后会讲解他的实际用途)
===
插入一个知识点:Label,中文一般译作标签
和basic还有c语言差不多,他的格式类似于:
.....
bra Label ;跳转到下面的Label处,然后继续执行,如果是用过C,那你可以理解为goto语句
.....
Label:
.....
就是这样,Label没有什么太多可说的,但你在之后的汇编开发中经常要用到。
===
跳转指令(本节重点开始了):
刚刚在讲Label的例子时用到了bra,他的全称是branch,就是无条件跳转
实际上跳转指令还有很多,因为他们打头都为B,所以一般被称为Bcc,cc就是以一个68000状态寄存器的值来进行跳转的条件
比如有:
BEQ Branch if equal 如果相等则跳转
BNE Branch if not equal 如果不相等则跳转
具体的下一节会详细讲解,不过仅根据这两个我们就可以看到,实际这些跳转指令和他们的英文全称只见很容易联系起来,因而数量虽然比较多但也不会太难记。
不过撇开这些有条件的跳转,我们先来仔细看看无条件跳转,无条件跳转有两种:
BRA 这个就是我们刚才Label例子里用的,一个短跳转
BRA的参数是一个16位的偏移量,也就是说刚才例子里那个bra Label实际对程序来说只是bra #$????(此处????是一个16进制数)
实际CPU执行时遇到BRA #$FF就往后跳过FF个字节(256字节)的地址然后执行那个地址里的指令
bra.b的跳转范围在-127...127,bra.w的跳转范围在-32 768...32 768
JMP 这个是长跳转,和BRA的区别就是他的参数是一个32位的绝对地址,也就是可以从$00000000一直到$FFFFFFFF
远大于Genesis的实际内存范围,所以用这个指令可以在Genesis系统的任意部分跳来跳去,也就可以做很多很强的事情
比如说,把卡带ROM里的一部分代码解压到系统内存,然后一个JMP,跳到内存的指定地址开始执行
最后再一个JMP跳会卡带ROM的范围继续执行卡带里的程序等等
这不是瞎想出来的,Sonic3里就有这样的代码,另外在Sega Game Can这个Sega CD游戏中,那些小游戏也是这样处理的。
你肯定会有疑问,JMP既然可以在全部范围内跳转,为什么不都用JMP来写代码?关键就在于生成的代码大小,因为参数是32位的,所以就编译后生成的机器码来说,JMP的要比BRA多将近一半,对于容量有限的卡带,字节数就是金钱,就是成本,区别就在这里。
比较了BRA和JMP,不得不提另一组B和J打头的指令,BSR和JSR:
BSR Branch to SubRoutine 跳转到子程序,如果你写过basic,那很好理解,就是GoSub了,它比BRA多做了一步,就是在跳转前先保留下一条指令的地址,然后让CPU去执行那段分支程序,一旦遇到rts指令,就返回到那条保留好的地址继续执行。这样就相当去调用了一段子程序后,返回原先的调用点,继续执行下去。
JSR Jump to SubRoutine 看过BRA和JMP的区别,不难猜到这条指令的含义吧。没错,就是接受参数的不同,BSR范围有限,而JSR可以在全机的整个内存访问范围内跳转。
例子:
jsr $FFFFFFF0
jsr Label
jsr a0
搞清楚这两组功能类似的B打头和J打头的指令还是必要的,尤其是在对rom做一些小hack的时候,插入或修改了一些自己的代码以后,你必须搞清改用B跳还是J跳引导原程序走到你写的代码上去。搞错了,执行是会出错的。
===
a7寄存器派什么用?
上一节我说了不要随便动a7寄存器,那他派什么用处呢?刚才那段BSR的介绍里里我提到了BSR比BRA多了一步保存返回地址的操作,这个返回地址必然是保存在一个内存区域里,这个区域叫做栈,随便找本汇编书都可以把栈的概念讲得很透彻,但我还是简单概括一下。栈就像一卷用来放硬币牛皮纸小卷(见过新包装好的一卷卷的1元硬币吗,没见过的话去超市问问超市阿姨。。。),硬币一个一个堆叠进去的话,如果不用暴力,那么你想取出硬币时必须再一个一个取出来,最后放进去的最先取出,也就是所谓的后进先出。
而a7这个寄存器就是用来保存栈顶地址指针的寄存器(你可以想象成那卷硬币的开口处),每存入一个数据前,a7里的数据自减一次。举例来说,系统初始时,a7指向$1000000,这是Genesis的内存末端,我们执行了一个涉及栈操作的指令后(比如BSR),某个longword型数据要入栈,那么a7=a7-4,a7就变成了$FFFFFC,然后把那个数据写入(a7)也就是写入$FFFFFC,这样入栈过程就完成了,要出栈时(比如BSR所跳到的SubRoutine结束,执行rts时),只要直接读取(a7),然后a7=a7+4就可以了。根据入栈和出栈数据的长度不同,a7的自增或自减的数也不同。
那么a7到底可不可以改动呢?当然你硬要改也是可以的,毕竟一卷硬币你用刀把中间划开也是可以从中取出硬币的。但请记得在改动a7以前,把他的值保存在内存的某个你指定的地方,以便恢复,否则栈一旦乱了,你的所有程序很有可能都会乱套(99.99%的程序在执行时要用到栈空间来存放临时数据)。
===
先前讲完了无条件跳转,那肯定还要讲有条件跳转
那有一个问题就是条件的来源是什么?
条件在高级语言中一般就是if之后的括号里的东西,比如
if (var1>0) then {do something;}
这里var1>0就是条件,如果var1比0大,那么条件为真,如果var1小于等于0那么条件为假
这个比较过程在汇编里不能直接用XXX>YYY来写,而要用cmp也就是compare来做比较:
movea.l var1,a0 ;把var1这个地址复制到a0
cmp.b (a0),0 ;比较a0地址里的数,也就是var1地址处的数和0的大小,比较方法是用后一个数减前一个数
ble.w Label1 ;如果关系是小于,也就是说后一个数小于前一个,就跳到Label1处去
;这里我用ble.w,因为我假设中间省略的代码大于127,用ble.b不够,实际应用中,如果距离短,也可用ble.b
......
Label1:
......
var1:
dc.b $4 ;定义一个数字等于4,因为它紧接着标签var1,所以它的地址实际就是var1
上面这段代码有新内容,也有我们已经学过的
先看最后一行这个dc.b,他表示define code,in byte也就是手工定义一个字节的机器码,自然也就可以作为手工定义数据的方法,我们定义了$4在这个内存区域。你也可以用dc.w,dc.l定义word和longword数据,更可以用dc.b 'SEGA'的方式定义四个字节的字符串。
第二行的cmp.b的操作是把后一个操作数减去前一个操作数,然后把一定的结果保存下来(保存在哪里下一节讲),然后第三行的条件跳转指令ble读取这个保存下来的结果,然后根据情况决定是否跳转。
整个代码的逻辑就比较清楚了 ,这一例中,它的执行顺序和结果是:先把var1这个地址复制到a0,然后将0减去var1这个地址里的值4,发觉是小于关系,符合ble的小于则跳转的条件,然后CPU就跳到Label1处开始执行代码了。
其他所有条件跳转的机制和ble是一样的,只是条件内容本身不太一样而已,下一节讲完那个保存比较结果的东西之后,我就会详细列出所有条件跳转指令,大家也可以先下载附件里的68000指令参考手册第10页自己翻翻,如果能看懂,那是再好不过了。
======
这一节重要的内容基本都结束了,那就顺便把开头时还没讲的几个move指令也讲掉吧。
===
MOVEQ和MOVE没啥大区别,它的意思是move quick,执行速度比较快,但每次只能复制1字节
例如:
moveq #0,d0
执行后,清空了整个d0寄存器,因为当你用moveq复制数据到寄存器时,他会置整个寄存器的位
如果你用move.l,完成同样的工作就需要更多的代码空间。
与MOVEQ类似的还有ADDQ和SUBQ,这两个请查阅附件里的68000指令参考手册自行学习(你可以用Ctrl+F搜索addq和subq,很快就能找到)。
===
MOVEM指令,即move mutiple,这个可能比较复杂些,但也不是太难懂。
他是一个批量复制的操作,我直接举些例子吧,这样更好懂。
MOVEM.L D0-D7/A0-A6,$1234 ;把d0,d1,d2,d3,d4,d5,d6,d7,a0,a1,a2,a3,a4,a5,a6寄存器里的内容
;按longword型存放到$1234开始的内存地址中去
;就是依次存放到$1234,$1238,$123C.......中去
MOVEM.L (A5),D0-D2/D5-D7/A0-A3/A6
MOVEM.W (A7)+,D0-D5/D7/A0-A6
MOVEM.W D0-D5/D7/A0-A6,-(A7)
以上三个请自己理解一下,其实MOVEM的特点就是一个参数为寄存器列表,另一个参数为单个的内存地址,然后把寄存器列表的数据依次存到单个内存地址开始的区域,或是从这个区域复制数据依次放到寄存器列表。
那它有什么实际应用呢?
一个最好的应用就是在进入SubRoutine时保存现场(就是保存所有寄存器状态),然后在这个SubRoutine返回前恢复现场。
例子,大家看一下这段有问题的代码:
move.b #$10,d0
bsr Subroutine
move.b d0,d1
...
Subroutine:
...
moveq #0,d0
moveq #0,d1
moveq #0,d2
...
rts
在这个代码里,有一个Subroutine子程序,他用到了d0,而在进入这个子程序前,d0里是保存着东西的,假设这个东西并不想让Subroutine更改,而是想在Subroutine执行后再使用的,那么这段代码就不符合要求,Subroutine把d0改为了0,并且返回时没有恢复d0的原有数值$10,使得外层代码在以后用到d0时获得了错误的数据,继而可能引发错误的结果,甚至当机。
那在寄存器有限的情况下,如何解决这种情况?
1.你可以打电话给M公司,大骂他们设计cpu时留了太少的寄存器。。。。但如果他们的客服比腾逊客服的态度更好些的话,因该会告诉你,你现在在看网页的这台电脑的cpu,寄存器也并不比68000多太多。。。。。实际上14个32位寄存器对于16bit的游戏机来说足够多了。。。
2.从你的程序道具箱里调出MOVEM,然后选择“使用”。(无限次道具,免装备 :目)
move.b #$10,d0
bsr MovemSubroutine
move.b d0,d1
...
MovemSubroutine:
movem d0-d2,-(sp) ;sp和a7是等价的,你也可以写成movem d0-d2,-(a7)
...
moveq #0,d0
moveq #0,d1
moveq #0,d2
...
movem (sp)+,d0-d2
rts
这样在Subroutine的开始处,对自己要用到的寄存器内容先进行了保存,然后在结束返回前进行了恢复,这样就不会影响到外部程序了。
MOVEM的参数组合可能性实在太多了,更多使用情况参照附件里的68000指令参考手册35页
今天就先写到这里,下一次讲解条件比较的结果到底存放在哪个“宝箱”里,还有就是所有的Bcc条件跳转指令
[SONIC3D 2007年9月20日13点50分]
|
|