通过这一点我们不仅可以比较出CALL与JMP的区别,而且也会发现CALL和INT具有一些相似之处,它也会在堆栈中保存返回地址。在这种情况下,最后一条RET指令的作用也就很清楚了:它会将保存在堆栈中的返回地址弹到IP寄存器中,使得CPU能从子程序正常返回到主程序。是不是这样呢?我们先把这个子程序执行完:
-g=10d 128 "128"是断点位置 01100001 此数字是程序所显示出来的 AX=0161 BX=0001 CX=0000 DX=6131 SP=FFFC BP=0000 SI=0000 DI=0000 DS=0A3E ES=0A3E SS=0A3E CS=0A3E IP=0128 NV UP EI PL ZR NA PE CY 0A3E:0128 C3 RET
这里用了G命令一个十分重要的功能──设置断点。如果大家在键入程序时保证没有错误,那么就不必再"单步"执行这个子程序,我们现在急于看到"RET"指令执行的过程,所以我们想使CPU连续执行这一段程序,并且在0128处停下来,因此我们使用"G"命令在0128处设了断点。
注意在"="后面的第一个地址是CPU开始执行指令的起始地址,空格后的地址才是设断点的位置。我们之所以强调用"G"命令执行程序时要使用等号明确指定起始地址就是为了和断点相区分。有关断点的更多说明将留在后面介绍。现在只需记住断点设在哪里,CPU执行到那个位置时就会停下来并显示出所有寄存器的情况,并等待用户输入新的命令。
① 注意内存地址的编排是,低地址在先,高地址在后。因此数据存储于内存中也是高字节在后。
我们继续使用"T"命令跟踪RET指令:
-t 注意堆栈指针SP寄存器恢复成FFFEH AX=0161 BX=0001 CX=0000 DX=6131 SP=FFFE BP=0000 SI=0000 I=0000 DS=0A3E ES=0A3E SS=0A3E CS=0A3E IP=0107 NV UP EI PL ZR NA PE CY 0A3E:0107 3C1B CMP AL,1B
可以看到CPU确实返回到0107处执行CMP指令
前面我们编写过一些程序用RET指令返回DOS,RET为什么会有这样的功能呢?
如果在启动DEBUG之后注意观察SP寄存器的初值,就会发现DOS将程序调入内存执行时已事先在堆栈中放入了数据,这个数据就是:
-dfffe DOS预先存于堆栈中的返回地址 0A3E:FFF0 00 00 ..
当CPU执行RET指令时,这个"0"将被弹到IP寄存器中,也就是说CPU将从CS:0处继续执行指令。那么在CS:0处有没有指令呢?我们可以把CS:0处的指令"反汇编"出来。在"-"后打入"U0"并回车:
-u0[Enter] 0A3E:0000 CD20 INT 20 0A3E:0002 FF9F009A CALL FAR [BX+9A00] 0A3E:0006 F0 LOCK 0A3E:0007 FE1D CALL FAR [DI] 0A3E:0009 F0 LOCK 0A3E:000A 4F DEC DI 0A3E:000B 038D048A ADD CX,[DI+8A04] ......
当我们用A命令输入指令时,这些指令都被DEBUG编译成机器码。我们要想通过机器码重新获得指令,就必须"Unassembled"(反汇编),这就是命令码"U"的来历。
此命令可以将内存中任意位置开始的机器码反汇编成指令,若打入"U100[Enter]",那么DEBUG将把CS:100处开始的指令显示在屏幕上。
"反"出来的指令和我们当初输入的指令应该是相同的。那些紧挨着逻辑地址后面的16进制数就是指令编译后的机器码。我们可以大概了解一些常用指令对应的机器码是什么,比如"INT"和"CD"之间的关系。
反汇编的结果真是不可思意,CS:0处竟然有一条"INT 20"指令。为什么会有这样的一条指令呢?
前面已经提到过,在我们的程序之前,DOS保留了256字节的内存空间安排了一些重要的数据,这个"CD 20"就是DOS打的小埋伏。这是因为在DOS正式成为PC机的标准操作系统之前,已有人编写了一个称为"CP/M-86"的操作系统,遗憾的是这个可怜的软件没能竞争DOS。不过 "MicroSoft"还是很重视这个对手的,因此在DOS中特意保留了CP/M的特征,以便于和CP/M-86兼容且易于将那些为CP/M编写的应用程序移植到DOS中。由此便有了这个"CD 20",也就有了前面的"RET之谜"。
讲到此我们对CALL/RET可以说已经有较完整的印象了。不过以上对这两条指令的讨论还是很粗浅的,这里面还有更多的滋味我们还没有尝到,还需要更细致地加以咀嚼。