汇编语言基础(一)

汇编笔记是根据C语言中文网完成的

汇编语言以隐晦难懂而著名,但是本教程从另一个角度来看它——它是一种几乎提供了全部信息的语言。程序员可以看到正在发生的所有事情,甚至包括 CPU 中的寄存器和标志!

本章侧重于 Microsoft MASM 汇编程序的基本组成部分。通过学习将会了解到如何定义常数和变量,数字和字符常量的标准格式,以及怎样汇编并运行你的第一个程序。

第一个汇编语言程序

汇编语言是一种几乎提供了全部信息的语言。程序员可以看到正在发生的所有事情,甚至包括 CPU 中的寄存器和标志!

但是,在拥有这种能力的同时,程序员必须负责处理数据表示的细节和指令的格式。程序员工作在一个具有大量详细信息的层次。现在以一个简单的汇编语言程序为例,来了解其工作过程。

程序执行两个数相加,并将结果保存在寄存器中。程序名称为 AddTwo:

1
2
3
4
5
main PROC
mov eax, 5 ;将数字 5 送入 eax 寄存器
add eax, 6 ;eax 寄存器加 6
INVOKE ExitProcess, 0 ;程序结束
main ENDP

现在按照一次一行代码的方法来仔细查看这段程序:

  • 第 1 行开始 main 程序(主程序),即程序的入口;
  • 第 2 行将数字 5 送入 eax 寄存器;
  • 第 3 行把 6 加到 EAX 的值上,得到新值 11;
  • 第 5 行调用 Windows 服务(也被称为函数)ExitProcess 停止程序,并将控制权交还给操作系统;
  • 第 6 行是主程序结束的标记。

程序中包含的注释,它总是用分号开头。程序的顶部省略了一些声明,稍后会予以说明,不过从本质上说,这是一个可以用的程序。

添加一个变量

现在让这个程序变得有趣些,将加法运算的结果保存在变量 sum 中。要实现这一点,需要增加一些标记,或声明,用来标识程序的代码数据区

1
2
3
4
5
6
7
8
9
.data                          ;此为数据区
sum DWORD 0 ;定义名为sum的变量
.code ;此为代码区
main PROC
mov eax,5 ;将数字5送入而eax寄存器
add eax,6 ;eax寄存器加6
mox sum,eax ;将eax寄存器中的值传给sum变量
INVOKE ExitProcess,0 ;结束程序
main ENDP

变量 sum 在第 2 行进行了声明,其大小为 32 位,使用了==关键字 DWORD==。汇编语言中有很多这样的大小关键字,其作用或多或少与数据类型一样

但是与程序员可能熟悉的类型相比它们没有那么具体,比如 int、double、float 等等。**这些关键字只限制大小,并不检查变量中存放的内容。**记住,程序员拥有完全控制权。

顺便说一下,那些被 .code 和 .data 伪指令标记的代码和数据区,被称为段。即,程序有代码段数据段

汇编语言常量

常量(constant)是程序中使用的一个确定数值,在汇编阶段就可以确定,直接编码于指令代码中,不是保存在存储器中可变的变量,因为是编码在指令中的量,和指令一起存储了,所以不用单独开辟主存空间,所以也就没法动态改变它了,这也正是高级语言常量无法修改的原因。

整数常量

整数常量(integer literal)(又称为整型常量(integer constant))由一个可选前置符号一个或多个数字,以及一个指明其基数的可选基数字符构成。

比如 26 就是一个有效的整数常量。它没有基数,所以假设其是十进制形式。如果想要表示十六进制数 26,就将其写为 26h。同样,数字 1101 可以被看做是十进制值,除非在其末尾添加“b”,使其成为 1101b (二进制)。下表列出了可能的基数值:

h 十六进制 r 编码实数
q/o 八进制 t 十进制(备用)
d 十进制 y 二进制(备用)
b 二进制

下面这些整数常量声明了各种基数。每行都有注释:

1
2
3
4
5
6
7
26         ;十进制
26d ;十进制
11010011b ;二进制
42q ;八进制
42o ;八进制
1Ah ;十六进制
0A3h ;十六进制

以字母开头的十六进制数必须加个前置 0,以防汇编器将其解释为标识符。

整型常量表达式

整型常量表达式 (constant integer expression) 是一种算术表达式,它包含了整数常量算术运算符。每个表达式的计算结果必须是一个整数,并可用 32 位 (从 0 到 FFFFFFFFh) 来存放。

下表列出了算术运算符,并按照从高 (1) 到低 (4) 的顺序给出了它们的优先级。对整型常量表达式而言很重要的是,要意识到它们只在汇编时计算。这里将它们简称为 整数表达式。

运算符 名称 优先级
() 圆括号 1
+,- 一元加、减 2
*, / 乘、除 3
MOD 取模 3
+, - 加、减 4

在表达式中使用圆括号来表明操作顺序,那么就不用去死记运算符优先级。

实数常量

实数常量(real number literal)(又称为浮点数常量(floating-point literal))用于表示十进制实数和编码(十六进制)实数。十进制实数包含一个可选符号,其后跟随一个整数一个十进制小数点一个可选的表示小数部分的整数,和一个可选的指数

下面是一些有效的十进制实数:

1
2
3
4
2.
+3.0
-44.2E+05
26.E5

至少需要一个数字和一个十进制小数点

字符常量

字符常量 (character literal) 是指,用单引号或双引号包含的一个字符。汇编器在内存中保存的是该字符二进制 ASCII 码的数值。例如:

1
2
'A'
"d"

表明字符常量在内部保存为整数,使用的是 ASCII 编码序列。因此,当编写字符常量“A”时,它在内存中存放的形式为数字 65 ( 或 41H)。

字符串常量

字符串常量 (string literal) 是用单引号或双引号包含的一个字符 ( 含空格符 ) 序列:

1
2
3
4
'ABC'
'X'
"Good night, Gracie"
'40961

嵌套引号也是被允许的,使用方法如下例所示:

1
2
"This isn't a test"
'Say "Good night," Gracie'

和字符常量以整数形式存放一样,字符串常量在内存中的保存形式为整数字节数值序列。例如,字符串常量“ABCD”就包含四个字节 41H、42H、43H、44H

汇编语言保留字

**保留字(reserved words)**有特殊意义并且只能在其正确的上下文中使用。默认情况下,保留字是没有大小写之分的。比如,MOV 与 mov、Mov 是相同的。

==保留字==有不同的类型:

  • 指令助记符,如 MOV、ADD 和 MUL。
  • 寄存器名称。
  • 伪指令,告诉汇编器如何汇编程序。
  • 属性,提供变量和操作数的大小与使用信息。例如 BYTE 和 WORD。
  • 运算符,在常量表达式中使用。
  • 预定义符号,比如 @data,它在汇编时返回常量的整数值。

下表是常用的保留字列表:

$ PARITY? DWORD STDCALL
? PASCAL FAR SWORD
@B QWORD FAR16 SYSCALL
@F REAL4 FORTRAN TBYTE
ADDR REAL8 FWORD VARARG
BASIC REAL10 NEAR WORD
BYTE SBYTE NEAR16 ZERO?
C SDORD OVERFLOW?
CARRY? SIGN?

汇编语言标识符及其命名规则

标识符(identifier)是由程序员选择的名称,它用于标识变量、常数、子程序和代码标签。

标识符的形成有一些规则:

  • 可以包含 1 到 247 个字符。
  • 不区分大小写。
  • 第一个字符必须为字母 (A—Z, a—z) A 下划线 (_)、@、? 或 $。其后的字符也可以是数字。
  • 标识符不能与汇编器保留字相同。

一般情况下,应避免用符号 @ 和下划线作为第一个字符,因为它们既用于汇编器,也用于高级语言编译器。

汇编语言伪指令

伪指令 (directive) 是嵌入源代码中的命令,由汇编器识别和执行。伪指令不在运行时执行,但是它们可以定义变量、宏和子程序;为内存段分配名称,执行许多其他与汇编器相关的日常任务

默认情况下,伪指令不区分大小写。例如,.data,.DATA 和 .Data 是相同的。

下面的例子有助于说明伪指令和指令的区别。DWORD 伪指令告诉汇编器在程序中为一个双字变量保留空间。另一方面,MOV 指令在运行时执行,将 myVar 的内容复制到 EAX 寄存器中

1
2
myVar DWORD 26
mov eax,myVar

尽管 Intel 处理器所有的汇编器使用相同的指令集,但是通常它们有着不同的伪指令。比如,Microsoft 汇编器的 REPT 伪指令对其他一些汇编器就是无法识别的。

定义段

汇编器伪指令的一个重要功能是定义程序区段,也称为段 (segment)。程序中的段具有不同的作用。如下面的例子,一个段可以用于定义变量,并用==.DATA== 伪指令进行标识

1
.data

.CODE 伪指令标识的程序区段包含了可执行的指令

1
.code

.STACK 伪指令标识的程序区段定义了运行时堆栈,并设置了其大小

1
.stack 100h

汇编语言指令详解

指令(instruction)是一种语句,它在程序汇编编译时变得可执行。汇编器将指令翻译为机器语言字节,并且在运行时由 CPU 加载和执行。

一条指令有四个组成部分:

  • 标号(可选)
  • 指令助记符(必需)
  • 操作数(通常是必需的)
  • 注释(可选)

1.标号

标号(label)是一种标识符,是指令和数据的位置标记标号位于指令的前端,表示指令的地址。同样,标号也位于变量的前端,表示变量的地址。标号有两种类型:数据标号代码标号

==数据标号==标识变量的位置,它提供了一种方便的手段在代码中引用该变量。比如,下面定义了一个名为 count 的变量:

1
count DWORD 100

汇编器为每个标号分配一个数字地址。可以在一个标号后面定义多个数据项。在下面的例子中,array 定义了第一个数字(1024)的位置,其他数字在内存中的位置紧随其后

1
2
array DWORD 1024, 2048
DWORD 4096, 8192

程序代码区(CODES SEGMENT,指令所在区段)的标号必须用冒号(:)结束代码标号用作跳转和循环指令的目标。例如,下面的 JMP 指令创建一个循环,将程序控制传递给标号 target 标识的位置:

1
2
3
4
target:
mov ax,bx
...
jmp target

**代码标号**可以与指令在同一行上,也可以自己独立一行:

1
2
L1: mov ax, bx
L2 :

标号命名规则要求,只要每个标号在其封闭子程序页中是唯一的,那么就可以多次使用相同的标号

2.指令助记符

指令助记符(instruction mnemonic)是标记一条指令的短单词。在英语中,助记符是帮助记忆的方法。相似地,汇编语言指令助记符,如 mov, add 和 sub,给出了指令执行操作类型的线索下面是一些指令助记符的例子

助记符 说明 助记符 说明
MOV 传送(分配)数值 MUL 两个数值相乘
ADD 两个数值相加 JMP 跳转到一个新位置
SUB 从一个数值中减去另一个数值 CALL 调用一个子程序

3.操作数

操作数是指令输入输出的数值。汇编语言指令操作数的个数范围是 0〜3 个,每个操作数可以是寄存器内存操作数整数表达式输入输岀端口

生成内存操作数有不同的方法,比如,使用变量名、带方括号的寄存器等。变量名暗示了变量地址,并指示计算机使用给定地址的内存内容。下表列出了一些操作数示例:

示例 操作数类型 示例 操作数类型
96 整数常量 eax 寄存器
2+4 整数表达式 count 内存

现在来考虑一些包含不同个数操作数的汇编语言指令示例。比如,STC 指令没有操作数:

1
stc                    ;进位标志位置 1

INC 指令有一个操作数:

1
inc eax                ;EAX 加 1

MOV 指令有两个操作数:

1
mov count, ebx         ;将 EBX 传送给变量 count

操作数有固有顺序。当指令有多个操作数时,通常第一个操作数被称为目的操作数第二个操作数被称为源操作数(source operand)

一般情况下,目的操作数的内容由指令修改。比如,在 mov 指令中,数据就是从源操作数复制到目的操作数。

IMUL 指令有三个操作数,第一个是目的操作数,第二个和第三个是进行乘法的源操作数:

1
imul eax,ebx,5		;EBX 与 5 相乘,结果存放在 EAX 寄存器中。

4.注释

注释有两种指定方法:

  • 单行注释,用分号(;)开始。汇编器将忽略在同一行上分号之后的所有字符。
  • 块注释,用 COMMENT 伪指令和一个用户定义的符号开始。汇编器将忽略其后所有的文本行,直到相同的用户定义符号出现为止。

块注释例子:

1
2
3
4
COMMENT !
This line is a comment.
This line is also a comment.
!

其他符号也可以使用,只要该符号不出现在注释行中:

1
2
3
4
COMMENT &
This line is a comment.
This line is also a comment.
&

汇编笔记是根据C语言中文网完成的