ITEEDU

2.3 回到DOS的怀抱

2.3节导航:第一页 第二页

通过图2-4可以看出,整个程序由两层循环构成。内层循环由"LOOP"指令控制完成,外层循环由"JMP"完成。这两层循环有着极大的差别,CPU执行LOOP指令时根据CX减量结果控制转移,这称为条件转移;而执行JMP指令时没有任何条件需要CPU判断。因此,用JMP完成的外层循环是"死"的,没有出口。我们要想返回操作系统,首先要解决"死循环"问题。这是很容易做到的,显而易见,用LOOP指令代替JMP指令,把程序改成PROG1-A那样,外层循环不就"活"了吗?

PROG1-A
0B01:0100 mov cx,800
0B01:0103 in al,61
0B01:0105 xor al,2
0B01:0107 out 61,al
0B01:0109 mov cx,500
0B01:010C loop 10C
0B01:010E loop 103
0B01:0110 

真能如我们所想的吗?如果用G命令执行这个新程序,那么后果仍然是"灾难性"的。让我们仔细分析这个程序:

第一条指令在CX中放了一个数字800H作为外层循环的计数值,也就是说CPU在首次执行最后一行的LOOP时CX中应该是800才对。可是不要忘记,程序中还有个内层循环,它同样用到了CX寄存器。事实上在执行到第五条指令时CX寄存器中的800就已经不复存在了。而且退出内循环后CX已经是0,最后一个LOOP指令已经失去了正确的计数值。

看来问题有点棘手,内循环破坏了外循环的计数值,怎么办?

问题并不难解决。很容易想到只要在进入内循环之前把CX寄存器中的数据做个副本,在退出内循环后恢复CX寄存器中的数据就可以了。程序PROG1-B就是依据此想法改进的。我们新增加了两条指令:

助记符:PUSH/POP
用 途:将寄存器或存存储单元中的16位数据压入/弹出"堆栈"
格 式:PUSH/POP 寄存器(16bit)
PUSH/POP 存储器(16bit)

执 行:PUSH指令使"堆栈"中存入了寄存器或存储单元中数据的副本,POP指令会使"堆栈"中最顶端的数据出栈并进入给出的寄存器或存储单元中

"堆栈"是内存中一段连续的存储单元,它主要用于临时存放数据。堆栈在内存中的位置可以是任何一段空闲存储单元,它的段地址由SS寄存器指示,而数据存放的位置由堆栈指针SP寄存器指示。那么堆栈段在应用上与一段存储段有何差别呢?

① 堆栈中保存的数据都是16位的,我们把16位的数据称为"字(WORD)"。8位数据无法用PUSH指令放入堆栈;
② 其次,和程序不一样,堆栈的起始于内存"高地址"位置,这可以通过R命令观察到。进入DEBUG之后使用R命令查看各个寄存器的值,可以看到指令指针IP寄存器指向偏移0100H,而"堆栈指针"SP寄存器则指向0FFEEH。
③ PUSH进栈的数据总是从高地址向低地址方向排列,每执行一次PUSH指令,SP寄存器就会自动减2,同时数据存入SP指向的位置。也就是说,只要不人为修改SP寄存器,那么SP将永远指向最后一个进入堆栈的数据;
④ 和PUSH指令相反,POP指令会把SP寄存器指向的数据取到指定寄存器中,同时SP会自动加2。所以,POP指令总是取出堆栈最后一个数据,即堆栈具有"后进先出"的性质。

图2-6 DEBUG状态下的堆栈设置

PROG1-B
0B1D:0100 MOV CX,0800
0B1D:0103 PUSH CX
0B1D:0104 IN AL,61
0B1D:0106 XOR AL,02
0B1D:0108 OUT 61,AL
0B1D:010A MOV CX,0500
0B1D:010D LOOP 010D
0B1D:010F POP CX
0B1D:0110 LOOP 0103
0B1D:0112 

 图2-6表示了进入DEBUG后代码和堆栈的位置。为了更好地说明堆栈的特性,我们用DEBUG编一小段程序:

C:\ASM\>DEBUG[Enter]
-a100[Enter]
0B01:0100 mov ax,1234
0B01:0103 mov bx,abcd
0B01:0106 push ax
0B01:0107 push bx
0B01:0108 pop ax
0B01:0109 pop bx
0B01:010A [Enter] 

输入这些内容

为了能够看到程序执行的细节情况,我们先学习DEBUG的一个新命令--T。T命令主要用于"跟踪"(TRACE)程序,这是DEBUG最重要的功能。利用T命令,我们可以一次执行一条指令,每执行一条指令后DEBUG都会把所有的寄存器内容列出来,这样即可看到指令执行的结果。我们现在跟踪这个程序。