【汇编】汇编系列五 - 汇编程序

一、汇编语言

使用汇编语言编写一个完整的程序,步骤大致如下:

  • 编写源代码,文件名拓展名为.asm
  • 编译、链接(可以使用微软的MASM编译器)
  • 调试、运行

1.1. 语言组成

汇编语言由2类指令组成:

  • 汇编指令:如movaddsub等,有对应的机器指令,可以被编译为机器指令,最终被CPU执行。
  • 伪指令:如assumesegmentendsend等,没有对应的机器指令,由编译器解析,最终不被CPU执行,注释以分号开头。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
assume cs:code ; assume是声明一下code段是cs段(代码段),code是自己命名的

; segment和ends的作用是定义一个段,两个必须配对使用
code segment ; segment代表一个段的开始
mov ax, 1122h
mov bx, 3344h
add ax, bx

; 注意:下面两行代码是退出程序指令(退出程序的固定写法)
mov ah, 4ch ; 等价于:mov ax 4c00h
int 21h ; 中断指令(int不是整形,这里的int是中断意思,英文全称:interrupt)
code ends ; ends代表一个段的结束

end ; 编译器遇到end时,就结束对源程序的编译

1.2. 中断

中断是由于软件的或硬件的信号,使得CPU暂停当前的任务,转而去执行另一段子程序。也就是说,在程序运行过程中,系统出现了一个必须由CPU立即处理的情况,此时,CPU暂时中止当前程序的执行转而处理这个新情况的过程就叫做中断。

中断的分类:

  • 硬中断(外中断),由外部设备(比如网卡、硬盘)随机引发的,比如当网卡收到数据包的时候,就会发出一个中断。
  • 软中断(内中断),由执行中断指令产生的,可以通过程序控制触发。

从本质上来讲,中断是一种电信号,当设备有某种事件发生时,它就会产生中断,通过总线把电信号发送给中断控制器。如果中断的线是激活的,中断控制器就把电信号发送给处理器的某个特定引脚。处理器于是立即停止自己正在做的事,跳到中断处理程序的入口点,进行中断处理。

可以通过指令int n产生中断。n是中断码,内存中有一张中断向量表,用来存放中断码对应中断处理程序的入口地址。CPU在接收到中断信号后,暂停当前正在执行的程序,跳转到中断码对应的中断向量表地址处,去执行中断处理程序。

常见中断指令:
int 10h:用于执行BIOS中断。
int 3:是“断点中断”,用于调试程序。
int 21h:用于执行DOS系统功能调用,AH寄存器存储功能号。

1.3. DOS系统功能调用

DOS系统功能调用由DOS提供的一组实现特殊功能的子程序供程序员在编写自己的程序时调用,以减轻编程的工作量。涉及屏幕显示、文件管理、I/O管理等等。

每个子程序都有一个功能号,所有的功能调用的格式都是一致的。调用的步骤大致如下:

  1. 系统功能号送到寄存器AH中;

  2. 入口参数送到指定的寄存器中;

  3. INT 21H指令执行功能调用;

  4. 根据出口参数分析功能调用执行情况。

如果将代码、数据、栈都放到一个段里面会显得混乱,编程时要随时注意何处是数据、何处是栈、何处是代码。一个段的大小<=64KB,这样就会让数据、代码、栈的大小受到极大的限制。所以,一般会考虑使用多个段来存放数据、代码、栈。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
assume cs:code, ds:data
; ---数据段---
data segment
string db 'Hello World!$' ; $代表字符串结束位置, string是自己定义的标签
data ends

code segment
start:
; 设置ds为数据段,assume仅仅是给开发者看的,没有实际意义
mov ax, data
mov ds, ax

mov dx, offset string ; 取出数据段中的字符串数据,offset string代表string的偏移地址,string会自动算出string之前定义的数据有多少
mov ah, 9h ; 功能号9h代表在屏幕显示字符串
int 21h ; 执行DOS系统功能调用
code ends

end start

1.4. 给数据起标号

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
age1 dw 20 ; 全局变量age1
age2 dw 30 ; 全局变量age2
data ends

code segment
start:
mov ax, data
mov ds, ax

mov ax, age1
add ax, age2 ; 最终ax的值是50(32H)

mov ah, 4ch
int 21h
code ends
end start

1.5. 在代码段中存放数据

如下,计算1122h、3344h、5566h的和,结果存放在ax中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
assume cs:code

code segment
dw 1122h, 3344h, 5566h

start:
mov ax, 0h
mov bx, 0h
mov cx, 3h
s: add ax, cs:[bx]
add bx, 2
loop s

mov ah, 4ch
int 21h
code ends

end start
  • dwdefine word):定义了3个字型数据,数据之间用逗号隔开。类似的还有dbdefine byte)、dddefine double word)。
  • startend start是对应的,end start标记程序的执行入口(start之前是定义的数据)。

对比高级语言的常量定义就可以发现,常量在编译的那一刻就已经确定了在内存中地址,因为常量最终是存放在数据段中的。

1.6. 段前缀

mov ax, [bx]中bx的值是偏移地址,段地址默认在ds中。我们也可以明确地标明段地址,比如:

1
2
3
4
mov ax, ds:[bx]
mov ax, cs:[bx]
mov ax, ss:[bx]
mov ax, es:[bx]

上面的ds:cs:ss:es:称为段前缀,由于cs、ss都有自己的特殊用途,一般就使用es作为附加段寄存器。

1.7. loop指令

loop指令和cx配合使用,用于循环执行重复的操作,类似于高级语言中的for、while循环。

使用格式:

1
2
3
mov cx, 执行次数
标号: 循环执行的程序段
loop 标号

执行流程:

  1. 让cx的值减一,即cx = cx – 1
  2. 判断cx的值:
    • 如果不为零转至标号处执行程序,然后重复1
    • 如果为零则执行loop后面的代码

案例:计算$2^6$

1
2
3
4
5
6
7
8
9
10
11
12
13
; 不使用loop和cx
mov ax, 2h
add ax, ax
add ax, ax
add ax, ax
add ax, ax
add ax, ax

; 使用loop和cx
mov ax, 2h
mov cx, 5
5: add ax, ax
loop 5

1.8. 打印HelloWorld

第一种方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
assume cs:code, ds:data

data segment ; ---数据段---
string db 'Hello World!$'
data ends

code segment ; ---代码段---
start:
mov ax, data
mov ds, ax

mov ah, 9h
mov dx, offset string
int 21h

mov ah, 4ch
int 21h
code ends

end start
  • 使用int 21h显示的字符串必须要以$结尾。
  • 字符串可以用双引号或者单引号括住。

第二种方式:

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
assume cs:code, ds:data

data segment ; ---数据段---
db "Hello World!",0
data ends

code segment ; ---代码段---
start:
mov ax, data
mov ds, ax
mov ax, 0b800h ; 显示缓冲区的段地址
mov es, ax

mov bx, 0
mov si, 0
mov ch, 0
print:
mov cl, ds:[si] ; 取出一个字符
jcxz exit ; 如果cx为0, 就直接退出程序

mov es:[bx], cl ; 拷贝字符到显示缓存区
mov es:[bx+1], 11001111b ; 显示的属性:红底白字

inc si
add bx, 2
loop print
exit:
mov ah, 4ch
int 21h
code ends

end start

通过一个字节来显示字符的颜色。

1
2
3
 0			000			 0		000
闪烁 RGB 高亮 RGB
背景色 前景色

二、寻址

8086指令能处理2种尺寸的数据:byte、word。

思考:mov [0], 20H指令是否正确?
mov byte ptr [0], 20H:将20H放入0位置内存的字节单元,占用1个字节。
mov word ptr [0], 20H:将20H放入0位置内存的字单元,占用2个字节。

很多指令都可以通过byte ptr或者word ptr来指明所需要操作内存的数据长度。

1
2
inc byte ptr [0]
add word ptr [0], 2

有些指令有默认的操作数据长度,比如push [0]pop [0]的操作数据长度只能是2个字节。