引言
在上篇博文中,我们主要学习了两个指令and和or,它们两个目前最主要的功能就是对英文字母进行大小写转换。学习这两个指令主要是为接下来的学习做铺垫,因为接下来的编程案例中需要使用到。
那么从本片开始,就正式进行灵活寻址的学习。本篇博文主要讲解有:
1、[bx+idata]
2、SI和DI
方向已定,就马上开始本篇学习吧~
数组形式寻址
我们都知道数组,在高级语言中,例如C语言,我们是如何访问数组中的数据呢?
代码:
int n[ 10 ]; /* n 是一个包含 10 个整数的数组 */
int i,j;
/* 初始化数组元素 */
for ( i = 0; i < 10; i++ )
{
n[ i ] = i + 100; /* 设置元素 i 为 i + 100 */
}
/* 输出数组中每个元素的值 */
for (j = 0; j < 10; j++ )
{
printf("Element[%d] = %d\n", j, n[j] );
}
从代码中我们可以看到:C语言中访问数组内数据格式为:数组名[下标]。
实际上上述C代码经过编译后,其数组名:n 会变成一个地址,该地址为数组的起始地址,同时也是数组中第一个元素的地址。我们通过打印 n 和 n[0] 的地址值,就会发现他俩是同一地址。[]内的下标,我们可以看成是基于数组起始地址的偏移地址,那么访问数组内的数据格式:数组名[下标] ,就可以看成是:数组名:下标。其中数组名是段地址,其下标为偏移地址。
通过上述分析,如何在汇编中实现数组,相信大家心里就会比较明朗了~
[bx+idata]
[bx+idata]的寻址形式将会是我们学习的第一个灵活寻址。该形式主要应用于访问数据段下定义的多个段空间。其中 idata 是一个自然数常量。
我们知道,[bx] 表示为一个内存单元,[bx+idata]同样也表示为一个内存单元,那么它的偏移地址就为:bx+idata。
例如:mov ax,[bx+200],即将以DX为段地址,偏移地址为bx中的数值加上200,该处的字单元数据放入AX寄存器中。我们可以使用数学化表示:ax = ((ds)*16+(bx)+200)
该寻址形式含义上是非常简单,但是要想理解它的用途我们还需要通过编程实例来学习。
例:编程示例如下:
assume cs:code,ds:data
data segment
db 'BaSiC'
db 'MinIX'
data ends
code segment
start:
code ends
end start
编程实现将data段中定义的两条字符串,第一条字符中的英文字母转为大写,第二条字符串中的英文字母转为小写字母。
首先说明一下伪指令db,是 define byte 的缩写,即申请以字节为单位的内存空间。我们之前学习的 dw,是 define word,即申请以字为单位的内存空间。两者之间的区别就是单位长度的不同,一个是字节单位,一个是子单位。
如果,我们先通过 [bx] 去实现,那么该如何去做?
分析:
[bx] 处理该编程问题,则需要两处循环
首先偏移从0开始,也就是说第一个循环内,要先处理 ‘BaSiC’ 字符串。每次循环后偏移bx加1,访问下一个字符,循环内进行且运算将字母转为大写。
等到第一个循环结束后,此时偏移bx也就指向了第二条字符串 ‘MinIX’ 的第一个字符,那么接着第二个循环,循环内进行或运算将字母转为小写。
分析完毕后,那么实现代码如下:
assume cs:code,ds:data ; 定义代码段、数据段
data segment ; 数据段开始
db 'BaSiC' ; 申请内存空间并初始化第一条字符串
db 'MinIX' ; 申请内存空间并初始化第二条字符串
data ends ; 数据段结束
code segment ; 代码段开始
start: ; 程序指令开始
mov ax,data
mov ds,ax ; 设置数据段
mov bx,0 ; 偏移从0开始
mov cx,5 ; 循环5次
s:mov al,[bx] ; 取一个英文字符到AL寄存器中
and al,5fH ; 将英文字符转为大写
mov [bx],al ; 将大写后英文字符放入原地址下
add bx,1 ; 偏移加1
loop s ; 判断循环是否结束
mov cx,5 ; 循环5次
s1:mov al,[bx] ; 取一个英文字符到AL寄存器中
or al,20H ; 将英文字符转为小写
mov [bx],al ; 讲小写后英文字符放入原地址下
add bx,1 ; 偏移加1
loop s1 ; 判断循环是否结束
mov ax,4c00H
int 21H ; 程序返回
code ends ; 代码段结束
end start ; 源程序结束,并标明程序入口
我们将上述程序编译连接,使用Debug加载运行:
加载后我们使用D命令查看数据段中的内容,可以看到此时数据段中两条字符串是连续存放的,下面我们使用T命令进行执行,为了快速调试,遇到Loop使用P命令跳过,通过第一次循环后,查看此时数据段中的内容:
可以看到,第一个字符串 ‘BaSiC’ 已经被全部转为了大写 ‘BASIC’,那么我们接着完成第二个循环:
经过第二次循环后,我们将第二条字符串 ‘MinIX’ 全部转为了小写 ‘minix’,至此我们使用 [bx] 完成了编程题目要求。
我们现在分析:上述程序可不可以在进行简化呢?比如上述程序中需要进行两次循环,那么可不可以只进行一次循环?
分析:
我们观察到,题目中需要处理的两条字符串长度都是相等的,那么我们只需要一次循环,在该循环内同时处理这两条字符串,该如何做到?
答案当然是需要使用 [bx+idata] 的寻址形式。两条长度相等的字符串,它们唯一的区别就是起始地址不同,第一条字符串起始地址为:ds:0,第二条字符串起始地址为:ds:5。
要想在访问第一条字符串的同时,去访问第二条字符串,那么只需要在当前可变偏移 bx 的基础上加上固定偏移长度5即可。
分析完毕,那么简化后的代码如下:
assume cs:code,ds:data
data segment
db 'BaSiC'
db 'MinIX'
data ends
code segment
start:
mov ax,data
mov ds,ax
mov bx,0
mov cx,5
s:mov al,[bx] ; 取第一个字符串中的字符到AL寄存器中
and al,5fH ; 将英文字符转为大写
mov [bx],al ; 将大写后的英文字符放到原地址下
mov al,[bx+5] ; 当前偏移bx加5,取到第二个字符串中的字符到AL寄存器中
or al,20H ; 将英文字符转为小写
mov [bx+5],al ; 将小写后的英文字符放到原地址下
add bx,1 ; 可变偏移bx加1
loop s ; 判断循环是否结束
mov ax,4c00H
int 21H
code ends
end start
我们将上述程序编译连接,在Debug中加载运行如下:
运行完毕后,我们可以看到数据段中结果正确无误,说明我们使用 [bx+idata] 的寻址形式在简化代码量的同时,亦实现了编程题目要求。
现在我们结合高级语言中的一维数组,来深入分析 [bx+idata] 的深层次含义。
如果是在C语言中,那么这道编程题目该怎么写?伪C代码如下:
char a[5] = "BaSiC"
char b[5] = "MinIX"
main(){
int i = 0
while(i<5){
a[i] = a[i] & 0x5F
b[i] = b[i] | 0x20
i++
}
}
从上述伪C代码中,我们可以看到与汇编代码的相似之处:
在C语言代码中:
a[i] 可以看成 0[bx]
b[i] 可以看成 5[bx]
数组名 a、b,也就是数组的起始地址,在我们的汇编代码中,使用 idata 来表示
a --> idata --> 0
b --> idata --> 5
实际上,idata 就相当于数组的起始地址,在我们定义的数据段中,第一个数组 ’BaSiC‘ 的起始地址为0,第二个数组 ‘MinIX’ 的起始地址为5。我们访问第一个数组,同样可以写成:ds:[bx+0],0无意义可以去掉,所以访问第一个数组就是:ds:[bx]。第二个数组的起始地址为5,5有意义,所以访问第二个数组就要写成:ds:[bx+5]。如果再加上一条数组 ‘kJHuO’ ,那么该数组的起始地址为10,所以我们要想访问这条数组就要写成:ds:[bx+10]。现在我们可以得出总结,数据段中存在多个字数据段的情况下,通过改变 idata 的值,可以同时访问多个字数据段。这个也就是 [bx+idata] 寻址形式的意义所在。
bx 的小伙伴
bx作为一个通用寄存器,它的最大功能就是配合DS段寄存器实现可变地址寻址。但是,拥有相同功能的寄存器可不单单只有bx一个,下面我们就说一下bx的小伙伴都有谁把!
si、di
有时候在汇编开发中,在复杂的业务逻辑下只有一个bx可供使用往往是捉襟见肘,完全不够使用的,所以人们又只好增加了两个寄存器,来填补bx无法覆盖的空白。那么这两个寄存器就是:si 和di。
si寄存器和di寄存器再使用上和bx寄存器是等效的,即:
mov ax,[bx]
mov ax,[si]
mov ax,[di]
上述三条指令可实现相同功能
si、di 当然也可以与idata进行搭配:
mov ax,[bx+idata]
mov ax,[si+idata]
mov ax,[di+idata]
上述三条指令同样实现相同功能
除了拥有和bx寄存器相同的功能外,si、di 组合使用,才能真正的发挥除它们两个的能力。通常情况下,si、di搭配对应的段寄存器,以实现数据拷贝复制功能。为了方便我们记忆,我们可以将si寄存器中的“s”理解成:source,source的意思是“来源”,所以在数据拷贝复制场景中,si寄存器指向源地址下的数据;我们可以将di寄存器中的“d”理解成:destination,destination的意思是“终点”,所以在数据拷贝复制场景下,di寄存器指向终地址下的数据。
为了更好的理解si、di,下面就让我们通过编程实例进一步学习:
题目:使用si、di 实现将字符串“welcome to masm!”复制到它后面的数据区中,示例如下:
assume cs:code,ds:data
data segment:
db 'welcome to masm!'
db '................'
data ends
code segment:
start:
code ends
end start
现在我们开始分析该如何实现。字符串“welcome to masm!”是复制的数据来源,所以我们需要使用 si寄存器指向它,复制到的目的地址是它后面的内存空间,所以我们需要使用 di寄存器指向它后面的内存空间,即复制的目的地址。“welcome to masm!”总共16个字符,一个字符占用一个字节单元,所以 si寄存器起始值为0,di寄存器起始值为16,一次复制一个字节,共循环16次。
实现代码如下:
assume cs:code,ds:data ; 定义代码段、数据段
data segment ; 数据段开始
db 'welcome to masm!' ; 申请16个字节空间,初始值为“welcome to masm!”
db '................' ; 申请16个字节空间
data ends ; 数据段结束
code segment ; 代码段开始
start: ; 程序开始
mov ax,data ;
mov ds,ax ; 设置数据段
mov si,0 ; 复制源的数据偏移从0开始
mov di,16 ; 复制的目的地址数据偏移从16开始
mov cx,16 ; 循环16次
s:mov al,[si] ; 从复制源地址取出一个字节放入al寄存器中
mov [di],al ; 将al寄存器中的数据写入复制目的地址下
add si,1 ; 复制源地址的偏移加1
add di,1 ; 复制目的地址的偏移加1
loop s ; 判断循环是否结束
mov ax,4c00H
int 21H ; 程序返回
code ends ; 代码段结束
end start ; 源程序结束,并指明程序入口地址
现在我们编译连接,在Debug中加载执行:结果如下:
通过循环后,我们D查看空间内数据,发现我们成功将0~f区域的数据复制到了10~1f内存空间中。该示例代码主要是带领大家领会和理解si、di这两个寄存器的主要功能作用。事实上,上述代码并不是最优写法,下面我们来进行优化。
优化
优化思路很简单,我们可以看到上述复制中,由于我们使用AL寄存器来作为接收方,所以我们一次传输数据长度为一个字节,16个字节大小我们就需要循环16次才能完成复制。那么可不可以减少循环次数呢?优化的思路就是将每次传输的数据长度变成一个字,那么就需要使用AX寄存器来当作数据载体。由于每次传输一个字的长度,16个字节只需要8次传输即可完成,所以循环便减为了8次。优化后代码如下:
assume cs:code,ds:data
data segment
db 'welcome to masm!'
db '................'
data ends
code segment
start:
mov ax,data
mov ds,ax
mov si,0
mov di,16
mov cx,8 ; 循环8次
s:mov ax,[si] ; 从复制源地址取出一个字放入ax寄存器中
mov [di],ax ; 将ax寄存器中的数据写入复制目的地址下
add si,2 ; 复制源地址的偏移加2
add di,2 ; 复制目的地址的偏移加2
loop s
mov ax,4c00H
int 21H
code ends
end start
由于一个字的长度是两个字节,所以每次偏移需要加2才可。循环次数变为8次,意味着CPU减少了8次循环,少做了16次的加法逻辑运算,所以程序执行效率得到了提高。
那么下面我们执行优化后的代码看下效果是否一致:
ok,达到同等效果。
继续优化
我们继续探究,上述示例是否还可以再次进行优化?答案是当然可以,使用我们上面学到的 [bx+idata] 形式寻址。我们观察到,在上述代码示例中,由于使用到 si、di两个寄存器,所以循环中就需要对这两个寄存器进行偏移操作,如果使用 [bx+idata] 的形式,那么我们就只需要在循环中对一个寄存器bx进行偏移操作,这样又再一次减少了循环中的算术逻辑处理,提高了程序执行效率。那么优化后代码如下:
assume cs:code,ds:data
data segment
db 'welcome to masm!'
db '................'
data ends
code segment
start:
mov ax,data
mov ds,ax
mov bx,0 ; 偏移从0开始
mov cx,8 ; 循环8次
s:mov ax,[bx] ; 取当前偏移下的一个字数据放到ax寄存器中
mov [bx+16],ax ; 将ax寄存器中的数据写入当前偏移加上16个字节处的内存地址下
add bx,2 ; 当前偏移加2
loop s
mov ax,4c00H
int 21H
code ends
end start
我们通过 [bx+idata] 直接寻址到复制目的地址下,将数据写进去。这样相比之前的代码示例,我们就省去了一个寄存器,对于CPU来说又节约了很多开支。
那么我们执行优化后的代码,看看是否达到一致的效果:
ok,实现了相同效果,证明我们优化方案是正确的。
思考
那么我们思考这么一个问题,通过上述不同优化方案的学习和探究,小伙伴们可能会产生疑惑:既然 [bx+idata] 的方式寻址在程序执行效率上要比使用 si、di的形式要高,那么我们为什么还要学习 si、di呢?
答案就是 [bx+idata] 存在比较大的局限性。
已知8086PC机中,偏移范围为:0000H~FFFFH。如果此时bx的值为 FFFEH,idata值为:2,那么 bx+idata 则超过了偏移规定的最大值,会发生程序异常。所以局限性就体现在:我们在开发中就要明确且清楚的知道 bx+idata 不会超过FFFFH,这样才可安然使用 [bx+idata]。
而使用si、di,由于它们两个是相互独立存在,所以不会存在该问题。而且在数据拷贝复制上,寻址会比 [bx+idata] 更加的灵活。si、di存在也是对bx的一个补充,使更加灵活的寻址方式实现提供了可能。
本篇结束语
在本篇博文中,我们主要学习了 [bx+idata] 灵活寻址,以及认识了两个bx的小伙伴:si、di。通过对编程实例的优化方案探究,学习了 [bx+idata] 和si、di之间的区别和功能。
那么在下篇博文中,我们将继续学习更多的灵活寻址形式,期待一下吧~
感谢围观,转发分享请标明出处,谢谢!