- http://www.xuyibo.org/article/50.htm
- 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 操作数尺寸设置
这一章包含开始使用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文件末尾添加下面的内容:
[Environment]Include = c:\fasmw\include
如果不设置好INCLUDE环境变量,当include文件的时候,就必须提供完整的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.6638 passes, 5.3 seconds, 77824 bytes.
当编译错误时,程序将显示错误信息。比如,当编译器找不到收入文件时,将显示下面的信息:
flat assembler version 1.66error: source file not found.
如果错误和部分源码相关,导致错误的源码行将被显示。相应行的位置也会给出,以帮助你快速定位错误,比如:
flat assembler version 1.66example.asm [3]:mob ax,1error: illegal instruction.
意思是example.asm的第三行编译时遇到了无法识别指令。如果导致错误行包含一个宏指令,生成错误指令的宏指令定义也将显示。比如:
flat assembler version 1.66example.asm [6]:stoschar 7example.asm [3] stoschar [1]:mob al,charerror: 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 |
表1.1:size操作符
类型 | 位数 | |
通用寄存器 | 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.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.3 数据伪指令
数据保留伪指令值允许跟着一个数值表达式,这个值定义了多少个指定大小单元空间将被保留。所有的数据定义伪指令都允许“?”值,意思是这个单元不应初始化为任何值,效果和数据保留伪指令相同。未初始化的数据可能没有存在于输出文件中,所以其值应当总是被认为是不可知的。
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 |
表1.4:算术和逻辑操作符优先级
表达式中的数字默认为十进制的,二进制数字可以在后面跟着字母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位代码,而不是强制它使用长格式指令。