程序解释
.386
这是一个汇编语言伪指令,他告诉编译器我们的程序是使用80386指令集编写的。您还可以使用 .486、.586, 但最安全的还是使用.386。对于每一种CPU有两套几乎功能相同伪指令: .386/.386P、 486/.486P、 586/.586P。 带P的指令标明您的程序中可以用特权级指令。特权级指令是保留给操作系统的,如虚拟设备驱动程序。在大多数时间,您的程序都无须运行在RING 0层,故用不带后缀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约定的混合体,它规定参数的传递是从右到左,恢复堆栈的工作交由被调用者。
option casemap:none 一句的意思是告诉 MASM 要区分标号的大小写,譬如:start 和 START 是不同的。
include ...,跟在其后的文件名所指定的文件在编译时将"插"在该处。当MASM处理到语句 include \masm\include\windows.inc 时,它就会打开文件夹\MASM32\include 中的文件windows.inc,这和您把整个文件都粘贴到您的源程序中的效果是一样的。includelib 伪指令和 include 不同,它仅仅是告诉编译器您的程序引用了哪个库。当编译器处理到该指令时会在生成的目标文件中插入链接命令告诉链接器链入什么库。当然您还可以通过在链接器的命令行指定引入库名称的方法来达到和用includelib指令相同的目的。
.DATA .DATA? .CONST .CODE
上面的四个伪指令是"分段"(SECTION)伪指令。Win32下没有"段"(SEGMENT)的概念,但是您可以把您的程序分成不同的"分段", 一个"分段"的开始即是上一个"分段"的结束。WIN32中只有两种性质的"分段":DATA和CODE。
其中DATA"分段"又分为三种:
.DATA 其中包括已初始化的数据。
.DATA? 其中包括未初始化的数据。比如有时您仅想预先分配一些内存但并不想指定初始值。使用未初始化的数据的优点是它不占据可执行文件的大小。
.CONST 其中包括常量定义。这些常量在程序运行过程中是不能更改的。 应用程序并不需要以上所有的三个"分段", 可以根据需要进行定义。
.CODE 这是代码"分段"。
start:
invoke MessageBox,NULL,addr MsgBoxText,addr MsgBoxCaption,MB_OK
invoke ExitProcess, NULL
end start
应用程序的执行是从 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函数的原型放在头文件中。这些头文件的扩展名为 INC,函数名和 DLL 中的函数名相同。现在回到ExitProcess 函数,参数uExitCode 是应用程序结束时传递 Windows 的。 可以这样写:
invoke ExitProcess,0
把这一行放到程序结束的地方,这个应用程序就会立即退出 Windows。
对话框函数的原型如下:
MessageBox PROTO hwnd:DWORD, lpText:DWORD, lpCaption:DWORD, uType:DWORD
l hWnd 是父窗口的句柄。句柄代表您引用的窗口的一个地址指针。它的值对您编 Windows 程序并不重要,您只要知道它代表一个窗口。当您要对窗口做任何操作时,必须要引用该窗口的指针。
l lpText 是指向您要显示的文本的指针。指向文本串的指针事实上就是文本串的首地址。
l lpCaption 是指向您要显示的对话框的标题文本串指针。
l uType 是显示在对话框窗口上的小图标的类型。
我们在.DATA"分段"定义了两个NULL结尾的字符串。我们用了两个常量:NULL 和 MB_OK。这些常量在windows.inc 文件中有定义,使用常量使得您的程序有较好的可读性。 addr 操作符用来把标号的地址传递给被调用的函数,它只能用在 invoke 语句中,譬如您不能用它来把标号的地址赋给寄存器或变量,如果想这样做则要用 offset 操作符。在 offset 和 addr 之间有如下区别:
1) 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 就会报错。
2) addr可以处理局部变量而 offset 则不能。局部变量只是在运行时在堆栈中分配内存空间。而 offset 则是在编译时由编译器解释,这显然不能用offset 在运行时来分配内存空间。编译器对 addr 的处理是先检查处理的是全局还是局部变量,若是全局变量则把其地址放到目标文件中,这一点和 offset 相同,若是局部变量,就在执行 invoke 语句前产生如下指令序列:
lea eax, LocalVar
push eax
因为lea指令能够在运行时决定标号的有效地址,所以有了上述指令序列,就可以保证 invoke 的正确执行了。
3.编译:
保存例子,取名为hello.asm。把 ml.exe 的路径放到 PATH 环境变量中,键入下面一行 进行编译:
ml /c /coff /Cp hello.asm (注意:命令行参数大小写是有区别的)
l /c 是告诉MASM只编译不链接。这主要是考虑到在链接前您可能还有其他工作要做。
l /coff 告诉MASM产生的目标文件用 coff 格式。MASM 的 coff 格式是COFF(Common Object File Format:通用目标文件格式) 格式的一种变体。在 UNIX 下的 COFF 格式又有不同。
l /Cp 告诉 MASM 不要更改用户定义的标识符的大小写。在.model 指令下加入 "option casemap:none" 语句,可达到同样的效果。
当您成功的编译了 hello.asm 后,编译器会产生 hello.obj 目标文件,目标文件和可执行文件只一步之遥,目标文件中包含了以二进制形式存在的指令和数据,比可执行文件相差的只是链接器加入的重定位信息。
好,我们来链接目标文件:
link /SUBSYSTEM:WINDOWS /LIBPATH:c:\masm32\lib hello.obj
l /SUBSYSTEM:WINDOWS 告诉链接器可执行文件的运行平台
l /LIBPATH:〈path to import library〉 告诉链接器引入库的路径。