汇编语言不会编?

汇编语言不会编?

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 #

segmentends 是一对成对使用的伪指令,它们的功能是定义一个段,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 寄存器中得到栈顶的地址。

pushpop 执行的时候只会修改 SP。

入栈时,栈顶从搞地质向低地址方向增长。

mov ax, 0123H
push ax
mov bx, 2266H
push bx
pop ax
pop bx

push ax 表示将寄存器 ax 中的数据送入栈中,pop ax 表示从栈顶取出数据送入 ax

其中,push ax 的执行由以下两步完成:

  1. 先更新 SP = SP - 2;
  2. ax 的值送入 SS:SP 的位置。

push 通过这样的步骤,保持了 SP 一直在最新位置上,pop 命令也一样:

  1. 先将 SS:SP 指向内存单元处的数据送入 ax;
  2. 再更新 SP = SP + 2

该操作的对象可以是寄存器、段寄存器、内存单元。

用栈来暂存以后需要恢复的寄存器中的内容时,出栈的顺序要和入栈的顺序相反,因为最后入栈的寄存器的内容在栈顶,所以在恢复时,要最先出栈。

转移指令 #

能够改变 CS、IP 的内容的指令被统称为转移指令。

JMP #

jmp 段地址:偏移地址 的功能为:用指令中给出的段地址修改 CS,偏移地址修改 IP。

jmp ax 功能为:仅修改 IP 的内容,含义上可理解为:mov IP, ax

CALL #

call 指令进行了两步操作:

  1. 将当前的 IP 或 CS 压入栈中。
  2. 转移

RET #

ret 指令用栈中的数据,修改 IP 的内容,从而实现近转移。ret 指令进行下面两步操作:

  1. (IP) = ((ss) * 16 + (sp))
  2. (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 指令的时候,要进行两步操作:

  1. (cx) = (cx) - 1
  2. 判断 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 #

逻辑左移指令,功能为:

  1. 将一个寄存器或内存单元中的数据向左移位。
  2. 将最后移出的一位写入 CF 中。
  3. 最低位用 0 补充。
mov al, 01001000B
shl al, 1

执行后, (al) = 10010000B,CF = 0。

SHR #

逻辑右移指令,功能为:

  1. 将一个寄存器或内存单元中的数据向右移位。
  2. 将最后移出的一位写入 CF 中。
  3. 最高位用 0 补充。
mov al, 10000001B
shr al, 1

执行后 (al) = 01000000B,CF = 1。

中断指令 #

int #

参考 #

汇编语言 - 王爽

https://www.felixcloutier.com/x86/index.html

本文共 2491 字,上次修改于 Oct 24, 2024,以 CC 署名-非商业性使用-禁止演绎 4.0 国际 协议进行许可。

相关文章

» CPU 与寄存器