前言

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

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

  1. 十六进制数均以H结尾
  2. 使用8086CPU作为案例
  3. 我们使用(地址或寄存器名称)表示一个寄存器或一个内存单元的内容,()内地址是且一定是物理地址
  4. 我们将idata视作常量
  5. 我们以reg表示一个寄存器包括ax、ah、sp、bp、si、di等,sreg表示一个段寄存器包括ds、ss、cs、es。

话不多说我们马上开始。

描述了单元长度的标号

我们之前,一直在代码段中使用标号来标记指令、数据、段的起始地址。比如,下面的代码将code段中的a标号处的8个数据累加,结果储存到b标号处的字中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
assume cs:code
code segment
a: db 1,2,3,4,5,6,7,8
b: dw 0
start:mov si,offset a
mov bx,offset b
mov cx,8
s: mov al,cs:[si]
mov ah,0
add cs:[bx],ax
inc si
loop s
mov ax,4c00H
int 21H
code ends
end start

程序中,code、a、b、start、s都是标号。这些标号仅仅表示了内存单元的地址。我们还可以使用另一种符号,这种符号不仅可以表示内存单元的地址,还表示了内存单元的长度,即表示在此标号处的单元,是一个字节单元,还是字单元,还是双字单元。上面程序还可以写成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
assume cs:code
code segment
a db 1,2,3,4,5,6,7,8
b dw 0
start: mov si,0
mov cx,8
s: mov al,a[si]
mov ah,0
add b,ax
inc si
loop s
mov ax,4c00H
int 21H
code ends
end start

在code段中使用的标号a、b后面没有”:”,它们是同时描述内存单元长度和内存地址的标号。标号a,描述了地址code:0,和从这个地址开始,以后的内存单元都是字节单元;而标号b描述了地址code:8,和从这个地址开始,以后的内存单元都是字单元。

因为这种标号包含了对单元长度的描述,所以在指令中,它可以代表一个段中的内存单元。比如,对于程序中的”b dw 0”:

指令:mov ax,b 相当于:mov ax,cs:[8]

指令中,标号b代表了一个内存单元,地址为code:8,长度为两个字节。如果使用指令mov al,b就会引起编译错误,因为b代表的内存单元是字单元。我们称这种标号为数据标号,它标记了存储数据的单元的地址和长度。

在其他段中使用数据标号

一般来说,我们不会在代码段里面定义数据,而是将数据定义到其他段中,在其他段中我们也可以使用数据标号来描述存储数据的单元的地址和长度。不过需要注意的是在后面加有“:”的地址标号,只能在代码段中使用,不能在其他段中使用。下面的程序将data段中a标号处的8个数据累加,结果存储到b标号处的字中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
assume cs:code,ds:data
data segment
a db 1,2,3,4,5,6,7,8
b dw 0
data ends
code segment
start: mov ax,data
mov ds,ax
mov si,0
mov cx,8
s: mov al,a[si]
mov ah,0
add b,ax
inc si
loop s
mob ax,4c00H
int 21H
code ends
end start

之前我们说将我们定义的段和相关的寄存器使用伪指令assume联系起来,比如上面的程序中我们就讲cs和code段,ds和data段联系在了一起。之所以这样做是因为我们后来在代码段中使用了数据标号,也就是说当我们将数据定义到其他段中后,想要在代码段使用数据标号就必须使用伪指令assume将段和段寄存器联系起来,否则编译器在编译的时候,无法确定标号的段地址在哪一个寄存器中,当然这种联系是编译器需要的,但绝对不是说,我们因为编译器的工作需要,用assume指令将段寄存器和某个段相联系,段寄存器中就会真的存放该段的地址。所以我们为了程序可以正确访问data段,我们会在代码段的开始使用指令,设置ds指向data段:

1
2
mov ax,data
mov ds,ax

我们可以将标号当做数据来定义,此时,编译器将标号所表示的地址当作数据的值,比如:

1
2
3
4
5
data segment
a db 1,2,3,4,5,6,7,8
b dw 0
c dw a,b
data ends

数据标号c处存储的两个字型数据为标号a、b的偏移地址相当于:

1
2
3
4
5
data segment
a db 1,2,3,4,5,6,7,8
b dw 0
c dw offset a,offset b
data ends

再比如:

1
2
3
4
5
data segment
a db 1,2,3,4,5,6,7,8
b dw 0
c dd a,b
data ends

数据标号c处存储的两个双字型数据为标号a、b的偏移地址和段地址相当于:

1
2
3
4
5
data segment
a db 1,2,3,4,5,6,7,8
b dw 0
c dw offset a,seg a,offset b,seg b
data ends

seg操作符功能为获取某一个标号的段地址。

直接定址表

接下来我们通过一个问题引入这个小节的学习,我们现在要编写一个子程序,以十六进制的形式在屏幕中间显示给定的字节型数据。首先一个字节需要两个十六进制数来表示,那我们可以将一个字节的高四位和低四位分开,分别用它们的值得到对应的数码字符。比如2BH,我们可以得到高四位的2和低四位的11,但我们怎么能得到对应的数码字符呢?

最简单的方法就是一个一个比较,0就是0,1就是1……10就是A,11就是B,但这样有太麻烦了,有太多的比较指令和转移指令了。我们要在0-15和0-F之间找到一种映射关系。

0-9和字符“0”-“9“的关系显而易见:数值+30H=对应字符的ASCII值,同样我们也可以知道10-15和”A“-”F“之间的对应关系:数值+37H=对应字符的ASCII值。现在我们就可以将数值转换为字符了,因为映射关系存在差异,我们要判断数值是否大于9。

虽然已经简化很多了,但人嘛,总是追求最简单,最省力的办法,因为0-15和字符“0”-“F”之间没有一致的映射关系存在,所以我们应该在它们之间建立新的映射关系。具体做法是建立一张表,表中依次存放字符“0”-“F”,我们可以通过数值0-15直接查找到对应的字符。子程序如下,其中使用al传送要显示的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
showbyte:jmp short show
table db '0123456789ABCDEF'
show: push bx
push es
mov ah,al
shr ah,1
shr ah,1
shr ah,1
shr ah,1 ;右移4位,ah中得到高四位的值
and ah,00001111b ;al中为低四位的值
mov bl,ah
mov bh,0
mov ah,table[bx] ;用高四位的值作为相对于table的偏移,取得对应的字符
mov bx,0B800H
mov es,bx
mov es:[160*12+40*2],ah
mov bl,al
mov bh,0
mov al,table[bx] ;用低四位的值作为相对于table的偏移,取得对应的字符
mov es:[160*12+40*2+2],al
pop es
pop bx
ret

可以看到我们在子程序中,使用了一个表建立了两个集合之间的关系,这样做的目的一般有以下三个:

  1. 为了算法的清晰和简洁
  2. 为了加快运算速度
  3. 为了使程序易于扩充

我们的程序通过给出的数据进行比较而得到的结果的问题,转化为用给出的数据作为查表的依据,通过查表得到结果的问题。具体的查表方法,是用查表的依据数据,直接计算出所要查找的元素在表中的位置。像这种可以通过依据数据,直接计算出所要找的元素的位置的表,我们称其为直接定址表。

程序入口地址的直接定址表

我们可以在直接定址表中存储子程序的地址,从而方便地实现不同子程序的调用。假设我们现在有四个子程序a,b,c,d,我们可以将这四个子程序的入口地址存储到一个表中,它们在表中的位置和功能号相对应。对应关系为:功能号*2=对应功能子程序在地址表中的偏移。程序如下:

1
2
3
4
5
6
7
8
9
10
11
setscreen:jmp short set
table dw a,b,c,d
set: push bx
cmp ah,3
ja sret
mov bl,ah
mov bh,0
add bx,bx
call word ptr table[bx]
sret: pop bx
ret

我们通过使用直接定址表,根据功能号调用对应子程序的做法,使程序结构清晰,便于扩充,如果我们未来想要加入一个新的功能只需要在地址表中加入它的入口地址就可以了。