前言

从0开始的汇编语言系列,选用的参考书籍是清华大学出版社,王爽老师的《汇编语言第四版》。该系列属于博主的笔记系列,文中会采用一些书中的例子,图片以及思考题供读者阅读,如需详细学习汇编语言可以购入一本,谢谢。

学习之前我们做如下约定(随着学习深入还会出现新的约定):

  1. 十六进制数均以H结尾
  2. 使用8086CPU作为案例
  3. 我们使用(地址或寄存器名称)表示一个寄存器或一个内存单元的内容,()内地址是且一定是物理地址
  4. 我们将idata视作常量

话不多说我们马上开始。

AND和OR指令

今天的开场比较直接,我们要学习一下and和or指令,因为我们想有一种更为灵活的定位内存地址的方法和相关的编程方法,话不多说开干!

我们来看一下and指令,and指令是逻辑与指令,按位进行与运算,例如:

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

这两个指令运行后的结果是(AL)=00100011B,可以通过and指令设定操作对象的相应位为0,其余位不变。

比如AND AL,10111111B 设置了AL的第6位为0。AND AL,11111110B设置了AL的第0位为0。

之后我们来看一下or指令,or指令是逻辑或指令,按位进行或运算,例如:

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

这两个指令运行后的结果是(AL)=00111011B,可以通过and指令设定操作对象的相应位为1,其余位不变。

比如OR AL,01000000B 设置了AL的第6位为0。OR AL,00000001B设置了AL的第0位为0。

以字符的形式给出数据

在上一篇我们定义了很多数据,但这些数据都是数字,在我们编程的过程中肯定会有用到字符的时候,所以在汇编语言中我们用’……’的方式指明数据是以字符的形式给出的,编译器将这些数据转化为对应的ASCII码。

1
2
3
4
5
6
7
8
9
10
11
12
assume cs:code,ds:data
data segment
db 'unIX'
db 'foRK'
data ends
code segment
start: mov AL,'a'
mov BL,'b'
mov ax,4c00H
int 21H
code ends
end start

上述代码中,db’ unIX’相当于“db 75H,6EH,49H,58H”。这样我们就以字符的形式给出了一组数据。

大小写转换问题

现在考虑这个问题,我们定义两个段,分别是codesg和datasg,我们在datasg中声明两个字符串’BaSiC’和’iNfOrMaTiOn’,我们要把第一个字符串转化为大写,第二个字符串转化为小写。那么我们要如何操作呢?

我们先来分析一下,我们知道在ASCII码标准中小写字母的ASCII码比大写字母的ASCII码大20H,所以只要大写字母减去20H,小写字母加上20H就可以实现大小写字母的转化。知道这点后,我们只要将第一个字符串中的小写字母转化为大写字母,第二个字符串的大写字母变小写字母就可以了,所以我们的程序还要能判断那个字母是大写的那个是小写的。OK!就这么干。呃呃,等会儿,这里有人知道怎么判断一个字母是大写还是小写嘛。。。。呃呃,这可有点难办。我们还没学怎么去判断呢。。那看来我们要从新找一个规律了。

二进制 十六进制 大写字符 二进制 十六进制 小写字符
01000001 41 A 01100001 61 a
01000010 42 B 01100010 62 b
01000011 43 C 01100011 63 c
01000100 44 D 01100100 64 d
01000101 45 E 01100101 65 e
01000110 46 F 01100110 66 f
01000111 47 G 01100111 67 g
01001000 48 H 01101000 68 h
01001001 49 I 01101001 69 i
01001010 4A J 01101010 6A j
01001011 4B K 01101011 6B k
01001100 4C L 01101100 6C l
01001101 4D M 01101101 6D m
01001110 4E N 01101110 6E n
01001111 4F O 01101111 6F o
01010000 50 P 01110000 70 p
01010001 51 Q 01110001 71 q
01010010 52 R 01110010 72 r
01010011 53 S 01110011 73 s
01010100 54 T 01110100 74 t
01010101 55 U 01110101 75 u
01010110 56 V 01110110 76 v
01010111 57 W 01110111 77 w
01011000 58 X 01111000 78 x
01011001 59 Y 01111001 79 y
01011010 5A z 01111010 7A z

我们掏出来我们的小对照表,我们再来分析分析。从十六进制的角度我们已经走不通了,那我们来看看二进制吧!

我们观察到大写字母的二进制ASCII码的第五位一定是0,小写字母的二进制ASCII码的第五位一定是1,而且大小写字母的二进制ASCII码也只有这个区别除了这个第五位,其他位都是一样的,所以我们只要将数据的第五位变成1就可以将大写字母变成小写字母,同样我们只要把数据的第五位变成0就可以将小写字母变为大写字母,那如何将一个二进制数据的某一位变成1或者0呢?当然是AND和OR啦!我们来看具体代码:

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
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]
and AL,11011111B
mov [bx],AL
INC bx
loop S
mov bx,5
mov cx,11
S0: mov AL,[bx]
or AL,00100000B
mov [bx],AL
INC bx
loop S0
mov ax,4c00H
int 21H
codesg ends
end start

[bx+idata]

相信小伙伴们,知道[bx]的含义后,肯定也能猜到[bx+idata]的含义,没错[bx+idata]也代表一个内存单元,它的偏移地址是(bx)+idata。我们举个例子,mov ax,[bx+200],这条指令的含义是将一个内存单元中的内容送入寄存器AX中,这个内存单元长度为2个字节,偏移地址为寄存器BX中的数值加上200,段地址在ds中,物理地址为(ds)×16+(bx)+200。当然了我们更习惯这样写:mov ax,[200+bx]、mov ax,200[bx]或者mov ax,[bx].200。

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

相信仔细研究过高级语言中数组的小伙伴们看到这个标题就已经隐隐约约的能体会到[bx+idata]如何进行数组的处理的了,那我们来好好的讨论一下到底是怎么进行数组处理的。我们稍微改一下问题条件,这次我们提供的两个字符串是’BaSiC’和’MinIX’,也是将他们分别转变成大写字符串和小写字符串。

我们来分析分析,首先字母’B’的地址是datasg:0,而字母’M’的地址是datasg:5,恰好这两个字符串还都是五个字符,所以我们可以这样做,来看具体代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
assume cs:codesg,ds:datasg
datasg segment
db 'BaSiC'
db 'MinIX'
datasg ends

start: mov ax,datasg
mov ds,ax
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
mov ax,4c00H
int 21H
codesg ends
end start

我们在写一个C语言的来对比一下,体会一下用[bx+idata]的方式进行数组的处理,来看代码:

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

C语言:a[i],b[i] 汇编语言:0[bx],5[bx]

相信大家经过对比可以体会到两种语言的相似之处,它们都有起始地址和偏移地址。[bx+idata]的方式为高级语言实现数组提供了便利机制。

[bx+si]和[bx+di]

寄存器si和di是8086CPU中和bx功能相近的寄存器,si和di不能被分成两个8位寄存器使用。通过之前的学习我们可以使用[bx(si或di)]和[bx(si或di)+idata]的方式来指明一个内存单元,我们还有更灵活的方式:[bx+si]和[bx+di]。我们举个例子:mov ax,[bx+si],这条指令的含义是将一个内存单元中的内容送入寄存器AX中,这个内存单元长度为2个字节,偏移地址为寄存器BX中的数值加上寄存器si中的数值,段地址在ds中,物理地址为(ds)×16+(bx)+(si)。当然了我们更习惯这样写:mov ax,[bx][si]。

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

终极版本,我们可以使用[bx+si+idata]和[bx+di+idata]指定内存单元,我们举个例子:mov ax,[bx+si+200],这条指令的含义是将一个内存单元中的内容送入寄存器AX中,这个内存单元长度为2个字节,偏移地址为寄存器BX中的数值加上寄存器si中的数值再加上200,段地址在ds中,物理地址为(ds)×16+(bx)+(si)+200。当然了我们更习惯这样写: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。

总结不同寻址方式

我们比较一下前面用到的几种定位内存地址的方法,就可以发现:

1.[idata]用一个常量表示地址,这样可以用来直接定位一个内存单元。

2.[bx]用一个变量表示内存地址,这样可以用来间接定位一个内存单元。

3.[bx+idata]用一个变量和常量表示地址,这样可以用来在一个起始地址的基础上用变量间接的定位一个内存单元。

4.[bx+si]用两个变量表示地址。

5.[bx+si+idata]用两个变量和一个常量表示地址。

我们创建两个场景体验不同的寻址方法带来的便利。

[bx+idata]

我们来思考这个问题,现在我们声明6个单词,我们的任务是将单词的第一个字母变为大写字母:

1
2
3
4
5
6
7
8
9
assume cs:codesg,ds:datasg
datasg segment
db '1. file '
db '2. edit '
db '3. search '
db '4. view '
db '5. options '
db '6. help '
datasg ends

每个单词占用16个字节大小,不足的位置使用空格添补,这6个单词因为是连续存储进去的,可以将这6个单词看成一个6行16列的二位数组。按照要求,我们需要修改每一个单词的第一个字母,即二维数组的每一行的第4列。那我们完全可以使用[bx+idata]来定位这一列,使用BX+16来换行。我们来看具体代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
assume cs:codesg,ds:datasg
datasg segment
db '1. file '
db '2. edit '
db '3. search '
db '4. view '
db '5. options '
db '6. help '
datasg ends

codesg segment
start: mov ax,datasg
mov ds,ax
mov bx,0
mov cx,6
s: mov AL,[bx+3]
and AL,11011111B
mov [bx+3],AL
add bx,16
loop s
mov ax,4c00H
int 21H
codesg ends
end

[bx+si]

我们再思考这个问题,现在我们再声明4个字符串,我们的任务是把所有字母都变为大写字母:

1
2
3
4
5
6
7
assume cs:codesg,ds:datasg
datasg segment
db 'ibm '
db 'dec '
db 'dos '
db 'vax '
datasg ends

每个字符串占用16个字节大小,不足的位置使用空格添补,这4个字符串因为是连续存储进去的,可以将这4个字符串看成一个4行16列的二位数组。按照要求,我们需要修改每一个行的前三列,即二维数组的每一行的前三列。那我们可以使用[bx+si]来定位我们用si定位列,bx定位行,进行一次嵌套循环,我们来看一下具体代码:

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
assume cs:codesg,ds:datasg
datasg segment
db 'ibm '
db 'dec '
db 'dos '
db 'vax '
datasg ends

codesg segment
start: mov ax,datasg
mov ds,ax
mov bx,0
mov cx,4
s0: mov si,0
mov cx,3
s: mov AL,[bx+si]
and AL,11011111B
mov [bx+si],AL
inc si
loop s
add bx,16
loop s0
mov ax,4c00H
int 21H
codesg ends
end

不知道大家会不会感觉哪里不太对,没错这个代码的逻辑是错误的,我们在两个循环中使用了一个循环计数器,这就导致了我们在内部循环中将外部循环的数值,多一个计数器又不可能,因为loop指令默认的计数器是cx,怎么办?

我们可以选择使用另一个寄存器来存储外部循环的计数,但万一别的寄存器也被用了呢?万一没有多余的寄存器给我暂存这种数据呢?所以使用另一个寄存器的方案不太可行,那我们可不可以使用内存来存储这个数据呢?我们只需要开辟一段内存空间来存储这个数据就好,只需要在数据段添加一句“dw 0”。

问题虽然被解决了但如果我们要同时保存多个这样的数据呢?那我们就要记住数据到底放到了哪个单元中,这太麻烦了,我们怎么优化一下这个方案呢?想想我们学到现在能使用的好像只有栈了,没错,一般来讲,我们在需要暂存数据的时候,我们都应该使用栈。这样我们只需要使用PUSH指令和POP指令就可以很方便存取数据。我们看一下最后改进好的程序:

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
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,0
mov bx,0
mov cx,4
s0: push cx
mov si,0
mov cx,3
s: mov AL,[bx+si]
and AL,11011111B
mov [bx+si],AL
inc si
loop s
add bx,16
pop cx
loop s0
mov ax,4c00H
int 21H
codesg ends
end