栈机制
8086CPU提供入栈和出栈指令,最基本的两个是PUSH和POP。下面一段指令的执行过程
mov ax,0123H
push ax
mov bx,2266H
push bx
mov cx,1122H
push cx
pop ax
pop bx
pop cx
以上指令先将0123H上的数据通过寄存器ax来push进栈中,再将2266H的数据push进栈中,接着将1122H中的数据push进栈中,最后再依次pop。
我们将10000H~10000FH这段内存当做栈来使用,那么CPU如何知道栈顶的位置?显然也应该有相应的寄存器来存放栈顶的地址,8086CPU中有两个寄存器,段寄存器SS与寄存器SP,栈顶的段地址存放在SS中,偏移地址存放在SP中。任意时刻,SS:SP指向栈顶元素。所以push ax的执行,其实完成了以下两部
- SP=SP-2,SS:SP指向当前栈顶前面的单元,以当前栈顶前面的单元为新的栈顶
- 将ax中的内容送入SS:SP指向的内存单元处。
但是这里还有一个问题:SS和SP只记录了栈顶的地址,但如何保证在栈操作的时候,栈顶不会超过栈空间?8086CPU没有帮助我们解决这个问题,我们在编程的时候要自己操心栈顶越界的问题。
栈段
上面我们将10000H~10000FH这段内存当做栈来使用,我们可以根据需要,将一组内存单元定义为一个段。将一段内存当做栈段,仅仅是我们在编程时的一种安排,CPU不会因为这种安排,就在执行push、pop等操作指令的时候将我们定义的栈段当做栈空间来访问。
我们需要做的,就是:
- 对于数据段,将它的段地址放在DS中
- 对于代码段,将它的段地址放在CS中,将段中第一条指令的偏移地址放在IP中
- 对于栈段,将它的段地址放在SS中,将栈顶单元的偏移地址放在SP中。
一段内存,可以既是代码的存储空间,又是数据的存储空间,还可以是栈空间,关键在于CPU中寄存器的设置,即CS、IP、SS、SP、DS的指向。
第一个程序
以下就是一个简单的汇编语言源程序
assume cs:codesg
codesg segment
mov ax,0123H
mov bx,0456H
add ax,bx
add ax,bx
mov ax,4c00H
int 21H
codesg ends
end
在汇编语言中,包含两种指令,一种是汇编指令,一种是伪指令,比如XXX segment和XXX ends,由编译器执行的指令,编译器根据编译器在进行相关的编译工作。
那么它是怎么运行的呢,我们在DOS(一个单任务系统)的基础上,简单讨论以下。
一个程序P2在可执行文件中,则必须由一个正在运行的程序P1,将P2从可执行文件加载入内存后,将CPU的控制权交给P2,P2才能运行。P2开始运行后,P1暂停运行。
那么程序返回是怎么做的呢,应该在程序末尾添加返回的程序段
mov ax,4c00H
int 21H
这两条指令所实现的功能就是程序返回。
编译源程序
在mac上可以使用自带的nasm来编译。
[BX]
[bx]和[0]有点类似,[0]表示内存单元,它的偏移地址是0。用[0]表示一个内存单元时,0表示单元的偏移地址,段地址默认在ds中。
比如下列指令(在Debug中使用)
mov ax,[0]
将一个内存单元的内容送入ax,这个内存单元的长度为2字节。存放一个字节,偏移地址为0,段地址在ds中。
mov al,[0]
将一个内存单元的内容送入al,这个内存单元的长度为1字节。存放一个字节,偏移地址为0,段地址在ds中。
要完整描述一个内存单元,需要两种信息:内存单元的地址和内存单元的长度。用[0]表示时,0表示单元的偏移地址,段地址默认在ds中,单元的长度可以由具体指令中的其他操作对象(比如寄存器)指出。
[bx]也同样表示一个内存单元,它的偏移地址在[bx]中,比如下面指令:
mov ax,[bx]
为了表达简洁,接下来我们用(ax)来表示ax中的内容。比如
((ds)*16+(bx))
可以理解为ds中的adr1为段地址,bx中的adr2作为偏移地址,内存adr1:adr2单元的内容。
那么
mov ax,[bx]
就可以改写为
(ax) = ((ds)*16+(bx))
当我们想操作其中的内容,比如自增,可以使用inc
inc bx
我们在Debug中写入过指令mov ax,[0],表示把ds:0的数据送入ax中。但以后我们约定符号idata来表示常量,比如mov ax,[idata]就代表mov ax,[1]、mov ax,[2]、mov ax,[3]。
段前缀
指令mov ax,[bx]中,内存单元的偏移地址为bx给出,而段地址默认在ds中,也可以显示给出内存单元的段地址所在的段寄存器。
比较一下汇编源程序中以下指令的含义
mov al,[0]含义为(al)=0,将常量0送入al中(与mov al,0含义相同)。
mov al,ds:[0]含义为(al)=((ds)*16+0),将内存单元中的数据送入al中。
mov al,[bx] 含义为(al)=((ds)*16+(bx)),将内存单元中的数据送入al中。
mov al,ds:[bx] 含义与mov al,[bx]相同。这里的ds:就是段前缀。
loop指令
loop指令的格式是:loop标号,CPU执行loop指令的时候,要进行两步操作:1. (cx) = (cx) - 1;2.判断cx中的值,如果不为零则转至标号处执行,如果为零则向下执行。
比如要计算2的11次方
assume cs:code
code segment
mov ax,2
mov cx,11
s: add ax,ax
loop s
mov ax,4c00h
int 21h
code ends
end
这里以cx中的值为准进行循环。
[bx]和loop的联合应用
考虑这样一个问题,计算ffff:0~ffff:b单元中的数据的和,结果存储在dx中。
我们先分析下,运算后的结果是否会超出dx所能存储的范围?ffff:0~ffff:b内存单元中的数据是字节型数据,为8位2进制,范围在0~255之间,12个这样的数相加,结果不会大于65535,可以在dx中存下。
我们能否将ffff:0~ffff:b中的数据直接累加到dx中?
因为ffff:0~ffff:b中的数据是8位的,不能直接加到16位寄存器中。
我们能否将ffff:0~ffff:b中的数据累加到dl中,并设置(dh=0),从而实现累加到dx中?
这也不行,因为dl是8位寄存器,能容纳的数据范围在0~255之间,很可能造成进位丢失。
所以这里有两个问题:类型的匹配和结果的不超界。
目前的方法暂时只有用一个16位寄存器来做中介,我们会这样来写
assume cs:code
code segment
mov ax,0ffffh
mov ds,ax 设置(ds)=ffffh
mov bx,0 初始化ds:bx指向ffff:0
mov dx,0 初始化累加器dx, (dx)=0
mov cx,12 初始化循环计数器寄存器cx
s: mov al,[bx]
mov ah,0
add dx,ax 向dx中加入ffff:0单元的数值
inc bx ds:bx指向下一个单元
loop s
mov ax,4c00h
int 21h
code ends
end