汇编语言学习笔记(七):更灵活的定位内存地址的方法

学习参考:汇编语言(第2版)王爽

  • and 和 or 指令
  • SI 和 DI
  • [bx+si] 和 [bx+di]
  • [bx+si+idata] 和 [bx+di+idata]
  • 不同的寻址方式的灵活应用

and 和 or 指令

1.and 指令:逻辑与命令,按位进行与运算

1
2
mov al,01100011B
and al,00111011B

执行后:al=00100011B

通过该指令可将操作对象的响应位设为 0,其他位不变

1
2
3
and al,10111111B    ; 将 al 的第 6 位设为 0 
and al,01111111B ; 将 al 的第 7 位设为 0
and al,11111110B ; 将 al 的第 0 位设为 0

2.or 指令:逻辑或指令,按位进行或运算

1
2
mov al,01100011B
or al,00111011B

执行后:al=01111011B

通过该指令可将操作对象的响应位设为 1,其他位不变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
or al,01000000B    ; 将 al 的第 6 位设为 1 
or al,10000000B ; 将 al 的第 7 位设为 1
or al,00000001B ; 将 al 的第 0 位设为 1


# [bx+idata]

> 表示一个内存单元,它的偏移地址为 (bx)+idata (bx 中的数值加上 idata)

# 用 [bx+idata] 的方式进行数组的处理

举个例子:在数据段定义两个字符串,第一个字符串变大写,第二个字符串小写,数据段定义如下
```asm
assume cs:codesg,ds:datasg

datasg segment
db 'BaSiC'
db 'iNfOrMaTiOn'
datasg ends

codesg segment
start:
codesg ends

end start

这里为了用已经学到的指令来完成任务,所以我们还需要进行一些分析,下面是 ASCII 表的一部分

经过观察可以发现,大写字母与小写字母的 ASCII 码的 16 进制数差 20H,(十进制差 32),然后再观察二进制数,除第 5 位(位数从 0 开始计算)外,大写字母和小写字母的其他各位都一样的。所以大写字母要变成小写字母,只需要让第 5 位变 1 即可,小写字母要变成大写字母,只需要让第 5 位变 0 即可。那么如何变呢?当然是用刚学过的 or 和 and 指令。

完整程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
assume cs:codesg,ds:datasg

datasg segment
db 'BaSiC'
db 'MinIX'
datasg ends

codesg segment
start:
mov ax,datasg
mov ds,ax ; 设置 ds 指向 dtasg 段

mov bx,0 ; 设置 (bx)=0,ds:bx 指向 'BaSic' 的第一个字母

mov cx,5 ; 因为有 5 个字母,所以设置循环 5 次
s: mov al,[bx] ; 将 ASCII 码从 ds:bx 所指向的单元中取出
and al,11011111B ; 将 al 中的 ASCII 码的第 5 位置为 0,变为大写字母
mov [bx],al ; 将转变后的 ASCII 码写回原单元
inc bx ; (bx) 加 1,ds:bx 指向下一个字母
loop s

mov bx,5 ; 设置 (bx)=5,ds:bx 指向 'MinIX' 的第一个字母

mov cx,5 ; 因为有 5 个字母,所以设置循环 5 次
s0: mov al,[bx]
or al,00100000B ; 将 al 中的 ASCII 码的第 5 位置为 1,变为小写字母
mov [bx],al
inc bx
loop s0

mov ax,4c00h
int 21h

codesg ends

end start

现在,我们有了 [bx+idata] 的方式,就可以用更渐变的方法来完成上面的程序。观察datasg 段中的两个字符串,一个的起始地址为0,另一个的起始地址为5。我们可以将这两个字符串看作两个数组,一个从0地址开始存放,另一个从5 开始存放。那么我们可以用[0+bx]和[5+bx]的方式在同一个循环中定位这两个字符串中的字符。在这里,0 和5 给定了两个字符串的起始偏移地址,bx 中给出了从起始偏移地址开始的相对地址。这两个字符串在内存中的起始地址是不一样的,但是,它们中的每一个字符,从起始地址开始的相对地址的变化是相同的。改进的程序如下.

1
2
3
4
5
6
7
8
9
10
11
   mov bx,0

mov cx,5
s: mov al,[bx] ; 定位第一个字符串
and al,11011111B
mov [bx],al
mov al,[bx+5] ; 定位第二个字符串
or al,00100000B
mov [bx+5],al
inc bx
loop s

程序也可以写成下面的样子:

1
2
3
4
5
6
7
8
9
10
11
   mov bx,0

mov cx,5
s: mov al,0[bx]
and al,11011111B
mov 0[bx],al
mov al,5[bx]
or al,00100000B
mov 5[bx],al
inc bx
loop s

便于理解,用 C 语言来描述上面的程序,大致程序如下:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int main()
{
char a[6] = "BaSiC";
char b[6] = "MinIX";
int i = 0;
do {
a[i] = a[i] & 0xDF; // 0xDF = 11011111
b[i] = b[i] | 0x20; // 0x20 = 00100000
} while (i < 5);
return 0;
}

SI 和 DI

si 和 di 是 8086CPU 中和 bx 功能相近的寄存器,si 和 di 不能分成两个 8 位寄存器来使用

[bx+si] 和 [bx+di]

[bx+si] 和 [bx+di] 的含义相似,我们以 [bx+si] 为例讲解

[bx+si] 表示一个内存单元,它的偏移地址为 (bx)+(si) (即 bx 中的数值加上 si 中的数值)

指令 mov ax,[bx+si] 的含义如下:

将一个内存单元的内容送入 ax,这个内存单元的长度为 2 字节(字单元),存放一个字,偏移地址为 bx 中的数值加上 si 中的数值,段地址在 ds 中。

数字化描述为:(ax)=((ds)*16+(bx)+(si))

该指令也可以写成如下格式(常用):

1
mov ax,[bx][si]

[bx+si+idata] 和 [bx+di+idata]

[bx+si+idata] 和 [bx+di+idata] 的含义相似,我们以 [bx+si] 为例讲解

[bx+si+idata] 表示一个内存单元,它的偏移地址为 (bx)+(si)+idata(即 bx 中的数值加上 si 中的数值)

指令 mov ax,[bx+si] 的含义如下:

将一个内存单元的内容送入 ax,这个内存单元的长度为 2 字节(字单元),存放一个字,偏移地址为 bx 中的数值加上 si 中的数值再加上 idata,段地址在 ds 中。

数字化描述为:(ax)=((ds)*16+(bx)+(si)+idata)

该指令也可以写成如下格式(常用):

1
2
3
4
5
mov ax,[bx+200+si]
mov ax,[200+bx+si]
mov ax,200[bx][si]
mov ax,[bx].200[si]
mov ax,[bx][si].200

不同的寻址方式的灵活应用

通过比较前面用到的几种定位内存地址的方法(可称为寻址方式),可以发现:

  • [idata] 用一个常量来表示地址,可用于直接定位一个内存单元
  • [bx] 用一个变量来表示内存地址,可用于间接定位一个内存单元
  • [bx+idata] 用一个变量和常量表示地址,可在一个起始地址的基础上用变量间接定位一个内存单元
  • [bx+si] 用两个变量表示地址
  • [bx+si+idata] 用两个变量和一个常量表示地址

Q: 编程,将 datasg 段中每个单词改写为大写字母

1
2
3
4
5
6
7
8
9
10
11
12
assume cs:codesg,ds:datasg

datasg segment
db 'ibm '
db 'dec '
db 'dos '
db 'vax '
datasg ends

codesg segment
start:
codesg ends

A: 类似这种问题,我们最自然能想到的就是使用二重循环来实现,那么在汇编中如何实现二重循环呢?

如果要使用二重循环的话,那么思路应该是这样的:

  • 首先定义外层循环,用来扫描每一行
  • 接着定义内存循环,用来扫描每一列
  • 将访问到的数据取出,做位与运算

处理过程大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
    r=第一行地址
mov cx,4

s0: c=第一列地址

mov cx,3
s: 改变 r 行,c 列的字母为大写
c=下一列地址
loop s

r=下一行的地址
loop s0

那么,再次根据这个大致过程,我们再来分析一下,我们知道,要使用 loop ,那么就需要用 cx 来保存循环次数,可是这里就有一个矛盾的地方,如果 cx 记录了外层循环的次数,那么内层循环怎么办?或者说记录内层的循环次数,外层的又怎么办?

这里可以提供一个思路,我们或许可以用寄存器,来暂时保存 cx 的值,比如我是用 dx 来暂时保存外层循环的次数,当内层循环执行完过后,再将 dx 的值还给 cx。可是这样有一个问题,如果 dx 在内部被使用了怎么办?我们只有 14 个寄存器,数量有限,显然这种方式在某些情况下不合适

那么再提供一个思路,我们能否在数据段内定义一个块内存单元,用来保存外层 cx 的值,比如再进行内层循环之前,执行这种操作 mov ds:[40H],cx ,需要使用的时候再从内存单元中恢复 `mov cx,ds:[40H]。似乎也可行,但是这种做法却很麻烦,如果你需要保存多个数据的话,那么就需要记住数据放到了哪个单元,这样程序容易混乱。

综合上述分析过后,一般来说,在需要暂存数据的时候,我们都应该使用栈

那么,再次改进我们的程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
assume cs:codesg,ds:datasg,ss:stacksg

datasg segment
db 'ibm '
db 'dec '
db 'dos '
db 'vax '
datasg ends

stacksg segment
dw 0,0,0,0,0,0,0,0
stacksg 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,3 ; cx 设置为内层循环的次数

s:
mov al,[bx+si]
and al,11011111b
mov [bx+si],al
inc si
loop s

add bx,16
pop cx ; 从栈顶弹出原 cx 的值,恢复 cx
loop s0 ; 外层循环的 loop 指令将 cx 中的计数值减 1

mov ax,4c00h
int 21h

codesg ends
end start
Author: Inno Fang
Link: http://innofang.github.io/2017/11/24/%E6%B1%87%E7%BC%96%E8%AF%AD%E8%A8%80%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0%EF%BC%88%E4%B8%83%EF%BC%89%EF%BC%9A%E6%9B%B4%E7%81%B5%E6%B4%BB%E7%9A%84%E5%AE%9A%E4%BD%8D%E5%86%85%E5%AD%98%E5%9C%B0%E5%9D%80%E7%9A%84%E6%96%B9%E6%B3%95/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-ND 4.0 unless stating additionally.