前面学习了 8086 CPU 通过从 CS:IP 指向的内存取指令并执行,内存中数据的方法为 DS:[xxx],以及栈的相关知识。从这里开始,我们正式开始编写汇编代码,来体验下真正和 CPU 寄存器打交道的乐趣。

第一个程序

程序如下:

assume cs:codesg
codesg segment

start: mov ax,0123H
mov bx,0456H
add ax,bx

mov ax,4c00h
int 21H
codesg ends
end

汇编语言中包含汇编指令和伪指令,汇编指令是最终要编译成机器码的,而伪指令没有对应的机器码,是由编译器执行的指令,编译器根据伪指令来进行相关的编译工作。

上面的程序中出现了三种伪指令分别是:

  • assume cs:codesg
  • xxx segment

    ……

    xxx ends

  • end

assume 伪指令的含义是假设,可以将有特定用途的段和相关寄存器关联起来

segment 和 ends 是成对使用的伪指令,segment说明一个段的开始

end 是汇编程序结束标志

mov ax,4c00h 和 int 21H 是程序的返回,就像高级语言中的 return。

计算 2 的 3 次方,代码如下:

assume cs:abc
abc segment
mov ax,2
add ax,ax
add ax,ax

mov ax,4c00h
int 21H
abc ends
end

[bx]

mov ax [bx] 命令的含义为将一个内存单元的内容送入 ax 中,这个内存单元的长度为 2 字节,存放一个字,偏移地址在 bx 中,段地址在 ds 中。

loop

loop 指令的格式是:loop 标号,其中需要循环的次数通常放在 cx 中,CPU 在执行 loop 指令的分为 2 步:

  • (cx)= (cx)- 1
  • 判断 cx 中的值,不为零则跳转到标号处执行程序,如果为零则向下执行

用 loop 指令计算 2 * 3,代码如下

assume cs:codesg
codesg segment
mov ax,0
mov cx,3
s: add ax,2
loop s

mov ax,4c00h
int 21H

codesg ends
end

在汇编程序中,数据不能以字母开头。例如 1234H 在汇编源程序中可以直接写 1234H,而 A000H 在汇编源程序中要写成 0A000H。

[bx] 和 loop 综合应用

计算 ffff:0 ~ ffff:b 单元中的数据的和,代码如下:

assume cs:code 
code segment
mov ax,0ffffh
mov ds,ax
mov bx,0
mov dx,0
mov cx,12
s: mov al,[bx]
mov ah,0
add dx,ax
inc bx
loop s

mov ax,4c00h
int 21H
code ends
end

其中 inc bx 表示 bx 中的内容加 1 。

段前缀及其应用

我们知道 mov ax,[bx] 表示把一个内存单元的数据的放入 ax 中,偏移地址由 bx 给出,段地址默认在 ds 中。也可以显示的给出段地址,例如:mov ax,ds:[bx]。8086 有 4 个 16 位段寄存器,分别为 CS、DS、SS、ES,那么很显然 mov ax,[bx] 默认的 ds 段地址也可以由 CS 或 SS 或 ES 显示的给出,如以下所示的都是合法的:

  • mov ax,cs:[bx]
  • mov ax,ss:[bx]
  • mov ax,es:[bx]

段前缀应用举例:将内存 ffff:0 ~ ffff:b 中的内容复制到 0020:0 ~ 0020:b 中,代码如下:

;将内存 ffff:0 ~ ffff:b 中的内容复制到 0020:0 ~ 0020:b 中
assume cs:code
code segment
mov ax,0ffffh
mov ds,ax
mov ax,0020h
mov es,ax
mov bx,0
mov cx,12

s: mov dl,[bx]
mov es:[bx],dl
inc bx
loop s

mov ax,4c00h
int 21H
code ends
end

编译执行完成后用 debug 查看内存情况,发现程序正确执行,把 ffff:0 ~ ffff:b 中的内容复制到了 0020:0 ~ 0020:b 中。

1
1

在程序中使用数据

之前的列子我们用到的数据都是在寄存器或者在内存中,比如计算 2 的 3 次方,数据直接放在寄存器中,把某段内存中的数值累加等。比如要计算已知的 n 个数的和该怎么做呢?当然可以把这个 n 个数放到内存中,累加计算其和,但是把这 n 个数放在内存中的什么地方呢?随便拿一块内存来存放数据操作系统允许你这样干吗?其实我们有两种方法取得一块合理的内存来使用,一是在操作系统装载程序的时候为程序分配内存,另一种方法是在程序中向操作系统申请内存。这里我们只讨论在操作系统装载程序为程序分配内存的情况。就像高级语言定义变量一样,汇编也可以定义一个段来存储数据。我们可以使用 db、dw、dd 来定义,它们的具体意义如下:

  • db 定义字节类型变量,一个字节数据占1个字节单元,读完一个,偏移量加 1
  • dw 定义字类型变量,一个字数据占2个字节单元,读完一个,偏移量加 2
  • dd 定义双字类型变量,一个双字数据占4个字节单元,读完一个,偏移量加 4

例如我们要计算 0123H,0456H,0789H,0abcH,0defH,0001H,0002H,0003H 这 8 个数据的和,结果存在 ax 中,代码可如下所示:

assume cs:code
code segment
dw 0001H,0002H,0003H,0004H,0005H,0006H,0007H,0008H
mov bx,0
mov ax,0
mov cx,8
s: add ax,cs:[bx]
add bx,2
loop s

mov ax,4c00h
int 21H
code ends
end

上面的代码编译链接后用 debug 查看,显示如下:

2
2

发现第一条指令并不是我们真正需要 CPU 执行的指令,真的指令从第 17 个字节开始,前面的 16 个字节其实是我们定义的 8 个数据。要想让 CPU 执行指令,在上面的代码中可以加入 start 标号,代码修改为下面的样子:

assume cs:code
code segment
dw 0001H,0002H,0003H,0004H,0005H,0006H,0007H,0008H
start: mov bx,0
mov ax,0
mov cx,8
s: add ax,cs:[bx]
add bx,2
loop s

mov ax,4c00h
int 21H
code ends
end start

这时候发现 IP 为 10H 才是我们希望 CPU 执行的指令

3
3

总结

通过本次学习,我们编写了第一个汇编程序,知道了常用的伪指令:assume、segment ends、end,明白了 [bx] 和 loop 的用法,学习了段前缀及其用法,掌握了在程序中使用数据的方法,并且清楚了 start 标号的用途,收获颇丰,继续努力!