flat assembler 1.41 编程者手册

柳向明
2023-12-01

第一章 简介

1.1 编译器概述
1.1.1 系统需求
1.1.2 命令行执行
1.1.3 编译器信息
1.1.4 输出格式

1.2 汇编参数
1.2.1 指令参数
1.2.2 数据定义
1.2.3 常量和标号
1.2.4 数值表达式
1.2.5 跳转和调用
1.2.6 操作数尺寸设置

第二章 指令系统

2.1 Intel指令系统(略,需要查询可参阅原文)
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 AMD 3DNow! 指令

2.2 控制伪指令
2.2.1 重复块指令
2.2.2 条件汇编
2.2.3 其他伪指令

2.3 预处理伪指令
2.3.1 包含源文件
2.3.2 符号常量
2.3.3 宏指令
2.3.4 结构

2.4 格式控制伪指令
2.4.1 MZ可执行文件格式
2.4.2 PE可执行文件格式
2.4.3 COFF文件格式格式


第一章 简介
---------------------------------------------------------------

本章包括你开始使用flat assembler需要了解的所有重要信息,如果你是一个熟练的汇编编程者,在使用之前你至少要读完本章.

1.1 编译器概述

Flat assembler是面向Intel体系结构处理器的快速汇编编译器,用多趟处理优化产生的机器二进制码.它采用了自展编译技术并且包含了适用于不同操作系统的版本,"fasm.exe"是既适用于Dos和Windows的具有双重功能的可执行文件,Linux版本的fasm是另外一个单独文件.所有的不同版本功能相同,均设计于在命令行下执行.

1.1.1 系统需求

所有的版本都需要32位的Intel体系结构处理器(至少80386),不过也可产生面向16位的Intel体系结构处理器的程序.Dos版本要求与MS DOS 2.0兼容的操作系统,Windows版本要求与Win 3.1 CUI模式兼容的操作系统.


1.1.2 命令行执行

从命令行执行编译器需要两个参数,第一个参数是源文件,第二个参数是目标文件.在显示了简单的编译器版本信息等之后,编译器就开始从源文件中读数据并开始编译.如果编译成功,编译器将产生的代码写入输出文件并显示编译过程的摘要信息;如果失败,则显示发生的错误.

源文件应该是任何文本编辑器均可创建的文本文件,换行符可以是Dos格式的,也可以是Unix格式的, 制表符作为空格处理.

除此之外,Fasm不需要任何其他附加的命令行参数.所需要的其他信息需要在源文件中指定,例如,为了指定输出格式,你需要在源文件中使用"format" 伪指令.

1.1.3 编译器信息

如上所述,如果编译成功Fasm会显示编译过程的摘要信息:包括处理了多少趟,耗用多少时间,输出文件多少字节等信息.下面是摘要信息的一个例子:

flat assembler version 1.41
38 passes, 5.3 seconds, 77824 bytes.


在编译过程中如果发生错误,Fasm会显示一个错误信息.例如,如果fasm找不到输入文件,会显示如下错误:

flat assembler version 1.41
error: source file not found.

如果是源代码的某特定部分出错,fasm会显示该行行号及该行代码,以便于发现错误.例如:

flat assembler version 1.41
example.asm [3]:
mob ax,1
error: illegal instruction.

含义是在"example.asm"中编译器遇到不可识别指令.当导致错误的行包含宏指令的时候,错误相对于宏内行数亦同时显示:

flat assembler version 1.41
example.asm [6] stoschar [2]:
stoschar 7
error: illegal instruction.

意思是"example.asm" 的第6行,宏指令的第2行包含不可识别的指令.

1.1.4 输出格式

默认情况下,编译器产生16位二进制指令,编译器仅仅是简单地把产生的二进制指令输出到目标文件中(纯二进制文件).你可以使用"use16"和"use32"伪指令来改变代码产生模式,当选用一些输出格式的时候,也会自动转换为32位模式,2.4节包含相关的更多信息.

所有输出的二进制代码按照源文件中指令的顺序排列,Fasm并不自作聪明地作任何改变.

1.2 汇编参数

以下信息主要是为了方便那些使用过其他编译器的编程者,如果你是初学者,那么应该先找些汇编编程教程来读.

Fasm默认使用Intel格式的汇编指令,你也可以用编译器提供的预处理能力(宏指令和符号常量)定制你自己喜欢的指令格式.当然Fasm本身也有一套专用于编译控制的伪指令.

源文件中定义的所有符号都是大小写敏感的.

1.2.1 指令参数

汇编语句是由换行符分隔的.每行分号后面的内容被视为注释(带引号的分号则不是),'/'表示下一行是本行的继续,编译器把下一行和该行视为一行加以处理.

每条指令都包含助记符和不同数目的以逗号分隔的操作数,操作数可以是寄存器,立即数或内存寻址的操作数.内存寻址和立即操作数可以加大小前缀指示伪指令来指示尺度(见表1.1).可用的寄存器见表1.2.立即数可以用任何数值表达式指定.

对内存寻址当操作数而言,数据地址应该用方括号括住或使用"ptr"前缀.例如

"mov eax,3" 把立即数3送EAX寄存器, "mov eax,[7]" 则把地址7处开始的32位值送eax寄存器,
"mov byte [7],3" 把立即数3送地址7处开始的字节, 也可写为 "mov byte ptr 7,3".
段超越前缀(即段寄存器名)应后跟冒号置于地址之前(亦应放在方括号里面或在"ptr"后面).

Table 1.1 Size Operators
Operator Bits Bytes
----------------------
byte 8 1
word 16 2
dword 32 4
fword 48 6
pword 48 6
qword 64 8
tword 80 10
dqword 128 16
------------------

Table 1.2 Registers
----------------------------------------------------------
Type Bits
----------------------------------------------------------
General 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
----------------------------------------------------------
Segment 16 es cs ss ds fs gs
--------------------------------------------------------
Control 32 cr0 cr2 cr3 cr4
----------------------------------------------------------------
Debug 32 dr0 dr1 dr2 dr3 dr4 dr5 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.2 数据定义


定义数据或保留空间,请使用表1.3中所列的伪指令.数据定义伪指令后面应该紧跟用逗号分隔的数值表达式.依据伪指令的不同,可定义了相应数据尺寸,比如"db 1,2,3"定义的是以字节为单位的数值
1,2,3.

"db" 和 "du"也接受任意长度的引号括起来的字符串,编译器会自动把字符串转化为相应的二进制数值串.当使用du的时候,产生的是高字节为0的word串,即UNICODE格式字串.例如"db 'abc'"定义了三字节的数据61,62和63.

"dp" 和 "df"可接受两个以逗号分隔的数值表达式作为参数,第一个值将成为高字,第二个值将成为远指针的低dword值."dd"也接受类似的以逗号分隔的以word为单位的数值."dt" 仅接受浮点数并自动转化为扩展精度实数储存.

"file"是一个特殊的伪指令,他的参数有所不同,其功用是含入指定文件特定字节数.他后面应紧跟用引号括起来的文件名,然后是可选的用于指定载入数据偏移的数值表达式,再后面是可选的载入字节数,如果未指定字节数,那么到文件末尾的所有数据均被含入.这样我们就不用象在Masm中要含入二进制数据文件先要用工具转换为db 格式了.

数据保留伪指令保留了其后紧跟的数值表达式乘以其特定尺寸大小的相应空间.所有的数据定义伪指令也接受"?"作为参数,这和使用数据保留指令的效果相同.输出文件中可能不包含未初始化数据,因此总应该认为未初始化的数据是未知值而不是零.

Table 1.3 Data directives
--------------------------
Size Define Reserve
(bytes) data data
---------------------
1 db rb
file
---------------------
2 dw rw
du
----------------------
4 dd rd
---------------------
6 dp rp
df rf
---------------------
8 dq rq
---------------------
10 dt rt
---------------------


1.2.3 常量和标号

在数值表达式中你也可以使用常量和标号来代替纯数字.应使用特定的伪操作符定义常量和标号,标号仅可定义一次,在源文件中任何地方皆可使用(包括在定义之前).常量可多次定义,在多次定义的情况下,常量只能在定义之后使用,其值是最近一次的定义值.如果仅定义一次的话,那么它就象标号一样可以在源文件的任何位置使用.

常量定义包括常量名字,"="和其后紧跟的数值表达式.常量定义数值总是在定义时计算的.例如你定义了"count = 17",然后在汇编指令里面使用"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比较操作数的尺寸.你可以使用尺寸超越前缀强迫编译器编译这条指令:"mov ax,word [char]",但是你要记住此指令读取的char处开始的两字节,char定义为一个字节,因此要注意越界问题.

最后也是最灵活的定义标号的方法是使用"label"伪指令,该伪指令后面跟标号名称,然后是可选的尺寸操作符(以逗号分隔),再后面是"at"操作符,"at"后面紧跟定义标号偏移的数值表达式.例如"label wchar word at char",在char处定义了一个16位的数据,现在"mov ax,[wchar]"在
编译后将和"mov ax,word [char]"等效了,如果不指定地址,"label"操作符就在当前位置定义标号. "mov [wchar],57568"将拷贝两字节,"mov [char],224"将拷贝1字节到同一地址偏移处.

以"."开头的标号作为局部标号,他附加在最近的全局标号后面组成完整的标号名.你可以在下一个全局标号出现前的任何地方使用以"."开头的短名称,但在除此以外的其他地方就不得不使用完整名称了.以".."双点号开头的标号是例外,他们和全局标号类似,但是不构成局部标号的前缀(也就是不影响前面的全局和局部标号机制).
"@@"表示匿名标号(和MASM的@@机制相同),源文件中@@标号数量不限.在指令或表达式中"@b"或"@r"会引用最接近的前面匿名标号,"@f"则引用最接近的后面的匿名标号.注意,这些特殊符号是大小写敏感的.
"load" 伪指令允许定义在编译过程中从文件载入特定偏移的二进制值.其后紧跟常量名称,然后是尺寸大小伪指令再后是"from *.file",其后可以跟逗号和指定偏移的数值表达式,如果未指定尺寸操作符,仅仅载入1个字节,如果不指定文件名而仅仅指定偏移,那么编译器将从已经编译好的二进制代码中提取数据,在此情况下,给定的偏移值应该是当前产生代码空间里面的有效地址,载入数据不能超过当
前偏移.

1.2.4 数值表达式

在以上例子中所有的数值表达式都是简单的数字,常量和标号.但实际上,用数学或逻辑操作符可以在编译时产生更复杂的表达式.表1.4列除了所有这些操作符的优先级值.

高优先级的操作符总是被首先计算,你可以象在数学中一样使用圆括号来改变优先级计算顺序."+", "-", "*" , "/"是标准的算术操作符,"mod"是取余数,类似C中的"%","and", "or", "xor","shl","shr" 和"not"和汇编指令中的相同名称指令功能相同. "rva"用于把PE中的地址转换为RVA,即相对虚拟地址.

表达式中的数字默认情况下被视为十进制,二进制应该后面加b,八进制应以数字"0"开头或后面加"o",16进制以0x(和C一样)或"$"(和pascal一样)开头或以"h"结尾,表达式中遇到引号括起来字符串,会被转换为数值,第一个字符会被转换成数值的最低位.(和MASM的习惯不同,注意.)

用于寻址的数值表达式也可以包含任何通用寄存器,他们可以用适当的数值加和乘,就象在Intel体系指令中一样.

在数值表达式中可以使用两个特殊符号,第一个是"$","$"总是等于当前的地址偏移值.第二个是"%",表示在使用某些特殊伪指令时候的当前重复次数.

任何数值表达式都可以包括一个以科学记数法表示的浮点数(在编译期Fasm不允许任何浮点操作),可以是以"f"结尾或至少包括一个"." 或 "E",因此"1.0", "1E0" 和 "1f"定义同样的浮点数值,而简单的"1'则定义一个整数.

Table 1.4 Arithmetical and logical operators by priority
-----------------------
Priority ?Operators
-----------------------
0 + -
-----------------------
1 * /
-----------------------
2 mod
-----------------------
3 and or xor
-----------------------
4 shl shr
-----------------------
5 not
-----------------------
6 rva
-----------------------


1.2.5 跳转和调用

任何jmp和call指令前面不仅可以接尺寸操作符,还可以指定跳转类型操作符:"near" 或 "far".例如在16位模式下"jmp dword [0]"就成为远跳转而在32位模式下就成为近跳转.为了使此指令改变默认含义,使用"jmp near dword [0]" 和 "jmp far dword [0]" 格式.

当跳转的操作数是立即数的时候,如可能,编译器会产生该指令的最短形式.(除非有尺寸操作数指明,否则不会在16位模式下产生32bit指令,也不会在32位模式下产生16bit指令.)通过指定尺寸操作符,你总是可以强制产生长格式或短格式,如果为不可能形式,则会产生错误.

1.2.6 操作数尺寸设置

如果地址值适合,默认情况下会产生短的8位值格式,但是你可以通过"word" 或 "dword"等尺寸操作符等来改变.(放在括号里面或在"ptr"后面)

指令"adc", "add", "and", "cmp", "or", "sbb", "sub" 和 "xor"在第二个操作数可以表示为8位有符号数的情况下,产生短格式指令,你当然也可以通过"word" 或 "dword"等尺寸操作符来改变该规则.

"push"指令后面的立即数如果没有尺寸操作前缀在话,在16位模式下作为word看待,在32位模式下作为dword值看待,如果可能,编译器会产生8位短操作数格式,"word" 或 "dword"可以强制产生特定长度的操作数格式.


第二章 指令系统
---------------------------------------------------------------

本章提供Fasm支持的所有指令和伪指令的详细信息,常数和标号定义伪指令已经在1.2.3中说明,其他伪指令在本章后面叙述.

2.1节略.
注:该节主要介绍intel指令格式,基本和使用其他编译器相同,约略不同请参照原文,因本译文主要针对有经验的asm使用者,因此主要介绍fasm的特色,故本节略掉.

2.2 控制伪指令

本节描述控制汇编过程的伪指令,他们在编译期可能导致某些指令块以不同方式编译或根本不被编译.

2.2.1 重复块指令

"times"重复指定次数指令.当使用"%"时,代表当前重复次数.例如"times 5 db %"定义1, 2,3, 4, 5共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,也就是意味着该块根本不会被编译.


2.2.2 条件汇编

"if"伪指令导致仅在特定条件下才编译某块代码,其后应紧跟条件表达式.其后的代码块仅在该条件满足时才被编译.可选的"else if"是在前面的"if"条件和"else if"条件不满足的时候才被评估的分支."else" 是在所有前面的条件均不满足的条件下才被编译的部分."end if" 结束伪 指令块.

逻辑表达式由逻辑值和逻辑操作符构成,"~"是逻辑非,"&"是逻辑与,"|"是逻辑或."~"的优先级最高.逻辑表达式也可以是数值表达式,如果等于0则为false值,否则为TRUE值.数值表达式可以用">" ,"<=",">=","=","<","<>"."eq"用于比较两个符号是否完全相同."in"用于检查指定符号是否在列表中,列表应该用"<>"括起来,其中元素应该用","分隔."defined" 和 "used"后面应跟符号名,"defined"检查给定符号是否被定义了,"used"检查给定符号是否在其他地方使用过了.

The following simple example uses the "count" constant that should be defined somewhere in source:
下面的例子使用已经在源码中定义的"count"常量.
if count>0
mov cx,count
rep movsb
end if

下面的例子更复杂一点,并假设"reg"符号常量已经定义了:

if reg in <cs,ds,es,fs,gs,ss>
mov dx,reg
add ax,dx
shl ax,1
else if reg eq ax
shl ax,2
else
add ax,reg
shl ax,1
end if

最后一个例子显示了如何定义一个还未定义的变量:

if ~defined var & used var
var db 0
end if


2.2.3 其他伪指令

"virtual"在特定地址定义虚拟数据.输出文件中不会包含这个这些数据.该伪指令后面可接at+数值表达式地址,否则使用当前地址,这就等价于"virtual at $".用"end virtual"结束虚拟数据定义.

该伪指令可用于创建一些变量的联合,例如:
GDTR dp ?
virtual at GDTR
GDT_limit dw ?
GDT_address dd ?
end virtual

为"GDTR"处的数据定义了两个标号。

virtual还可为以寄存器寻址的某些结构定义标号,例如:
virtual at bx
LDT_limit dw ?
LDT_address dd ?
end virtual

如此定义之后 "mov ax,[LDT_limit]" 将被编译为 "mov ax,[bx]".

在virtual块内定义数据和指令也是有用的,因为"load"伪指令可以将虚拟产生的代码载入一个常量。"load"伪指令应在virtual块内但是在要载入的值后面使用,因为仅可以从同一代码空间载入数值。例如:
virtual at 0
xor eax,eax
and edx,eax
load zeroq dword from 0
end virtual

上面的代码定义了包含在virtual块内的指令机器码的前4个字节"zeroq"常量。

"display" 伪指令在编译时显示提示信息,后面应跟引号括起来的字符串,用逗号分隔,可以用此方法来显示常量值,例如:

d1 = '0'+ $ shr 12 and 0Fh
d2 = '0'+ $ shr 8 and 0Fh
d3 = '0'+ $ shr 4 and 0Fh
d4 = '0'+ $ and 0Fh
if d1>'9'
d1 = d1 + 'A'-'9'-1
end if
if d2>'9'
d2 = d2 + 'A'-'9'-1
end if
if d3>'9'
d3 = d3 + 'A'-'9'-1
end if
if d4>'9'
d4 = d4 + 'A'-'9'-1
end if
display 'Current offset is 0x',d1,d2,d3,d4,13,10

"display"前面的伪指令计算4个16位值并把他们转换成字符显示。

2.3 预处理伪指令

所有的预处理伪指令在主汇编过程之前处理,因此不受控制伪指令的影响。此时所有的注释都已去掉。

2.3.1 包含源文件

"include" 伪指令在其使用处包含特定的源文件,后面的文件名应用引号括起。例如:

include 'macros.inc'

整个包含文件会在"include" 的处理下一行先行处理,只要你有足够的内存,包含文件个数无数目限制。

路径中可以包括用"%"括起来的环境变量,在编译时会用实际变量值来取代之,在"file" 和 "load"后的路径也适用同样规则。

2.3.2 符号常量

符号常量和数值常量不同,在汇编过程之前所有的符号常量都被用其对应的真实值替代,其真实值可以为任何符号。

符号常量定义中如果包含另外的符号常量,那么首先会用该符号常量的实际值来替代。例如:
d equ dword
NULL equ d 0
d equ edx

在上述3行定义之后,"NULL"常量值就是"dword 0",而"d" 常量值是 "edx"。"push NULL"就被汇编成"push dword 0","push d" 将被汇编成"push edx".

"restore"伪指令会将重新定义的符号常量恢复为前一个值,其后应紧跟一个或多个以逗号分隔的符号常量名,因此"restore d"会使"d"常量恢复为"dword",如果"restore" 后面的常量值没有定义或不存在,那么"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

因为符号常量亦允许空值,所以正好可以利用该特点在任何地址前面如masm般使用"offset" 修饰符,不过首先要定义如下:
offset equ

在该定义后"mov ax,offset char"就是合法指令了,因为他等价于"mov ax,char".

2.3.3 宏指令

"macro" 伪指令允许你定义称为宏指令的复杂指令,利用宏指令可以极大地简化编程过程.最简单的宏指令和符号常量类似.例如下面的定义就仅仅是"test al,0xFF"的替代.

macro tst {test al,0xFF}

"macro"伪指令之后就是宏指令名和以"{" 和"}"括起来的他的实际定义,在该定义后你可以在源代码任何地方使用"tst",均在编译过程中被替换为"test al,0xFF".也可以定义一个符号常量达到同样的目的,宏指令的定义可以包含若干行,因为"{" 和 "}"不必在同一行,例如
macro stos0
{
xor al,al
stosb
}

任何地方再使用"stos0"都会替换为上述两条指令.

正如指令需要一些操作数一样,宏指令也可定义为接受参数.参数应跟在"macro" 后面,如果为多个应以逗号分隔.下面是一个用于二进制输出文件对齐的宏的例子:

macro align value { rb (value-1)-($+value-1) mod value }

在此宏定义之后如果使用"align 4"的话,就会用"rb (4-1)-($+4-1) mod 4"来代替这条宏指令.如果宏指令在其定义内使用了和该宏指令同名的指令,那么就会使用前面已定义的含义.这可以用来重复定义宏指令,例如:

macro mov op1,op2
{
if op1 in <ds,es,fs,gs,ss> & op2 in <cs,ds,es,fs,gs,ss>
push op2
pop op1
else
mov op1,op2
end if
}

这条宏指令扩展了"mov"的功能,因为他允许两个操作数同为段寄存器,例如"mov ds,es" 将被编译为"push es" 和 "pop ds".在其他情况下就默认使用真正的mov指令."mov"指令还可以通过下面的定义进一步扩展,其定义内的"mov"指令使用该"mov"的定义:

macro mov op1,op2,op3
{
if arg3 eq
mov op1,op2
else
mov op1,op2
mov op2,op3
end if
}

允许 "mov"使用三操作数,但仍然允许两个操作数.当使用3个操作数的时候,就展开成前面定义的两条宏指令,因此"mov es,ds,dx"将编译成"push ds", "pop es" 和 "mov ds,dx".

"purge"伪指令允许消除最近一次的特定宏指令定义.其后接以逗号分隔的宏名字,如果宏指令未定义,也不会出现错误.例如定义了上面的扩展参数的"mov"宏指令,使用"purge mov"就会恢复到两参数状态,如果再次使用宏指令,才会完全禁止"mov"的宏定义.

如果在"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
}

每次使用该宏指令的时候,"move"都会被赋予唯一的名字,因此你不会碰到通常情况下的标号重复定义错误.forward", "reverse" 和 "common" 伪指令把宏指令分成块,每块都在前面的块处理完之后才被处理,他们仅在宏指令使用组参数的时候才有所不同."forward"后面的指令块对每组参数而言都是按照从左到右的顺序处理的."reverse"后面的块对每组参数而言都是按照和"forward"相反的顺序处理,"common"伪指令后的指令块对所有的组而言仅处理一次.在其中定义的局部标号对其他块的同组参数而言是可见的,当然common块中的标号对所有组而言仅仅可能出现一次.

这有一个创建指针数组的例子,其参数是多个指令串:

macro strtbl name,[string]
{
common
label name dword
forward
local label
dd label
forward
label db string,0
}

这个宏的第一个参数是数组起始地址的标号,其后的参数是字符串.第一个common块仅处理一次,定义了一个起始标号.forward块为每个字符串声明了一个标号,并且定义了一个包含所有字符串地址的数组,最后一个forward块定义了以前面forward块声明的标号开头的Asciiz串.

这三个伪指令可以和其后的代码放在同一行,就象下面的例子所示:

macro stdcall proc,[arg]
{
reverse push arg
common call proc
}

这条宏指令可用于调用STDCALL调用约定的程序,参数以从右到左的顺序压栈,"stdcall foo,1,2,3"将被编译为:

push 3
push 2
push 1
call foo

如果宏指令内的某变量可能具有多个值,并且用在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 message arg
{
if arg eq +arg
mov dx,arg
else
local str
jmp @f
str db arg,0Dh,0Ah,24h
@@:
mov dx,str
end if
mov ah,9
int 21h
}

上面的宏用于在dos下显示一条消息.当宏参数是数字或标号的时候,就显示该地址处的字串,如果参数是字符串的话那么就直接显示,只不过在其后加了回车和换行符.

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" 引址的一个结构包括两个成员:"my.x" 为 7 ,"my.y" 为 11.
Next example shows how to extend the data definition directive "db" with
ability to calculate the size of defined data by using the structure
macroinstruction:
下面的例子显示了如何使用结构定义来扩展"db"的功能,新定义的"db"可以自动获得字符串的长度
struc db [data]
{
common
label .data byte
db data
.size = $-.data
}

例如 "msg db 'Hello!',13,10" 也定义了"msg.size" 变量, 为实际数据的大小.

定义以寄存器寻址的结构或以绝对地址值开始的数据结构应当使用"virtual" 指令(见 2.2.3).

2.4 格式控制伪指令

"format" 用于选择输出格式,应该置于源文件的头部.默认文件输出格式是二进制文件,也可以使用"format binary"伪指令来显式选择.

"use16" 和 "use32"伪指令强制编译器忽略默认设置强制产生16位或32位代码.org"操作符设置其后代码在内存中的出现地址,应该接一个指定地址的数值表达式.

以下简要介绍不同的输出格式以及与特定输出格式相关的伪指令.

2.4.1 MZ可执行文件格式

使用MZ输出文件格式用"format MZ"伪指令,默认代码为16位.

"segment"伪指令定义新段,其后可以跟段名,可选"use16"或"use32"指定使用16或32位模式,段起始值以小节对齐(16 bytes),所有的标号值将全部相对于该段开始.

"entry" 用于指定程序入口,其格式是段:偏移.

"stack" 设置Mz文件的堆栈大小,其后可跟指定堆栈大小的数值表达式或初始堆栈地址的的远指针(如果你想手动设置堆栈的话).如果未显式设置堆栈,堆栈默认为4096字节.

后跟16位数字指定以节为单位的最大的附加堆占用内存的数目.使用"heap 0"分配程序实际需要的堆.默认值为65535.

2.4.2 PE可执行文件格式

选择PE格式,使用"format PE"伪指令,后跟"console", "GUI" 或 "native"选择子系统(可用浮点数指定子系统版本号),"DLL"指定输出为动态链结库,"at"后指定PE基址,"on"指定dos stub文件(如果给定文件不是MZ文件则被视为二进制文件并被转换成MZ文件格式),默认为32位代码模式.

"section" 伪指令定义一个新节.后面可以用引号括起节的名字,其后跟一个或多个标志.可用标志有: "code", "data", "readable", "writeable","executable", "shareable", "discardable". 还有几个特殊标志可以指定该节为特殊数据:export", "import", "resource" 和"fixups".如果指定了"fixups"标志,那么会自动产生重定位信息,不需要额外定义.节的起始值以页(4096 bytes)对齐.

"entry" 指定程序入口.

"stack" 伪指令用于设置PE的堆栈大小,后面跟保留大小,可选跟提交的堆栈大小,,以逗号分隔.如果未指定堆栈大小,默认设为4096字节.


"heap" 伪指令用于设置PE的堆大小,后面跟保留大小,可选跟提交的堆大小,以逗号分隔.如果未指定堆大小,默认保留设为65536字节,提交大小设为0.

"data"伪指令用于开始特殊的PE数据定义,后面可跟data类型标识("export", "import", "resource" 或"fixups")或PE头部数据目录项的数目.数据定义结束应以"end data" 结尾.如果选用了fixups数据类型,那么不需要作任何定义重定位数据会自动产生.

2.4.3 COFF文件格式

用"format COFF" 或 "format MS COFF"选择使用标准的COFF文件格式还是Microsoft's 的COFF文件格式,默认代码编译模式为32位.

"section"伪指令定义新节,后面可以用引号括起字符串标识节名,还可指定对两种COFF格式都适用的标志: "code" 和 "data"."readable", "writeable", "executable", "shareable" and "discardable"标志仅适用于Microsoft的COFF格式.节的起始值以页(4096 bytes)对齐.

"extrn" 伪指令声明在其他模块中定义的外部符号,应紧跟符号名字和可选的尺寸大小操作符.


"public" 伪指令将已存在的符号声明为公有,后面跟要声明的符号名字.

译者后话:
在工具世界里面总有一些让人意外的惊喜,fasm就是其中之一,希望这个简短的说明能带你进入fasm的自由世界,fasm不是最强大的,尤其是在编Win32程序的时候,但毫无疑问,用他来写程序是很有趣的.译文不好,大家凑合着看,信达雅达不到,只要能使大家明白,那么我的初衷就已经达到.

[心灵永远的寂寞,使我走入这片神奇地带,于是,我不再孤独.----YJ.Hume]
2002.11.23

 类似资料: