上一篇博客,我已经简单的介绍汇编语言的一些常用的知识,同时也写了一个简单的程序。这篇博客,我会带着大家写一些循环的程序,废话不多说,直接开始。
[bx]是什么呢?和[0]有些类似,[0]表示内存单元,它的偏移地址是0。要完整的描述一个内存单元,需要两种信息:内存单元的地址;内存单元的长度(类型) [BX]表示的意思就是偏移地址是BX寄存器中的值。
为了描述上的简洁,在以后的博客中,我们将使用一个描述性的符号()来表示一个寄存器活做一个内存单元中内容。
约定符号idata表示常量
看一看下面指令的功能
mov ax,[bx]
功能:bx中存放的数据作为一个偏移地址EA,段地址SA 默认在ds中,将SA:EA处的数据送入ax中。即(ax)=((ds)*16+(bx))
mov [bx],ax
功能:bx中存放的数据作为一个偏移地址EA,段地址SA 默认在ds中,将ax中的数据送入到SA:EA处的中。即((ds)*16+(bx))=(ax)
看下下面的问题,写出程序执行后,21000H~21007H单元中的内容
mov ax,2000H mov ds,ax mov bx,1000H mov ax,[bx] inc bx inc bx mov [bx],ax inc bx inc bx mov [bx],ax inc bx mov [bx],al inc bx mov [bx],al
先看一下程序的前3条指令:
mov ax,2000H mov ds,ax mov bx,1000H
这三条指令执行后,ds=2000H,bx=1000H
接下来,第4条指令:
mov ax,[bx]
指令执行前:ds=2000H,bx=1000H,则mov ax,[bx]将内存 2000:1000处的字型数据送入ax中。该指令执行后,ax=00beH。
接下来,第5、6指令:
inc bx inc bx
这两条指令执行前bx=1000H,执行后bx=1002H。
接下来,第7条指令:
mov [bx],ax
指令执行前:ds=2000H,bx=1002H,则mov [bx],ax 将ax中数据送入到内存 2000:1002处中。该指令执行后,2000:1002单元的内容为BE,2000:1003单元的内容为00。
接下来,第8、9条指令
inc bx inc bx
这两条指令执行前bx=1002H,执行后bx=1004H。
接下来,第10条指令
mov [bx],ax
指令执行前:ds=2000H,bx=1004H,则mov [bx],ax 将ax中数据送入到内存 2000:1004处中。该指令执行后,2000:1004单元的内容为BE,2000:1005单元的内容为00。
接下来,第11条指令
inc bx
这条指令执行前bx=1004H,执行后bx=1005H。
接下来,第12条指令
mov [bx],al
指令执行前:ds=2000H,bx=1005H,则mov [bx],ax 将ax中数据送入到内存 2000:1005处中。该指令执行后,2000:1005单元的内容为BE。
接下来,第13条指令
inc bx
这条指令执行前bx=1005H,执行后bx=1006H。
接下来,第14条指令
mov [bx],al
指令执行前:ds=2000H,bx=1006H,则mov [bx],ax 将ax中数据送入到内存 2000:1006处中。该指令执行后,2000:1006单元的内容为BE。
最终的情况如下:
接下来我们验证一下,走来先写汇编的代码,具体的如下:
我们可以看到CS=073F IP=0100,程序执行是从CS:IP开始的,所有我们写的指令从这儿开始即可,如下:
我们的代码已经书写完成了,这个时候需要修改我们内存中的数据,如下:
已经修改完成了,这个时候需要运行我们的代码,如下:
我们再来看下内存中的数据,如下:
可以发现我们的结果是对的
loop指令的格式是:loop标号,CPU执行loop指令的时候,要进行两步操作,(cx)=(cx)-1;判断cx中的值,不为零则转至标号处执行程序,如果为零则向下执行。通常cx中存放循环次数。
举例:编程计算2^12。
分析:212=2*2*2*2*2*2*2*2*2*2*2*2,若设(ax)=2,可计算(ax)=(ax)*2*2*2*2*2*2*2*2*2*2*2*2,最后(ax)中为212的值。N*2可以用N+N实现,程序如下:
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
接下来我要对上面的代码,进行相应的解释。
标号
在汇编语言中,标号代表一个地址,上面的程序一个标号s,它实际上标识了一个地址,这个地址处有一条指令:add ax, ax
Loop s
对应的两次操作,第一步(cx)=(cx)-1。第二步判断cx中的值,不为0则转至标号s所标识的地址处执行,如果为零则执行下一条命令
以下3条指令
mov cx,11 s:add ax,ax loop s
执行loop s的时候,首先要将(cx)减1,然后若(cx)不为0,则向前转至s处执行add ax,ax。所以,可以利用cx来控制add ax,ax的执行次数。
我们可以书写对应的程序,然后再debug中单步跟踪一下,我们我现在vim中写出上面的代码,然后编译,连接,最后通过debug运行,具体的如下:
可以看到我们的loop后面的是0006,存的是add ax,ax的指令的地址,这个时候我们单步跟踪一下,具体的如下:
可以发现执行到loop 0006的时候,cx寄存中值变成了10,然后再去执行add ax,ax也验证我们上面说的理论,继续看减到0的时候,具体的如下:
可以看到这儿CX已经等于1了,然后到了执行loop 0006指令,这个时候,cx中值减1就变成了0,这个时候不会执行add ax,ax指令了,这个也证明了前面的理论了。
虽然我们在上一节简单的介绍了一下怎么用debug来跟踪程序,但是讲的不够详细,这节就好好讲一下。先来看一个例子。
计算ffff:0006单元中的数乘以3,结果存储在dx中。我们先来分析一下
运算后的结果是否会超过dx所能存储的范围?
ffff:0006单元中的数是一个字节型的数据,范围在0~255之间,则用它和3相乘结果不会大于65535,可以在dx中存放下。
用循环累加来实现乘法,用哪个寄存器进行累加?
将ffff:0006单元中的数赋值给ax,用ds进行累加。先设(dx)=0,然后做3次(dx)=(dx)+(ax)
ffff:0006单元是一个字节单元,ax是一个16位寄存器,数据的长度不一样,如何赋值?
注意:我们说的是“赋值”,就是说,让ax中的数据的值(数据的大小)和ffff:0006单元中的数据的值(数据的大小)相等。8位数据01H和16位数据0001H的数据长度不一样,但它们的值是相等的。
那么我们如何赋值?设ffff:0006单元中的数据是xxh,若要ax中的值和ffff:0006单元中的相等,ax中的数据应为00xxh。所以,若实现ffff:0006单元向ax赋值,应该令(ah)=0,(al)=(ffff6h)。
想清楚上面的问题后,就可以编写的代码了,具体如下:
assume cs:code code segment mov ax,0ffffh mov ds,ax mov bx,6 ;以上,设置ds:bx指向ffff:6 mov al,[bx] mov ah,0 ;以上,设置(al) = ((ds*16)+(bx)),(ah)=0 mov dx,0 ;累加寄存器清0 mov cx,3 ;循环3次 s: add dx,ax loop s ;以上累加计算(ax)*3 mov ax,4c00h int 21h ;程序返回 code ends end
注意:在汇编源程序中,数据不能以字母开头,所以这儿写出了0fffh。
这儿我们对上面的程序进行编译和连接,然后查看寄存器中内容,具体的如下:
看汇编指令,具体的如下:
单步跟踪,cx不等于0的时候
单步跟踪,cx等于0的时候
但是这儿有个问题,就是如果我们的循环的次数过多的话,如果我们也确定没有存的情况下,那么单步跟踪不是要很多次,比较麻烦。有没有一步跳过的方法。具体的指令如下:
我们可以用p的指令,直接执行完循环,同时还有别的指令,如下:
同时我们可以使用g 要执行代码的位置,这样我们就可以直接执行到对应的代码的位置。
我们在Debug中写过如下的指令
mov ax,[0]
表示将ds:0处的数据送入ax中
但是在汇编源程序中,指令 mov ax,[0]被编译器当做指令 mov ax,0处理。
目前的解决办法:可将偏移地址送入bx寄存器中,用[bx]的方式来访问内存单元。但是这样写也过于麻烦。我们可以显示给出段寄存器,具体的如下:
mov ax,ds:[0]
计算ffff:0~ffff:b单元中的数据的和,结果存储在dx中。我们先来分析一下
运算后的结果是否会超过dx所能存储的范围?
ffff:0ffff:b单元中的数据是字节型数据,范围在0255之间,12个这样的数据相加,结果不会大于65535,可以在dx中存放下。
我们能否将ffff:0~ffff:b中的数据直接累加到dx中?
当然不行,因为ffff:0~ffff:b中的数据都是8位的,不能直接加到16位寄存器dx中。
我们能否将ffff:0~ffff:b中的数据累加到dl中,并设置(dh)=0,从而实现累加到dx中?
这也不行,因为dl是8位寄存器,能容纳的数据的范围在0255之前,ffff:0ffff:b中的数据也都是8位,如果仅向dl中累加12个8位数据,很有可能造成进位丢失。
我们到底怎样将ffff:0~ffff:b中的8位数据,累加到16位寄存器dx中?
从上面的分析中,可以看到,这里面有两个问题:类型的匹配和结果的不超界。具体的说,就是在做加法的时候,我们有两种方法:
(dx)=(dx)+内存中的8位数据;
(dl)=(dl)+内存中的8位数据。
第一种方法中的问题是两个运算对象的类型不匹配,第二种方法中的问题是结果有可能超界。
那我们如何解决?目前的方法就是得用一个16位寄存器来做中介。将内存单元中的8位数据赋值到一个16位寄存器ax中,再将ax中的数据加到dx上,从而使两个运算对象的类型匹配并且结果不会超界。
于是我们开始编程,具体的代码如如下:
assume cs:code code segment mov ax,0ffffh mov ds,ax; 设置(ds)=fffh mov dx,0;初始化累加寄存器,(dx)=0 mov al,ds:[0] mov ah,1;(ax)=((ds)*16+0)=(ffff0h) add dx,ax;向dx中加上ffff:0单元的数值 mov al,ds:[1] mov ah,0;(ax)=((ds)*16+1)=(ffff1h) add dx,ax;向dx中加上ffff:1单元的数值 mov al,ds:[2] mov ah,2;(ax)=((ds)*16+2)=(ffff2h) add dx,ax;向dx中加上ffff:2单元的数值 mov al,ds:[3] mov ah,3;(ax)=((ds)*16+3)=(ffff3h) add dx,ax;向dx中加上ffff:3单元的数值 mov al,ds:[4] mov ah,4;(ax)=((ds)*16+4)=(ffff4h) add dx,ax;向dx中加上ffff:4单元的数值 mov al,ds:[5] mov ah,5;(ax)=((ds)*16+5)=(ffff5h) add dx,ax;向dx中加上ffff:5单元的数值 mov al,ds:[6] mov ah,6;(ax)=((ds)*16+6)=(ffff6h) add dx,ax;向dx中加上ffff:6单元的数值 mov al,ds:[7] mov ah,7;(ax)=((ds)*16+7)=(ffff7h) add dx,ax;向dx中加上ffff:7单元的数值 mov al,ds:[8] mov ah,8;(ax)=((ds)*16+8)=(ffff8h) add dx,ax;向dx中加上ffff:8单元的数值 mov al,ds:[9] mov ah,9;(ax)=((ds)*16+9)=(ffff9h) add dx,ax;向dx中加上ffff:9单元的数值 mov al,ds:[a] mov ah,a;(ax)=((ds)*16+a)=(ffffah) add dx,ax;向dx中加上ffff:a单元的数值 mov al,ds:[b] mov ah,b;(ax)=((ds)*16+b)=(ffffbh) add dx,ax;向dx中加上ffff:b单元的数值 mov ax,4c00h;程序返回 int 21h code ends end
代码是写出来了,但是太长了,有没有什么办法将它改成循环的方式,于是有了下面的代码
assume cs:code code segment mov ax,0ffffh mov ds,ax mov bx,0 ;初始化ds:bx指向ffff:0 mov dx,0 ;初始化累加寄存器,(dx)=0 mov cx,12 ;初始化循环计数寄存器cx,(cx)=12 s:mov al,[bx] mov ah,0 add dx,ax ;间接向dx中加上((ds)*16+bx)单元的值 inc bx ;ds:bx指向下一个单元 loop s mov ax,4c00h;程序返回 int 21h code ends end
在实际编程中,经常会遇到,用同一种方法处理地址连续的内存单元中的数据访问问题。我们需要用循环来解决这类问题,同时我们必须能够在每次循环的时候按照同一种方法来改变要访问的内存单元的地址。这时,就不能用常量来给出内存单元的地址,而应用变量。mov al,[bx]中的bx就可以看作一个代表内存单元地址的变量,我们可以不写新的指令,仅通过改变bx中的数值,改变指令访问的内存单元。
指令 mov ax,[bx]中,内存单元的偏移地址由bx给出,而段地址默认在ds中。我们可以在访问内存单元的指令中显示第给出内存单元的段地址所在的段寄存器。
用于显式地指明内存单元的段地址的ds:,cs:,ss:,es:
在汇编语言中称为段前缀。
在8086模式中,随意想一段内存空间写入内容是很危险的,因为这段空间中可能存放着重要的系统数据或代码。
将内存ffff:0ffff:b单元中的数据复制到0:2000:20b单元中。分析一下:
0:2000:20b单元等同于0020:00020:b单元,它们描述的是同一段内存空间
复制的过程应用循环实现,简要描述如下:
初始化:x=0
循环12次:将ffff:x单元中的数据送入0020:x(需要用一个寄存器中转)X=X+1
在循环中,源始单元ffff:X和目标单元0020:X的偏移地址X是变量。我们用bx来存放。
将0:2000:20b用0020:00020:b描述,就是为了使目标单元的偏移地址和源始单元的偏移地址从同一数值0开始。
程序的代码如下:
assume cs:code code segment mov bx,0 ;(bx)=0,偏移地址从0开始 mov cx,12 ;(cx)=12,循环12次 s:mov ax,0ffffh mov ds,ax ;(ds) = 0ffffh mov dl,[bx] ;(dl)=((ds)*16+(bx)),将ffff:bx中的数据送入dl mov ax,0020h mov ds,ax ;(ds) = 0020h mov [bx],dl ;((ds)*16+(bx)) = (dl),将中dl的数据送入0020:bx inc bx ;(bx)=(bx)+1 loop s mov ax,4c00h int 21h code ends end
因源始单元ffff:x和目标单元0020:x相距大于64KB,在不同的64KB段里,上面的程序,每次循环要设置两次ds。这样做是正确的,但是效率不高。我们可以使用两个段寄存器分别存放源始单元ffff:x和目标电源0020:x的段地址,这样就可以省略循环中需要重复做12次的设置ds的程序段。改进后代码如下:
assume cs:code code segment mov ax,0ffffh mov ds,ax ;(ds)=0ffffh mov ax,0020h mov es,ax ;(es)=0020h mov bx,0 ;(bx)=0,此时ds:bx指向ffff:0,es:bx指向0020:0 mov cx,12 ;(cx)=12,循环12次 s: mov dl,[bx] ;(dl)=((ds)*16+(bx)),将ffff:bx中的数据送入dl mov es:[bx],dl ;((es)*16+(bx))=(dl),将中dl的数据送入0020:bx inc bx ;(bx)=(bx)+1 loop s mov ax,4c00h int 21h code ends end
这样我们就使用的两个寄存器来完成对应的内容的复制。同时也使用的段前缀。
前面提到的安全的空间,只有256个字节,如果我们的字节数超过了这个长度,该怎么办?在操作系统的环境中,合法地通过操作系统取得的空间都是安全的,因为操作系统不会让一个程序所用的空间和其他程序以及系统自己的空间相冲突。在操作系统允许的情况下,程序可以取得任意容量的空间。
程序取得所需空间的方法有,在加载程序的时候为程序分配,再就是程序在执行的过程向系统申请。
编程计算以下8个数据的和,结果存在ax寄存器中:
0123h、0456h、0789h、0abch、0defh、0fedh、0cbah、0987h
在前面的内容中,我们都是累加某些内存单元中的数据,并不关心数据本身。可现在要累加的就是给定了数值的数据。我们可以将它们一个一个地加到ax寄存器中,但是,我们希望可以用循环的方法来进行累加,所以在累加前,要将这些数据存储在一组地址连续的内存单元中。
但是我们去哪找这些连续的空间呢?这个时候就应该让系统为我们分配。我们可以在程序中,定义我们希望处理的数据,这些数据就会被编译、连接程序作为程序的一部分写到可执行文件中。当可执行文件中的程序被加载入内存时,这些数据也同时被加载入内存中。
于是我们写出如下的代码:
assume cs:code code segment dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h mov bx,0 mov ax,0 mov cx,8 s:add ax,cs:[bx] add bx,2 loop s mov ax,4c00h int 21h code ends end
解释一下,程序第一行中的 dw
的含义是定义字型数据。dw 即 define word在这里,使用dw定义了8个字型数据(数据之间以逗号分隔),它们所占的内存空间的大小为16个字节。这些数据存在什么地方呢?因为用dw定义的数据处于代码段的最开始,所以偏移地址为0,这8个数据就在代码段的偏移0、2、4、6、8、A、C、E处。程序运行时,它们的地址就是CS:0、CS:2、CS:4、CS:6、CS:8、CS:A、CS:C、CS:E。所以上面的程序bx每次加上2。我们还是编译连接查看一下,具体的如下:
汇编的指令如下:
但是有个问题,就是CS:IP并没有指向我们要执行的第一条指令,这个时候要运行程序需要修改我们的IP的值,但是这样太麻烦了。有没有什么好用的方法,于是我们修改对应的汇编程序,具体的如下:
assume cs:code code segment dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h start:mov bx,0 mov ax,0 mov cx,8 s:add ax,cs:[bx] add bx,2 loop s mov ax,4c00h int 21h code ends end start
上面的代码我们就加了start,这个时候我们需要弄懂就是end的作用,end出了通知编译器程序结束为,还可以通知编译器程序的入口在什么地方。这儿的意思就是 mov bx,0是程序的第一条指令。
小结:
我们若要CPU从何处开始执行程序,只要在源程序中用 end 标号 指明就可以了。
我们修改原来的代码,编译连接,然后在debug中查看,具体的如下:
可以看到我们的IP地址已经改变了,指向mov bx,0指令了。
利用栈,将下面定义的好的数据逆序存放。
dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h
大致的思路就是:程序运行是,定义的数据存放在cs:0~cs:F单元中,共8个字单元。依次将这8个字单元中的数据入栈,然后再依次出栈到这8个字单元中,从而实现数据的逆序存放。
于是我们写出如下的代码:
assume cs:codesg codesg segment dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 ;用dw定义16个字型数据,在程序加载后,将取得16个字的内存空间,存放这16个数据。在后面的程序中将这段空间当作栈来使用 start: mov ax,cs mov ss,ax mov sp,30h ;将设置栈顶ss:sp指向cs:30 mov bx,0 mov cx,8 s: push cs:[bx] add bx,2 loop s ;以上将代码段0~15单元中的8个字型数据依次入栈 mov bx,0 mov cx,8 s0: pop cs:[bx] add bx,2 loop s0 ;以上依次出栈8个字型数据到代码段0~15单元中 mov ax,4c00h int 21h codesg ends end start ;指明程序的入口处在start处
上面的代码其实可以修改,空间可以利用的少点,就是定义8个字型的数据,然后栈顶指向CS:20,你们可以想一想,我这儿就不贴代码了。
在前面的程序中用到了数据和栈,将数据、栈和代码都放在了一个段里面。但是这样就比较容易混乱。前面的代码长度也没有超过64K所以放在一个段中没有问题。于是我们需要用多个段来存放数据、代码和栈。于是我们将之前的代码改改,放在不同的段中,具体的代码如下:
assume cs:code,ds:data,ss:stack data segment dw 0123h,0456h,0789h,0abch,0defh,0fedh,0cbah,0987h data ends stack segment dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 stack ends code segment start: mov ax,stack mov ss,ax mov sp,20h ;设置栈顶ss:sp指向stack:20 mov ax,data mov ds,ax ;ds指向data段 mov bx,0 ;ds:bx指向data段中的第一个单元 mov cx,8 s: push [bx] add bx,2 loop s ;以上将data段中的0~15单元中的8个字型数据依次入栈 mov bx,0 mov cx,8 s0: pop [bx] add bx,2 loop s0 ;以上依次出栈8个字型数据到data段的0~15单元中 mov ax,4c00h int 21h code ends end start
解释:
定义多个段的方法
这点,我们从程序中可明显地看出,定义一个段的方法和前面所讲的定义代码段方法没有区别,只是对于不同的段,要有不同的段名
对段地址的引用
程序中有多个段了,如何访问段中的数据?当然通过地址,而地址是分为两部分,即段地址和偏移地址。在程序中,段名就相当于一个标号,它代表了段地址。所以指令mov ax,data
的含义就是将名称为data的段的段地址送入ax.一个段中的数据的段地址可由段名代表,偏移地址就要看它在段中的位置了。程序中data
段中的数据0abch
的地址就是:data:6
代码段、数据段、栈段完全是我们的安排
本篇博客主要介绍了汇编中的循环的使用,同时也定义了多个段,也定义了数据,越来越有高级语言的味道了。