汇编学习教程:灵活寻址(二)

引言

在上篇博文中,我们主要学习了两个指令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之间的区别和功能。

那么在下篇博文中,我们将继续学习更多的灵活寻址形式,期待一下吧~

感谢围观,转发分享请标明出处,谢谢!

  • 7
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值