FASM 1.67 程序者手册
From: xuyibo.org Updated: 2008-04-20
邮件通知 | 当更新时自动发送邮件通知。 |
评论本文 | 有什么建议或评论,可以贴一下。 |
我要捐助 | 你的支持,让我们做的更好。 |
1. 简介
-
1.1 编译器概述
-
1.1.1 系统需求
1.1.2 编译器使用
1.1.3 编译器选项
1.1.4 在命令行下执行编译器
1.1.5 命令行编译器消息
1.1.6 输出格式
1.2 汇编语法
-
1.2.1 指令语法
1.2.2 数据定义
1.2.3 常数和标号
1.2.4 数值表达式
1.2.5 跳转和调用
1.2.6 操作数尺寸设置
2. 指令集
-
2.1 x86 体系指令
-
2.1.1 数据传送指令
2.1.2 类型转换指令
2.1.3 二进制算术指令
2.1.4 十进制算术指令
2.1.5 逻辑指令
2.1.6 控制转移指令
2.1.7 I/O 指令
2.1.8 字符串操作指令
2.1.9 标志控制指令
2.1.10 条件操作指令
2.1.11 其他指令
2.1.12 系统指令
2.1.13 FPU 指令
2.1.14 MMX 指令
2.1.15 SSE 指令
2.1.16 SSE2 指令
2.1.17 SSE3 指令
2.1.18 AMD 3DNOW!指令
2.1.19 x86-64长模式指令
2.2 控制伪指令
-
2.2.1 数值常量
2.2.2 条件汇编
2.2.3 重复块指令
2.2.4 地址空间
2.2.5 其他伪指令
2.2.6 多遍扫描
2.3 预处理伪指令
-
2.3.1 包含源文件
2.3.2 符号常量
2.3.3 宏指令
2.3.4 结构
2.3.5 重复宏指令
2.3.6 条件宏指令
2.3.7 处理顺序
2.4 格式伪指令
-
2.4.1 MZ 格式
2.4.2 PE 格式
2.4.3 COFF 格式
2.4.4 ELF 格式
3. Windows 编程
-
3.1 基本头文件
-
3.1.1 结构
3.1.2 导入表
3.1.3 过程
3.1.4 导出表
3.1.5 COM(组件)
3.1.6 资源
3.1.7 字符编码
3.2 扩展头文件
-
3.2.1 过程参数
3.2.2 结构化源码
1 简介
这一章包含开始使用FASM前的所有必须知识,在使用FASM前应至少阅读此章。
1.1 编译器概述
FASM是一个x86体系处理器下的汇编语言编译器,它可以通过多遍扫描来优化生成的机器码。
这篇文档还描述了用于windows系统的IDE版本,这个版本带有界面,并且有一个集成的编辑器。但从编译的角度,它和命令行版本是一样的。IDE版本的可执行文件为fasmw.exe,命令行的为fasm.exe
1.1.1 系统需求
所有版本都需要x86平台32位处理器(至少80386),虽然可以生成x86体系处理器16位程序。Windows控制台版本需要任意Win32操作系统;GUI版本需要Win32 GUI 4.0或更高版本,所以它可以运行在任何兼容Windows 95的系统上。
这个版本提供的example代码需要设置INCLUDE变量为FASM包目录下的include目录才能正确编译。 比如FASM包位置为d:\fasm:右键点击【我的电脑】->【属性】->【高级】->【环境变量】,在弹出的对话框中,在下面的系统变量中,如果里面存在INCLUDE环境变量,那么双击其并将d:\fasm\include添加到变量值中,注意必须用分号分隔变量;如果不存在INCLUDE环境变量,点击添加INCLUDE环境变量。
如果你使用FASMW来编译,还有另一个方法,你可以在d:\fasm\fasmw.ini文件末尾添加下面的内容:
如果不设置好INCLUDE环境变量,当include文件的时候,就必须提供完整的include文件路径。[Environment] Include = c:\fasmw\include
1.1.2 编译器使用
开始使用FASM,可以简单的双击fasmw.exe文件图标,或者拖拽一个源码文件到此图标。你也可以打开fasmw.exe后使用菜单【文件】->【打开】来打开源码文件,或者拖拽文件到编辑窗口。你可以一次编辑多个文件,每一个文件都在编辑窗口底部占用一tab按钮,点击相应的按钮就可以切换到该文件。FASM默认将编译当前编辑的文件,但你可以通过右键点击该文件的tab按钮,让编译器来强制每次编译此文件。一次只能有一个文件可以指派给编译器。
当你的源码文件都准备好后,你可以执行运行菜单中的编译来执行此文件。编译成功后,编译器将显示编译过程总结;否则将显示发现的错误。编译总结包括编译了多少遍、消耗的时间、写入多少字节到目标文件。它还包含一个【显示】文本框,用来显示任何源码中的display指令。错误总结至少包含错误信息和一个显示文本框。如果错误和源码中的某些行有关,总结将包含指令段,用来显示预处理后导致错误的指令,和源码列表,显示和错误相关的源码行位置,如果你从列表中选择一行,那么编辑窗口也将选择相应的行。(如果此行的文件还没有加载,那么将自动加载。)
运行命令也调用执行编译器,并且在编译成功后如果此格式能在Windows环境下执行的话执行编译的程序;否则将弹出消息提示此类型文件不能执行。如果发生错误,编译器显示和编译命令相同的提示。
如果编译器运行超出内存,你可以在【选项】菜单中的【编译器设置】对话框中增加内存分配。你可以设置编译器应当使用多少KB字节,以及编译线程的优先级。
1.1.3 编辑器选项
在【选项】菜单中还包含一些编辑器选项,用来影响编辑器行为的开关。这一节中将描述此选项。
安全选择 - 当打开此选项的时候,当开始键入的时候,选择的文本将不会被删除。当你做任何文本修改操作时,选择部分将被撤销,不会影响任何选中的文本,并且之后会执行那个命令。当这个选项关闭的时候,当你键入的时候,选中的文本将被删除,Del键也会删除选中的块(当安全选中开启时,你必须使用Ctrl+Del才能删除此文件)。
自动填充 - 当你键入任何开始括号的时候,编辑器将自动键入关闭括号。
自动缩进 - 当你键入回车开始新行时,光标停在和上一行第一个非空格所在的位置。当你分割行时,新的行也会开始在相同的缩进位置,任何新行后面的空白字符将被忽略掉。
智能制表键 - 当你按下Tab键的时候,将移动到上一行非空白字符开始处的下一个tab位置。如果在上一行没有找到相应的位置,将缩进8个字符。
保存优化 - 如果允许此选项,当保持文件的时候,空白区域将被优化的tab和空格填充来减少文件的大小。如果关闭此选项,空白区域将填充为空格(不保存最后一行的空格)。
Revive dead keys - left to do.
1.1.4 在命令行下执行编译器
在命令行下执行编译需要运行fasm.exe。fasm接受两个参数 - 第一个提供源码文件,第二个提供目标文件。如果没有给定第二个文件,输出文件名称将自动猜测一个。当显示简短的程序名称和版本后,编译器从源码文件中读取数据并且编译它。当编译成功,编译器将写入生成的文件到目标文件,并且显示编译过程总结;否则将显示发生的错误信息
源码文件必须是文本格式的,行结束符接受DOS(CR+LF)和Unix(LF)两种格式,tab将被当做空格处理。
在命令行中你可以指定-m选项用来指定fasm汇编器最大使用的内存(KB)。在DOS版本中,这个选项仅用来限定扩展内存的使用。-p选项后面用来指定汇编器要执行的遍数。如果代码不能再指定的遍后生成,汇编器将结束并给出错误信息。最大值为65536,默认值为100。这个参数可以用来限制汇编器最多执行的遍数,-p参数跟随一指定的最大遍数即可。
没有命令行参数来影响输出,flat汇编器仅需要源码文件来包含真正需要的信息。例如,为了制定输出格式你可以在源码文件开头使用format指令。
1.1.5 命令行编译器消息
如上面描述的那样,当成功编译后,编译器将显示编译总结。它包含执行了多少遍,消耗的时间,以及写入了多少字节到目标文件。下面是一个编译总结例子:
当编译错误时,程序将显示错误信息。比如,当编译器找不到收入文件时,将显示下面的信息:flat assembler version 1.66 38 passes, 5.3 seconds, 77824 bytes.
如果错误和部分源码相关,导致错误的源码行将被显示。相应行的位置也会给出,以帮助你快速定位错误,比如:flat assembler version 1.66 error: source file not found.
flat assembler version 1.66 example.asm [3]: mob ax,1 error: illegal instruction.
意思是example.asm的第三行编译时遇到了无法识别指令。如果导致错误行包含一个宏指令,生成错误指令的宏指令定义也将显示。比如:flat assembler version 1.66 example.asm [6]: stoschar 7 example.asm [3] stoschar [1]: mob al,char error: illegal instruction.
它的意思是example.asm的第六行宏指令生成了一个无法识别的指令,以及宏指令的第一行定义。1.1.6 输出格式
当源码中没有format指令时,flat简单的把生成的代码到输出文件中,创建flat二进制文件。默认生成的是16位代码,你可以通过use16或use32指令打开16位或者32位模式。一些选择一些输出格式时将切换到32位模式 - 更多你可以选择的格式可以参考 2.4节。
输出文件的扩展名编译器将根据输出格式自动选择
所有输出代码顺序将和源码文件的顺序一样。
1.2 汇编语法
下面的信息主要是给使用过其他汇编器的汇编程序员看的。如果你是初学者,你应当寻找汇编编程的教程。
Flat汇编器默认采用Intel的语法,虽然你可以使用预处理(宏指令和符号常量)来定制。它也包含一套伪指令 - 编译器的指令。
源码中定义的所有符号都区分大小写的。
操作符 | 位 | 字节 |
byte | 8 | 1 |
word | 16 | 2 |
dword | 32 | 4 |
fword | 48 | 6 |
pword | 48 | 6 |
qword | 64 | 8 |
tbyte | 80 | 10 |
tword | 80 | 10 |
dqword | 128 | 16 |
类型 | 位数 | |
通用寄存器 | 8 | al cl dl bl ah ch dh bh |
16 | ax cx dx bx sp bp si di | |
32 | eax ecx edx ebx esp ebp esi edi | |
段寄存器 | 16 | es cs ss ds fs gs |
控制寄存器 | 32 | cr0 cr2 cr3 cr4 |
调试寄存器 | 32 | dr0 dr1 dr2 dr3 dr6 dr7 |
FPU | 80 | st0 st1 st2 st3 st4 st5 st6 st7 |
MMX | 64 | mm0 mm1 mm2 mm3 mm4 mm5 mm6 mm7 |
SSE | 128 | xmm0 xmm1 xmm2 xmm3 xmm4 xmm5 xmm6 xmm7 |
1.2.1 指令语法
指令在汇编语言中是用行结束来分割的,一条指令占用一行文本。如果一行包含分号,除了双引号字符串中的分号外,这行剩余部分为一注释,编译器将忽略掉。如果一行为“\”字符(后面可能出现分号和注释),下一行将被连在“\”所在位置。
源码中每一行都是一些元素序列,其中可能为3中方式的一种。一种是符号字符,用来分割元素即使它们没有空格分开。任意的 +-*/=<>()[]{}:,|&~#‘ 为符号字符。其它字符,用空格或者符号字符串分割为符号。如果符号的第一个字符为单引号或双引号,其后的任意字符序列,甚至特殊字符,将被当做引用字符串。不为符号字符和引用字符串,可以用作名称,也叫做名称符号。
每一个指令包含助记符和一些用逗号分隔的操作数。操作数可以为寄存器、立即数或者内存中的数据,在操作数之前可以跟着size操作符,用来定义或重写大小(表1.1)。表1.2列出了可用的寄存器的名称,他们的大小是不能覆盖的。立即数可以指定为任意数值表达式。
当操作数为内存中数据是,数据的地址(也可以为任意数值表达式,但必须包含寄存器)必须用中括号括起来或者之前包含ptr运算符。例如:
指令 mov eax, 3 将把立即数3送到eax寄存器;
指令 mov eax, [7] 将把32位数据从地址7送到eax;
指令 mov byte [7], 3 将把立即数3赋值给地址7,也可以写成:mov byte ptr 7, 3。
为了指定寻址所用的段寄存器,段寄存器紧跟冒号,放在地址值前面(在中括号中或ptr运算符后面)。
1.2.2 数据定义
定义数据或保留空间,可以使用表1.3中的伪指令。数据定义伪指令必须跟着一个或多个逗号分隔的数值表达式。这些表达式定义的数据单元大小取决于使用的伪指令。例如:db 1,2,3将分别定义3个字节数据1, 2, 3。
db和du指示符还接受任意长度的字符串。当使用db时,将被转换为字节序列;使用du的时候,将被转换为高字节为0的字序列。例如db 'abc'将定义三个字节数据61,62和63。
dp指示符和其等价的df接受两个用冒号分隔的数字表达式为参数,第一个为高字,第二个将变成far指针值的低DWORD。dd也允许两个用冒号分隔的word指针,dt只允许接受一个浮点参数并以扩展双精度浮点格式创建数据。
上面的任意伪指令都允许使用dup操作符来重复拷贝给定的值。重复次数必须在该操作符前面,后面为要重复的值 - 也可以为一串用逗号分隔的值,如果这样的话必须用括号将这些值括起来,如db 5 dup(1,2)定义了五份给定两个字节序列的拷贝。
file是特殊的伪指令,其语法也是不同的。这个伪指令包含来自文件的字节流,它后面必须跟着文件名,然后是可选的文件偏移数值表达式(前面有一冒号),然后也是可选的逗号和要包含多少字节的数值表达式(如果没有指定的话将包含文件中的所有数据)。例如:
file 'data.bin'将包含这个文件为二进制数据。
应该是file 'data.bin':10h,4将只包含从10h文件偏移开始后的4个字节。
大小(字节) | 定义数据 | 保留数据 |
1 | db file | rb |
2 | dw du | rw |
4 | dd | rd |
6 | dp df | rp rf |
8 | dp df | rp rf |
10 | dt | rt |
数据保留伪指令值允许跟着一个数值表达式,这个值定义了多少个指定大小单元空间将被保留。所有的数据定义伪指令都允许“?”值,意思是这个单元不应初始化为任何值,效果和数据保留伪指令相同。未初始化的数据可能没有存在于输出文件中,所以其值应当总是被认为是不可知的。
1.2.3 常数和标号
在数值表达式中你可以使用常数或者标号来替代数字。常数或标号定义应当使用特殊的伪指令。每一个标号只允许定义一次,它可以在源码中的任何地方使用(即使在定义前)。常数可以定义多次,此时它只能在定义后才能使用,而且其值总是等于使用位置前最后一次定义的值。当常数在源码中只定义了一次,那么和标号相同可以在源码中的任何位置使用。
常数定义包含常数名后面跟着“=”字符以及数值表达式,这个在常数定义时计算数据表达式的值将成为常数的值。例如你可以使用伪指令“count=17”定义count常数,然后再汇编指令中使用它,比如mov cx, count - 编译时将变成mov cx, 17。
有不同的几种方式来定义标号。最简单的是在标号名后面跟着冒号,这条伪指令同行的后面甚至可以跟着其他指令。它定义的标号的值为定义位置的偏移。这种方式通常用来定义代码中的标号。另一种方式是标号名(没有冒号)后面跟着一些数据伪指令。它定义的标号的值为定义数据起始位置的偏移,并作为一个标号记住这个数据,其单元大小由表1.3中的数据伪指令指定。
标号可以当作标记代码或数据位置偏移的常数值。例如当你使用标号伪指令“char db 224”定义了数据,为了将这个数据的偏移放到bx寄存器,你应当使用“mov bx, char”指令,为了将char处的字节数据移动到dl寄存器,你应当使用“mov dl,[char]”(或者“mov dl, ptr char”)。当当你试图汇编“mov ax, [char]”,将会产生错误,因为FASM会比较操作数的尺寸,以确保它们是相等的。你可以通过size覆盖来强制汇编那条指令:“mov ax, word [char]”,但记住这条指令将在char位置读取两个字节,而实际上char只定义了一个字节。
最后也是最灵活的定义标号的方式是使用label伪指令。这条伪指令后面为标号名,然后是可选的size操作符,后面是可选的at操作符,以及标号定义地址的数值表达式。例如:“label wchar word at char”将为char地址的16位数据定义一个新的标号。现在“mov ax, [wchar]”将和“mov ax, word[char]”编译后的结果相同。如果没有指定任何地址,label伪指令就在当前位置定义标号。因此“mov [wchar], 57568”将拷贝两个字节,而“mov [char], 224”将拷贝一个字节到同一的地址。
以"."开头的标号被认为是局部标号,它附加在最后一个全局标号的后面(名称不以点开头的)来组成完整的标号名。所以你可以在定义另一个全局标号前使用这个标号的短名称(以点开头的),在其他位置你就必须使用完整的标号名。以两个点开头的“..”标号是个特例 - 它们如同全局变量,但它们不会成为局部标号的前缀。
@@为匿名标号,你可以在源码中多次定义它们。符号@b(或者等价于@r)引用最近的前面的匿名标号,符号@f引用最近的后面的匿名标号。这些特殊标号都不区分大小写。
1.2.4 数值表达式
在上面的例子中所有的数值表达式的都是简单的数字、常数或标号。通过编译期间计算的算术或者逻辑操作符也可以变得更复杂些。所有这些操作符和他们的优先级都列在表1.4中。高优先级运算操作先计算,当然你可以通过将某部分表达式用括号括起来来改变它的优先级。+、-、*和/是标准的算术运算操作,mod计算除操作后的余数。and、or、xor、shl、shr和not执行和汇编指令中同名指令相同的逻辑操作。rva用来转换一个地址到重定位的偏移,特定于某些输出格式。(见2.4)
优先级 | 操作符 |
0 | + - |
1 | * / |
2 | mod |
3 | and or xor |
4 | shl shr |
5 | not |
6 | rva |
表达式中的数字默认为十进制的,二进制数字可以在后面跟着字母b,八进制的跟着字母o,十六进制的以0x字母开头(如果C语言)或者以$开头(如果Pascal语言)或者以h字母结尾。当在表达式中遇到字符串时将被转换为数字 - 第一个字符将成为数字的最低位。
数值表达式用作地址可以用任意通用寄存器来寻址。它们可以加上或者乘以某个合适的值。
数值表达式中也可以使用一些特殊符号。第一是“$”,其值等于当前偏移值,而“$$”和当前地址空间的基地址相等。还有“%”,表示在某部分代码中使用特殊伪指令(见2.2)时当前重复次数。还有%t符号,等同于当前的时间戳。
任何数值表达式都可以包含科学计算法表示的单浮点数值(FASM不允许编译期间的浮点运算),它们可以以字母f结尾,否则它们必须至少包含字符"."或"E"。所以"1.0","1E0"和"1f"定义了相同的浮点数据,而简单的"1"定义了一个整型值。
1.2.5 跳转和调用
任何的jmp和call指令操作数前面不仅放size操作符,也可以放跳转类型操作符:short,near或far。例如当汇编器在16位模式下,指令jmp dword [0]将成为远跳转,而当在32位模式下,它将成为near跳转。为了强制这条指令来区分对待,可以使用jmp near dword [0]或者jmp far dword[0]格式。
当near跳转的操作数为立即数时,如果可能的话,汇编器将生成最短格式的跳转指令(但不要在16位模式下创建32位指令,也不要在32位模式下创建16位代码,除非它前面有size操作符)。通过指定跳转类型你可以强制生成长格式(例如“jmp near 0”)或者生成短格式并且如果不可能的话将产生错误(例如“jmp short 0”)。
1.2.6 操作数尺寸设置
当指令使用内存寻址时,如果寻址值在某个范围内,默认将使用短偏移来生成最短格式的指令。这可以通过在中括号中地址前面的word或dword操作符(或者ptr操作符后面)来重写,以强制使用长偏移。当地址不基于任何寄存器时,这些操作符将选择绝对寻址的合适模式。
指令adc、add、cmp、or、sbb、sub和xor第一个操作数为16位或者32位默认生成段的8位格式,如果第二个操作数为带符号字节范围内的立即数,将产生短格式的指令。可以通过在立即数前面放置word或dword操作符来重写。imul指令简单的规则是最后一个操作数为立即数。
push指令后面如果为立即数且没有size操作前缀的话,在16位模式下将当作一word值,在32位模式下将作为一dword值,如果可能将使用这条指令的短的8位格式,word或者dword size操作符强制push指令生成指定大小的长格式。pushw和pushd助记符强制汇编器生成16位或32位代码,而不是强制它使用长格式指令。
2 指令集
2.1 x86 体系指令
这一章讲述了汇编语言指令语法和功能。更多的技术信息可以阅读Intel软件开发者手册。
汇编指令有助记符(指令名称)和0到3个操作符组成。如果有大于两个的操作符,通常第一个为目的操作符第二个为源操作符。每个操作符都可以为寄存器,内存或立即数(操作符语法见1.2节)。每条指令描述后悔有操作符不同用法的例子。
一些指令用作前缀,可以和其他指令放在同行一起使用,一行也允许有多个前缀。段寄存器也是指令助记符前缀,但推荐在方括号中段重写来替代这些前缀。
2.1.1 数据传送指令
mov从源操作符传送字节,字或双字道目的操作符。它可以在通用寄存器之间,通用寄存器到内存,或从内存到通用寄存器间传送数据,但不能在内存间传送数据。它也可以在立即数到通用寄存器或内存,段寄存器到通用寄存器或内存,通用寄存器或内存到段寄存器,控制或调试寄存器到通用寄存器以及通用寄存器到控制或调试寄存器间传送数据。只有当源操作符和目的操作符大小相同时mov才能被汇编。下面是一些例子:xchg置换两个操作数内容。它可以用来置换两个字节、字或者双字操作数。操作数的顺序并不重要。操作数可以为两个通用寄存器,或者通用寄存器同内存。例如:mov bx,ax ; 通用寄存器到通用寄存器 mov [char],al ; 通用寄存器到内存 mov bl,[char] ; 内存到通用寄存器 mov dl,32 ; 立即数到通用寄存器 mov [char],32 ; 立即数到内存 mov ax,ds ; 段寄存器到通用寄存器 mov [bx],ds ; 段寄存器到内存 mov ds,ax ; 通用寄存器到段寄存器 mov ds,[bx] ; 内存到段寄存器 mov eax,cr0 ; 控制寄存器到通用寄存器 mov cr3,ebx ; 通用寄存器到控制寄存器
push递减堆栈指针(esp寄存器),然后传送操作数到esp执行的栈顶。操作数可以为内存,通用寄存器,段寄存器或字、双字立即数。如果操作数为没有指定大小的立即数时,汇编器在16位模式下默认将当作16位值,在32位模式下将当作32位值。pushw和pushd助记符为push指令的变种,分别用来压入16位,32位大小值到堆栈。如果同行后指定了更多参数(空格分隔,而非逗号),将汇编为一串push指令。下面为带有单一操作符的例子:xchg ax,bx ; 置换两个通用寄存器 xchg al,[char] ; 寄存器和内存置换
pusha压入8个通用寄存器的内容到堆栈,这条指令没有操作数。这条指令有两个版本,一个16位的和一个32位的,汇编器自动根据当前模式生成正确的版本,但也可以使用pushaw或pushad助记符重写为只为16位或32位版本。16位版本的这条指令将以以下顺序压入通用寄存器:ax,cx,dx,bx,压入ax前的sp值,bp,si和di。32为版本将以相同顺序压入等价的32位通用寄存器。push ax ; 压入通用寄存器到堆栈 push es ; 压入段寄存器 pushw [bx] ; 压入内存值 push 1000h ; 压入立即数
pop传送当前栈顶的字或双字到目的操作符,然后递增esp指向新的栈顶。操作符可以为内存,通用寄存器或段寄存器。popw和popd助记符为pop指令的变种,分别用来弹出字或双字。如果同行后指定了更多参数(空格分隔,而非逗号)将汇编为一串pop指令。
popa弹出堆栈中由pusha指令压入的寄存器,将忽略其中保存的sp(或esp)值。使用popaw或popad助记符来强制汇编16位或32位版本的这条指令。pop bx ; 弹出栈顶数据到通用寄存器 pop ds ; 弹出到段寄存器 popw [si] ; 弹出到内存
2.1.2 类型转换指令
类型转换指令转换字节为字,字为双字,双字为四字。这些转换可以为符号扩展或零扩展的。符号扩展将用符号位来填充,而零扩展将使用0来填充。
cwd和cdq分别用来扩展ax和eax大小,并将额外位存储到dx和edx中。转换将使用符号扩展。这些指令没有操作数。
cbw符号扩展al的值到ax,cwde符号扩展ax到eax。这些指令也没有操作数。
movsx使用符号扩展将字节转换为字或双字,字转换为双字。movzx类似,只是它使用0扩展。源操作数可以为通用寄存器或内存,目的操作数必须为通用寄存器。例如:
movsx ax,al ; 字节寄存器转换为字寄存器 movsx edx,dl ; 字节寄存器转换为双字寄存器 movsx eax,ax ; 字寄存器转换为双字寄存器 movsx ax,byte [bx] ; 字节内存之后为字寄存器 movsx edx,byte [bx] ; 字节内存转换为双字寄存器 movsx eax,word [bx] ; 字内存转换为双字寄存器
2.1.3 二进制算术指令
add替换目的操作数的值为源操作数和目的操作数的和,并且在溢出时设置CF标志。操作数可以为字节,字或双字。目的操作数可以为通用寄存器或内存,源操作数可以为通用寄存器或立即数,如果目的操作数为寄存器也可以为内存。
adc和add类似,只是如果设置CF的话结果还将递增1。add后跟着多个adc指令能用来计算大于32位值的和。add ax,bx ; add 寄存器到寄存器 add ax,[si] ; add 内存到寄存器 add [di],al ; add 寄存器到内存 add al,48 ; add 立即数到寄存器 add [char],48 ; add 立即数到内存
inc将操作数值递增1,它不影响CF。操作数可以为通用寄存器或内存,操作数大小可以为字节,字或双字。
sub用目的操作数值减去源操作数,并且用结果替换目的操作数。如果需要借位,将设置CF。操作数规则和add指令相同。inc ax ; 寄存器值递增1 inc byte [bx] ; 内存值递增1
sbb和sub类似,只是如果设置CF的话结构还将递减1。操作数规则和add质量相同。sub后跟着多个sbb指令能用来计算大于32位值的差。
dec将操作数值递减1,它不影响CF。操作数规则和inc指令相同。
cmp用目的操作数减去源操作数,类似sub指令更新标志值,但它不改变源和目的操作符。操作数规则和sub指令相同。
neg用0减去带符号的整数操作数。这条指令的效果是将带符号的操作数从正数变为负数或者从负数变为正数。操作数规则和inc指令相同。
xadd交换目的和源操作数,然后载入两个值的和到目的操作数。操作数规则和add指令相同。
所有上面的二进制算术指令都将更新SF,ZF,PF和OF标志。SF被设置为结果符号位的值,ZF当结果为0时设置为1,PF当低8位存在偶数个1时设置,OF在结果对于正数太大或对于负数太小(超过符号位)以放到目的操作数中时设置。
mul计算无符号操作数和累加器的积。如果为8位操作数,将和al计算积,16位结果返回到ah和al中。如果4,为16位操作数,将和ax计算积,32位结果返回到dx和ax中。如果为32位操作数,将和eax计算积,64位结果返回到edx和eax中。当结果高半部分不为零时将设置标志CF和OF,否则将清除该标志。操作数规则和inc指令相同。
imul执行符号乘法运算。这条指令有3种用法。第一种允许一个操作数,和mul指令类似。第二种有两个操作数,此时将计算目的操作数和源操作数的积,并将结果替换目的操作数。目的操作数可以为16位或32位通用寄存器,源操作数可以为通用寄存器,内存或立即数。第三种有3个操作数,目的操作数必须为16位或32位通用寄存器,源操作数可以为通用寄存器或内存,第三种操作数必须为立即数。源操作数乘以立即数并将结果保存到目的寄存器。所有上面三种形式都将计算出双倍大小的结构,并当结果高半部分不为零时设置标志CF和OF。所以第二种和第三种形式也能用作无符号操作数,因为,无论操作数是否为有符号无符号,结果的低半部分是相同的。下面的所有三种形式的乘法指令使用例子:
imul bl ; 累加器和寄存器 imul word [si] ; 累加器和内存 imul bx,cx ; 寄存器和寄存器 imul bx,[si] ; 寄存器和内存 imul bx,10 ; 寄存器和立即数 imul ax,bx,10 ; 寄存器,立即数,值到寄存器 imul ax,[si],10 ; 内存,立即数,值到寄存器
div计算操作数和累加器无符号运算的商。被除数(累加器)为两倍大小的除数(操作数),商和余数和除数有相同尺寸。如果除数为8位,被除数为ax,商和余数分别保存到al和ah中。如果除数为16位,被除数的商的高半部分从dx获取,低半部分从ax获取,商和余数分别保存到ax和dx中。如果除数为32位,被除数的高半部分从edx获取,低半部分从eax获取,商和余数分别保存到eax和edx中。操作数规则和mul指令相同。
idiv计算操作数和累加器有符号运算的商。它使用和div指令相同的寄存器,操作数的规则也是一样的。
2.1.4 十进制算术指令
十进制算术指令用来调整上一节的二进制算术操作以生成有效的压缩或未压缩十进制结果,或调整输入为一个二进制算术操作序列以使该操作能生产一个有效的压缩或解压缩十进制结果。
daa调整al中两个有效压缩十进制操作数和的值。daa必须跟着两对压缩十进制数(每半字节一个点)的和来得到一对有效压缩十进制数字结果。如果需要进位将设置CF标志。这条指令没有操作数。
das调整al中两个有效压缩十进制操作数差的值。das必须跟着两对压缩十进制数(每半个字节一个点)的差来得到一对有效压缩十进制数字结果。如果如要进位将设置CF标志。这条指令没有操作数。
aaa修改al中的内容为有效的未压缩十进制数字,并将高四位清零。aaa必须跟着al中两个有效未压缩十进制操作数和。如果需要进位将设置CF标志并递增ah的值。这条指令没有操作数。
aas修改al的值为一个有效的未压缩十进制数据,并将高四位清零。aas必须跟着al中两个有效未压缩十进制操作数差。如果需要进位将设置CF标志并递减ah的值。这条指令没有操作数。
aam修正两个有效未压缩十进制数的积。aam必须跟着两个十进制数字的积来生成一个有效的十进制结果。数字的高位在ah中,低位在al中。这条指令用来调整ax来生成两个任何基数的未压缩数字。标志版本的这条指令没有操作数,另一种有一个操作数 - 一个指定创建数字基数的立即数。
aad修改ah保存的分子和ah和ah中两个有效未压缩十进制操作数的商,所以计算的商将为一个未压缩十进制数据。ah为高位,al为低位。这条指令调整al的值,结果也在al中,而ah内容为0.这条指令用来调整任何基数的两个未压缩数字。操作数规则和aam质量相同。
2.1.5 逻辑指令
not将指定操作数求反。它不影响标志。操作数规则和inc指令相同。
and,or和xor质量执行标准的逻辑操作。它们更新标志SF,ZF和PF。操作数规则和add指令相同。
bt,bts,btr和btc指令只能处理一个在内存或寄存器中的位。该位位置由操作数的低位指定。偏移有第二个操作数指定,它可以为字节立即数或一个通用寄存器。这些指令首先将选择的位送到标志CF。bt指令不会做更多操作,bts设置选择位为1,btr将选择为置为0,btc修改将改位值求反。第一个操作数可以为字或双字。
bt ax,15 ; 测试寄存器中的位 bts word [bx],15 ; 测试并设置内存值中的位 btr ax,cx ; 测试并重置寄存器中的位 btc word [bx],cx ; 测试并求反内存值中的位
bsf和bsr质量扫描字或双字第一个为1的位,并将改为索引保存到必须为通用寄存器的目的操作数。源操作数可以为通用寄存器或内存。当整个串为0时设置ZF标志;否则将置为0。如果没有找到为1的位,谜底寄存器的值为未定义的。bsf从低位到高位扫描(从位索引0开始)。bsr从高位到低位扫描(16位时从第15位,32位时从31位开始)。
bsf ax,bx ; 向前扫描寄存器 bsr ax,[si] ; 逆向扫描内存值
shl左移目的操作数为第二个操作数指定的位数。目的操作数可以为字节,字,或双字通用寄存器或内存。第二个操作数可以为立即数或cl寄存器。左移的最后一位将被放到标志CF中。sal和shl为相同指令。
shl al,1 ; 左移寄存器一位 shl byte [bx],1 ; 左移内存值一位 shl ax,cl ; 左移寄存器为cl中的值 shl word [bx],cl ; 左移内存值为cl中的值
shr和sar右移目的操作数为第二个参数指定的位数。操作数规则和shl指令相同。shr右移的最后一位将放到标志CF中。sar保留操作数符号位,如果操作数为正数用0左移,否则用1左移。
shld左移目的操作数(第二个操作数)为第三个操作数指定的位数,lefto do。目的操作数为字或双字通用寄存器或内存,源操作数必须为通用寄存器,第三个操作数可以为立即数或cl寄存器。
shld ax,bx,1 ; 左移寄存器1位 shld [di],bx,1 ; 左移内存值一位 shld ax,bx,cl ; 左移寄存器为cl中的位数 shld [di],bx,cl ; 左移内存值为cl中的位数
shrd右移目的操作数,lefto to do。不修改源操作数。操作数规则和shld指令相同。
rol和rcl左转字节,字或双字目的操作数为第二个操作数指定的位数。对于每次转动,左转出来的位数将成为这个值新的低位。rcl指令还将把高位放到标志CF中。操作数规则和shl指令相同。
ror和rcr右转字节,字或双字目的操作数为第二个操作数指定的位数。对于每次转东,右转出来的位数将成为这个值新的高位。rcr指令还将把低位放到标志CF中。操作数规则和shl质量相同。
test执行和and指令相同的操作,但它不会修改目的操作数的值,只更新标志。操作数规则和and指令相同。
bswap翻转32位通用寄存器:0到7位翻转为23到31位,8到15位翻转为16位到23位。这条指令用来转换little-endian值为big-endian格式,反之亦然。
bswap edx ; 翻转寄存器值
2.1.6 控制转移指令
jmp无条件转移控制到目的位置。目的地址可以直接在指令中指定或间接通过寄存器或内存,允许的地址大小取决于跳转类型为near或far(通过在操作数前指定near或far操作数来指定)以及指令是否为16位或32位。对于16位指令near跳转操作数为16位,32位指令为32位。16位far跳转操作数大小为32位,32位指令为64位。一个直接jmp指令 包括作为指令一部分的目的地址(可以包含short,near或far操作符),指定地址的操作数对于near或短跳转为数值表达式,对于far跳转为两个用冒号分隔的数值表达式。第一个指定段选择子,第二个为段中偏移。pword操作符可强制为32位far调用,dword强制为16位far调用。间接jmp指令间接从寄存器或指针变量中获取目的地址,操作数应为通用寄存器或内存。细节见1.2.5节。
jmp 100h ; 直接near跳转 jmp 0FFFFh:0 ; 直接far跳转 jmp ax ; 间接near跳转 jmp pword [ebx]; 间接far跳转
call转移控制到过程,保存call后指令地址到堆栈,稍后将被ret(返回)指令使用。操作数规则和jmp指令相同,但call没有直接种类因此它不是最优的。
ret,retn和retf指令结束过程执行将转移控制给堆栈中call指令压入的地址。ret等价于retn,retn从near调用过程返回,而retf从far调用过程返回。这些指令默认地址大小和当前代码设置适合,但也可以使用retw,retnw和retfw助记符来强制大小为16位,使用retd,retnd和retfd助记符强制大小为32位。这些指令可可选的指定一个立即数操作数,用它和堆栈指针相加,它的作用是在执行call指令之前移除调用程序压入堆栈的参数。
iret返回控制到中断过程。它不同于ret的地方是它还将弹出堆栈中的标志到标志寄存器。这个标志是由中断机制保存的。它默认返回地址为当前代码设置,但也可以使用iretw或iretd助记符来强制使用16位或32位。
条件转移指令根据指令执行时CPU标志状态来决定是否转移控制。条件跳转助记符可以通过j助记符后面跟着表格2.1列出的条件助记符来得到,例如jc指令在CF设置时转移控制。条件跳转可以为short或near,仅能直接跳转,并且能优化(见1.2.5),操作数为指定目的地址的立即数。
loop指令为使用cx(或ecx)中指定软循环次数的条件跳转。所有loop指令自动递减cx(或ecx),并且在cx(或ecx)为0时结束循环。使用cx还是ecx取决于当前代码设置为16位还是32为,但也可以使用loopw助记符强制使用cx或使用loopd助记符强制使用ecx。loope和loopz是相同指令,用作标准loop,但它也在ZF为1时结束循环。loopew和loopzw强制使用cx寄存器,looped和loopzd强制使用ecx寄存器。loopne和loopnz是相同指令,用作标准loop,但它也在ZF为0时结束循环。loopnew和loopnzw助记符强制使用cx寄存器,loopned和loopnzd强制使用ecx寄存器。每一个loop指令都需要一个立即数值来指定目的地址,它只能为短调整(跟在loop指令前128字节和指令后127字节范围)。
jcxz在cx值为0时跳到指定标号,jecxz类似,但在ecx为0时跳到指定标号。操作数规则和loop指令类似。
int激活操作数指定的中断服务过程,中断号范围在0到255之间。中断服务过程以iret指令结束,返回控制给int后的指令。int3助记符为短格式的调用中断3的指令。into指令当OF为1的话调用中断。
bound检查指定寄存器中的符号值是否在指定范围内。如果不在这个范围将产生中断5。它需要两个参数,第一个操作数为要测试的寄存器,第二个操作数为范围。操作数大小为word或dword。
bound ax,[bx] ; 检查word数据边界 boudn eax,[esi]; 检查dword数据边界
助记符 | 测试条件 | 描述 |
o | OF=1 | 溢出 |
no | OF=0 | 不溢出 |
c b nae | CF=1 | 进位 小于 不大于 |
nc ae nb | CF=0 | 不进位 不大于 不小于 |
e z | ZF=1 | 相等 零 |
ne nz | ZF=0 | 不相等 不为0 |
be na | CF或ZF=1 | 小于或等于 不大于 |
a nbe | CF或ZF=0 | 大于 不小于不等于 |
s | SF=1 | 有符号 |
ns | SF=0 | <无符号/td> |
p pe | PF=1 | 偶校验 |
np po | PF=0 | 奇校验 |
l nge | SF异或OF=1 | 小于 不大于不等于 |
ge nl | SF异或OF=0 | 大于或等于 不小于 |
le ng | (SF异或OF)或ZF=1 | 小于或等于 不大于 |
g nle | (SF异或OF)或ZF=0 | 大于 不小于不等于 |
2.1.7 I/O 指令
in从输入端口传输字节,字或双字到al,ax,或eax。I/O端口可以用与指令一起编码的字节立即数直接寻址,或间接使用dx寄存器。目的操作数为al,ax或eax寄存器。源操作数应当为0到255之间的立即数,或dx寄存器。
in al,20h ; 从端口20h输入字节 in ax,dx ; 从dx寻址的端口输入字
out传送字节,字,或者双字到al,ax,或eax指定的输出端口。程序可以使用与in指令相同的方法指定端口号。目的操作数应当为0到255之间的立即数,或dx寄存器。源操作数应为al,ax或eax寄存器。
out 20h,ax ; 输出字道端口20h out dx,al ; 输出字节到dx寻址的端口
2.1.8 字符串操作指令
字符串操作针对字符串的一个元素。字符串元素可以为字节,字或双字。字符串元素用si和di(或esi和edi)寻址。每次字符串操作后si和或di(或esi和或edi)自动更新指向字符串中后一个元素。如果DF(方向标志位)为0,将递增索引寄存器,否则将递减。取决于字符串元素的大小在递增或递减大小为1,2或4。每一个字符串操作指令都有不使用任何操作数的简短格式,在16位下使用si或和di,在32位下使用esi或和edi。si和esi默认从ds段中定位数据,di和edi默认从es段中定位数据。当字符串操作助记符后跟着指定字符串元素大小的字母时将使用短格式,“b”为字节元素,“w”为字元素,“d”为双字元素。字符串操作完整格式需要指定尺寸操作符和内存地址的操作数,操作数可以为跟有任何段前缀的si或esi,di或edi通常和es段前缀使用。
movs传送si(或esi)指向的字符串元素到di(或edi)指向的地址。操作数尺寸可以为byte,word或dword。目的操作数应当为di或edi寻址的内存,源操作数应当为跟着任何段前缀si或esi寻址的内存。
movs byte [di],[si] ; 传送字节 movs word [es:di],[ss:si] ; 传送字 movsd ; 传送双字
cmps用目的字符串元素减去源字符串元素并更新标志AF,SF,PF,CF和OF,但它不会修改任何比较元素。如果字符串元素相当,ZF设置为1,否则为0。第一个操作数为带有任何段前缀的si或esi定位的源字符串元素,第二个操作数为di或edi定位的目的字符串。
cmpsb ; 比较字节 cmps word [ds:si],[es:di] ; 比较字 cmps dword [fs:esi],[edi] ; 比较双字
scas用al,ax,或eax(取决于字符串元素的尺寸)减去目的字符串元素并更新标志AF,SF,ZF,PF,CF和OF。如果值相等,ZF将设置为1,否则为0.操作数因为di或edi定位的目的字符串元素。
scas byte [es:di] ; scan 字节 scasw ; scan 字 scas dword [es:edi] ; scan 双字
lods载入字符串元素到al,ax,或eax。操作数因为带有任何段前缀的si或esi寻址的字符串元素。
lods byte [ds:si] ; load 字节 lods word [cs:si] ; load 字 lodsd ; load 双字
stos将al,ax,或eax的值放到目的字符串元素。字符串规则和scas指令相同。
ins从dx寻址的输入端口传送一个字节,字或者双字到目的字符串元素。目的操作数应当为di或edi寻址的内存,源操作数应当为dx寄存器。
insb ; input 字节 ins word [es:di],dx ; input 字 ins dword [edi],dx ; input 双字
outs传送源字符串元素到dx寄存器寻址的输出端口。目的操作数应当dx寄存器,源操作数应当为带有可带有任何段前缀的si或esi寻址的内存。
outs dx,byte [si] ; output 字节 outsw ; output 字 outs dx,dword [gs:esi] ; output 双字
重复前缀rep,repe/repz,和repne/repnz指定重复字符串操作。当一个字符串操作指令包含重复前缀时,操作将重复执行,每一次将使用不同的字符串元素。当前缀指定的一个条件满足时结束重复。每次操作后所有3个前缀自动递减cx或ecx寄存器(取决于是否字符串操作指令使用16位或32位寻址),并且重复指定的操作指导cx或ecx为0。repe/repz和repne/repnz仅和scas和cmps指令使用(下面讲述的)。当使用这些前缀时,取决于ZF标志重复后面的指令,此外,当ZF为0时repe和repz结束执行,当ZF为1时,repne和repnz结束执行。
rep movsd ; 传送多个双字 repe cmpsb ; 比较字节直到不相等
2.1.9 标志控制指令
标志控制指令用来直接修改标志寄存器中的状态位。这节讲述的所有指令都没有操作数。
stc设置进位标志CF为1,clc清零CF,cmc逆反CF的值。std设置方向标志DF为1,cld清零DF,sti设置中断标志IF为1以允许中的,cli清零IF以禁止中断。
lahf拷贝SF,ZF,AF,PF,和CF到ah寄存器的位7,6,4,2,和0。其余位将不受影响。 标志位保持不变。
sahf将ah的位7,6,2,和0传送到SF,ZF,AF,PF,和CF。
pushf将esp值递减2或4,压入低16位或32位的符号寄存器到堆栈,压入数据大小取决于当前代码设置。pushfw强制压入16位,pushfd强制压入32位。
popf从栈顶弹出16位或32位数据到符号寄存器,然后递减esp值为2或4,递减大小取决于当前代码设置。popfw强制弹出16位,popfd强制弹出32位。
2.1.10 条件操作指令
这些指令有set助记符,条件助记符(见表2.1)组成,如果条件为true设置一个字节为1否则为置为0。操作数必须为8位的通用寄存器或内存中字节。
setne al ; 如果ZF为0设置al seto byte [bx] ; 如果溢出设置byte
salc指令当CF为0时设置al的所有位为1,否则都置为0.这条指令没有参数。
cmov助记符后面跟着条件助记符组成的指令,仅当条件满足时传送通用寄存器中的word或dword到通用寄存器。目的操作数必须为通用寄存器,源操作数可以为通用寄存器或内存。
cmove ax,bx ; 当ZF为1时传送 cmovnc eax,[ebx] ; 当CF为0时传送
cmpxchg比较al,ax或eax和目的操作数。如果两个值相等,源操作数载入到目的操作数,否则目的操作数载入到al,ax或eax寄存器。目的操作数可以为通用寄存器或内存,源操作数必须为通用寄存器。
cmpxchg dl,bl ; 和寄存器比较并交换 cmpxchg [bx],dx ; 和内存比较并交换
cmpxchg8b比较edx和eax组成的64位值和目的操作数比较。如果值相等,ecx和ebx中64位值将保存到目的操作数。否则目的操作数值保存到edx和eax寄存器。目的寄存器必须为内存中的qword。
cmpxchg8b [bx] ; 比较并交换8字节
2.1.11 其他指令
nop指令占用一个字节但除了指令指针外没有任何作用。这条指令没有操作数,不会执行任何操作。
ud2指令生成一个无效的指令异常。这条指令用作软件测试来显式字生成一个无效指令。这条指令没有操作数。
xlat替换al寄存器字节为bx或ebx寻址的转换表中al索引的字节。操作数必须为可带有任何段前缀的bx或ebx寻址的内存中一个字节。这条指令有一个没有任何操作数的短格式xlatb,它使用ds段寄存器中bx或ebx(取决于当前代码设置)中的地址。
lds转移源操作数中的指针变量到ds和目的寄存器。源操作数必须为内存操作数,目的寄存器必须为通用寄存器。ds寄存器接受段选择子,目的寄存器接受指针偏移部分。les,lfs,lgs和lss操作和lds类似,只是它们分别使用es,fs,gs和ss寄存器,而不是ds寄存器。
lds bx,[si] ; 载入指针到ds:bx
lea传输源操作数偏移(而不是值)到目的寄存器。源操作数必须为内存操作数,目的寄存器必须为同一寄存器。
lea dx,[bx+si+1] ; 载入有效地址到dx
cpuid返回处理器标识和特性信息到eax,ebx,ecx和edx寄存器。指令执行前eax寄存器为参数。该指令没有操作数。
pause指令延迟指定时间执行下一条指令。它可用来提高死等效率。这条指令没有操作数。
enter创建堆栈框架,可用作实现块结构高级语言的范围规则。leave指令在过程结束后和过程开头的enter一起用来简化堆栈管理,并用作嵌套过程中控制访问变量。enter指令有两个参数。第一个指定堆栈中要分配的动态存储字节大小。第二个参数为相应嵌套层数,范围为0到31。指定层数决定了多少堆栈框架指针从前面一个框架中拷贝新的堆栈框架。堆栈框架通常叫做显示。显示的第一个word(当代码为32位时为dword)为最后的堆栈框架。这个指针允许leave指令通过废弃上一个堆栈帧来逆向前面的enter指令动作。当enter为过程创建一个新的显示后,通过递减esp为指定字节来分配需要的动态存储空间。允许过程寻址显示,enter保留bp(后ebp)指向新堆栈框架。如果嵌套层数为0,enter压入bp(或ebp),拷贝sp到bp(或esp到ebp),然后esp递减第一个操作数大小。对于嵌套层数大于0的,处理器在调整堆栈指针前压入额外的框架指针。
enter 2048,0
2.1.12 系统指令
lmsw载入操作数到机器状态字(CR0的0到15位),而smsw保存机器状态字到目的操作数。这两条指令操作数可以为16位通用寄存器,对于smsw还可以为32位通用寄存器。
lmsw ax ; 从寄存器载入机器状态字 smsw [bx] ; 载入机器状态字到内存
lgdt和lidt指令分别用来载入操作数中的值到全局描述表寄存器和中断描述表寄存器。sgdt和sidt分别用来保存全局描述表或中断描述表寄存器到目的操作数。操作数必须为内存中的6个字节。
lgdt [ebx] ; 载入全局描述表
lldt载入操作数到局部描述表寄存器的选择子,sldt保存局部描述表寄存器段选择子到操作数。ltr载入操作数到任务寄存器段选择子,str保存任务寄存器选择子到操作数。操作数规则和lmsw,smsw指令相同。
lar载入源操作数指定的选择子对应的段描述符访问权限到目的操作数,并设置ZF标志。目的操作数可以为16位或32为通用寄存器。源操作数必须为16位通用寄存器或内存。
lar ax,[bx] ; 载入访问权限到word lar eax,dx ; 载入访问权限到dword
lsl从源操作数选择子指定的段描述符的段限制到目的操作数并设置ZF标志。操作数规则和lar指令相同。
verr和verw检查操作数指定代码或数据段是否能以当前特权级上读或写。操作数必须为word,可以为通用寄存器或内存。如果可用段并且可读(对于verr)或可写(对于verw),将设置ZF为1,否者ZF置为0。操作数规则和lldt指令相同。
arpl比较两个段选择子的RPL(请求特权级)。第一个操作数包含一个段选择子,第二个包含另一个。如果目的操作数RTL小于源操作数的RPL,ZF置为1,否则置为0,这条指令不影响目的操作数。目的操作数可以为16位通用寄存器或内存,源操作数必须为通用寄存器。
arpl bx,ax ; 调整寄存器选择子RPL arpl [bx],ax ; 调整内存选择子RPL
clts清零CR0寄存器的任务切换TS位。这条指令没有操作数。
lock前缀导致处理器在执行该指令期间断言总线锁定信号,总线锁定信号保证处理器在信号断言期间独占使用任何共享内存。lock前缀只能用在以下指令,并且目的操作数为内存:add,adc,and,btc,btr,bts,cmpxchg,cmpxchg8b,dec,inc,neg,not,or,sbb,sub,xor,xadd和xchg。如果lock前缀和上面其中一指令使用并且源操作数为内存,可能会产生未定义指令异常。一个未定义指令异常也可能在lock和不在上面列出的指令一起使用的时候产生。xchg指令常用来断言总线锁定信号无论是否使用lock前缀。
invlpg无效(写)操作数指定的转换后援缓冲项TLB。处理器定位这些地址包含的页并为这些页回写TLB项。
rdmsr载入64位MSR(model specific register)ecx中的地址到edx和eax。wrmsr写edx和eax到ecx寄存器指定的64位MSR。rdtsc从64位MSR载入当前处理器时间戳到edx和eax寄存器。处理器每一时钟周期递增MSR时间戳,每次处理器重置时重置时间戳为0。rdpmc载入edx寄存器指定的40位性能监视计数器到edx和eax。这些指令没有操作数。
wbinvd回写处理器内部缓冲中所有修改的缓冲行到主内存,并且无效内部缓冲。然后创建一个特殊函数总线周期来指导外部缓冲也回写外部修改数据以及另一个时钟周期来标识外部缓冲无效。这条指令没有操作数。
rsm从系统管理模式返回到当处理器接受SMM中断时所处的模式。这条指令没有操作数。
sysenter执行到ring 0系统过程的快速调用,sysexit执行到ring 3的快速返回。这些指令是否可用有MSR相关位标识。这些指令没有操作数。
2.1.13 FPU 指令
浮点单元FPU指令操作三种格式的浮点数据:单精度(32位),双精度(64位)和扩展双精度(80位)。FPU寄存器构成一个堆栈,并且它们都是扩展双精度的。当从堆栈中压入或弹出一些值时,FPU寄存器移动,所以st0一直为FPU堆栈栈顶的值,st1为栈顶下的第一个值。st0和st是同义词。
fld压入浮点数据到FPU寄存器堆栈。操作数可以为32位,64位或80位内存地址或FPU寄存器,其值将稍后载入到FPU寄存器堆栈栈顶(也就是st0寄存器),并且自动转换为扩展双精度格式。
fld dword [bx] ; 从内存载入单精度浮值。 fld st2 ; 压入st2的值到寄存器堆栈
fld2,fldz,fldl2t,fldl2e,fldpi,fldlg2和fldln2载入常用的常量到FPU寄存器堆栈。载入的常量分别为:+1.0,+0.0,log2|10,log2|e,pi,log10|2和ln2。这些指令没有操作数。
fild转换一个源操作数整数为扩展双精度浮点格式,并将结果压入FPI寄存器堆栈。源操作数可以为16位,32位,或64位内存地址。
fild qword [bx] ; 从内存载入64位整数
fst拷贝st0寄存器的值到目的操作数,目的操作数可以为32位或64位内存地址或另一个FPU寄存器。fstp执行和fst相同的操作,只是它还将弹出寄存器堆栈。fstp执行和fst相同的操作,只是它还将压入一个80位内存中的值。
fst st3 ; 拷贝st0值到st3寄存器 fstp tword [bx] ; 存储内存中值并弹出堆栈
fist转换st0值为一整数,并保存结果到目的操作数。操作数可以为61位或32位内存地址。fistp执行相同操作,但很将弹出寄存器堆栈,并能存储值到64位内存,操作数规则和fild指令相同。
fbld转换压缩BCD整数为扩展双精度浮点格式并压入值到FPU堆栈。fbstp转换st0中的值为18个数字压缩BCD整数,保存结果到目的操作数并弹出寄存器堆栈。操作数应为80位内存地址。
fadd计算目的和源操作数的和并保存结果到目的操作数。目的操作数一直为FPU寄存器,如果源操作数为内存地址,目的操作数为st0寄存器并且只指定源操作数。内存操作数可以为32位或64位值。
fadd qword [bx] ; 计算扩展双精度和st0的和 fadd st2,st0 ; 计算st0和st2的和
faddp计算目的和源操作数的和,并保持结果到目的位置,然后弹出堆栈。目的操作数必须为FPU寄存器,源操作数必须为st0。当没有指定操作数时,将使用st1作为目的操作数。
faddp ; 计算st0和st1的和并弹出堆栈 faddp st2,st0 ; 计算st0和st2的和并弹出堆栈
fiadd指令转换源操作数整数为扩展双精度浮点数,并和目的操作数相加。操作数必须为16位或32位内存地址。
fiadd word[bx] ; word整数和st0相加
fsub,fsubr,fmul,fdiv,fdivr指令和fadd类似,操作数规则和fadd相同。fsub计算目的操作数和源操作数的差,fsubr计算源操作数和目的操作数的差,fmul将目的和源操作数相乘。fdivr计算目的操作数和源操作数的差,fdivr计算源操作数和目的操作数的差。fsubp,fsubrp,fmulp,fdivp,fidivr在转换源操作数整数为浮点数据后执行这些操作,它们操作数的规则和fiadd指令相同。
fsqrt计算st0寄存器中值的开方。fsin计算值的sin,fabs清除符号位来得到绝对值,frndint根据当前四舍五入模式来得到最接近的整数值。f2xm1计算2的以st0为幂的指数,并减去1.0,st0的值的范围必须在-1.0和+1.0之间。所有这些指令保存结果到st0并且没有操作数。
fsincos计算st0值的sin和cos,保存sin结果到st0,压入cos值到FPU寄存器堆栈。fptan计算st0的tag值,保存结果到st0,并压入值1.0堆栈。fpatan计算st1的arctag,并和st0相除,保存结果到st1并弹出寄存器堆栈。fyl2x计算st0的二进制算术结果,乘以st1值,保存结果到st1,然后弹出FPU寄存器堆栈。fyl2xp1执行相同操作,但它在计算对数前和1.0相加,保存结果到st0。fprem计算st0和st1相除的余数到st1,结果到st0。fprem1执行和fprem相同的操作,但它计算IEEE标志754指定的余数。fscale截去st1的值并和st0值相加。fxtract分隔st0值为指数和有效数字,保存指数到st0,压入有效数字到寄存器堆栈。fnop不执行任何操作。这些指令没有操作数。
fxch交换st0和另一个FPU寄存器的内容。这个操作数必须为FPU寄存器,不用指定操作数,st0和st1内容将被交换。
fcom和fcomp比较st0和源操作数,并格局结构设置FPU状态字标志。fcomp执行操作后还将弹出寄存器堆栈。操作数可以为内存中单精度或双精度浮点或FPU寄存器。当没有指定源操作数时将使用st1.
ficom word [bx] ; 16位整数和st0比较
fcomi,fcomip,fucomi,fucomip用st0和另一个FPU寄存器比较并根据结果设置标志ZF,PF和CF。fcomip和fucomip还将在执行操作后弹出寄存器堆栈。fcmov助记符后面跟着表2.2列出的FPU条件助记符组成的指令如果给定测试条件为true时传送指定FPU寄存器到st0寄存器。这些指令有两种不同语法,一种是跟着指定源FPU寄存器的单一操作数,另一种带有两个操作数,此时目的操作数为st0,第二个操作数为源FPU寄存器。
fcomi st2 ; 比较st0和st2并设置标志 fcmovb st0,st2 ; 如果小于传送st2到st0
助记符 | 测试条件 | 描述 |
b | CF=1 | 小于 |
e | ZF=1 | 等于 |
be | CF或ZF=1 | 不大于 |
u | PF=1 | 无序的 |
nb | CF=0 | 不小于 |
ne | ZF=0 | 不相等 |
nbe | CF且ZF=0 | 大于 |
nu | PF=0 | 有序的 |
ftst比较st0和0.0并根据结果设置FPU状态字标志。fxam检查st0内容并设置FPU状态字来标识该寄存器值类型。这些指令没有操作数。
fstsw和fnstsw保存当前FPU状态字到目的位置。目的操作数可以为16位内容或ax寄存器。fstsw在保持状态字前检查未知的没有屏蔽的FPU异常,而fnstsw不这么做。
fstcw和fnstcw保存当前FPU状态字到指定的内存中目的地址。fstcw在保持状态字前检查未知没有屏蔽的FPU异常,而fnstcw不这样。fldcw载入操作数到FPU控制字。操作数必须为16位内存地址。
fstenv和fnstenv保存当前FPU操作环境到目的操作数指定的内存地址,然后屏蔽所有FPU异常。fstenv在处理前检查待处理的未屏蔽的FPU异常,fnstenv将不检查。flden从内存中载入完整的操作环境到FPU。fsave和fnsave保存当前FPU状态(操作环境和寄存器堆栈)到内存中定制的目的地址并重新初始化FPU。fsave在处理前检查待处理的非屏蔽FPU异常,fnsave不检查。frstor从指定内存位置载入FPU状态。所有这些指令都需要一个内存位置操作数。
finit和fninit设置FPU操作环境到默认状态。finit在处理前检查待处理非屏蔽FPU异常,而fninit不检查。fclex和fnclex清除FPU状态字中FPU异常标志。fclex在处理前检查待处理非屏蔽FPU异常,fnclex不检查。wait和fwait为相同指令,将导致处理器检查待处理的非屏蔽FPU异常并在处理前处理它们。这些指令没有操作数。
ffree设置和指定FPU寄存器相关的tag为0。操作数必须为一个FPU寄存器。
fincstp和fdecstp翻转FPU堆栈为1或栈顶指针减1.这些指令没有操作数。
2.1.14 MMX 指令
MMX指令 操作压缩整数或MMX寄存器,MMX寄存器为80位FPU寄存器的低64位。因为此MMX指令不能和FPU指令一起使用。他们可以操作压缩字节(八个8位整数),压缩字(四个16位整数)或压缩双字(两个32位整数),使用压缩格式允许一次对多个数据执行操作。
movq从源操作数拷贝8字节到目的操作数。至少一个操作数必须为MMX寄存器,第二个可以为MMX寄存器或64位内存地址。
movq mm0,mm1 ; 寄存器到寄存器移动8字节 movq mm2,[ebx] ; 内存到寄存器移动8字节
movd从源操作数移动双字到目的操作数。其中一个操作数必须为MMX寄存器,第二个可以为通用寄存器或32位内存地址。只使用MMX寄存器的低双字。
所有通用MMX操作有两个操作数,目的操作数应当为MMX寄存器,源操作数可以为MMX寄存器或64位内存地址。对源和目的操作数执行相应操作并保存数据单元到目的操作数。paddb,paddw和paddd计算压缩字节,压缩字,压缩双字的和。paddsb,paddsw,psubsb和psubsw执行压缩字节或压缩字的带符号saturation的和。paddusb,paddusw,psubusb,psubusw类似,但将计算无符号saturation。pmulhw和pmullw符号乘压缩字,保存结果的高位或低位到目的操作数。pmaddwd乘压缩字,加上四个立即双字对来生成压缩双字结果。pand,por和pxor执行qword逻辑操作。pcmpeqb,pcmpeqw和pcmpeqd比较压缩字节,压缩字或压缩双字是否相等。如果某对数据元素相等,目的操作数中相应数据元素将填充为1,否则填充0.pcmpgtb,pcmpgtw和pcmpgtd执行相同操作,但它们用来检查是否目的操作数中数据元素大于源操作数中数据元素。packsswb转换带压缩符号字为压缩带符号字节,使用saturation来处理溢出。packuswb转换压缩符号字道压缩无符号字节。源操作数中转换后的数据单元存储到目的操作数的低部分,目的操作数中转换后的数据单元存储到高半部分。punpckhbw,punpckhwd和punpckhdq从源操作数和目的操作数高半部分插入数据单元并保持结果到目的操作数。punpcklbw,punpcklwd和punpckldq执行相同操作,但它们使用源和目的操作数的低半部分。
paddsb mm0,[esi] ; 计算压缩字节符号saturation和 pcmpeqw mm3,mm7 ; 比较压缩字是否相当
psllw,pslld和psllq对压缩字,压缩双字或目的操作数中的一个qword执行逻辑左移,左移位数由源操作数指定。pswlw,psrld和psrlq对压缩字,压缩双字或目的操作数中的一个qword执行逻辑右移。psraw和psrad对压缩字或双字执行算术右移。目的操作数因为MMX寄存器,而源操作数可以为MMX寄存器,64位内存为孩子,或8位立即数。
psslw mm2,mm4 ; 逻辑左移word psrad mm4,[ebx] ; 算术右移dword
emms是得FPU寄存器可用。如果使用了MMX指令,它必须在使用FPU指令前使用。
2.1.15 SSE 指令
SSE扩展增加了更多MMX指令,并且能操作压缩单精度浮点数。128位压缩单浮点格式由4个单精度浮点数组成。128位SSE寄存器设计用来操作这种数据类型。
movapshemovups传送源操作数中一个包含单进度值的双qword操作数到目的操作数。至少一个操作数必须为SSE寄存器,第二个操作数可以为SSE寄存器或128位内存地址。movaps指令的内存操作数必须对齐在16位字节边界,movups指令操作数不需要对齐。
movups xmm0,[ebx] ; 传送未对其双qword
movlps在内存和SSE寄存器低qword之间移动两个压缩单精度数据。movhps在内存和SSE寄存器高qword之间移动两个压缩单精度数据。其中一个操作数必须为SSE寄存器,另一个必须为64位内存地址。
movlps xmm0,[ebx] ; 移动内存到xmm0低qword movhps [esi],xmm7 ; 移动xmm7高qword到内存
movlhps从源寄存器的低qword移动压缩的两个浮点数据到目的寄存器。movhlps从源寄存器高qword移动两个压缩单浮点数到目的寄存器的低qword。这两个操作数都必须为SSE寄存器。
movmskps传送SSE寄存器中4个单浮点数据的最高位到一个通用寄存器的低4位。源操作数必须为SSE寄存器,目的操作数必须为通用寄存器。
movss在源和目的操作数(只传送低dword)传送单浮点数据。至少一个操作数必须为SSE寄存器,第二个操作数可以为SSE寄存器或32位内存地址。
movss [edi],xmm3 ; 移动xmm3低dword到内存
每一个SSE算术操作都有两种。当助记符以ps结尾时,源操作数可以为128位内存地址或SSE寄存器,目的操作数必须为SSE寄存器,操作压缩的四个浮点数据,对于对应数据元素对,结果保存在目的寄存器。当助记符以ss结尾时,源操作数可以为32位内存地址或SSE寄存器,目的操作数必须为SSE寄存器,操作于单浮点数据,此时只适用SSE寄存器的低dword。addps和addss计算和,mulps和mulss计算积,divps和divss计算目的值和源值的商,rcpps和rcpss计算源操作数的近似倒数,sqrtps和sqrtss计算源操作数的开放,rsqrtps和rsqrtss计算源值的开方的近似倒数,maxps和maxss比较源和目的值并返回大的值,minps和minss计算源和目的值并返回小的值。
mulss xmm0,[ebx] ; 乘以单浮点数据 addps xmm3,xmm7 ; 加上压缩单浮点数据
andps,andnps,orps和xorps对压缩单精度数据执行逻辑操作。源操作数可以为128位内存地址或SSE寄存器,目的操作数必须为SSE寄存器。
cmpps比较压缩单精度数并返回结果掩码到目的操作数,目的操作数只能为SSE寄存器。源操作数可以为128位诶从地址或SSE寄存器,第三个参数必须为表2.3中列出的8个比较条件操作数立即数。cmpss对单浮点数据执行相同的操作,但它只影响目的寄存器的低dowrd,此时源操作数可以为32位内存地址或SSE寄存器。这两个指令也包含只有两个操作数和条件编码的助记符。这些助记符有cmp助记符后跟着表2.3列出的助记符,以及ps或ss构成。
cmpps xmm2,xmm4,0 ; 比较压缩单精度值 cmpltss xmm0,[ebx] ; 比较单精度数据
comiss和ucomiss比较单精度并设置标志ZF,PF和CF来表示结果。目的操作数必须为SSE寄存器,源操作数可以为32位内存地址或SSE寄存器。
代码 | 助记符 | 描述 |
0 | eq | 相等 |
1 | lt | 小于 |
2 | le | 小于后等于 |
3 | unord | 无序 |
4 | neq | 不等 |
5 | nlt | 不小于 |
6 | nle | 不小于不等于 |
7 | ord | 有序 |
shufps从目的操作数移动任何两个四单精度数据到目的操作数的低qword,源操作数中4个值的任何两个到目的操作数的高qword。目的操作数必须为SSE寄存器,源操作数可以为128位内存地址或SSE寄存器,第三个操作数必须8位立即数来指定选择移动那些数据到目的操作数。位0和1选择移动目的操作数到结果的低dword,位2和3移动目的操作数到第二个dword,位4和5移动源操作数的到结果的第三个dword,位6和7移动源操作数到结果的高dword。
shufps xmm0,xmm0,10010011b ; 搅乱dword
unpckhps执行从源和目的操作数高部分插入的未压缩数据,并保存结果到目的操作数。源操作数可以为128位内存地址或SSE寄存器。unpcklps执行从源和目的操作数低部分插入的未压缩数据,并保持结果到目的操作数,操作数规则相同。
cvtpi2ps转换压缩的2dword整数到压缩的2单浮点数据,并保存结果到目的操作数的低qword,目的操作数应为SSE寄存器。源操作数可以为64位内存地址或MMX寄存器。
cvtpi2ps xmm0,mm0 ; 整合为单精度数
cvtsi2ss转换dword整数位单精度浮点数并保存结果到目的操作数的低dword,目的操作数必须为SSE寄存器。源操作数可以为32位内存地址或32位通用寄存器。
ctsi2ss xmm0,eax ; 整合为但精度数
cvtps2pi转换2单精度浮点数为压缩2dword整数并保存结果到目的操作数,目的操作数必须为通用寄存器。源操作数可以为64位内存地址或SSE寄存器,只适用SSE寄存器的低qword。cvttps2pi操作结果类似,除了截去近似为整数,操作数规则相同。
cvtps2pi mm0,xmm0 ; 单精度数到整数
cvtss2si转换2单精度浮点数为压缩2dword整数并保存结果到目的操作数,目的操作数必须为32位通用寄存器。源操作数可以为32位内存地址或SSE寄存器,只适用SSE寄存器的低qword。cvttss2pi操作结果类似,除了截去近似为整数,操作数规则相同。
cvtss2pi eax,xmm0 ; 单精度数到整数
pextrw拷贝第三个操作数指定的源操作数word到目的操作数。源操作数必须为MMX寄存器,目的操作数必须为32位通用寄存器(仅影响低word),第三个操作数必须为8位立即数。
pextrw eax,mm0,1 ; 取word到eax
pinsrw插入第三个操作数指定的word到目的操作数中第三个操作数指定的位置,第三个操作数必须为8位立即数。目的操作数必须为MMX寄存器,源操作数可以为16位内存地址或32位通用寄存器(只适用寄存器的低word)。
pinsrw mm1,ebx,2 ; 从ebx插入word
pavgb和pavgw计算压缩字节或字平均值。pmaxub返回压缩无符号字节的最大值,pminub返回压缩无符号字节的最小值,pmaxsw返回压缩无符号字的最大值,pminsw返回压缩无符号字的最小值。pmulhuw执行无符号压缩字乘法并保存结果到目的操作数的高word。psadbw计算压缩无符号字节绝对差别,汇总不同点,并保存汇总到目的操作数的低word。所有这些指令操作数规则和上一节讲述的MMX操作相同。
pmovmskb创建源操作数每一个字节的自高位掩码,并保存结果到目的操作数的低byte。源操作数必须为MMX寄存器,目的操作数必须为32位通用寄存器。
pshufw插入word源操作数到目的操作数中第三个操作数指定的位置。目的操作数必须为MMX寄存器,源操作数可以为64位内存地址或MMX寄存器,第三个操作数必须8位立即数用来选择那些值将移动到目的操作数,和shufps指令第三个操作数相同方式。
ovntq使用非临时缓冲提示以最小缓冲损失方式从源操作数移动qword到内存。源操作数必须为MX寄存器,目的寄存器应为64位内存地址。movntps使用非临时提示从SSE寄存器保存压缩单精度数据到内存。源操作数必须为SSE寄存器,目的操作数必须为128位内存地址。maskmovq使用非临时提示方式保存第一个操作数指定的字节到64位内存地址。两个操作数都必须为MMX寄存器,第二个操作数决定源操作数中那些字节将写到内存中。内存地址由DS段中DI(或EDI)寄存器指向。
prefetcht0,prefetcht1,prefetcht2和prefetchnta获取操作数指定的字节所处内存数据行到指定位置。操作数应为8位内存地址。
sfence同步所有在它之前所有创建指令操作。这条指令没有操作数。
ldmxcsr载入32位内存操作数到MXCSR寄存器。stmxcsr保存MXCSR内容到32位寄存器。
fxsave保存FPU,MXCSR寄存器当前状态,和所有FPU和SSE寄存器内容到512字节目的操作数指定的内存地址。fxrstor重新载入前面用fxsave指令保存的512字节内存地址。这两条指令内存操作数必须对齐在16字节边界上,它不能声明任何指定大小操作数。
2.1.16 SSE2指令
SSE2扩展用来操作压缩双精度浮点数据,扩展MMX指令语法,并且增加了新的指令。
movapd和movupd从源操作数传送包含压缩双精度数据的双qword操作数到目的操作数。这些指令类似movaps和movups,操作数规则也相同。
movmskpd传送SSE寄存器两个双精度数最高位到通用寄存器低两位。这条指令和movmskps类似并有相同操作数规则。
movsd在源和目的操作数之间传送双精度数(值传送低qword)。其中至少一个操作数为SSE寄存器,第二个可以为SSE寄存器或64位内存地址。
双精度值算术操作有:addpd,addsd,subpd,subsd,mulpd,mulsd,divpd,divsd,sqrtpd,sqrtsd,maxpd,maxsd,minpd,minsd,它们和上一节讲述的单浮点算术操作类似。当助记符以pd而不是ps结尾时,操作针对于压缩的2双精度数,但操作数规则相同。当助记符以sd而不是ss结尾时,源操作数可以为64位内存地址或SSE寄存器,目的寄存器必须为SSE寄存器并且操作于双精度数,此时只适用SSE寄存器的低qword。
andpd,andnpd,orpd和xorpd对压缩双精度值执行逻辑操作。它们和针对单精度的逻辑操作类似并且有相同的操作数规则。
cmppd比较压缩双精度数并返回掩码结果到目的操作数。这条指令和cmpps类似,并且有相同的操作数规则。cmpsd对双精度数据执行相同操作,但它只影响目的寄存器的低qword。接受两个操作数的指令由cmp助记符,表2.3列出的条件助记符和pd或sd组成。
comisd和ucomisd比较双精度操作数并设置标志ZF,PF和CF来表示结果。目的操作数必须为SSE寄存器,源操作数可以为128位内存地址或SSE寄存器。
shufpd从目的操作数移动任何两个双精度数到目的操作数的低qword,源操作数任何两个值值到目的寄存器的高qword。这条指令和shufps类似并且有相同的操作数规则。第三个操作数位0指定将移动到目的操作数的值,位1选择将从源操作数移动的值,其他位为保留的必须为0。
unpckhpd在源和目的操作数之间执行压缩高qword,unpcklpd在源和目的操作数之间执行未压缩低qword。它们是unpckhps和unpcklps类似,并且有相同的操作数规则。
cvtps2pd转换压缩2单精度浮点数据为两个压缩的双精度浮点数据,目的操作数必须为SSE寄存器,源操作数可以为64位内存地址或SSE寄存器。 cvtpd2ps转换压缩2扩展双精度浮点数据为压缩2单精度浮点数据,目的操作数必须为SSE寄存器,源操作数可以为128位内存地址或SSE寄存器。cvtss2sd转换单精度浮点数据为双精度浮点数据,目的操作数必须为SSE寄存器,源操作数可以为32位内存地址或SSE寄存器。cvtsd2ss转换扩展双精度数据为单精度浮点数据,目的操作数必须为SSE寄存器,源操作数可以为64位内存地址或SSE寄存器。
cvtpi2pd转换压缩2 dword整数位压缩双精度浮点数据,目的操作数必须为SSE寄存器,源操作数可以为64位内存地址或MMX寄存器。cvtsi2sd转换一个dword整数为双精度浮点数据,目的操作数必须为SSE寄存器,源操作数可以为32位内存地址或32位通用寄存器。cvtpd2pi转换压缩双精度浮点数据为压缩2 dword整数,目的操作数应为MMX寄存器,源操作数可以为128位内存地址或SSE寄存器。cvttpd2pi执行类似操作,除了它将源值截断到整数,操作数规则也一样。cvtsd2si转换双精度浮点数据为dword整数,目的操作数应为32位通用寄存器,源操作数可以为64为内存地址或SSE寄存器。cvttsd2si执行相同吃哦啊在,除了将源值截为整数,操作数规则也一样。
cvtps2dq和cvttps2dq转换压缩单精度浮点数据为压缩4 dword整数,保存它们的值到目的操作数。cvtpd2dq和cvttpd2dq转换压缩双精度浮点数据为压缩2 dword整数,保存结果到目的操作数的低qword。cvtdq2ps转换压缩4dword帧数为压缩单精度浮点数据。cvtdq2pd从源操作数低qword转换压缩2 dword整数为压缩双精度浮点数据。所有这些指令目的操作数必须为SSE寄存器,源操作数可以为128位内存地址或SSE寄存器。
movdqa和movdqu传送源操作数中双qword大小的压缩整数为目的操作数。至少其中一个操作数必须为SSE寄存器,第二个可以为SSE寄存器或128位内存地址。movdqa指令内存操作数必须16字节对齐,movdqu指令操作数不需要对齐。
movq2dq移动MMX源寄存器内容到目的SSE寄存器低qword。movdq2q传送源SSE寄存器低qword到目的MMX寄存器。
movq2dq xmm0,mm1 ; MMX寄存器传送到SSE寄存器 movdq2q mm0,xmm1 ; SSE寄存器传送到MMX寄存器
所有MMX指令操作的64位压缩整数(用p开头的助记符)扩展能操作SSE寄存器中128位压缩整数。left to do。pshufw指令另外,它不需要扩展语法,但有两种新的变种:pshufhw和pshuflw,他们只允许扩展语法,并且分别针对操作数高或低qword执行和pshufw相同操作。此外pshufd为新增指令,用来执行和pshufw相同的操作,但它操作dword而不是word,它只允许扩展语法。
psubb xmm0,[esi] ; 减16压缩字节 pextrw eax,xmm0,7 ; 提取最高word到eax
paddq执行两个压缩qword的和,psubq执行两个压缩qword的差,puludq执行无符号乘法每一个对应qword的低dword,并返回结果到压缩qword。这些指令和2.1.14讲述的通用MMX操作有相同规则。
pslldq和psrldq执行逻辑左或右移双dqword目的操作数,移动位数由源操作数指定。目的操作数必须为SSE寄存器,源操作数应为8位立即数。
punpckhqdq源操作数高qword和目的操作数高qword,并将结果写到目的SSE寄存器中。punpcklqdq插入源操作数低qword和目的操作数低qword,并将结果写到目的SSE寄存器中。源操作数可以为128位内存地址或SSE寄存器。
movntdq使用非临时提示从SSE寄存器保存压缩整数数据到内存。源操作数应为SSE寄存器,目的操作数应为128位内存地址。movntpd使用非临时提示从SSE寄存器保存压缩双精度数据到内存。源操作数应为32位通用寄存器,目的操作数应为32位内存地址。maskmovdqu使用非临时提示从第一个参数保存选择位到128位内存地址。这两条指令操作数都应为SSE寄存器,第二个操作数选择了那些字节将从源操作数写到目的操作数。内存地址有DS段中DI(或EDI)寄存器指定,不需要对齐。
clflush写并且无效指定操作数地址字节的缓冲行,指定操作数必须为8位内存位置。
lfence执行载入同步。mfence执行访问同步。所以它组合了sfence(上一节讲述的)和lfence指令功能。这些指令没有任何操作数。
2.1.17 SSE3指令
Prescott技术发明了新的指令来提高SSE和SSE2性能 - 称为SSE3。
fisttp行为和fistp指令相似,并且允许相同操作数,唯一区别是它总是会截操作,无论当前的舍入模式。
movshdup载入目的操作数为原值同样尺寸用两个重复的高dword填充每一个qword的128位值。movsldup执行相同动作,除了它拷贝低dword。目的操作数应为SSE寄存器,源操作数可以为SSE寄存器或128位内存地址。
movddup载入64为源值,赋值它到目的操作数的高和低qword。目的操作数应当为SSE寄存器,源操作数可以为SSE寄存器或64位内存地址。
lddqu是和movdqu执行等价功能的指令,但能在源操作数跨缓冲行边界时提高性能。目的操作数必须为SSE寄存器,源操作数必须为128位内存地址。
addsubps执行第二和第四组单精度和,第一和第三组单精度差。addsupd执行第二组双精度和,第一组双精度差。haddps执行源和目的操作数每个qword的两个单精度和,保存结果到目的操作数低qword,源操作数结果到目的操作数高qword。haddpd对每个操作数执行两个双精度值和,并保存目的操作数中结果到目的操作数的低qword,源操作数结果到目的操作数高qword。所有这些指令都需要SSE寄存器为目的操作数,源操作数可以为SSE寄存器或128位内存地址。
monitor创建回写地址行监视。它需要三个有顺序的操作数EAX,ECX和EDX。mwait等待回写到monitor创建的地址区域。它使用带有额外参数的两个操作数,第一个为EAX,第二个为EDX。2.1.18 AMD 3DNOW!指令
3DNow!扩展增加新的2.1.14列出MMX指令,并且能操作64位压缩浮点数据,每一个有两个单精度浮点数据组成。
这些指令规则和通用MMX操作相同,目的操作数必须为MMX寄存器,源操作数可以为MMX寄存器或64位内存地址。pawgusb计算压缩无符号字节平均值。pmulhrw执行带符号压缩字的乘积,舍入每一个dword结果高半部分到目的操作数。pi2fd转换压缩dword整数为压缩浮点数。pf2id使用舍入转换压缩浮点数据为压缩dword整数。pi2fw转换压缩字整数到压缩浮点数,只是哟个源操作数中每个dword的低word。pf2iw转换压缩浮点数据为压缩word整数,结果使用符号扩展扩展为压缩浮点数。pfadd计算压缩浮点数的和。pfsub和pfsubr计算压缩浮点数的差,第一个用目的值减去源值,第二个用源值减去目的值。pfmul计算压缩浮点数的积。pfacc计算目的操作数地和高浮点数的和,保存结果到目的操作数的低dword,并且计算源操作数的低和高dword的和,保存结果到目的寄存器的高dword。pfnacc用目的操作数的高浮点数减去低浮点数,保存结果到目的操作数的低dword,并结算源操作数的高和低dword的差,保存结果到目的操作数的高dword。pfpnacc用目的操作数的高浮点数据减去低浮点数据,保存结果到目的操作数的低dword,并计算源操作数低和高浮点数的和,保存结果到目的操作数的高dword。pfmax和pfmin计算浮点数的最大和最小值。pswapd翻转源操作数的高低dword。pfrcp返回原操作数的近似浮点值倒数。pfrsqrt返回原操作数的开方的近似倒数。pfrcpit1执行第一步Newton-Raphson迭代开方。pfrcpit2计算第二步Newton-Raphson迭代开发。pfcmpeq,pfcmpge和pfcmpgt比较压缩浮点数并根据比较结果设置目的操作数中相应数据元素的位(全部置为1或置为0),第一个检查值是否相等,第二个检查目的值是否大于或等于源之,第三个检查是否目的值大于源值。
prefetch和prefetchw从内存载入 包含操作数指定字节的数据行,prefetchw指令必须当缓冲行中数据被修改时使用,否则应使用prefetch指令。操作数必须为8位内存地址。
femms执行快速清除MMX状态。它没有操作数。
2.1.19 x86-64长模式指令
AMD64和EM64T体系(我们将使用x86-64作为通用名称)扩展x86指令集以用作64位处理。而原始和兼容模式使用相同的寄存器和指令集。新的长模式扩展x86操作64位,并且发明了一个新的寄存器。你可以使用use64伪指令来生成这个模式的代码。
每一个通用寄存器都被扩展为64位,增加了8个新的通用寄存器和8个新的SSE寄存器。表2.4列出了新增的这些寄存器。通用寄存器的小的尺寸为大的值的低部分。你仍然可以在长模式下访问ah,bh,ch和dh寄存器,但你不能在新的指令中使用任何新的寄存器。
通常x86体系中任何指令,允许16位或32位操作数尺寸,在长模式下还允许64位操作数。64位操作数必须在长模式下寻址,也允许32位寻址,但不能使用基于16位寄存器的地址。下面为长模式中的mov指令例子:
mov rax,r8 ; 传送64位通用寄存器 mov al,[rbx] ; 传送通过64位寄存器寻址的内存
类型 | 通用寄存器 | SSE | |||
位 | 8 | 16 | 32 | 64 | 128 |
rax | |||||
rcx | |||||
rdx | |||||
rbx | |||||
spl | rsp | ||||
bpl | rbp | ||||
sil | rsi | ||||
dil | rdi | ||||
r8b | r8w | r8d | r8 | xmm8 | |
r9b | r9w | r9d | r9 | xmm9 | |
r10b | r10w | r10d | r10 | xmm10 | |
r11b | r11w | r11d | r11 | xmm11 | |
r12b | r12w | r12d | r12 | xmm12 | |
r13b | r13w | r13d | r13 | xmm13 | |
r14b | r14w | r14d | r14 | xmm14 | |
r15b | r15w | r15d | r15 | xmm15 |
长模式也使用基于地址的指令指针,你可以手动用RIP指定,但这些地址也能自动由FASM生成,因此在长模式下没有64位绝对地址。你可以通过中括号中dword尺寸重写地址来强制汇编器使用32位绝对地址。也有一个使用64位绝对寻址的例外,它为mov后跟着其中一个为累加器,第二个为内存操作数的情况。使用qword来强制汇编器使用64位绝对寻址。当没有指定尺寸操作符时,汇编器自动生成最佳格式。
mov [qword 0],rax ; 绝对64位寻址 mov [dword 0],r15d ; 绝对32位寻址 mov [0],rsi ; 自动RIP相对寻址 mov [rip+3],sil ; 手动RIP相对寻址
作为64位操作立即数只可能为32位数,唯一例外是带有64位通用寄存器目的操作数的mov指令。试图其他指令使用64位立即数将导致错误。
如果在长模式下操作32位通用寄存器的指令,64位寄存器的高32位填充为0.这不同于16位或32位那些指令操作,它们保留高位。
新增三条类型转换指令。cdqe符号扩展EAX中dword到qword并保持结果到RAX。cqo符号扩展RAX qword为双qword并保存额外位到RDX寄存器。这些指令没有操作数。movsxd符号扩展dword源操作数到64位目的操作数,源操作数可以为32为寄存器或内存,目的操作数必须为寄存器。没有零扩展类似指令,因为它自动由32位寄存器完成,上一段中说明的那样。movzx和movsx指令遵守通常规则,可以使用64位目的操作数,允许扩展字节或字到qword。
所有二进制算术和逻辑指令提升以允许在长模式下操作64位操作数。在长模式下禁止使用十进制算术指令。
堆栈操作,比如push和pop在长模式下默认为64位操作数,不能使用32位操作数。pusha和popa在长模式下不可用。
间接near调整和调用在长模式下默认为64位操作数,它不能使用32位操作数。另外,间接far调整和调用允许任何x86体系允许的操作数,也允许使用80位内存操作数(仅EM64T实现了),80位内存操作数有第一个定义偏移的8字节和指定选择子的最后两个字节组成。长模式下不允许直接far调整和调用。
I/O指令,in,out,ins和outs为例外指令,它们不允许在长模式下操作qword操作数。但其他串操作可以。他们有新的段格式movsq,cmpsq,scasq,lodsq和stosq。RSI和RDI寄存器默认用来寻址这些串元素。
lfs,lgs和lss用来扩展以接受80位元内存操作数和64位目的寄存器(仅EM64T实现了)。lds和les不能在长模式下使用。
系统指令,比如需要48位内存操作数的lgdt,在长模式下需要80为内存操作数。
cmpxchg16b为64位的cmpxchg8b等价指令,它使用双qword内存操作数和64为寄存器来执行类似操作。
swapgs为新增指令,它置换GS寄存器和KernelGSbase MSR寄存器的内容(MSR地址为0C0000102h)。
syscall和sysret为新增指令,用来在长模式下提供和sysenter和sysexit相似功能的指令,而sysenter和sysexit在长模式下不允许使用。
2.2 控制伪指令
这章将讲述控制汇编流程的伪指令,它们在汇编时处理,并可能会引起某些指令块的不同汇编或完全不被汇编。
2.2.1 数值常量
=伪指令可以用来定义数值常量。它前面为常量名,后面为提供值的数值表达式。常量值可以为数字或地址,但不同于标号,数值常量不允许拥有基于寄存器的地址。除了这点不同外,数值常量和标号非常类似,它们甚至可以前置引用(在它们的定义前使用它们)。
当你用已经定义的数值常量定义数值常量时,汇编器把常量当作汇编期间的变量,并且允许赋给新值,但不允许前置引用(很显然的原因)。让我们看一个例子:
dd sum x = 1 x = x+2 sum = x
这里x为汇编期间的变量,每次访问它的值为最后一次使用的值。因此如果在x定义前访问它,比如将“dd sum”指令改为“dd x”,将导致错误。当通过指令“x = x + 2” 重新定义x时,将用先前x的值来计算新的x值。所以当常量sum定义时,x值为3并将赋给sum。因为sum在源码中只定义了一次,它是标准的数值常量,能够前置引用。所以“dd sum”将被汇编为“dd 3”。可阅读2.2.6节来了解汇编器是如何做到解析的。
数值常量值前可以放尺寸操作符,用来确保值在某个给定范围内,并能影响数值表达式中的这些计算是怎样执行的。例如:
c8 = byte -1 c32 = dword -1
上面的语句将定义两个不同的常量,第一个存放8位,第二个存放于32位。
当你用地址值(可以基于寄存器寻址)定义常量时,可以使用label伪指令的扩展语法(已在1.2.3节中描述)。例如:
label myaddr at ebp+4
上面的语句声明一个ebp+4地址处的标号。然而标号不同于数值常量,不能成为汇编期间的变量。
2.2.2 条件汇编
if伪指令导致跟着的指令块仅在特定条件下汇编。它必须跟着指定条件的逻辑表达式,当条件满足时汇编后一行的指令,否则跳过。可选的else if伪指令后面跟着指定条件的逻辑表达式,当前面的条件不满且这个条件满足时汇编后面的指令块。可选的else伪指令开始的指令块,当所有条件都不满足时才汇编。end if伪指令结束最后的指令块。
应当注意的是if伪指令是在汇编阶段处理的,因此它不会影响任何预处理伪指令,比如符号常量和宏指令的定义 - 当汇编器识别出if伪指令时,所有的预处理已经完成。
逻辑表达式由逻辑值和逻辑操作符组成。逻辑操作符有“~”逻辑非,“&”逻辑与,“|”逻辑或。逻辑非操作优先级最高。逻辑值可以为数值表达式,如果等于0为false,否则为true。两个数值表达式可以使用以下操作符做比较来生成逻辑值:=(等于),<(小于),>(大于),<=(小于或等于),>=(大于或等于),<>(不相等)。
跟着符号名的used操作符为逻辑值,用来检查给定符号是否在某些地方使用过(即使在检查位置后使用它也能返回正确的结果)。defined操作符后面可以跟着任何表达式,通常为一符号名;它检查只包含唯一符号的给定表达式是否在源码中已定义并能在当前位置访问。
下面例子中使用的count常量必须在源码中某处定义:
if count>0 mov cx,count rep movsb end if
当count大于0时将汇编其中的两条指令。下面为更加复杂的条件结构:
if count & ~ count mod 4 mov cx,count/4 rep movsd else if count>4 mov cx,count/4 rep movsd mov cx,count mod 4 rep movsb else mov cx,count rep movsb end if
当count不为零且能被4整除时将汇编第一个指令块,如果不满足,将评估跟在else if后面的逻辑表达式,如果为true将汇编第二个指令块,否则汇编else中的指令块。
还有用来比较符号串操作符。eq比较两个值是否精确相等。in操作符检查给定值是否在其后面给的列表中,列表必须放在中括号中,列表项以逗号分隔。对于汇编器来说,当符号们有相同含义时被认为相同 - 例如对于汇编器pword和fword是相同的。“16 eq 10h”条件为true,而“16 eq 10+4”条件为false。
eqtype操作符检查比较的两个值结构是否相同,以及结构的元素是否为相同类型。
常见的类型有数值表达式,单独的字符串,浮点数,地址表达式(在中括号内或者前面有ptr操作符),指令助记符,寄存器,size操作符,跳转类型和代码类型操作符。并且每一个用作分隔符的特殊字符,如逗号或冒号,用来分隔类型。例如,两个由寄存器名和逗号和数值表达式构成的值,将被认为是相同类型,无论使用了什么寄存器和复杂的数值表达式;除非字符串和浮点数,它们为特殊类型的数值表达式,被认为为不同的类型。所以“eax,16 eqtype fs,3+7”条件为true,但“eax,16 eqtype eax,1.6”为false。
2.2.3 重复块指令
times伪指令重复一条指令到指定次数。它后面必须跟着重复次数数值表达式和重复指令(可选的冒号用来分隔数字和指令)。当在指令中使用特殊符号“%”时,其值等于当前的重复次数。例如“times 5 db %”将定义五字节的数据1,2,3,4,5。也允许递归使用times伪指令,所以“times 3 times % db %”将定义6字节数据1,1,2,1,2,3。
repeat伪指令重复整个指令块。它后面必须跟着重复次数数值表达式。重复的指令出现在另一行,最后以end repeat伪指令结束,例如:
repeat 8 mov byte [bx],% inc bx end repeat
上面的语句生成的代码将把从1到8存储到BX寻址的内存单元内。
重复次数可以为0,此时不会汇编任何指令。
break伪指令停止前面的重复,继续汇编end repeat后面的第一行。和if伪指令一起使用,可以在某些特殊条件下停止重复,如:
s = x/2 repeat 100 if x/s=s break end if s = (s+x/s)/2 end repeat
当while伪指令后面的逻辑表达式为真时重复指令块。重复指令块必须以end while伪指令结束。在每一次重复前逻辑表达式都将评估,当值为false时,将汇编end while后第一行。此时“%”也用来保存当前重复值。break伪指令也可以像repeat指令那样结束循环。前面的例子可以用while指令重写为:
s = x/2 while x/s <> s s = (s+x/s)/2 if % = 100 break end if end while
if,repeat和while定义的块可以任意嵌套,但它们必须以它们开始的顺序依次结束。break伪指令常用来停止处理repeat或while开始的块。
2.2.4 地址空间
org伪指令设置后面代码将被期望出现在内存中的地址。它必须跟着指定地址的数值表达式。这个伪指令开始新的地址空间,后面的代码并不会做任何移动,但将影响其中定义的所有标号和$符号,如同被放到给定地址一样。然而程序员必须负责将代码放到执行时正确的位置。
load伪指令用来从已汇编代码的二进制值中定义常量。这条伪指令必须跟着常量名,可选的尺寸操作符,from操作符和指定了当前地址空间中有效地址的数值表达式。尺寸操作符此时有特殊含义 - 它声明了多少字节(最多为8)将被加载组成常量的二进制值。如果没有指定尺寸操作符,将载入一个字节(因此值的范围为0到255)。载入的数据不能超过当前的偏移。
store伪指令通过替换前面生成的代码为给定数值表达式来修改已生成的代码。这个表达式可以放置可选的尺寸操作符来指定要定义多大的值。如果没有定义尺寸操作符默认为一个字节。然后at操作符和数值表达式定义了当前地址代码空间中的有效地址。这是个高级伪指令,应当小心使用。
load和store伪指令操作都限制在当前地址空间中。$$总是等于当前地址空间的基地址,$为当前地址空间的当前地址,因此这两个值定义了load和store能够操作的区域界限。
load和store一起使用可以为已生成的代码编码。例如在当前地址空间中编码生成的所有代码可以使用这样的伪指令块:
repeat $-$$ load a byte from $$+%-1 store byte a xor c at $$+%-1 end repeat
其中代码中每个字节将和常量c异或。
virtual在指定位置定义虚拟数据。这个数据不会包含到输出文件中,但定义的标号可以在源码中使用。这条伪指令可以跟着at操作符和指定虚拟数据地址的数值表达式(如果没有指定地址将使用当前地址,和“virtual at $”相同)。数据定义指令在另一行,最后以end virtual伪指令结束。virtual指令本身是一个独立的地址空间,当它结束后,将恢复前面的地址空间上下文。
virtual伪指令可以用来创建一些变量的联合,例如:
GDTR dp ? virtual at GDTR GDT_limit dw ? GDT_address dd ? end virtual
它在GDTR地址处为48位的变量定义了2个标号。
它也可以为寄存器寻址的结构定义标号:
virtual at bx LDT_limit dw ? LDT_address dd ? end virtual
有了上面的定义后,指令“mov ax, [LDT_limit]”将被汇编为“mov ax,[bx]”。
在虚拟块中声明已定义的数据值或指令也是有用的,因为load伪指令可以从虚拟的生成代码中载入数据到常量。这条伪指令必须在载入的代码后,虚拟块结束前使用,因为它只能从当前地址空间载入数据。例如:
virtual at 0 xor eax,eax and edx,eax load zeroq dword from 0 end virtual
上面的代码将定义zeroq常量,包含4字节定义在虚拟块中的机器码指令。也可以用这个方法从外部文件载入二进制数据。例如:
virtual at 0 file ’a.txt’:10h,1 load char from 0 end virtual
上面的语句从文件a.txt 10h文件偏移处载入一字节到char常量。
2.4中描述的任何section伪指令都将开始一新地址空间。
2.2.5 其他伪指令
align伪指令对齐代码或数据到指定边界。它必须跟着对齐大小的数值表达式。边界值必须为2的指数。
为了实现对齐,align伪指令将把跳过的字节用nop指令填充,同时标识这段区域为已初始化数据,所以它和其他未初始化数据一起将不会出现在输出文件中,对齐字节将执行相同方式。如果你需要填充对齐区域为其他值,可以用align和需要的对齐值virtual自己创建对齐,例如:
virtual align 16 a = $ - $$ end virtual db a dup 0常量a在对齐后地址和virtual块地址之间定义不同(见上一部分),所以它等于需要对齐空间的大小。
display伪指令在汇编期间显示信息。它必须跟着字符串或者字节数据,用逗号分隔。它也可以用来显示常量,例如:
bits = 16 display ’Current offset is 0x’ repeat bits/4 d = ’0’ + $ shr (bits-%*4) and 0Fh if d > ’9’ d = d + ’A’-’9’-1 end if display d end repeat display 13,10
这个伪指令计算4个16位值的16进制数字,并且以字符显示。注意如果当前地址空间为可重定位的是不可用(比如可能出现在PE或COFF输出格式中),此时只能使用绝对的值。绝对的值可以通过相对地址的计算得到,如同$-$$,或rva $(PE格式中)。
2.2.6 多遍扫描
因为汇编器允许在标号或者常量实际定义前引用它们,所以它必须预测这些标号的值。只有有一种情形下怀疑预测失败,它将再次执行扫描,汇编所有源码,基于上遍扫描中得到的标号值来做更好的预测。
标号值的变化将引起一些指令有不同长度的编码,并能再次引起标号值的变化。既然在表达式中可以使用影响控制伪指令的标号和常量,整个源码在新的一遍扫描中可能完全不同的处理,每一次都试图做更好的预测,直到所有的值都预测正确。它使用各种方法来预测值,对于大多数程序通过几遍扫描就能找到的最短路径。
一些错误,如值不能符合要求的边界,不能在中间扫描中提示,它可能在某些值预测好后会才出现。如果汇编器遇到一些非法语法或未知指令,它将立即停止。此外多次定义一个标号也将导致错误,因为它会引起无效的预测。
只有display伪指令创建的消息能在最后一次扫描中显示。当汇编器因为错误停止时,这些消息将返回还没有解析正确的预测值。
扫描遍数有个上限,当汇编器达到上限后,它将停止并提示无法生成正确代码的消息。看看下面的例子:
if ~ defined alpha alpha: end if
当defined操作符后的表达式在此处能计算时将传给true值,此处意思为alpha标号在某处已定义。但上面的块仅当defined操作符给定的值为false时才会定义,将导致一个xx和是其不能解析那些代码。当处理if伪指令,汇编器必须预测标号alpha是否在某处定义(如果在这遍扫描前已定义就不需要预测了),无论预测出什么,总会发生相反的。一次汇编器将会失败,除非标号alpha在上面指令块之前某地定义 - 此时,正如已经提到的,不需要预测并且块将被跳过。
上面的例子也可以写成当它没有定义时定义标号。如果失败,因为defined操作符将检查标号是否已定义,它包括在条件处理块中的定义。然后增加些条件就能让它解析:
if ~ defined alpha | defined @f alpha: @@: end if
@f总和源码后面最近的@@符号有相同的标号,所以上面的例子中,如果使用了任何唯一名称而不是匿名标号,意思是相同的。当alpha没有在源码其他地方定义时,唯一的可能是当这个块定义并且此时他不会导致XX,因为你们标号标号将会是这个块自行创建。为了更好的理解这点,看看下面这个除了自我创建外没有任何东西的块:
if defined @f @@: end if
这是一个可以有多个方案的源码,当这个块被处理或没有所有这些情形都正确。取决于汇编器算法 - 预测算法。回到前面的例子,当alpha没有在任何地方定义,if条件块不为false,所以你只有一种可能方案,我们希望汇编器能够到达。灵位,当alpha在某些地方定义时,我们将再次得到两种可能方案,但其中将导致alpha定义两次,那样的错误将导致汇编器立即终止汇编,这是一个极深的扰乱解析过程的错误种类,我们得到将取决于汇编器内部的选择。
然而这些选择中存在某些确定的事实。当汇编器需要检查给定符号是否定义并且它已经在当前扫描中定义时,不需要任何预测 - 这已经在前面说明。当给定符号从未在以前定义,包括所有已经完成的扫描,汇编器将预测它没有定义。知道这点,我们可以期待如同上面的简单的自我创建块不被汇编,上个例子中在外面的条件块前当alpha在某地定义时将被正确解析,当它在前面没有定义时,它家那个定义alpha,因此潜在的导致错误,因为如果alpha在后面某处定义将有两个定义。
used操作符类似,然而任何预测都不是简单的,你不应依赖它们。
2.3 预处理伪指令
所有预处理指令在主汇编过程前处理,因此不受控制伪指令的影响。此时所有的注释都已被去除。
2.3.1 包含源文件
include伪指令在其使用位置包含指定源文件。它后面必须跟着要包含的文件名,例如:
include 'macros.inc'
整个包含文件会在下一行被预处理前预处理。要包含的文件无个数限制。
文件路径可以包含“%”括起的环境变量,它们将被替换为实际的值,“\”和“/”都允许作为地址分隔符。如果没有指定文件的绝对地址,将首先在包含它的那个文件路径下搜索该文件,没有找到的话,将在主源码文件目录搜索(命令行中指定的)。这些规则也适用与file伪指令。
2.3.2 符号常量
符号常量不同于数值常量,在汇编过程前所有符号常量都被它们的值替代,它们的值可以为任何东西。
符号常量定义由常量名后面跟着equ伪指令组成。这条伪指令后面跟着的任何东西都将成为常量的值。如果符号常量的值包含其他符号常量,他们将在赋值给新的常量值前会先替换它们的值。例如:
d equ dword NULL equ d 0 d equ edx
在这三个定义后,NULL常量的值为dword 0,d的值为edx。所以“push NULL”将被汇编为“push dword 0”,“push d”将被汇编为“push edx”。然后下面的行:
d equ d,eax常量d将得到新的值“edx,eax”。这种方式可以用来定义增长的符号列表。
restore伪指令用来恢复上次定义的常量的值。它后面应当跟着一个或多个用逗号分隔的符号常量。所以在上面定义后“restore d”将给常量d恢复到值edx,再来一次“restore d”将恢复至dword,再次恢复d将变成初始时的含义(常量d没有定义)。如果没有定义给定名称的常量,restore不会导致错误,它将忽略掉。
符号常量可以用来调节汇编器语法。例如下面的定义为尺寸操作符提供了方便的快捷方式:
b equ byte w equ word d equ dword p equ pword f equ fword q equ qword t equ tword x equ dqword
因为符号常量允许为空值,可以在任何地址值前使用offset语法:
offset equ
这样定义后“mov ax,offset char”将拷贝变量char的偏移到寄存器ax,因为offset被替换为空值,因此将被忽略掉。
符号常量也可以用fix伪指令来定义,它和equ有相同的语法,但定义常量的优先级更高 - 它们甚至在处理预处理伪指令和宏指令前被替换,唯一例外的是fix伪指令本身,它有最高可能的优先级,这点可以用来重新定义常量。
fix伪指令可以用来调整预处理器伪指令的语法,这通常不能用equ伪指令实现。例如:
incl fix include
上面的语句将为include伪指令定义一个短名称,而同样的equ伪指令定义不能得到那样的结果,因为标准的符号常量是在搜索预处理伪指令之后才进行替换的。
2.3.3 宏指令
macro伪指令允许定义自己的复杂指令,称为宏指令。宏指令极大的简化编程过程。最简单的形式类似符号常量的定义。例如,下面为指令“test al,0xFF”定义快捷方式。
macro tst {test al,0xFF}
macro伪指令之后为宏指令名和以"{" 和"}"括起来的内容。在这个定义后你可以在任何地方使用tst指令,它将被汇编为“test al,0xFF”。定义符号常量tst将有相同的效果,但不同的是宏指令名仅作为指令助记符识别。此外,宏指令在符号常量替换之前被替换相应代码。所以如果你定义了同名的宏指令和符号常量,并且当作指令助记符使用,其内容将被替换为宏指令的,但如果在操作数使用将被替换为符号常量的值。
宏指令的定义可以有多行组成,因为字符“{”和“}”不必和macro宏指令在同一行,例如:
macro stos0 { xor al,al stosb }
任何使用宏指令stos0的地方将被两条汇编指令替换。
如同指令需要一些操作数一样,宏指令也可定义为接受一些一些用逗号分隔的参数。需要的参数名称必须跟在同行macro宏指令后面。宏指令中出现的任何参数名都将被替换为宏指令使用时的对应值。下面是一个用于二进制输出格式数据对齐的宏指令:
macro align value { rb (value-1)-($+value-1) mod value }
这条宏指令定义后,当发现“align 4”指令后,value将为4,所以结果为“rb (4-1)-($+4-1) mod 4”。
如果宏指令定义中出现了同名的指令,那么将使用前面定义的含义。这可以用来重定义宏指令,例如:
macro mov op1,op2 { if op1 in & op2 in push op2 pop op1 else mov op1,op2 end if }
这条宏指令扩展了mov指令的语法,允许两个操作数都为段寄存器。例如“mov ds,es”将被汇编为“push es”和“pop ds”。其他情形下将使用标准的mov指令。这个mov的语法还能进一步的扩展,比如:
macro mov op1,op2,op3 { if op3 eq mov op1,op2 else mov op1,op2 mov op2,op3 end if }
它允许mov指令接受三个操作数,但仍然允许两个操作数,因为当宏指令传入比实际需要少的参数时,剩余的参数将为空值。当三个参数都给定时,这条宏指令将变成两个前面定义的宏指令,所以“mov es,ds,dx”将被汇编为“push ds, pop es”和“mov ds,dx”。
参数名后带有字符“*”表明这个参数是必须的-预处理器不允许这个参数为空值。例如上面的宏指令可以这样声明:“macro mov op1*,op2*,op3”来确保头两个参数必须给定非空值。
当传给宏指令包含逗号的参数时,参数必须用尖括号括起来。如果包含不止一个字符“<”,应当使用同样数目的字符“>”来结束参数值。
purge伪指令消除上次定义的宏指令。它必须跟着一个或多个逗号分隔的宏指令名。如果这个宏指令没有定义,将不会产生任何错误。例如上面定义的扩展mov宏指令后,你可以通过“purge mov”宏指令来删除三个操作符的宏指令。另外“purge mov”将删除2个操作符的宏指令,以及所有这些伪指令将没有任何效果。
如果macro伪指令后存在一些方括号括起的参数名,那么当使用这条宏指令时它允许为这个参数给定多个值。这个宏指令将会依次展开给定参数。这也是为什么中括号后不年能再有任何参数的原因。宏指令的内容将被参数组分别处理。一个简单的包含一个在中括号中的参数例子:
macro stoschar [char] { mov al,char stosb }
这条宏指令接受无限数目的参数,并且每一个将分别处理成两天指令。例如stoschar 1,2,3将被汇编出以下指令:
mov al,1 stosb mov al,2 stosb mov al,3 stosb
有一些只能在宏指令定义中使用特殊的宏指令。local宏指令定义局部名称,每次宏指令使用时将被替换为唯一值。它必须跟着用逗号分隔的名称。如果参数名以“.”或“..”开头,由每一个计算的宏指令生成的唯一标号将有相同的属性。这通常用来定义在宏指令内部使用的常量和标号。例如:
macro movstr { local move move: lodsb stosb test al,al jnz move }
每次宏指令使用,mov在其指令中将变成唯一的名称,因此不会产生标号重复定义错误。
forward,reverse和common伪指令把宏指令分成块,每块都在前面的块完成后处理。它们的不同点仅在当宏指令接受多个参数组时。forward伪指令后的指令块将依次处理每个参数组,从第一个到最后一个 - 类似默认的块(前面没有任何这些伪指令)。跟在reverse伪指令的块将以相反的顺序处理 - 从最后一个到第一个。跟在common伪指令的块只处理一次,通常是对于所有的参数组。在处理同组参数时,在某块中定义的局部名字对其他块而言是可见的。当然common块中定义的局部标号对所有块都是可见的,与处理的是哪个参数组无关。
下面是一个在字符串之前创建字符地址数组的例子::
macro strtbl name,[string] { common label name dword forward local label dd label forward label db string,0 }
这个宏指令的第一个参数将成为地址表的标号,后面的参数应当为字符串。第一个块仅处理一次,它定义了标号。第二个块为每个字符串声明局部名,并且定义一个表项来存放字符串地址。第三个块用相应的标号名为每个字符串定义数据。
开始块的伪指令可以和其后的代码放在同一行,比如:
macro stdcall proc,[arg] { reverse push arg common call proc }
这条宏指令用STDCALL约定来调用过程,堆栈以相反的方向压栈。例如“stdcall foo,1,2,3”将被汇编为:
push 3 push 2 push 1 call foo
如果宏指令中一些名称有不同的值(既可以为方括号中的参数,也可以为跟在forward或reverse伪指令块中定义的局部名称)并且用在common伪指令块中,它将被替换为以逗号分隔的所有的值。例如下面的例子把所有参数传递给前面定义的stdcall宏:
macro invoke proc,[arg] { common stdcall [proc],arg }
它可以用来STDCALL方式间接调用(通过内存指针)过程。
在宏指令内部也可以使用特殊的#操作符。这个操作符将两个名称连接在一起。由于连接是在所有的参数和局部变量都用真实值代替之后,所以有时可能会很有用。下面的宏指令将根据cond参数生成条件跳转:
macro jif op1,cond,op2,label { cmp op1,op2 j#cond label }
例如“jif ax,ae,10h,exit”将被汇编为“cmp ax,10h”和“jae exit”指令。
#操作符也能合并两个字符串。宏指令中也可以用'操作符将名称转换为字符串。它转换后面的名称为字符串,但注意,当它后面跟着多个符号宏参数时,只有第一个被转换,也就是说'操作符值转换后面紧跟着的那个符号。下面是使用这两个特性的一个例子:
macro label name { label name if ~ used name display ‘name # " is defined but not used.",13,10 end if }
以这种方式定义的宏在源文件中未使用的时候,宏就会发出警告信息,通知哪个标号未使用。
为使宏能够根据参数类型的不同其行为有所不同,可使用"eqtype"比较操作符。下面是一个区分字符串和其他参数的宏指令:
macro message arg { if arg eqtype "" local str jmp @f str db arg,0Dh,0Ah,24h @@: mov dx,str else mov dx,arg end if mov ah,9 int 21h }
上面的宏用于DOS程序中显示信息。当这个宏的参数为一些数字,标号,或变量,将会显示该地址的字符串,但当参数为字符串时,创建的代码将显示后面包含回车和换行字符的字符串。
宏指令中也可以在另一个宏指令声明,这样可以使用一个宏定义另外一个宏,但这样做存在一个问题宏指令中不能出现字符“}”,因为它表示宏定义的结束。为了解决这个问题,可以在宏指令内部使用转义字符。通过在任何符号(甚至特殊字符)前放置一个或多个字符“\”。预处理将它们作为一个单一的符号处理,每次在处理宏指令时遇到这些符号,都将取出前面的字符“\”。例如“\}”被当作一单一符号,当处理宏指令时变成了符号“}”。这允许在一个宏指令定义中嵌套另一个:
macro ext instr { macro instr op1,op2,op3 \{ if op3 eq instr op1,op2 else instr op1,op2 instr op2,op3 end if \} } ext add ext sub
ext宏定义正确,当使用它时,“\{”和“\}”变成了符号“{”和“}”。所以当处理ext add时,宏的内容变成有效的宏指令定义,add宏将被定义。同样ext sub定义了sub宏。这里使用符号“\{”并不是必须的,但这样做可以让定义更清晰些。
如果某些只用于宏指令的伪指令,比如local或common也需要在以这种方式嵌套在宏中,它们也可以以同种方式转义。用多于一个的“\”转义符号也是允许的,这允许用来定义多层嵌套的宏。
另一种在一个宏指令中定义另一个的技术是fix伪指令,当某些宏指令仅以另一个的定义开始时,而没有结束它时将变得很有用处。例如:
macro tmacro [params] { common macro params { } MACRO fix tmacro ENDM fix }
定义了另一种定义宏指令的语法,如同:
MACRO stoschar char mov al,char stosb ENDM
注意,这样个性化的定义必须使用"fix"来定义,因为只有更高优先级的常量才能在预处理器在定义宏时查找"}"字符之前处理。这对于那些想在定义结束之后执行一些额外操作的时候可能是一个问题,但还有一个特性可能有助于这个问题的处理。也就是说可以将任何伪指令、指令或宏指令放在结束宏的"}"字符之后,这就会使得他像被放到了下一行一样进行处理。
2.3.4 结构
struc伪指令是用于定义数据的"macro"的变例。使用"struc" 定义的宏在使用时前面必需加一个标号(和数据定义伪指令一样),这个标号会附加在所有以"."开头的元素名称前面。以"struc"定义的宏和使用"macro"定义的宏名字可以相同,不会相互影响。所有适用于"macro"的规则也同样适用于"struc"。以下是一个结构宏指令的例子
下面为一个结构宏指令例子:
struc point x,y { .x dw x .y dw y }
例如my point 7,11将定义结构标号my,包含两个值为7的变量my.x和值为11的变量my.y。
如果在结构定义中出现字符“.”,它将被替换为结构的标号名,而且此时不会自动定义该标号,以允许完全的手动定义。下面为利用这个特性来扩展数据定义伪指令db以能够计算定义数据的大小例子:
struc db [data] { common . db data .size = $ - . }
这样定义后,“msg db 'Hello!',13,10”还将定义“msg.size”常量,其值等于定义数据占用的字节大小。
定义寄存器或绝对值寻址的数据结构应当使用和结构宏指令一起使用的virtual伪指令(见2.2.5)。
restruc伪指令消除上次的结构定义,如同purge处理宏指令,restore处理符号常量一样。它也有相同的语法 - 跟着一个或多个逗号分隔的结构宏指令。
2.3.5 重复宏指令
rept伪指令是特殊种类的宏指令,用来重复用括号括起来的块到指定数目。rept伪指令的基本语法是跟着数字(不能为表达式,因为预处理器不做计算,如果需要重复的值基于汇编器计算,使用汇编器处理的代码重复指令,见2.2.3),然后是字符“{”和“}”之间的源码块。一个简单的例子:
rept 5 {in al, dx}
将变成5份重复的in al,dx行。和可以在宏指令内部使用的标准宏指令和任何特殊操作符和伪指令同样方式定义的指令块也允许使用。当重复次数为0时,块将被简单的跳过,如果你定义宏指令但从不使用它。重复次数可以跟着重复次数符号名称,它将被替换为当前生成的重复次数。如同:
rept 3 counter { byte#counter db counter }
将生成:
byte1 db 1 byte2 db 2 byte3 db 3
应用于rept块的重复机制和宏指令中处理多个参数组的相同,所以像forward,common和reverse伪指令都可以使用。因此伪指令:
rept 7 num { reverse display ‘num }
将显示数字7到1为文本。local伪指令的行为和带有多个参数组的伪指令中的相同,所以:
rept 21 { local label label: loop label }
将为每次重复生成唯一的标号。
重复次数符号默认次数为1,你可以在计数器名称后面跟着冒号和起始次数来改变默认的起始次数。例如:
rept 8 n:0 { pxor xmm#n,xmm#n }
生成的代码将 清除8个SSE寄存器的值。你可以用逗号来定义多个计数器,每一个都可以有不同的起始计数。
irp伪指令通过给定的列表参数来逐一重复。irp后面跟着参数名,然后是逗号和一串参数。参数的指定类似宏指令中的方式,所以他们可以以逗号分隔并且每一个都可以包含在“<”和“>”之间。另外 参数名称也可以跟着字符“*”来说明某个参数不能传空值。这样的块:
irp value, 2,3,5 { db value }
将生成:
db 2 db 3 db 5
irps伪指令从给定列表符号中重复,它必须跟着参数名,然后逗号和任何符号序列。序列中的每一个符号,无论是否为名称符号,符号字符或字符串,都将成为一次重复的参数。如果逗号后没有任何符号,将不会执行任何重复。例如:
irps reg, al bx ecx { xor reg,reg }
将生成:
xor al,al xor bx,bx xor ecx,ecx
irp和irps定义的块将和任何宏指令以相同方式处理,所以只用于宏指令的操作符和伪指令此时也可以自由使用。
2.3.6 条件宏指令
match伪指令将引起一些块被预处理,而且仅当给定符号序列匹配指定模式时传给汇编器。模式在前,以逗号结束,然后是执行匹配操作的符号,最后是在“{”和“}”之间的块源码。
有几条规则用来创建匹配表达式,第一是任何符号字符和字符串应当精确匹配。例如:
match +,+ { include ’first.inc’ } match +,- { include ’second.inc’ }
第一个文件将被包含,因为逗号后的“+”匹配模式“+”,第二个文件将不会包含,因为不匹配。
为了照字面意思匹配,模板前面必须放置字符“=”,为了匹配字符“=”,或“,”,必须使用“==”和“=,”。例如模板“=a==”就匹配“a=”序列。
如果在模板中放置一些名称符号,它匹配其中至少一个符号的任何序列,并且这个名称将被替换为匹配序列,类似宏指令的参数。例如:
match a-b, 0-7 { dw a,b-a }
将生成dw 0,7-0指令。每一个名称都将匹配为尽可能小的符号,留下剩余做为后面的一个,所以在这个例子中:
match a b, 1+2+3 {db a}
名称a匹配符号1,剩下+2+3序列来匹配b。而:
match a b, 1 {db a}
将不会为b剩下任何东西匹配,所以这个块将不会处理任何事情。
match定义的源码块将和宏指令以相同方式处理,所以宏指令的任何操作符也都能在这里使用。
使得match伪指令更加有用的事实是执行匹配前在匹配序列符号中(逗号后源码块开始前的任意地方)它替换符号常量为它们的值。这样可以用来在一些符号常量给定了值的某些条件下处理源码块:
match =TRUE, DEBUG { include ’debug.inc’ }
当定义了符号常量DEBUG为TRUE时将包含文件。
2.3.7 处理顺序
当组合各种不同的预处理器特性时,知道它们处理的先后顺序是很重要的。正如上面已经提到的,最高优先级为fix伪指令和用它替换定义的。在做任何预处理前完成的,一次下面的源码:
V fix { macro empty V V fix } V
成为一个有效的空宏指令定义。它可以被解析为:fix伪指令和有优先级的的符号常量在单独阶段处理,所有其他的预处理将在此后的源码中完成。
标准的预处理在后面,在每个识别为别的第一个符号开头。它从检查预处理伪指令开始,当没有发现时,预处理将检查是否第一个符号为伪指令。如果没有发现伪指令,它移动到第二行的符号,再一次的开始检查伪指令,此时只有equ伪指令,如同在第二行符号只有一个。如果没有伪指令,第二个符号将被检查结构宏指令,当所有这些都没有检查出正值时,符号常量将被替换为他们的值,并且这些行将传给汇编器。
为了在例子中验证这点,假设有存在一个已定义的宏指令foo和结构宏指令bar。下面的行:
foo equ foo bar
将都被解析为宏指令foo调用,因为第一个符号的意思将覆盖第二的。
宏指令从它们的定义块生成新的行,用参数的值替换参数,然后处理符号中的转义字符“#”“'”。转换操作符比连接操作符有更高优先级,如果它们包含转义字符,在完成处理前转义字符将被忽略。当这完成时,新生成的行将执行上面描述的标准预处理。
虽然符号常见通常仅在行中替换,当即没有预处理伪指令也没有宏指令发现时,在这里包含伪指令的替换位置有一些特例。第一是符号常量的定义,在equ关键词后的任何地方完成的的替换,结果将被赋给新的常量(见2.3.2)。第二中情形是match伪指令,替换将在匹配模板前逗号后面的符号完成。这些特性可以用来维护列表,如下面的定义:
list equ macro append item { match any, list \{ list equ list,item \} match , list \{ list equ item \} }
list常量将被初始化为空值,append宏指令可以增加新的以逗号分隔的项到列表。这个宏指令第一个匹配仅发生在当它们列表值不为空(见2.3.6),此时新的值为上一次值并且新的值将被添加到后面。第二个匹配只当列表不为空时发生,并且此时列表定义中只包含新值。所以从空表开始,append 1将定义list equ 1,后面跟着的append 2将定义list equ 1,2。作为某些伪指令的参数也可以使用。但它不能在直接使用 - 如果foo为伪指令,foo list将作为宏参数传给符号list,因为符号常量此阶段还没有回滚。
match params, list {foo params}
如果list值不为空,和params匹配,当有括号块中定义的新行将会稍后被替换为匹配值。所以如果list含有值1,2,上面的行将生产包含foo 1,2的行,这些都会稍后进入标准处理。
还有一个特殊情形 - 当预处理检查行中第二个参数为冒号(如同标号定义那样由汇编器解析),它在当前位置停止并完成第一个符号的预处理(所以如果它是符号常量它将回滚),如果它仍为标号,它将从标号后位置执行标准预处理。这允许在标号后放置预处理伪指令和宏指令,类似汇编器处理指令和伪指令,例如:
start: include 'start.inc'
然而如果在预处理时断掉(例如当为空的符号常量时),只剩余行中的符号常量将继续。
它必须记住,预处理执行的工作首先为文本符号,他们在主汇编过程前有一个简单的单趟处理。预处理结果后的文本将传给汇编器,然后汇编器将进行多趟操作。因此仅有汇编器识别和处理的控制伪指令 - 它们取决于数值可能在不同趟中改变 - 不能有汇编器以任何方式扫描并且影响预处理。考虑下面的例子:
if 0 a = 1 b equ 2 end if dd b
当预处理上面的语句,唯一有预处理社别的伪指令为equ,它定义了符号常量b,稍后再源码中符号b的值被替换为2.除了这个替换外,对于汇编器其他航没有变化。当预处理后,上面源码将变成:
if 0 a = 1 end if dd 2
此时当汇编器传入它是,if的条件为false,常量a没有定义。然后符号常量b将做普通处理,即使它的定义放在a之后。所以每次你必须非常小心的混合伪指令和汇编器特性 - 经常想象你的源码在预处理后将变成什么,汇编器将看到的,以及它的多遍扫描。
2.4 格式伪指令
格式伪指令实际上一些控制伪指令,其目的是控制生成代码的格式。
format伪指令用来选择输出格式。这条伪指令必须放在源码任何开头。默认的输出格式为flat二进制文件,也可以通过使用“format binary”伪指令来选择。
use16和use32伪指令强制汇编器生成16位或者32位代码,忽略输出格式的默认设置。use64能够为x86-64处理器的长模式生成代码。
下面描述了不同的输出格式以及指定这些格式的伪指令。
2.4.1 MZ 格式
使用“format MZ”伪指令来选择MZ输出格式。此格式代码设置默认为16位。
segment伪指令定义定义新段,后面必须跟着标号,其值将成为定义段的数字。可选的use16或use32可以跟在后面用来指定这个段中的代码为16位还是32位。段是16字节对齐的。所有定义的标号都有一个相对于段开始位置的值。
entry伪指令用来设置MZ可执行文件的入口,它必须指向该入口的远地址(段名,冒号和段中的偏移)。
stack伪指令为MZ可执行文件创建堆栈。它后面可以跟着指定堆栈(将自动创建)大小的数值表达式或者初始堆栈的远地址(如果你想手动设置堆栈)。当没有定义堆栈时,将创建默认的4096大小的堆栈。
heap伪指令应当跟着16位定义额外堆最大值(这个堆除堆栈和未定义数据外)。使用“heap 0”用来分配程序实际需要的内存。堆默认大小为65535。
2.4.2 PE 格式
使用“format PE”伪指令来选择PE输出格式。它可以跟着额外的格式设置:使用console,GUI或者native操作符来选择目标子系统(可以指定子系统版本的浮点数),DLL表示输出文件为一动态库。然后也可以跟着at操作符和指定PE文件基地址的数值表达式,然后可选的跟着on操作符以及自定义PE程序MZ头的文件名(如果指定的文件不是MZ可执行文件,它将被当做flat二进制可执行文件并转化为MZ格式)。这个格式默认的代码设置为32位。一个所有特性的PE格式声明如下:
format PE GUI 4.0 DLL at 7000000h on ’stub.exe’
创建x86-64体系的PE文件,使用PE64关键词而不是PE,这种情况下某人生成长模式的代码。
section伪指令定义一个新的段,它后面必须跟着段名,然后是一个或多个段标志。可能的标志为:code,data,readable,writeable,executable,shareable,discardable,notpageable。段是页对齐的(4096字节)。一个声明PE段的例子:
section ’.text’ code readable executable
除了这些标志也可以指定一些特殊的PE数据标识符来标识整个段为特殊数据,可能的标识符有:export,import,resource和fixups。如果一个段标识为包含fixups,重定位数据将自动创建,不需要在这个段中定义任何数据。资源数据也可以从资源文件中自动创建,它可以通过resource标识符、from操作符和文件名来实现。下面是包含特殊PE数据段的例子:
section ’.reloc’ data discardable fixups section ’.rsrc’ data readable resource from ’my.res’
entry伪指令用来设置PE的入口函数,后面跟着入口函数值。
stack伪指令设置PE的堆栈大小,后面跟着保留的堆栈大小,后面可以跟着可选的用逗号隔开的提交堆栈大小。当没有定义堆栈时,默认大小将为4096字节。
heap伪指令用来选择PE堆大小,后面应当跟着保留的堆大小,后面可以跟着可选的用逗号隔开的提交堆大小。当没有定义堆时,默认大小将为65535字节。当没有指定提交的堆大小时,默认大小为0。
data伪指令用来定义特殊的PE数据,它后面必须跟着一个或多个数据标识符(export,import,resource或fixups)或者PE头中的数据项个数。数据必须在另一行定义,并且以“end data”伪指令结束。当选择了fixups数据时,它们将自动创建不需要定义任何额外数据。同样的规则也适用于resource标识符后面跟着from和文件名 - 此时数据将从给定的资源文件名获取。
rva操作符可以在数值表达式中使用,用来通过它提供的值获取指定项的RVA。
2.4.3 COFF 格式
使用“format COFF”(创建简单的COFF文件)或“format MS COFF”(创建微软COFF文件)伪指令来选择COFF格式。这种格式默认的代码为32位。创建x86-64体系下的微软COFF格式需要使用“format MS64 COFF”,此时将生成长模式代码。
section伪指令定义一个新段,它后面必须跟着段名,然后为一个或多个段标志。两种COFF格式的段都可用的标志为code和data,而readable,writeable,executable,shareable,discardable,notpageable,linkremove和linkinfo只用于微软COFF格式。
默认段是4字节对齐的,微软的COFF格式可以用align操作符后面跟着对齐的大小来指定其他对齐(任意不超过8192的2的指数)。
extrn伪指令定义外部符号,后面必须跟着符号名和可选的size操作数以指定这个符号数据标号的大小。符号名可以包含外部符号名和as操作符。可以这样声明一些外部符号:
extrn exit extrn ’__imp__MessageBoxA@16’ as MessageBox:dword
public伪指令将存在的符号声明为公共的,它必须跟着符号名,可选的as操作符和这个符号外部可用的名称。下面是一些公共符号的声明:
public main public start as ’_start’
2.4.4 ELF 格式
可以使用“format ELF”伪指令选择ELF输出格式。这个模式下默认代码设置为32位。使用“format ELF64”伪指令创建x86-64体系下的ELF文件,此时默认将生成长模式代码。
section伪指令定义一个新段,它必须跟着段名,人啊后是一个或多个executable和writeable标志,可选的align以及指定段对齐数字是可选的(2的指数),如果没有指定对齐,将使用默认的4或8,取决于选择的格式。
extrn和public伪指令和COFF输出格式有着同样的意思和语法(将上一节中的描述)。
这个格式下也可以使用as操作符(当目标体系不为x86-64时),它将地址转换为相对于GOT表格的偏移。还有一个特殊的plt操作符,它允许通过过程链接表格来调用外部函数。你甚至可以为外部函数创建别名,使其总能通过PLT来调用,如同这样的代码:
extrn ’printf’ as _printf printf = PLT _printf
使用跟有executable关键词的format伪指令来创建可执行文件。它允许使用entry伪指令来设置程序入口。另外extrn和public伪指令将不可用,而且只能使用segment伪指令而不是section,segment后面跟着一个或多个允许的标志。段是页对齐的(4096字节),可用的标志有:readable,writeable和executable。
3 Windows编程
Windows 版本的FASM包含一个开发包用来协助开发Windows环境下的程序。这个开发包中,根目录下包含头文件、子目录为一些特殊用途的文件。通常Win32头文件已经为你包含必须的文件。
有六个Win32头文件可供你选择,它们都是以WIN32打头后面跟着字母A(ASCII编码)或者字母W(WideChar编码),其中:
WIN32A.INC、WIN32W.INC为基本头文件。
WIN32AX.INC、WIN32WX.INC为扩展头文件,提供了更多的宏指令,这些扩展将被分别讨论。
WIN32AXP.INC、WIN32WXP.INC是包含过程调用参数个数检查的扩展头文件。
你可以以任何你喜欢的方式包含这些头文件,全路径或者使用自定义的环境变量,但最简单的方式就是定义INCLUDE环境变量指向头文件所在目录,然后就可以这些使用它们:
include 'win32a.inc'
必须注意的是所有的宏指令和内部伪指令不同,它们都是区分大小写的,并且大多数情况下都使用小写。如果想使用默认外的,必须使用fix伪指令来做适当的调整。
3.1 基本头文件
基本头文件WIN32A.INC和WIN32W.INC包含Win32常数和结构定义,还提供了一些标准的宏指令。
3.1.1 结构
所有的头文件都允许使用struct宏指令,以比struc伪指令更简单的类似其他汇编器的方式来定义结构。结构的定义必须以struct宏指令开始,然后是结构名,最后以ends结束。在中间值只允许数据定义伪指令,其中的标号将成为结构字段名。
struct POINT x dd ? y dd ? ends
这样定义了POINT结构后,就可以这样定义一个point1变量:
point1 POINT
上面这条语句将声明一个包含point1.x和point1.y的point1结构,初始化它们默认的值 - 结构定义中提供的值(在这里默认值都是未初始化的值)。定义结构也可以包含参数,参数个数和结构字段个数相同,指定的参数将覆盖结构定义中的默认值。例如:
point2 POINT 10,20
上面这条语句将初始化point2.x值为10,point2.y的值为20。
struct宏不仅声明了指定的结构,也为其中的每个元素定义了偏移。例如POINT结构定义了POINT.x和POINT.y标号为POINT结构中的偏移,sizeof.POINT.x、sizeof.POINT.y为相应字段的大小。sizeof.POINT为结构的大小。标号偏移可以用来间接寻址。比如,当ebx包含指向POINT结构的指针时:
mov eax,[ebx+POINT.x]
当这样寻址时FASM将检查字段的大小。
结构本身也允许内部结构定义,所以结构中也可以包含其他结构字段
struct LINE start POINT end POINT ends
当子结构中没有指定默认值时,如同上面的定义,将使用子结构定义中的默认值。
既然结构声明中每个字段值都是一个单一参数,为了初始化子结构,必须用尖括号将它们括起来:
line1 LINE <0,0>,<100,100>
上面这条语句初始化line1.start.x和line1.start.y值为0,line1.end.x和line1.end.y为100。
如果结构定义字段大小比定义中的小,那么将用未定义的字节来填充到定义中大小(当超过时,将产生错误)。例如:
struct FOO data db 256 dup(?) ends some FOO <"ABC",0>
将给some.data的头四个字节定义数据并且保留剩下的。
结构中也可以定义联合(union)和匿名子结构。联合以union开头以ends结束,例如:
struct BAR field_1 dd ? union field_2 dd ? field_2b db ? ends ends
union的每一个字段都有相同的偏移并且共享同一块内存。只有union的第一个字段将被初始化,其他字段将被忽略(然后如果其中一些域需要比第一个更大的内存时,union将用未定义的字节来填充到指定大小)。整个union用单一参数来初始化。
匿名子结构定义方式和union类似,只是以struct开头,例如:
struct WBB word dw ? struct byte1 db ? byte2 db ? ends ends
匿名子结构只允许接收一个参数来初始化,这个参数可以为一组参数,比如上面的结构可以这样定义:
my WBB 1,<2,3>
union和匿名子结构字段都可以如同父结构的字段一样来访问。例如上面定义中的my.byte1和my.byte2都是正确的标号。
子结构和union可以无限深度的内嵌:
struct LINE union start POINT struct x1 dd ? y1 dd ? ends ends union end POINT struct x2 dd ? y2 dd ? ends ends ends
结构定义也可以基于一些已经定义的结构,并且继承此结构所有字段,例如:
struct CPOINT POINT color dd ? ends
上面的定义等价于:
struct CPOINT x dd ? y dd ? ends
所有头文件都定义了CHAR数据类型,可以用来定义数据结构中的字符串。
3.1.2 导入表
import宏指令用来帮助构造PE文件的导入数据(通常将导入表放在一个单独的段中)。有两个相关宏指令:其一为library,必须放在导入数据的开头,它定义了哪些库将被导入,后面跟着任何长度的参数对,每一对指定了库的标号以及用引号括起来的库的名称,例如:
library kernel32,'kernel32.dll',\ user32,'user32.dll'
上面的语句从kernel32.dll和user32.dll两个库定义导入数据。对于每个库,导入表必须在导入数据中其它地方定义。这通过import宏指令来实现,第一个参数定义标号(同前面library宏中定义相同),每一对参数中包含导入指针和引号括起的函数名(同库导出的函数名相同)。例如上面的library定义可以用以下import定义来完善:
import kernel32,\ ExitProcess,’ExitProcess’ import user32,\ MessageBeep,’MessageBeep’,\ MessageBox,’MessageBoxA’
每一对参数中的第一个参数将传给import宏一个DWORD指针地址,当载入PE后将被填充为导出函数的地址。
如果不用字符串来导入函数,也可以使用序号数字来定义导入函数,例如:
import custom,\ ByName,’FunctionName’,\ ByOrdinal,17
导入宏优化了导入数据,使得只有程序中实际使用了的导入函数才会被放到导入表中,而且假如没有使用某个库的任何函数,那么整个库都不会被引用。这样我们可以很方便让每个库包含完整的导入表 - 对于一些常见的库开发包中包含这样的导入表,他们保存在APIA和APIW子目录下。每一个文件包含一个导入表,小写的文件名作为标号。一个导入KERNEL32.DLL和USER32.DLL库完整导入表的定义如下(假设已经设置好INCLUDE环境变量到开发包到include目录):
library kernel32,’KERNEL32.DLL’,\ user32,’USER32.DLL’ include ’apia\kernel32.inc’ include ’apiw\user32.inc’
3.1.3 过程
有四个宏指令用于带参数传递的过程调用。stdcall声明第一个参数指定的函数使用STDCALL方式的函数调用,其他参数为过程的参数,且以相反的方式压栈。invoke宏和stdcall类似,只是通过第一个参数的标号间接的调用过程,因此invoke可以用来调用导入表中的过程:
invoke MessageBox,0,szText,szCaption,MB_OK
等价于:
stdcall [MessageBox],0,szText,szCaption,MB_OK
它们都将生成下面的代码:
push MB_OK push szCaption push szText push 0 call [MessageBox]
ccall、cinvoke同stdcall、invoke类似,只是它们使用C调用方式,必须由调用者来负责清理堆栈。
定义一个使用参数堆栈和局部变量的过程,可以使用proc宏指令。最简单的格式就是过程名后面跟着所有的参数名,如同:
proc WindowProc,hwnd,wmsg,wparam,lparam
过程名和参数之间的逗号是可选的。过程指令必须另起一行,并且以endp结束proc宏指令。堆栈帧自动在过程入口创建,EBP作为基址来访问参数,所以应当避免把这个寄存器用作其他用途。参数名用来定义基于EBP的标号,可以类似变量那样访问过程参数。例如mov eax,[hwnd]指令等同于mov eax,[ebp+8]。这些标号的范围仅限为本过程。
这样每一个压栈的参数都是DWORD类型的,你也可以自定义参数大小:在参数后面跟着冒号和size操作符。上面的例子可等价的写为:
proc WindowProc,hwnd:DWORD,wmsg:DWORD,wparam:DWORD,lparam:DWORD
如果给定大小小于DWORD,则该标号应用于压栈的DWORD的一部分。如果给定大小大于DWORD,例如四字指针,两个DWORD参数将用来保存这个值,并且标识为一个标号。
过程可以跟着stdcall或者c关键字,用来定义函数调用约定,默认为stdcall。也可以包含uses关键字,其后跟着的一串寄存器(空格分隔)将会自动在过程入口保存并且在退出时恢复。如果这样寄存器列表和第一个参数之间就需要逗号了。一个完整格式的过程语句类似这样:
proc WindowProc stdcall uses ebx esi edi,\ hwnd:DWORD,wmsg:DWORD,wparam:DWORD,lparam:DWORD
local宏指令定义定义局部变量,其后跟着一个或多个逗号分隔的声明,每一个都包含变量名、冒号以及变量类型 - 类型既可以为标准类型(必须为大写)或者结构名。例如:
local hDC:DWORD,rc:RECT
定义局部数组,变量名称后面跟着用中括号括起的数组大小,例如:
local str[256]:BYTE
另一定义局部变量的方式是在一个locals宏指令开头、endl结尾的块中声明局部变量,这样就可以像定义普通数据那样定义局部变量了。上面的的例子可以等价为:
locals hDC dd ? rc RECT endl
局部变量可以在过程中任意地方定义,唯一限制是必须在使用它们之前定义。变量标号的定义域限制为这个过程。如果你给局部变量一些初始值,宏指令将生成指令初始化这些变量,而且这些指令的位置将和它们的声明所在位置相同。
ret可以放置在过程中任意地方,生成完整的必须的代码以正确的退出过程、恢复堆栈以及过程中使用的寄存器。如果你需要生成原始的返回指令,使用retn助记符,或者带有数字参数ret,这样它们将被解析为单一指令。
综述:一个完整的过程定义如同这样:
proc WindowProc uses ebx esi edi,hwnd,wmsg,wparam,lparam local hDC:DWORD,rc:RECT ; the instructions ret endp
3.1.4 导出表
export宏指令用来构造PE文件的导出数据(它必须放在一个标识为export的段中,或者在一个data export块中)。第一个参数为库名称,其余的为任何数目的参数对。每对参数的第一个为源码某地定义的过程名,第二个参数导出函数名。例如:
export ’MYLIB.DLL’,\ MyStart,’Start’,\ MyStop,’Stop’
上面的语句将用源码中的MyStart和MyStop导出两个函数Start和Stop。由于PE结构的需要,宏指令还会自动排序导出表。
3.1.5 COM(组件)
interface宏用来声明COM对象指针,第一个参数为接口名,然后是一串方法,例如:
interface ITaskBarList,\ QueryInterface,\ AddRef,\ Release,\ HrInit,\ AddTab,\ DeleteTab,\ ActivateTab,\ SetActiveAlt
comcall宏用来调用给定对象的方法。该宏的第一个参数为对象句柄,第二个参数为该对象实现的COM接口名,以及方法名和方法参数。例如:
comcall ebx,ITaskBarList,ActivateTab,[hwnd]
使用ebx寄存器存放COM对象,ITaskBarList为接口,调用ActivateTab方法,以[hwnd]为参数。
COM接口名可以类似结构名那样使用,定义变量用来保存给定类型对象的句柄:
ShellTaskBar ITaskBarList
上面一行定义了一个DWORD变量,其中可以存放COM对象句柄。当其中存放了对象句柄后,就可以使用cominvk来调用它的方法。cominvk只需要存放接口的变量名以及方法名作为头两个参数,后面为方法的参数。一个对象句柄保存到ShellTaskBar变量中,调用对象ActivateTab方法可以这样调用:
cominvk ShellTaskBar,ActivateTab,[hwnd]
等同于:
comcall [ShellTaskBar],ITaskBarList,ActivateTab,[hwnd]
3.1.6 资源
有两种方法定义资源,一种是包含外部资源文件,另一种是手动创建资源段。后一种方法,虽然不需要借助任何外部程序,但比较费时。标准头文件提供了一套基本的宏指令帮助组合资源段。
directory宏指令必须放在手动构造资源数据的开头,它定义了包含了哪些类型的资源。它后面跟着一对数据,第一个为资源类型标识,第二个为资源类型子目录的标号。例如:
directory RT_MENU,menus,\ RT_ICON,icons,\ RT_GROUP_ICON,group_icons
子目录可以放在资源区域内主目录后面的任何位置,而且他们必须使用resource宏指令来定义:第一个参数为子目录标号(对应主目录中的标号)后面跟着三个参数 - 每一项中第一个参数定义了资源标识符(这个值可以随意选择,其后程序使用该标识符来访问该资源),第二个参数指定语言,第三个为资源标号。标准的常数可以用来创建资源标识符。例如菜单子目录可以这样定义:
resource menus,\ 1,LANG_ENGLISH+SUBLANG_DEFAULT,main_menu,\ 2,LANG_ENGLISH+SUBLANG_DEFAULT,other_menu
如果资源是语言类型无关的,应当使用LANG_NEUTRAL标识符。有特殊的宏指令来定义各种不同的资源,应当放在资源区域中。
位图资源使用RT_BITMAP类型标识符。定义位图资源可以使用bitmap宏指令,后面第一个参数为资源标号(和位图子目录项对应),第二个参数为位图文件路径,如同:
bitmap program_logo,’logo.bmp’
有两个资源类型和图标有关,RT_GROUP_ICON为资源类型,链接一个或多个RT_ICON类型的资源,每一个都包含一单一图片。它允许在同一资源标识符下定义不同大小和色深的图片。这个RT_GROUP_ICON类型的标识符稍后可以传给LoadIcon函数,它可以选择图片的尺度。定义图标可以使用icon宏指令,第一个参数为GT_GROUP_ICON资源标号,后面为声明图片的参数对。每个参数对的第一个为RT_ICON资源标号,第二个为图标文件路径。当定义一个图标只包含一个图片,可以这样:
icon main_icon,icon_data,’main.ico’
菜单为RT_MENU资源类型,并且使用menu宏指令来定义。menu本身只接收一个参数 - 资源标号。menuitem定义了菜单中的项,它接收五个参数,但只有两个是必须的 - 第一个参数为菜单项名称,第二个为标识符(当用户从菜单上选择一菜单项时将返回此值)。menuseparator不接收任何参数,用来定义菜单中的分隔符。
可选的第三个参数menuitem指定了菜单资源的标志。有两个这样的标志可供选择 - MFR_END为最后菜单项的标志,MFR_POPUP表明指定菜单项为子菜单,而且稍后直到MFR_END标志为止将组成子菜单。MFR_END标志也可以作为memseparator的参数,而且这个宏指令只接受一个参数。为了完整的定义菜单,每一个子菜单都应当用MFR_END标志结束,而且整个菜单也必须这样结束。下面是一个完整的菜单定义:
menu main_menu menuitem ’&File’,100,MFR_POPUP menuitem ’&New’,101 menuseparator menuitem ’E&xit’,109,MFR_END menuitem ’&Help’,900,MFR_POPUP + MFR_END menuitem ’&About...’,901,MFR_END
可选的第四个参数menuitem指定了给定菜单项的状态标志,这些标志和API函数中使用的相同,比如:MFS_CHECK、MFS_DISABLED。类似,第五个参数为类型标志。例如下面定义了一个选中的单选按钮:
menuitem ’Selection’,102, ,MFS_CHECKED,MFT_RADIOCHECK
对话框为RT_DIALOG资源类型,并且使用dialog宏指令后面跟着任何数目的dialogitem开头enddialog结尾的项来定义。
dialog接收十一个参数,只有前七个是必须的。第一个为资源标号,第二个为对话框标题字符串,后面四个参数为水平和垂直坐标、对话框的宽度和高度。第七个参数为对话框窗口样式,可选的第八个参数为对话框的扩展样式。第九个参数为窗口菜单 - 必须为菜单资源的标识符,和子目录中指定的RT_MENU类型相同。最后第十和第十一个参数用来定义对话框的字体 - 其中的第一个为字体名称的字符串,后面的为字体大小。当没有可选的参数时,将使用默认的MS Sans Serif的8号字体。
下面这个例子为dialog宏指令,除了menu(为空值)外给出了所有参数,可选部分在第二行。
dialog about,’About’,50,50,200,100,WS_CAPTION+WS_SYSMENU,\ WS_EX_TOPMOST, ,’Times New Roman’,10
dialogitem有8个必须参数和一个可选参数。第一个参数为窗口类。第二个参数既可以为对话框项字符串或者资源的标识符(当对话框项必须使用另外的一些资源定义,例如包含SS_BITMAP样式的STATIC类)。第三个参数为对话框项的标识符,用作API函数来标识此项。另外的四个参数指定了水平、垂直坐标,宽度和高度。第八个参数为样式,可选的第九个参数为扩展样式。一个对话框项的定义如下:
dialogitem ’BUTTON’,’OK’,IDOK,8,8,45,15,WS_VISIBLE+WS_TABSTOP
一个包含位图的static项,假设存在一个标识符为7的位图:
dialogitem ’STATIC’,7,0,10,50,50,20,WS_VISIBLE+SS_BITMAP
对话框资源的定义可以包含任何数据的对话框项或者没有,最后必须使用enddialog宏指令来结束。
RT_ACCELERATOR资源类型使用accelerator宏指令来创建。第一个参数为资源标号,它们必须跟着三个参数组成的参数对 - accelerator标志,虚拟键或者ASCII字符,以及标识符(和菜单项标识符相同)。一个简单的accelerator定义可以如下:
accelerator main_keys,\ FVIRTKEY+FNOINVERT,VK_F1,901,\ FVIRTKEY+FNOINVERT,VK_F10,109
版本信息为RT_VERSION资源类型,使用versioninfo宏指令来创建。在资源标号后面,第二个参数指定了PE文件的操作系统(通常值为VOS_WINDOWS32),第三个参数为文件类型(对于可执行程序为VFT_APP,对于动态库为VFT_DLL),第四个为子类型(通常为VFT2_UNKNOWN),第五个为语言标识符,第六个为代码页,后面为字符串参数,为属性名称和对应值组。最简单的版本定义如下:
versioninfo vinfo,VOS__WINDOWS32,VFT_APP,VFT2_UNKNOWN,\ LANG_ENGLISH+SUBLANG_DEFAULT,0,\ ’FileDescription’,’Description of program’,\ ’LegalCopyright’,’Copyright et cetera’,\ ’FileVersion’,’1.0’,\ ’ProductVersion’,’1.0’
其它类型的资源可以使用resdata宏指令来定义,resdata只接受一个参数 - 资源标号,后面可以用任何指令来定义数据,最后用endres宏指令结束,例如:
resdata manifest file ’manifest.xml’ endres
3.1.7 字符编码
resource宏指令使用du指令来定义资源中的unicode字符串 - 然后这个伪指令只是简单的零扩展字符为16位,对于包含非ASCII字符的字符串,du可能需要重新定义。对于一些编码,在ENCODING子目录下,宏指令重新定义了du操作符来生成正确的UNICODE字符串。比如:如果源码是以Windows 1250代码页,下面这一行必须放在文件开头:
include ’encoding\win1250.inc’
3.2 扩展头文件
扩展头文件WIN32AX.INC和WIN32WX.INC提供了基本头文件的所有功能,还包括一些复杂的宏指令。而且如果没有声明PE format的话,扩展头文件也将自动声明之。WIN32AXP.INC和WIN32WXP.INC是扩展头文件的另一种,他们执行额外的过程调用参数个数检查。
3.2.1 过程参数
在扩展头文件下,调用过程的宏指令允许比基本头文件中头文件中的DWORD数据更多参数类型。首先,当一个字符串作为参数时,通常需要定义一个字符串数据,然后传给过程一个字符串指针。
invoke MessageBox,HWND_DESKTOP,"Message","Caption",MB_OK
如果参数前有addr,它的意思是这个值为一个DWORD地址并且将传给过程,即使不能直接传入 - 比如局部变量是基于EBP寻址的,在这种情况下EDX将临时用来计算地址,并且传给过程,例如:
invoke RegisterClass,addr wc
当wc为局部变量,地址为ebp-100h,将生成下面的指令:
lea edx,[ebp-100h] push edx call [RegisterClass]
当给定的地址没有任何关联寄存器时,它将直接保存。
如果参数前面包含double,它将被当做64位值,将被当做两个32位值传递。例如:
invoke glColor3d,double 1.0,double 0.1,double 0.1
上面的语句将传入三个64位参数。如果double参数为以内存操作数,不允许包含大小运算符,因为double已经包含了size覆盖。
最后,过程调用可以嵌套,也就是说一个过程可以当作另一个过程的参数。这种情况下,EAX中的返回值将传递给嵌套的那个函数,例如:
invoke MessageBox,,\ "Message","Caption",MB_OK
函数嵌套没有层数限制。
3.2.2 结构化源码
扩展头文件中包含一些宏指令用来帮助简化源码结构。.data和.code为定义数据段和代码段的快捷方式。.end宏指令应当放在程序末尾,带有一个指定程序入口的参数,而且它将自动使用标准的导入表创建导入段数据。
.if宏指令生成一些在执行期间检查的条件语句,根据执行结果决定继续执行下面的块还是跳过。块必须以.endif结尾。
条件可以使用比较运算符 - =, <, >, <=, >=, 和 <>。第一个必须为寄存器或者内存操作数。比较将执行无符号比较。如果你只提供了一个值作为条件,那么它将和零做比较,例如:
.if eax ret .endif
上面的语句当EAX为零时跳过执行ret。
还有一些特殊的符号用来作为条件:ZERO?当ZF为1时为true,类似的还有CARRY?、SIGN?、OVERFLOW?和PARITY?,分别对应CF、SF、OF和PF标志。
上面简单的条件可以组合成复杂的条件表达式:&表示条件与、|条件或、~用来取反,以及括号。例如:
.if eax<=100 & ( ecx | edx ) inc ebx .endif
上面的语句将生成比较和跳转指令,当EAX小于或等于100并且至少ECX或者EDX不为零时执行给定的块。
.while宏指令生成的指令只要条件为true将重复执行给定块(.while宏指令必须以.endw结尾)。.repeat和.until宏指令执行给定块直到.until后面的条件满足。例如:
.repeat add ecx,2 .until ecx>100