既然我们有了一些汇编编码的方法,我们就可以开始检查语言本身了。汇编代码被写入一个纯文本文档,该文档由 MASM 汇编,并在编译时链接到我们的程序,或者存储在库中供以后使用。汇编和链接大多是在后台由 Visual Studio 自动完成的。
| | 注意:汇编语言文件不是说编译,而是说汇编。汇编汇编代码文件的程序叫做汇编程序,而不是编译器(在我们的例子中是 MASM)。 |
除了字符串之外,在汇编代码文件中完全忽略空行和其他空格。和所有编程一样,空白的智能使用可以让代码更易读。
MASM 不区分大小写。所有寄存器名称、指令助记符、指令和其他关键字不需要匹配任何特定的大小写。在本文档中,在任何代码示例中,它们总是以小写形式编写。按名称引用时,指令和寄存器将以大写字母书写(这一约定已从 AMD 程序员手册中采用,它使寄存器名称更容易阅读)。
| | 注意:如果希望 MASM 以区分大小写的方式处理变量名和标签,可以在程序集代码文件的顶部包含以下选项:“option casemap: none” |
汇编中的语句称为指令;他们通常非常简单,做一些微小的,几乎无关紧要的任务。它们直接映射到中央处理器知道如何执行的实际操作。CPU 只使用机器代码。编写汇编程序时键入的指令是内存辅助工具,因此您不需要记住机器代码。为此,用于指示的词语(MOV
、ADD
、XOR
等。)通常被称为助记符。
汇编代码由一个接一个的这些指令的列表组成,每个指令在一个新的行上。没有复合指令。这样,汇编就与高级语言大不相同了,在高级语言中,程序员可以自由地用更简单的形式和括号创建复杂的条件语句或数学表达式。MASM 实际上是一个高级汇编程序,使用它的宏工具可以形成复杂的语句,但这在本书中没有详细介绍。此外,MASM 通常允许用数学表达式代替常数,只要表达式的求值结果是常数(例如,MOV AX, 5
与MOV AX, 2+3
相同)。
最基本的原生 x64 汇编文件仅由写在文件顶部的End
组成。这个示例文件稍微有用一些;它包含一个. data 和一个. code 段,尽管实际上不需要任何段。
.data
; Define variables here
.code
; Define procedures here
End
基本 32 位程序集文件的框架比 64 位版本稍微详细一些。
.xmm
.model flat, c
.data
.code
Function1 proc export
push ebp
mov ebp, esp
; Place your code here
pop ebp
ret
Function1 endp
End
第一行描述了程序运行的中央处理器。我用过。xmm,这意味着程序需要一个带有 SSE 指令集的 CPU。该指令集将在第 8 章中详细讨论。现在使用的几乎所有的 CPU 都在一定程度上有这些指令集。
| | 注意:其他一些可能的中央处理器值是。MMX . 586 . 286。最好使用您希望程序运行的最好的中央处理器,因为选择旧的中央处理器将实现向后兼容,但代价是现代、强大的指令集。 |
我在这个骨架中加入了一个叫做函数 1 的程序。有时不需要 push、mov 和 pop 行,但我在这里包含它们是为了提醒您,在 32 位汇编中,参数总是在栈上传递,与 64 位相比,在 32 位汇编中访问它们是非常不同的。
分号(;)是一个评论。注释可以单独放在一行中,也可以放在指令之后。
; This is a comment on a line by itself
mov eax, 24 ; This comment is after an instruction
| | 注意:对几乎每条装配线进行注释是一个好主意。调试未注释的程序集非常耗时,甚至比未注释的高级语言代码还要耗时。 |
您也可以在示例代码中显示的注释指令中使用多行或块注释。注释指令后跟一个字符;这个字符由程序员选择。在下一次出现相同字符之前,MASM 会将所有文本视为注释。通常使用克拉(^
)或波浪号(~
)字符,因为它们在常规汇编代码中并不常见。任何字符都可以,只要它不出现在注释文本中。
CalculateDistance proc
comment ~
movapd xmm0, xmmword ptr [rcx]
subpd xmm0, xmmword ptr [rdx]
mulpd xmm0, xmm0
haddpd xmm0, xmm0
~
sqrtpd xmm0, xmm0
ret
CalculateDistance endp
在示例代码中,注释指令以波浪号出现。这将注释掉由波浪号包围的四行代码。实际上只有最后两条线是由 MASM 汇编的。
在本文中,指令的参数将被称为参数、操作数或目标和源。
**目的地:**这几乎总是第一个操作数;它是写入答案的操作数。在大多数双操作数指令中,目标也充当源操作数。
**来源:**这几乎总是第二个操作数。计算的来源可以是两个操作数中的任何一个,但在本书中,我使用了术语“来源”来专门表示第二个参数。
例如,考虑以下内容。
add rbx, rcx
RBX
是目的地;这是存放答案的地方。RCX
是源头;它是被添加到目的地的价值。
汇编程序由许多称为段的部分组成;每个片段通常都有特定的用途。代码段保存要执行的指令,这是 CPU 运行的实际代码。数据段保存程序的全局数据、变量、结构和其他数据类型定义。当程序执行时,每个段驻留在内存的不同页面中。
在高级语言中,通常可以将数据和代码混合在一起。虽然这在汇编时是可以的,但是很乱,不推荐。段通常由以下快速指令之一定义:
表 1:通用段指令
| 指示的 | 段 | 特征 | | 。密码 | 代码段 | 读取,执行 | | 。数据 | 数据段 | 读,写 | | 。常数 | 恒定数据段 | 阅读 | | 。数据? | 未初始化的数据段 | 读,写 |
| | 注:。代码,。数据和上表中提到的其他段指令是预定义的段类型。如果您需要针对您所在细分市场的特点提供更大的灵活性,请查阅微软针对 MASM 的细分市场指令。 |
常量数据段保存只读数据。未初始化的数据段保存初始化为 0 的数据(即使数据被定义为具有其他值,它也被设置为 0)。当程序员不关心应用程序第一次启动时数据应该有什么值时,未初始化的数据段很有用。
| | 注意:除了使用未初始化的数据段,简单地使用常规的。数据段并用“?”初始化数据元素。 |
示例表中的“特性”列指出了可以对数据段中的数据做什么。例如,代码段是只读和可执行的,而数据段是可读写的。
可以通过将名称放在 segment 指令之后来命名段。
.code MainCodeSegment
这对于在不同文件中定义同一段的各个部分,或者将数据和代码混合在一起非常有用。
| | 注意:每个片段都成为编译的一部分。exe 文件。如果在数据段中创建 5mb 阵列,则。exe 将大 5 MB。数据段中定义的数据不是动态的。 |
标签是使用JMP
指令 IP 可以跳转到的代码段中的位置。
[LabelName]:
其中[LabelName]
是任何有效的变量名。要跳转到已定义的标签,您可以使用JMP
、Jcc
(条件跳转)或CALL
指令。
SomeLabel:
; Some code
jmp SomeLabel ; Immediately moves the IP to SomeLabel
您可以将标签存储在寄存器中,并间接跳转到它。这实际上是将寄存器用作代码段中某个点的指针。
SomeLabel:
mov rax, SomeLabel
jmp rax ; Moves the IP to the address specified in RAX, SomeLabel
有时,为一段代码中的所有标签想名字是不方便的。您可以使用匿名标签语法来代替命名标签。匿名标签由@@:
指定。MASM 会给它一个独特的名字。
使用@F
作为JMP
指令的参数,可以向前跳转到高于当前指令指针(IP)的地址。您可以使用@B
作为JMP
指令的参数,向后跳转到低于当前 IP 的地址。
@@: ; An anonymous label
jmp @F ; Instruction to jump forwards to the nearest anonymous label
jmp @b ; Instruction to jump backwards to the nearest anonymous label
匿名标签往往会变得混乱和难以维护,除非只有少量的匿名标签。通常最好自己定义标签名。
来自任何高级语言的大多数熟悉的基本数据项也是汇编所固有的,但是它们都有不同的名称。
下表列出了程序集和 C++ 引用的数据类型。数据类型的大小在程序集中非常重要,因为指针算法不是自动的。如果你给一个整数(dword)指针加 1,它会移动到下一个字节,而不是 C++ 中的下一个整数。
一些数据类型没有标准化的名称;例如,XMM 字和 REAL10 只是 128 位和 80 位的组。在本书中,它们被称为 XMM 单词或 REAL10,尽管这不是它们的名字,而是对它们大小的描述。
ASM 列中的一些数据类型在括号中有一个简短的版本。在数据段中定义数据时,可以使用长名称或短名称。简称是缩写。例如,“定义字节”变成了“数据库”。
| | 注意:在这本书里,我总是把双精度词称为双字,把双精度浮点数称为双精度。 |
表 2:基本数据类型
类型 | 空对地导弹 | C++ | 位 | 字节 |
---|---|---|---|---|
字节 | 字节(db) | 茶 | eight | one |
有符号字节 | sbyte(字节) | 茶 | eight | one |
单词 | 单词(dw) | 无符号短 | Sixteen | Two |
签名字 | 剑 | 短的 | Sixteen | Two |
双字 | dword (dd) | 无符号整数 | Thirty-two | four |
带符号双字 | dword | (同 Internationalorganizations)国际组织 | Thirty-two | four |
四倍长字 | qword (dq) | 无符号长整型 | Sixty-four | eight |
有符号四字 | 剑 | 很长很长 | Sixty-four | eight |
XMM word (dqword) | xmmword | One hundred and twenty-eight | Sixteen | |
YMM 词 | ymmword | One hundred and twenty-eight | Sixteen | |
单一的 | real4 | 漂浮物 | Thirty-two | four |
两倍 | real8 | 两倍 | Sixty-four | eight |
十字节浮点 | 实数 10(TB、dt) | Eighty | Ten |
数据通常以最高有效位在左边,最低有效位在右边的方式绘制。内存中没有真正的方向,但这本书会以这种方式引用数据。所有的数据类型都是字节的集合,除了 REAL10 之外的所有数据类型占用的字节数都是 2 的幂。
对中央处理器来说,相同大小的数据类型没有区别。REAL4 和 dword 完全一样;两者都只是 4 字节的内存块。中央处理器可以将一个 4 字节的代码块视为 REAL4,然后在下一条指令中将同一个代码块视为 dword。正是这些指令定义了中央处理器是使用特定的内存块作为 dword 还是 REAL4。没有为中央处理器定义变量类型;它们是为程序员定义的。最好在数据段中正确定义数据,因为 Visual Studio 的调试窗口根据其声明将数据显示为有符号或无符号以及整数或浮点。
有几种数据类型在 C++ 中没有原生的等价物。XMM 和 YMM 的字类型是单指令多数据(SIMD),而相当奇怪的 REAL10 是来自旧的 x87 浮点单元。
| | 注:本书不会涉及 x87 浮点单元的指令,但值得注意的是,这个单元虽然是遗留的,但实际上能够执行现代 SSE 指令无法执行的任务。REAL10 类型通过在 C++ 双精度之上使用额外的 2 字节精度,为浮点计算增加了很高的精度。 |
x86 和 x64 处理器使用小字节序(与大字节序相反)来表示数据。所以多字节数据类型(字、数据字等)最低地址的字节。)是最低有效位,最高地址的字节是最高有效位。将内存想象成从左到右的单个长字节数组。
如果在某个地址有一个字或 2 字节的整数(让我们使用 0x00f08480,尽管实际上一个四字将被用来存储这个指针,所以它将是两倍长),值 153 在高位字节,34 在低位字节,那么 34 将在该字的确切地址(0x00f08480)。高字节将有 153,并且将位于下一个字节地址(0x00f08481),高一个字节。在本例中,单词存储的数字是这些字节的组合,以 256 为基数(34+153×256)。
图 11
这个单词实际上是整数 39,202。它可以被认为是一个以 256 为基数的数字,其中 34 是第一个数字,153 是第二个数字,或者 39202 = 34+153×(256^1).
除了小端字节序,x86 和 x64 处理器还使用二进制补码来表示带符号的负数。在这个系统中,最高有效位(通常画在最左边)是符号位。当该位为 0 时,表示的数字为正,当该位为 1 时,表示的数字为负。此外,当一个数字为负数时,它所代表的数字与翻转所有位并将 1 加到该结果上的数字相同。例如,带符号字节中的位模式 10110101 是负的,因为左边的位是 1。要找到数字的实际值,翻转所有位并加 1。
翻转 10110101 的每一位,就得到 01001010。
01001010 + 1 = 01001011
二进制中的 01001011 是十进制中的数字 75。
所以在一个系统中,用二进制补码表示有符号数的有符号字节中的位模式 10110101 表示值-75。
| | 注意:翻转位被称为一的补码、按位补码或补码。翻转位加一被称为二的补码或负数。计算机使用二进制补码,因为它能使用于加法的相同电路用于减法。使用二进制补码意味着 0 的单一表示,而不是-0 和+0。 |