.386
这是一个汇编语言伪指令,他告诉编译器我们的程序是使用80386指令集编写的。您还可以使用 .486、.586, 但最安全的还是使用.386。对于每一种CPU有两套几乎功能相同伪指令: .386/.386P、 486/.486P、 586/.586P。 带P的指令标明您的程序中可以用特权级指令。特权级指令是保留给操作系统的,如虚拟设备驱动程序。在大多数时间,您的程序都无须运行在RING0层,故用不带后缀P的伪指令已足够了。
.model flat, stdcall
.MODEL 是用来指定内存模式的伪指令,在Win32下,只有一种内存模型,那就是FLAT。 STDCALL 告诉编译器参数的传递约定。参数的传递约定是指参数传达时的顺序(从左到右或从右到左)和由谁恢复堆栈指针(调用者或被调用者)。在Win16下有两种约定:C 和 PASCAL。C 约定规定参数传递顺序是从右到左,即最右边的参数最先压栈,由调用者恢复堆栈指针。
例如:为调用函数 foo ( int first_param, int second_param, int third_param ); 按C约定的汇编代码应该是这样的:
push [third_param] push [second_param] push [first_param] call foo add esp, 3 * 4 ;调用者自己恢复堆栈指针
PASCAL约定和C约定正好相反,它规定参数是从左向右传递,由被调用者恢复堆栈。Win16采用了PASCAL约定, 因为PASCAL约定产生的代码量要小。当不知道参数的个数时,C约定特别有用。如在函数wsprintf () 中, wsprintf预先并不知道要传递几个参数,所以它不知道如何恢复堆栈。STDCALL是C约定和PASCAL约定的混合体,它规定参数的传递是从右到左,恢复堆栈的工作交由被调用者。Win32只用STDCALL约定,但除了一个特例,即:wsprintf。 option casemap :none ; case sensitive 此处";"为注释
用option语句定义的选项有很多,如option language定义和option segment定义等,在Win32汇编程序中,需要的只是定义option casemap:none,这个语句定义了程序中的变量和子程序名是否对大小写敏感,由于Win32 API中的API名称是区分大小写的,所以必须指定这个选项,否则在调用API的时候会有问题。 include \masm32\include\windows.inc include \masm32\include\kernel32.inc include \masm32\include\user32.inc
includelib\masm32\lib\user32.lib includelib \masm32\lib\kernel32.lib
伪指令 include,跟在其后的文件名所指定的文件在编译时将“插”在该处。在我们上面的程序段中,当MASM处理到语句 include \masm\include\windows.inc 时,它就会打开文件夹\MASM32\include 中的文件windows.inc,这和您把整个文件都粘贴到您的源程序中的效果是一样的。
您的应用程序除了从 windows.inc 中得到相关变量结构体的定义外,还需要从其他的头文件中得到函数原型的声明,这些头文件都放在 \masm32\include 文件夹中。 在我们上面的例子中调用了驻扎在 kernel.dll 中的函数,所以需要包含有这个函数原型声明的头文件 kernel.inc。如果用文本编辑器打开该文件您会发现里面全是从 kernel.dll中引出的函数的声明。如果您不包含kernel.inc,您仍然可以调用(call)ExitProcess,但不能够调用(invoke)ExitProcess(这会无法通过编译器和连接器的参数合法性检查)。所以若用 invoke 去调用一个函数,您就必须事先声明,当然不一定要包含我们的头文件,您完全可以在调用该函数前在源代码的适当位置进行声名。包含头文件主要是为了节省时间(译者:当然还有正确性)
接下来我们来看看 includelib 伪指令,和 include 不同,它仅仅是告诉编译器您的程序引用了哪个库。当编译器处理到该指令时会在生成的目标文件中插入链接命令告诉链接器链入什么库。当然您还可以通过在链接器的命令行指定引入库名称的方法来达到和用includelib指令相同的目的,但考虑到命令行仅能够传递128个字符而且要不厌其烦地在命令行敲字符,所以这种方法是非常不可取的。
.data
.DATA .DATA? .CONST .CODE 上面的四个伪指令是"分段"(SECTION)伪指令。我们上面刚讲过Win32下没有"段"(SEGMENT)的概念,但是您可以把您的程序分成不同的"分段", 一个"分段"的开始即是上一个"分段"的结束。WIN32中只有两种性质的"分段":DATA和CODE。 其中DATA"分段"又分为三种: .DATA 其中包括已初始化的数据。 .DATA? 其中包括未初始化的数据。比如有时您仅想预先分配一些内存但并不想指定初始值。使用未初始化的数据的优点是它不占据可执行文件的大小,如:若您要在 .DATA? 段中分配10,000字节的空间,您的可执行文件的大小无须增加10,000字节,而仅仅是要告诉编译器在装载可执行文件时分配所需字节。 .CONST 其中包括常量定义。这些常量在程序运行过程中是不能更改的。 应用程序并不需要以上所有的三个"分段", 可以根据需要进行定义。
MsgBoxCaption db "Hello",0 MsgBoxText db "Hello World!",0
在.DATA“分段”定义了两个NULL结尾的字符串, 末尾跟0表示字符串结束
.code
.CODE 这是代码"分段"。 <译者注:实际上,分段并不是象在 Dos 下一样,为不同的段分别指出不同的段寄存器,因为 Windows 下只有一个 4GB 的段,Windows 程序中的分段表现在当程序装载时,赋予不同的分段不同的属性,比如说当你的程序加载时,对于 Ring3 程序来说,.code 段是不可写的,而 .data 段是可写的,如果你尝试象在 Dos 下一样写自己的代码部分,你会得到一个蓝屏错误>
start:
end 是用来唯一标识您的代码范围的标签, 两个标签必须相同,应用程序的所有可执行代码必修在两个标签之间。
invoke MessageBox, NULL, addr MsgBoxText, addr MsgBoxCaption, MB_OK
该函数的原型如下:
MessageBox PROTO hwnd:DWORD, lpText:DWORD, lpCaption:DWORD, uType:DWORD
hWnd 是父窗口的句柄。句柄代表您引用的窗口的一个地址指针。它的值对您编 Windows 程序并不重要(译者注:如果您想成为高手则是必须的),您只要知道它代表一个窗口。当您要对窗口做任何操作时,必须要引用该窗口的指针。 lpText 是指向您要显示的文本的指针。指向文本串的指针事实上就是文本串的首地址。 lpCaption 是指向您要显示的对话框的标题文本串指针。 uType 是显示在对话框窗口上的小图标的类型。
MB_OK 在windows.inc 文件中有定义
addr 操作符用来把标号的地址传递给被调用的函数,它只能用在 invoke 语句中,譬如您不能用它来把标号的地址赋给寄存器或变量,如果想这样做则要用 offset 操作符。在 offset 和 addr 之间有如下区别:
addr不可以处理向前引用,offset则能。所谓向前引用是指:标号的定义是在invoke 语句之后,譬如在如下的例子: invoke MessageBox,NULL, addr MsgBoxText,addr MsgBoxCaption,MB_OK
......
MsgBoxCaption db "Iczelion Tutorial No.2",0 MsgBoxText db "Win32 Assembly is Great!",0
如果您是用 addr 而不是 offset 的话,那 MASM 就会报错。 addr可以处理局部变量而 offset 则不能。局部变量只是在运行时在堆栈中分配内存空间。而 offset 则是在编译时由编译器解释,这显然不能用offset 在运行时来分配内存空间。编译器对 addr 的处理是先检查处理的是全局还是局部变量,若是全局变量则把其地址放到目标文件中,这一点和 offset 相同,若是局部变量,就在执行 invoke 语句前产生如下指令序列: lea eax, LocalVar push eax 因为lea指令能够在运行时决定标号的有效地址,所以有了上述指令序列,就可以保证 invoke 的正确执行了。
invoke ExitProcess,0
应用程序的执行是从 END 定义的标识符后的第一条语句开始的。在上面的框架程序中就是从 START 开始。程序逐条语句执行一直到遇到 JMP,JNE,JE,RET 等跳转指令。这些跳转指令将把执行权转移到其他语句上,若程序要退出 Windows,则必须调用函数 ExitProcess。
ExitProcess proto uExitCode:DWORD
上面一行是函数原型。函数原型会告诉编译器和链接器该函数的属性,这样在编译和链接时,编译器和链接器就会作相关的类型检查。 函数的原型定义如下:
FunctionName PROTO [ParameterName]:DataType,[ParameterName]:DataType,...
简言之,就是在函数名后加伪指令PROTO,再跟一串由逗号相隔的数据类型链表。在前面的 ExitProcess 定义中,该函数有一个 DWORD 类型的参数。当您使用高层调用语句 INVOKE 时,使用函数原型定义特别有用,您可以简单地认为 INVOKE 是一个有参数类型检查的调用语句。譬如,假设您这样写:
call ExitProcess
若您事先没把一个DWORD类型参数压入堆栈,编译器和链接器都不会报错,但毫无疑问,在您的程序运行时将引起崩溃。但是,当您这样写:
invoke ExitProcess
连接器将报错提醒您忘记压入一个 DWORD 类型参数。所以我建议您用 INVOKE 指令而不是CALL去调用一个函数。INVOKE 的语法如下:
INVOKE expression [,arguments]
expression 既可以是一个函数名也可以是一个函数指针。参数由逗号隔开。大多数API函数的原型放在头文件中。 如果您用的是 hutch 的 MASM32,这些头文件在文件夹MASM32/include 下, 这些头文件的扩展名为 INC,函数名和 DLL 中的函数名相同,譬如:KERNEL32.LIB 引出的函数 ExitProcess 的函数原形声明于kernel.inc中。您也可以自己声明函数原型。 在我的教学课程中都使用 hutch 的windows。inc,这些头文件您可以从http://win32asm.cjb.net下载。
好,我们现在回到ExitProcess 函数,参数uExitCode 是您希望当您的应用程序结束时传递 Windows 的。 您可以这样写:
invoke ExitProcess,0
把这一行放到开始标识符下,这个应用程序就会立即退出 Windows,当然毫无疑问个应用程序本身是一个完整的 Windows 程序。
end start