汇编语言(2)

栈机制

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

猜你喜欢

转载自blog.csdn.net/sysuzhyupeng/article/details/78308757