将数据代码放入不同的段
汇编语言程序可以将数据,栈和代码都放到一个段里面,但是也可以将程序,栈和代码分别放到不同的段里,下图就是定义多个段的程序
assume cs:codesg,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
codesg segment
start:mov ax,stack
mov ss,ax
mov sp,20h ;设置栈顶指向stack:20
mov ax,data
mov ds,ax ;设置ds指向data段
mov bx,0
mov cx,8
s:push [bx]
add bx,2
loop s
mov bx,0
mov cx,8
s0:pop [bx]
add bx,2
loop s0
mov ax,4c00h
int 21h
codesg ends
end start
上述程序定义了三个段,分别是:
- 存放数据的数据段
- 存放堆栈数据的堆栈段
- 存放程序的代码段
上图所示的代码中有一个需要注意的地方是堆栈指针的取值,也就是
mov sp,20h
要将明白sp的取值,就不得不明白数据在内存中的存储形式,下图示字在内存中的存储,内存中存储的数据分别是:4E20H和0012H。这里要明白字和字节的换算关系:
1字节 = 8位 (1byte = 8bite)
1字 = 2字节 = 16位 (1word = 2byte = 16bite)
由上图可以知道,内存中存储4E20H这个数据时,是把4E20这个数据拆分成了两个数据进行分别存储,4E和20,并将高位存储在高地址中,把低位存储在低地址中。
回过头来,再来分析SP的值,我们可以知道在初始化时Stack中存放了如下所示的数据:
dw 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
上述数据是字型的,也就是一个数据有2个字节,因此,栈中所有的数据所占的存储空间是16*2 = 32个字节,而32是10进制数,转换为16进制也就是20H,,因此堆栈指针的初始值应该设置为20H。
and 和 or指令
为了更好的解释and和or指令,这里通过大小写字符转换的问题为例子来说明这两个指令的作用。
大小写字母的关系
要实现大小写字符的转换,那就需要知道大小写字符之间的关系是什么。我们分别列出大小写字符的ASCII码,如下表所示:
大写 | 十六进制 | 二进制 | 小写 | 十六进制 | 二进制 |
---|---|---|---|---|---|
A | 41 | 01000001 | a | 61 | 01100001 |
B | 42 | 01000010 | b | 62 | 01100010 |
C | 43 | 01000011 | c | 63 | 01100011 |
通过对比,我们可以发现。小写字母的ASCII码比都比大写字母的ASCII码要大20H,这反映在二进制上,也就是大写字母的第六位总是0,小写字母的第六位总是1,因此,要实现大小写字母的转换,只需要将小写字母的第六位变为0即可。
下面代码关于and和or的例子:
assume cs:codesg,ds:datasg
datasg segment
db 'BaSiC' ;将这里的大写字母转换为小写字母
db 'iNfOrMaTiOn' ;将这里的小写字母转换为大写字母
datasg ends
codesg segment
start:mov ax,datasg
mov ds,ax
mov bx,0
mov cx,5
s:mov al,[bx]
or al,00100000b
mov [bx],al
inc bx
loop s
mov bx,5
mov cx,11
s1:mov al,[bx]
and al,11011111b
mov [bx],al
inc bx
loop s1
mov ax,4c00h
int 21h
codesg ends
end start
用[bx+idata]的方式进行数组的处理
上述程序我们用[bx]的方式来指明一个内存单元,还可以用一种更加灵活的方式来指明内存单元:[bx+idata]表示一个内存单元,它的偏移地址为(bx)+idata。下面举一个例子说明这个语句的用法:
mov ax,[bx+200]
上述语句的含义是:将一个内存单元的内容送入ax,这个内存单元的长度为2个字节(字单元),存放一个字。偏移地址为bx中的数值加上200,段地址在ds中。
数学描述如下:
(ax) = ((ds)*16+(bx)+200)
下图所示代码是用[bx+idata]的方式进行数组的处理
assume cs:codeseg,ds:dataseg
dataseg segment
db 'BaSiC' ;大写变小写 0->1
db 'MinIX' ;小写变大写 1->0
dataseg ends
codeseg segment
start:mov ax,dataseg
mov ds,ax
mov bx,0
mov cx,5
s:mov al,[bx]
;mov al,0[bx]
or al,00100000b
mov [bx],al
;mov 0[bx],al
mov al,[bx+5]
;mov al,5[bx]
and al,11011111b
mov [bx+5],al
;mov 5[bx],al
inc bx
loop s
mov ax,4c00h
int 21h
codeseg ends
end start
添加了[bx+idata]的方式,实现了类似于C原因数组的效果,其中注释掉的代码是作为[bx+idata]的另一种表达形式。也是可以进行运行的。
SI 和 DI
si和di是8086CPU中和bx功能相近的寄存器,需要注意的是si和di不能够分成两个8位寄存器来使用。下面的三组指令实现的同样的功能
(1)mov bx,0
mov ax,[bx]
(2)mov si,0
mov ax,[si]
(3)mov di,0
mov ax,[di]
下面的代码利用si和di将字符串复制到它后面的区域中去:
assume cs:codeseg,ds:datasg
datasg segment
db 'welcome to masm!'
db '................'
datasg ends
codeseg segment
start:mov ax,datasg
mov ds,ax
mov si,0
mov cx,8
s:mov ax,[si]
;mov ax,0[si]
mov [si+16],ax
;mov 16[si],ax
add si,2
loop s
mov ax,4c00h
int 21h
codeseg ends
end start
上述代码中运用来了si实现了同bx一样的功能,但是与bx不同的一点是,si只实现以字为单位的运算,因此,虽要复制得字符串有16个字节,但是loop循环的次数却只有8次。
[bx+si]和[bx+di]
在上述的代码中,我们使用了形如[bx(si或di)]和[bx(si或di)+idata]的方式来进行寻址,这里介绍一种更为灵活的寻址方式:[bx+si]、[bx+di]
现有如下指令:
mov ax,[bx+si]
上述指令的含义是:将一个内存单元送入ax,这个内存单元的长度是2字节(字单元),存放一个字,偏移地址为bx中数值加上si中的数值,段地址在ds中。数学化表示为:
(ax) = ((ds)*16+(bx)+(si))
该指令也可以写成如下常用的形式:
mov ax,[bx][si]
下面为[bx+si指令的简单运用:
mov ax,2000h
mov ds,ax
mov bx,1000h
mov si,0
mov ax,[bx][si]
inc si
mov cx,[bx][si]
inc si
mov di,si
add cx,[bx][di]
上述代码的主要作用就是分别访问了2000:1000、2000:1001、2000:1002的地址的数据,分别将其赋值给ax,cx。
[bx+si+idata]和[bx+di+idata]
[bx+si+idata]表示一个内存单元,它的偏移地址为(bx)+si+idata。
现有人如下指令
mov ax,[bx+si+idata]
用数学表达式描述为:
(ax)=((ds)*16+(bx)+(si)+idata)
该指令通常也可以写成如下的形式:
mov ax,[bx][si].200
下面试运用[bx+si+idata]处理问题的相关程序
assume cs:codesg,ss:stacksg,ds:datasg
stacksg segment
dw 0,0,0,0,0,0,0,0
stacksg ends
datasg segment
db '1. display '
db '2. brows '
db '3. replace '
db '4. modify '
datasg ends
codesg segment
start:mov ax,stacksg
mov ss,ax
mov sp,16
mov ax,datasg
mov ds,ax
mov bx,0
mov cx,4
s0:push cx
mov si,0
mov cx,4
s:mov ax,[bx][si].3
;mov ax,[bx+3+si]
and al,11011111b
;mov [bx+3+si],ax
mov [bx][si].3,ax
inc si
loop s
add bx,16
pop cx
loop s0
mov ax,4c00h
int 21h
codesg ends
end start
上述代码的作用是将dataseg段的每个单词前的四个字母改为大写字母,程序的实现思路是采用了如下的几种方法
- 循环嵌套的方法。
- 第一层循环用于控制对哪一个单词进行寻址,用于每一个单词的字节大小均为16,因此在操作完第一个单词后,只需要将bx加16就可定位到下一个单词。
- 第二层循环用于控制对单词的各个字母进行寻址,从而能够改变到每一个单词的各个字母。
- 堆栈保存循环次数
- cx值压栈。由于控制汇编语言进行循环次数的寄存器均采用的是cx寄存器,因此,在第二层循环执行前必须将cx的值保存下来,这里采用堆栈的机制保存cx的值,在第二层循环执行前将cx的值进行压栈。
- cx值出栈。 然后为了能够使第一层循环正常运行,在第二层循环执行完一次后,需要将堆栈里的值弹出,赋值给cx,从而保证了第一层循环的正确运行。
- cx值压栈。由于控制汇编语言进行循环次数的寄存器均采用的是cx寄存器,因此,在第二层循环执行前必须将cx的值保存下来,这里采用堆栈的机制保存cx的值,在第二层循环执行前将cx的值进行压栈。
指令要处理的数据长度
8086CPU中可以处理两种长度的数据,因此,在机器指令中要指明处理的数据长度是多长的。在汇编语言中,主要存在如下几种方法来指明数据的长度:
- 通过寄存器名指明要处理的数据的尺寸
- 字操作:mov ax,1 、mov ds,ax
- 字节操作:mov al,1、mov al,bl
- 在没有寄存器名存在的情况下,用操作符X pyr指明内存单元的长度
- 字操作:mov word ptr ds:[0],1、inc word ptr [bx]
- 字节操作:mov byte ptrr ds:[0],1、inc byte ptr ds:[0]
div指令
div是除法指令,使用div的时候应该注意如下的问题:
- 除数:有8位和16位两种,在一个reg或内存单元中。
- 被除数:默认放在AX或DX和AX中,如果除数为8位,被除数则为16位,默认在AX中存放;如果除数为16位,被除数则为32位,在DX和AX中存放,DX中存放高16位,AX存放低16位
- 结果:如果除数为8位,则AL存储除法操作的商,AH存储除法操作的余数;如果除数为16位,则AX存储除法操作的商,DX存储除法操作的余数。
具体操作例子:
div byte ptr ds:[0]
含义:
(al) = (ax)/((ds)*16+0)的商
(ah)=(ax)/((ds)*16+0)的余数
div word ptr es:[0]
含义:
(ax)= [(dx)*10000H+(ax)]/((es)*16+0)的商
(ax)= [(dx)*10000H+(ax)]/((es)*16+0)的余数
伪指令dd,db,dw,dup
- db:字节型数据
- dw:字型数据
- dd:定义双字型数据
比如如下例子:
data segment
dd 100001
dw 100
dw 0
data ends
- dup:dup是一个操作符,在汇编语言中同db、dw、dd等一样,也是由编译器识别处理的符号。
比如如下例子:
db 3 dup (0)
相当于 db 0,0,0
db 3 dup (0,1,2)
相当于 db 0,1,2,0,1,2,0,1,2
使用dd能够使得程序变得更加简洁,简短。
综合例子
寻址方式在结构化数据访问中的应用
详细的程序代码:
assume cs:codeseg,ds:dataseg,es:tableseg
dataseg segment
db '1975','1976','1977','1978','1979','1980','1981','1982','1983'
db '1984','1985','1986','1987','1988','1989','1990','1991','1992'
db '1993','1994','1995'
;以上是表示21年的21个字符
dd 16,22,382,1356,2390,8000,16000,24486,50065,97479,140417,197514
dd 345980,590827,803530,1183000,1843000,2759000,3753000,4649000,5937000
;以上是表示21年公司总收入的21个dword型数据
dw 3,7,9,13,28,38,130,220,476,778,1001,1442,2258,2793,4037,5635,8226
dw 11542,14430,15257,17800
;以上表示21年公司雇员人数的21个word型数据
dataseg ends
tableseg segment
db 21 dup ('year summ ne ?? ')
tableseg ends
codeseg segment
start:mov ax,dataseg
mov ds,ax
mov ax,tableseg
mov es,ax
mov bx,0
mov si,0
mov di,0
mov cx,21
s:mov ax,[bx]
mov es:[si],ax ;先将年份的低位存入
mov ax,[bx+2]
mov es:[si+2],ax ;后将年份的高位存入
mov ax,[bx+84]
mov es:[si+5],ax ;先将总收入的低位存入
mov ax,[bx+84+2]
mov es:[si+7],ax ;后将总收入的高位存入
mov ax,[di+168]
mov es:[si+10],ax ;将雇员人数的低位存入
mov ax,[bx+84] ;将总收入的低位存入ax
mov dx,[bx+84+2] ;将总收入的高位存入dx
div word ptr ds:[di+168]
mov es:[si+13],ax
add si,16
add bx,4
add di,2
loop s
mov ax,4c00h
int 21h
codeseg ends
end start
下图是代码运行结束后内存中各个数据的存储位置
由图中可以看出:
- (1)号标号所框选的内容就是具体的年份存储的形式,因为年份在存储的时候采用的是字符存储的方式,每一个数字占一个字节,一共占了4个字节。另外,字符在计算机内存中存储时采用的是以ASCII码的形式进行存储,图中的每一个数字代码一个字节。
- (2)号标号中所框选的内容就是年份存储年份后的一个空格,空格在内存中的存储值是20H。因此,对于table列表中的其他空格存储的值都是20H。图中的第四列,第六列,第8列也都是存储的空格。
- (3)号标号中存储的是总的收入,因为总的收入需要占用4个字节,因此,也就需要四列来存储所有的收入指。其中,在进行读取的时候,要遵循高位数据存放在高位内存中,低位数据存放在低位内存中。例如,在读取最后一个数据时,正确的顺序应该是00 5A 97 68
- (5)号标号存储的内容是公司的雇员数,雇员数占2个字节,也就是图中占两列
- (7)号标号存储的内容是公司员工的人均收入,占两个字节。