汇编语言不会编?
1月 21, 2021
上篇已经介绍了 CPU 的寄存器种类,知道了程序是由指令和数据组成的,以及 CPU 是如何通过寄存器运行程序的,那么现在可以真正了解下汇编指令集了。
前提须知 #
- 因为不同 CPU 架构的指令集都不尽相同,汇编语言通常不具有可移植性,这里介绍的还是以 16 位的 8086CPU 为主。
- 下面的描述,用
()
表示一个寄存器或内存单元中的内容,比如(ax)表示 ax 中的内容,(20000H)表示内存 2000H 单元的内容。 - 下面的描述,用
idata
表示常量,比如mov ax, [idata]
就代表mov ax, [1]
、mov ax, [2]
、mov ax, [3]
。 - reg 表示一个寄存器,比如:ax、bx、cx、dx、ah、bh、bl、ch、cl、dh、dl、sp、bp、si、di。
- sreg 表示一个段寄存器,比如:ds、ss、cs、es。
伪指令(宏) #
用汇编语言写的源程序,包括伪指令和汇编指令。首先主要知道伪指令,它区别于常见的汇编指令。伪指令没有对应的机器指令,最终也不会被 CPU 执行,而是由编译器执行的指令,用于指示编译器如何汇编源程序。
segment #
segment
和 ends
是一对成对使用的伪指令,它们的功能是定义一个段,segment 是段的开始,ends 说明一个段结束,同时一个段必须有一个名称来标识。下面的代码是定义一个名为 codesg
的段。
assume cs:codesg
codesg segment
mov ax, 0123H
mov bx, 0456H
add ax, bx
add ax, ax
mov ax, 4c00H
int 21H
codesg ends
end
而这个 codesg
也会在编译器的编译、链接过程中,处理成一个段的段地址,从而成为真正可以被 CPU 执行的程序。
end #
end
是一个汇编程序的结束标记,编译器在编译汇编程序的过程中,如果碰到了伪指令 end,就结束对源程序的编译。end 和上面的 ends 不一样。ends 是和 segment 成对使用的。
end 除了通知编译器程序结束外,还可以通知编译器程序的入口在什么地方。
assume cs:code
code segment
dw 0123H,0456H,0789H,0abcH
start: mov ax, 2
mov cx, 11
s:add ax,ax
loop s
mov ax, 4c00h
int 21h
code ends
end start
在编译链接后,由 “end start” 指明的程序入口,被转化为一个入口地址,存储在可执行文件的描述信息中。当程序被加载后,读到了程序的入口地址,设置 CS:IP
(这里为 CS:A),这样 CPU 就从我们希望的地址处开始执行。
assume #
assume
的含义为假设。它假设某一个段寄存器和程序中的某一个 segment…ends 定义的段相关联。例如在上面的程序中,assume
将代码段的 codesg
和 CPU 的段寄存器 CS
联系起来。
DB、DW 和 DD #
db 定义字节类型变量,一个字节数据占 1 个字节单元,读完一个,偏移量加 1。
dw 定义字类型变量,一个字数据占 2 个字节单元,读完一个,偏移量加 2。
dd 定义双字类型变量,一个双字数据占 4 个字节单元,读完一个,偏移量加 4。
dw 0123H,0456H,0789H,0abcH
程序在运行的时候 CS 中存放代码段的地址。dw
命令定义的数据一般处于代码段的最开始,所以偏移地址为 0。配合 loop 就可以循环 (bx) = (bx) + 2
得到所有的值。
ASCII 码 #
在汇编程序中,用 'xxx'
的方式指明数据是以字符的形式给出的,编译器将它们转化为相应的 ASCII 码。
db 'UNIX'
相当于 db 75H,6EH,49H,58H
,因为“U”、“N”、“I”、“X”的 ASCII 码分别为 75H、6EH、49H、58H。
将数据、代码、栈放入不同的段 #
在 8086 模式下,一个段最大的大小为 64 K(2 的 16 次方),如果数据、代码、栈加起来所需要的空间大于 64KB,就不能放在一个段中。另外,都放在一个段中会使程序变得混乱。这种情况可以定义多个段。
assume cs:code
data segment
dw 0123H, 0456H, 0789H, 0abcH, 0defH, 0fedH, 0cbaH, 0987H ; 0H ~ fH
data ends
stack segment
dw 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ; 0H ~ 1fH
stack ends
code segment
start: mov ax, data
mov ds, ax ; 设置 ds 指向 data 段
mov ax, stack
mov ss, ax
mov sp, 20H
mov bx, 0
mov cx, 8
s0: push [bx]
add bx, 2
loop s0
mov bx, 0
mov cx, 8
s1: pop [bx]
add bx, 2
loop s1
mov ax, 4c00H
int 21H
code ends
end start
另外 8086CPU 不允许将一个数值直接送入段寄存器中,比如 mov ds, data
就是错误的。
传送指令 #
MOV #
mov 指令可以改变大部分寄存器的值,还可以改变内存单元的值。
mov ax, 8
mov ax, bx
mov ax, [0]
mov [0], ax
mov ds, ax
涉及内存单元的,一般和 DS 寄存器有关,即段地址为 DS,偏移地址为 [0]
的内存单元。
mov ax, [bx]
表示 bx 中存放的数据作为一个偏移地址 EA,段地址 SA 默认在 ds 中,将 SA:EA 处的数据放入 ax 中,即:
(ax) = ((ds) * 16 + (bx))
PUSH 和 POP #
栈顶的段地址存放在 SS 中,偏移地址存放在 SP 中。
执行 push 和 pop 指令时,CPU 从 SS:SP
寄存器中得到栈顶的地址。
push
和 pop
执行的时候只会修改 SP。
入栈时,栈顶从搞地质向低地址方向增长。
mov ax, 0123H
push ax
mov bx, 2266H
push bx
pop ax
pop bx
push ax
表示将寄存器 ax
中的数据送入栈中,pop ax
表示从栈顶取出数据送入 ax
。
其中,push ax
的执行由以下两步完成:
- 先更新 SP = SP - 2;
- 将
ax
的值送入SS:SP
的位置。
push
通过这样的步骤,保持了 SP
一直在最新位置上,pop
命令也一样:
- 先将
SS:SP
指向内存单元处的数据送入 ax; - 再更新 SP = SP + 2
该操作的对象可以是寄存器、段寄存器、内存单元。
用栈来暂存以后需要恢复的寄存器中的内容时,出栈的顺序要和入栈的顺序相反,因为最后入栈的寄存器的内容在栈顶,所以在恢复时,要最先出栈。
转移指令 #
能够改变 CS、IP 的内容的指令被统称为转移指令。
JMP #
jmp 段地址:偏移地址
的功能为:用指令中给出的段地址修改 CS,偏移地址修改 IP。
jmp ax
功能为:仅修改 IP 的内容,含义上可理解为:mov IP, ax
。
CALL #
call 指令进行了两步操作:
- 将当前的 IP 或 CS 压入栈中。
- 转移
RET #
ret 指令用栈中的数据,修改 IP 的内容,从而实现近转移。ret 指令进行下面两步操作:
- (IP) = ((ss) * 16 + (sp))
- (SP) = (sp) + 2
RETF #
retf 指令用栈中的数据,修改 CS 和 IP 的内容,从而实现远转移。
IRET #
循环控制指令 #
LOOP #
assume cs:code
code segment
mov ax, 2
mov cx, 11
s:add ax,ax
loop s
mov ax, 4c00h
int 21h
code ends
end
CPU 执行 loop 指令的时候,要进行两步操作:
- (cx) = (cx) - 1
- 判断 cx 中的值,不为 0 则转至标号处执行程序,为 0 则向下执行。
可以得出,上面的 demo 代码将会执行 11 次 add ax, ax
,然后再向下执行。
运算指令 #
INC #
自增 1,例如:
mov bx, 0
inc bx
执行后,(bx) = 1。
ADD #
逻辑与,按位进行与运算。例如:
mov al, 01100011B
and al, 00111011B
执行后,al = 00100011B。
OR #
逻辑或,按位进行或运算。例如:
mov al, 01100011B
or al, 00111011B
执行后,al = 01111011B。
SUB #
减法指令
DIV #
除法指令
SHL #
逻辑左移指令,功能为:
- 将一个寄存器或内存单元中的数据向左移位。
- 将最后移出的一位写入 CF 中。
- 最低位用 0 补充。
mov al, 01001000B
shl al, 1
执行后, (al) = 10010000B,CF = 0。
SHR #
逻辑右移指令,功能为:
- 将一个寄存器或内存单元中的数据向右移位。
- 将最后移出的一位写入 CF 中。
- 最高位用 0 补充。
mov al, 10000001B
shr al, 1
执行后 (al) = 01000000B,CF = 1。