徐艺波个人网站 | Make everything as simple as possible, but not simpler. Albert Einstein | ||
| 有什么好的建议,可以贴一下。 | |||
| 你的支持,让我们做的更好。 | |||
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伪指令来做适当的调整。
基本头文件WIN32A.INC和WIN32W.INC包含Win32常数和结构定义,还提供了一些标准的宏指令。
所有的头文件都允许使用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数据类型,可以用来定义数据结构中的字符串。
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 ’apiakernel32.inc’ include ’apiwuser32.inc’
有四个宏指令用于带参数传递的过程调用。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
export宏指令用来构造PE文件的导出数据(它必须放在一个标识为export的段中,或者在一个data export块中)。第一个参数为库名称,其余的为任何数目的参数对。每对参数的第一个为源码某地定义的过程名,第二个参数导出函数名。例如:
export ’MYLIB.DLL’, MyStart,’Start’, MyStop,’Stop’
上面的语句将用源码中的MyStart和MyStop导出两个函数Start和Stop。由于PE结构的需要,宏指令还会自动排序导出表。
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]
有两种方法定义资源,一种是包含外部资源文件,另一种是手动创建资源段。后一种方法,虽然不需要借助任何外部程序,但比较费时。标准头文件提供了一套基本的宏指令帮助组合资源段。
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
resource宏指令使用du指令来定义资源中的unicode字符串 – 然后这个伪指令只是简单的零扩展字符为16位,对于包含非ASCII字符的字符串,du可能需要重新定义。对于一些编码,在ENCODING子目录下,宏指令重新定义了du操作符来生成正确的UNICODE字符串。比如:如果源码是以Windows 1250代码页,下面这一行必须放在文件开头:
include ’encodingwin1250.inc’
扩展头文件WIN32AX.INC和WIN32WX.INC提供了基本头文件的所有功能,还包括一些复杂的宏指令。而且如果没有声明PE format的话,扩展头文件也将自动声明之。WIN32AXP.INC和WIN32WXP.INC是扩展头文件的另一种,他们执行额外的过程调用参数个数检查。
在扩展头文件下,调用过程的宏指令允许比基本头文件中头文件中的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
函数嵌套没有层数限制。
扩展头文件中包含一些宏指令用来帮助简化源码结构。.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