您的位置: 网站首页 > 程序开发 > 汇编语言与微机原理教程 > 第11章 模块化程序设计 > 【11.3 Win32汇编语言的编程】

11.3 Win32汇编语言的编程

 

11.3  Win32汇编语言的编程

11.3.1  Win32可执行文件的开发过程

DOS下生成一个可执行文件的步骤比较简单,用编译器将源程序编译为OBJ文件,再用链接器将OBJ文件链接成EXE文件,不同语言的开发过程都差不多。

DOS可执行文件中的内容基本上是由源程序中所写的代码和数据定义转换而来的。唯一的例外是带覆盖部分的EXE文件,它在基本的EXE文件后附加了一些自定义的数据,可执行部分的长度由文件头偏移0002H0004H中的长度给出,后面是附加的数据。这样,即使一个带覆盖的EXE文件大小远远超过640KB,在DOS下也能运行。因为操作系统只装入真正的可执行部分,然后由程序读取覆盖部分的数据。DOS对可执行文件覆盖部分的数据格式并没有规定,它是程序员按自己的方式组织的。如果程序员愿意,也可以把这些数据单独放在另外一个文件中。

Win32的可执行文件叫做PE文件。PE文件的基本结构和DOS可执行文件有很大的不同。它把程序中的不同部分分成各种节区(section),其中有一个节区是放置各种资源的,如菜单、对话框、位图、光标、图标和声音等。虽然可以把资源部分理解成类似DOS可执行文件中的覆盖部分,但资源是Win32可执行文件的标准组成部分,而且是非常重要的组成部分,所以与DOS软件的开发过程相比,Win32软件的开发中多了一个创建资源文件的步骤。

代码部分的开发工作和在DOS下写代码的步骤是一样的。程序员用文本编辑器书写汇编源代码(ASM文件)。ASM文件中也可以用INCLUDE语句包含数据定义和函数声明的头文件。Win32汇编的头文件一般用.inc作为扩展名,如MASM 32软件包附带的Windows.inc文件定义了Win32 API中的很多参数和数据结构,其他的INC文件则是不同DLL中的Win32 API函数声明。最后,ASM文件经汇编编译器编译成以.obj为扩展名的目标文件。Win32可执行文件的开发过程如图11-7所示。

资源文件中可以包括对话框、快捷键、菜单、字符串、版本信息和一些图形资源等内容。资源文件的源文件是一种类似“脚本”的文本文件。其中用不同的语法定义了不同类型的资源。资源脚本文件的扩展名一般为.rc,经过资源编译器编译成资源文件.RES。资源脚本文件同样用到很多预定义值,所以软件包中一般也有资源头文件可供源文件来导入。MASM 32软件包中的资源头文件是Resource.h

在资源文件中,对话框资源只记录定义值,如对话框的大小、位置等,并非真正存储对话框最后显示在屏幕上的像素。这些大小、位置等信息最后由Windows解释后才在屏幕上被绘画成像素;菜单、字符串、快捷键等由文本构成;图形资源则真正由像素组成,它们在资源脚本中被定义为一个文件名,由资源编译器从磁盘文件导入。Windows在资源文件中支持的图形文件有BMP位图文件、CUR光标文件和ICO图标文件。这些图形文件可以用其他图形处理软件生成。Windows在资源文件中所支持的还包括WAV声音文件。

当运行中要用到资源的时候,必须借助API函数装入后才能用。

11-7  Win32可执行文件的开发过程

编译好目标文件OBJ和资源文件RES后,最后一步是用链接器将它们链接成可执行文件。链接的时候要用到函数库。在DOS环境下编程的时候使用的函数库是静态库,静态库是一些已经编译好的代码模块。当用户在源程序中用到某个函数的时候,链接器从库文件中将这个函数的二进制代码取出,和OBJ文件合在一起生成最终的EXE文件。但在Win32环境下,大部分的公用函数封装在DLL文件中,以动态链接的方式供用户程序调用。这时候库文件中只需要包含函数在DLL中的位置信息,不再需要有二进制代码部分。所以链接的时候也只是把库文件中的位置信息取出放入最后的可执行文件中。Win32中这种只包含位置信息的库文件称为导入库。

11.3.2  Win32汇编源程序的结构

任何种类的计算机语言总是有基本的源程序结构规范。下面通过一个简单的程序来说明Win32汇编程序的结构。

11-6 编程实现弹出一个消息框,在上面显示“HelloWorld!”,并在文字的下面显示一个“确定”按钮,用户单击“确定”按钮,程序退出,同时消息框消失。

源程序:

.386

.MODEL FLAT,stdcall

OPTION casemap:none,case sensitive

 

INCLUDE\MASM32\INCLUDE\Windows.inc   

INCLUDE\MASM32\INCLUDE\User32.inc

INCLUDE\MASM32\INCLUDE\Kernel32.Inc

INCLUDE\MASM32\INCLUDE\Gdi32.inc

INCLUDELID\MASM32\lib\User32.1ib

INCLUDELID\MASM32\lib\Kernel32.1ib

INCLUDELID\MASM32\lib\Gdi32.1ib

; 数据段

.DATA

szCaption  DB  'A MessageBox!',0

szText  DB  'Hello,World!',0

 

; 代码段

 

.CODE

START

INVOKE  MessageBox ,NULL,OFFSET szText,OFFSET szCaption,MBOK

INVOKE  ExitProcess , NULL

11-8  程序运行结果

END  START

运行结果如图11-8所示。

这个小程序从结构来看真可谓“麻雀虽小,五脏俱全”,用它来举例说明Win32汇编源程序的框架是最合适不过的了。本节将从这个程序出发介绍Win32汇编程序的结构。

1.模式定义

程序的第一部分是模式和源程序格式的定义语句:

.386.MODEL FLAT , stdcall OPTION casemap:none

这些指令定义了程序使用的指令集、工作模式和格式。

1)指定使用的指令集。

.386语句是汇编语言的伪指令,它在低版本的宏汇编中就已经存在,类似的指令还有:.8086.186.286.386/.386P.486/.486P.586/.586P等,用于告诉编译器在本程序中使用的指令集。在DOS的汇编中默认使用的是8086指令集,如果在源程序中写入80386所特有的指令或使用32位的寄存器就会报错。为了在DOS环境下进行保护模式编程或仅为了使用32位寄存器,常在DOS的汇编中使用.386来定义。Win32环境工作在80386及以上的处理器中,所以这一句:.386是必不可少的。后面带P的伪指令则表示程序中可以使用特权指令,如:

MOV cr0,EAX

这一类指令必须在特权级0上运行。如果只指定.386,那么使用普通的指令是可以的,但编译到这一句就会报错。如果要写的程序是VxD等驱动程序,中间要用到特权指令,那么必须定义.386P。在应用程序级别的Win32编程中,程序都是运行在优先级3上,不会用到特权指令,只需定义.386就够了。80486Pentium处理器指令是80386处理器指令的超集,同样道理,如果程序中要用80486处理器或Pentium处理器的指令,则必须定义.486.586。另外,Intel公司的80x86系列处理器从Pentium MMX开始增加了MMX指令集,为了使用MMX指令,除了定义.586之外,还要加上一句.MMX伪指令。

.586

.MMX

2.MODEL语句。

.MODEL语句在低版本的宏汇编中已经存在,用来定义程序工作的模式,它的使用方法是:

.MODEL内存模式[, 语言模式][, 其他模式]

内存模式的定义影响最后生成的可执行文件。可执行文件的规模从小到大,可以有很多种类型。在DOS的可执行程序中,既有只用64KBCOM文件,也有大大小小的EXE文件。到了Win32环境下,又有了可以用4GB内存的PE格式可执行文件。编写不同类型的可执行文件要用.MODEL语句定义不同的参数,具体模式如表11-3所示。

11-3  内存模式的定义及使用方式

模式

内存使用方式

TINY

SMALL

MEDIUM

COMPACT

LARGE

HUGE

FLAT

用来建立COM文件,所有的代码、数据和堆栈都在同一个64KB段内

建立代码和数据分别用一个64KB段的EXE文件

代码段可以有多个64KB段,数据段只有一个64KB

代码段只有一个64KB段,数据段可以有多个64KB

代码段和数据段都可以有多个64KB

LARGE,并且数据段的一个数组也可以超过64KB

Win32程序使用的模式,代码和数据段使用同一个4GB

Windows程序在保护模式下运行,系统为每一个Win32程序分配一个独立的虚拟空间,也就是说Win32程序只有一种内存模式:FLAT模式。地址空间从0GB4GB,也不用使用DSES来访问内存数据。

Win32API使用stdcall格式,所以在.MODEL中加上stdcall参数。

3OPTION语句。

Win32中只用定义OPTION casemap:none, 说明程序中变量和子程序名是对大小写敏感(因为Win32中的API区分大小写)。

2.段的定义

1)段的概念。

前面讲到的选项、模式等定义并不会在编译好的可执行程序中产生什么内容,它们只是“说明”,而真正的数据和代码是定义在各个段中的,如上面的.DATE段和.CODE段。考虑到不同的数据类型还可以有其他种类的数据段,下面是包含全部段的源程序结构。

.386

.MODEL FLATstdcall

OPTION casemapnone

<一些INCLUDE语句>.STACK[堆栈段的大小]

.DATA

<一些初始化过的变量定义>

.DATA?

<一些没有初始化过的变量定义>

.CONST

<一些常量定义>

.CODE<代码>

<开始标号>

<其他语句>

END开始标号

.STACK.DATA.DATA?.CONST.CODE是分段伪指令。Win32中实际上只有代码和数据之分,.DATA.DATA?.CONST是数据段,.CODE是代码段。和DOS汇编不同,Win32汇编不必考虑堆栈,系统会为程序分配一个向下扩展的、足够大的段作为堆栈段,所以.STACK段定义常常被忽略。

2)数据段。

.DATA.DATA?.CONST定义的是数据段分别对应不同方式的数据定义,在最后生成的可执行文件中也分别放在不同的节区(section)中。程序中的数据定义一般可以归纳为3类。

第一类是可读可写的变量。这些数据在源程序中已经被定义了初始值,而且在程序的执行中有可能被更改,如一些标志等。这些数据必须定义在.DATA段中,.DATA段是已初始化的数据段,其中定义的数据是可读可写的,在程序装入完成的时候,这些值就已经在内存中了。.DATA段存放在可执行文件的.DATA节区内。

第二类是可读可写的未定义变量。这些变量一般当作缓冲区或者在程序执行后才开始使用,这些数据可以定义在.DATA段中,也可以定义在.DATA?段中,但一般把它放到.DATA?段中。虽然定义在这两种段中都可以正常使用,但定义在.DATA?段中不会增大EXT文件的大小。举例来说,如果要用到一个100KB的缓冲区,可以在数据段中定义:

bBuffer DB l00*1024 DUP(?)

如果放在.DATA段中,编译器认为这些数据在程序装入时就必须有效,所以它在生成可执行文件的时候保留了所有的100 KB的内容,即使它们是全零。如果程序其他部分的大小是50KB,那么最后的EXE文件就会是150KB大小,如果缓冲区定义为lMB,那么EXE文件会增大到1050KB.DATA?段则不同,其中的内容编译器会认为程序在开始执行后才会用到,所以在生成可执行文件的时候只保留了大小信息,不会为它浪费磁盘空间。在与上面同样的情况下,即使缓冲区定义为1MB,可执行文件同样只有50KB。总之,.DATA?段是未初始化数据段,其中的数据也是可读可写的,但在可执行文件中不占空间。.DATA?段在可执行文件中存放在_BSS节区中。

第三类数据是一些常量。如一些要显示的字符串信息,它们在程序装入的时候也已经有效,但在整个执行过程中不需要修改,这些数据可以放在.CONST段中。.CONST段是常量段,它是可读不可写的。一般为了方便起见,在小程序中常常把常量一起定义到.DATA段中,而不另外定义一个.CONST段。在程序中如果不小心加入了对.CONST段中的数据做写操作的指令,会引起保护错误,Windows会显示一个如图11-9所示的提示框并结束程序。

如果不怕程序可读性不佳的话,把.CONST段中定义的内容混到.CODE段中也可以正常使用,因为.CODE段也是可以读的。

11-9  .CONST段写操作引起的非法操作

3)代码段。

.CODE段是代码段,所有的指令都必须写在代码段中。在可执行文件中,代码段是放在_TEXT节区中的。Win32环境中的数据段是不可执行的,只有代码段有可执行的属性。对于工作在特权级3的应用程序来说,.CODE段是不可写的。

11.3.3  Windows层功能模块的调用

Win32程序是构筑在Win32 APIApplication Programming InterfaceAPI)基础上的。在Win32 API中包括了大量的函数、结构和消息等,它不仅为应用程序所调用,也是Windows自身的一部分,Windows自身的运行也调用这些API函数。通过Win32 API调用Windows系统相当于在MS-DOS中通过中断方式调用系统功能。就像DOS汇编程序中随处可见的INT 21H指令一样,Windows应用程序中Win32 API也随处可见。Win32的系统功能模块放在Windows的动态链接库(DLL)中,DLL是一种Windows的可执行文件,采用的是和EXE文件同样的PE格式,在PE格式文件头的导出表中,以字符串形式指出了这个DLL能提供的函数列表。应用程序使用字符串类型的函数名指定要调用的函数。

应用程序在使用的时候由Windows自动装入DLL程序并调用相应的函数。实际上,Win32的基础就是由DLL组成的。Win32 API的核心由3DLL提供,它们是:

·    Kernel32.dll系统服务功能。包括内存管理、任务管理和动态链接等。

·    Gdi32.dll图形设备接口。利用VGADRV之类的显示设备驱动程序完成显示文本和矩形等功能。

·    User32.dll用户接口服务。建立窗口和传送消息等。

当然,Win32 API还包括其他很多函数,这些也是由DLL提供的,不同的DLL提供了不同的系统功能。如使用TCP/IP协议进行网络通信的DLLWsock32.dll,它所提供的API称为Socket API;专用于电话服务方面的API称为TAPI,包含在Tapi32.dll中。所有的这些DLL提供的函数构成了现在所用的Win32编程环境。

1API函数调用

与在DOS中用中断方式调用系统功能一样,用API方式调用存放在DLL中的函数同样必须约定一个规范,用来定义函数的调用方法、参数的传递方法和参数的定义。几百兆字节的Windows系统比起才几百千字节规模的DOS,其系统函数的规模和复杂程度都上了一个数量级,所以在使用一个API时,带的参数数量多达十几个是常有的事,在DOS下用寄存器来传递参数的方法显然已经不能胜任了。

Win32 API是用堆栈来传递参数的,调用者把参数一个个压入堆栈,DLL中的函数程序再从堆栈中取出参数处理,并在返回之前将堆栈中已经无用的参数丢弃。在Microsoft公司发布的Microsoft Win32 Programmers Reference中定义了常用的API参数和函数声明。先来看消息框函数的声明:

int MessageBox(

HWND hWnd l                 //窗口句柄

LPCTSTR lpText,             //消息框中的文本

LPCTSTR lpCaption,          //消息框标题

UINT uType                  //消息框样式

);

最后还有一句说明:

LibraryUse User32.lib

上述函数声明说明了MessageBox4个参数,它们分别是HWND类型的窗口句柄(hWnd),LPCTSTR类型要显示的字符串地址(lpText)和标题字符串地址(lpCaption),还有UINT类型的消息框类型(uType)。这些数据类型看起来很复杂,但对于汇编语言来说,有一点是很重要的,Win32环境中的参数实际上只有一种类型,那就是一个32位的整数,所有这些HWNDLPCTSTRUINT实际上就是汇编中的DWORDDouble WORD),之所以定义为不同的字符,是用来说明用途的。由于Windows是用C语言写成的,世界上的程序员好像也是用C语言的最多,所以Windows所有编程资料发布的格式也是C格式。

上面的声明用汇编的格式来表达就是:

MessageBox Proto hWndDWORDlpTextDWORDlpCaptionDWORDuTypeDWORD

最后一句LibraryUse User32.lib则说明了这个函数包括在User32.dll中。有了函数原型的定义以后,接下来就是调用的问题了。Win32 API调用中要把参数放入堆栈,顺序是最后一个参数最先进栈,用汇编调用MessageBox函数的方法如下:

PUSH uType

PUSH lpCaption

PUSH lpText

PUSH hWnd

CALL MessageBox

在源程序编译链接成可执行文件后,CALL MessageBox语句中的MessageBox会被转换成一个地址,指向可执行文件中的导入表,导入表中指向MessageBox函数的实际地址会在程序装入内存的时候,根据User 32.dll在内存中的位置由Windows系统动态填入。

1)使用INVOKE语句。

API是可以调用了,另一个问题又出现了,Win 32API经常就是十几个参数,整个源程序一眼看上去基本上都是把参数压入堆栈的PUSH指令,参数的个数和顺序很容易搞错,由此引起的莫名其妙的错误源源不断,源程序的可读性看上去也很差。如果写的时候少写了一句PUSH指令,程序在编译和链接的时候都不会报错,但在执行的时候必定会崩溃,原因是堆栈对不齐了。有没有解决的办法呢?最好是像C语言一样,能在同一句中打入所有的参数,并在参数使用错误的时候能够提示。Microsoft公司在MASM 6.11中提供了一个伪指令实现了这个功能,那就是INVOKE伪指令,它的格式是:

INVOKE函数名[,参数1][,参数2]……

MessageBox的调用在MASM中可以写成:

INVOKE MessageBoxNULLOFFSET szTextoffset szCaptionMB_OK

注意:INVOKE是一个MASM编译器的伪指令,在编译的时候它把上面的指令展开成需要的4PUSH指令和1CALL指令,同时进行参数数量的检查工作,如果参数数量和声明时的数量不符,编译器会报错。

2API函数的返回值。

有的API函数有返回值,如MessageBox定义的返回值是int类型的数,返回值的类型对汇编程序来说只有DWORD一种类型,它永远放在EAX中。如果要返回的内容不是一个EAX所能容纳的,Win32 API采用的方法一般是返回一个指针,或者在调用参数中提供一个缓冲区地址,直接把数据直接返回到缓冲区中。

3)函数的声明。

在调用API函数的时候,函数原型也必须预先声明,否则,编译器不会识别这个函数,INVOKE伪指令也无法检查参数个数。声明函数的格式是:

函数名PROTO[距离][语言][参数1]:数据类型,[参数2]:数据类型……

句中的PROTO是函数声明的伪指令,距离可以是NEARFARNEAR16NEAR32FAR16FAR32Win32中只有一个平坦的段,无所谓距离,所以在定义时是默认的;语言类型就是.MODEL那些类型,如果省略,则使用.MODEL定义的默认值。后面就是参数的列表,对Win32汇编来说只存在DWORD类型的参数,所以所有参数的数据类型永远是DWORD。另外对于编译器来说,它只关心参数的数量,参数的名称在这里是“无用”的,仅是为了可读性而设置的,可以省略,所以下面两句消息框函数的声明实际上是一样的。

MessageBox PROTO hWndDWORDlpTextDWORDlpCaptionDWORDuTypeDWORD

MessaqeBox PROTODWORD,:DWORD,:DWORD,:DWORD

Win32环境中,与字符串相关的API共有两类,分别对应两个字符集:一类是处理ANSI字符集的,另一类是处理Unicode字符集的。前一类函数名字的尾部带一个“A”字符,处理Unicode的则带一个“W”字符。我们比较熟悉的ANSI字符串是以NULL结尾的一串字符数组,每一个ANSI字符占一个字节宽。对于欧洲语言体系,ANSI字符集已足够了,但对于有成千上万个不同字符的几种语言体系来说,Unicode字符集更有用。每一个Unicode字符占两个字节的宽度,这样就可以在一个字符串中使用65536个不同的字符了。MessageBox和显示字符串有关,同样它有两个版本。严格地说,系统中有两种定义。

MessageBoxA PROTO hWndDWORDlpTextDWORDlpCaptionDWORDuTypeDWORD

MessageBoxW PROTO hWndDWORDlpTextDWORDlpCaptionDWORDuTypeDWORD

虽然Win32程序参考中只有一个MessageBox定义,但User32.dll中确确实实没有MessageBox,而只有MessageBoxAMessageBoxW,那么为什么还可以使用MessageBox呢?实际上在程序的头文件User32.inc中有一句:

MessageBox EQU<MessageBoxA>

它把MessageBox变成了MessageBoxA。在源程序中继续沿用MessageBox是为了程序的可读性以及保持和手册的一致性,但对于编译器来说,实际是在使用MessageBoxA。由于并不是每个Win32系统都支持W系列的API,如在Windows 9x系列中,对Unicode是不支持的,很多的API只有ANSI版本,只有Windows NT系列才对Unicode完全支持。为了编写在几个平台中通用的程序,一般应用程序都使用ANSI版本的API函数集。

4INCLUDE语句。

对于所有要用到的API函数,在程序的开始部分都必须预先声明,但这一个步骤显然是比较麻烦的。为了简化操作,可以采用各种语言通用的解决办法,就是把所有的声明预先放在一个文件中,在用到的时候再用INCLUDE语句包含进来。现在回到例11-6程序,这个程序用到了两个API函数:MessageBoxExitProcess,它们分别在User32.dllKernel32.dll中,在MASM 32工具包中已经包括了所有DLLAPI函数声明列表,每个DLL对应<DLL.inc>文件,在源程序中只要使用INCLUDE语句包含进来就可以了。

INCLUDE User32.inc

INCLUDE Kernel32.inc

当用到其他的API函数时,只需增加对应的INCLUDE语句。INCLUDE语句还用来在源程序中包含别的文件,当多个源程序用到相同的函数定义、常量定义甚至源代码时,可以把相同的部分写成一个文件,然后在不同的源程序中用INCLUDE语句包含进来。编译器对INCLUDE语句的处理仅是简单地把这一行用指定的文件内容替换而已。INCLUDE语句的语法是:

INCLUDE文件名

INCLUDE<文件名>

当遇到包含的文件名和MASM的关键字同名等可能会引起编译器混淆的情况时,可以用< >将文件名括起来。

5INCLUDELIB语句。

DOS汇编中,使用中断调用系统功能是不必声明的,处理器事先知道到中断向量表中获取中断地址。在Win32汇编中使用API函数,程序必须知道调用的API函数存在于哪个DLL中,否则,操作系统必须搜索系统中存在的所有DLL,并且无法处理不同DLL中的同名函数。所以,必须有个文件包括DLL库正确的定位信息,这个任务是由导入库来实现的。在使用外部函数的时候,DOS下有函数库的概念,那时的函数库实际上是静态库,静态库是一组已经编写好的代码模块,在程序中可以自由引用,在源程序编译成目标文件,最后要链接成可执行文件的时候,由LINK程序从库中找出相应的函数代码,一起链接到最后的可执行文件中。DOSC语言的函数库就是典型的静态库。库的出现为程序员节省了大量的开发时间,但缺点是每个可执行文件中都包含了要用到的相同函数的代码,占用了大量的磁盘空间,在执行的时候,这些代码同样重复占用了宝贵的内存。

Win32环境中,程序链接的时候仍要使用函数库来定位函数信息,只不过由于函数代码放在DLL文件中,库文件中只留有函数的定位信息和参数数目等简单信息,这种库文件叫做导入库,一个DLL文件对应一个导入库,如User32.dll文件用于编程的导入库是User32.libMASM 32工具包中包含了所有DLL的导入库。为了告诉链接程序使用哪个导入库,使用的语句是:

INCLUDELIB库文件名

INCLUDELIB<库文件名>

INCLUDE的用法一样,在包括容易使编译器混淆的文件名时,可以用< >将文件名括起来。

INCLUDE语句的处理不同,INCLUDELIB不会把LIB文件插入到源程序中,它只是告诉链接器在链接的时候到指定的库文件中去找而已。

2.参数中的等值定义

再回过头来看显示消息框的语句:

INVOKE MessageBoxNULLOFFSET szTextOFFSET szCaptionMB_OK

uType这个参数中使用了MB_OK,这个MB_OK是什么意思呢?先来看Win32程序参考中的说明。

uType——定义消息框的类型,这个参数可以是以下参数。

1)要定义消息框上显示的按钮,用下面的某一个标志:

MB_ABORTRETRYIGNORE——消息框有3个按钮,“终止”、“重试”和“忽略”。

MB_HELP——消息框上显示一个“帮助”按钮,按下后发送WM_HELP消息。

MB_OK——消息框上显示一个“确定”按钮,这是默认值。

MB_OKCANCEL——消息框上显示两个按钮,“确定”和“取消”。

MB_RETRYCANCEL——消息框上显示两个按钮,“重试”和“忽略”。

MB_YESNO——消息框上显示两个按钮,“是”和“否”。

MB_YESNOCANCEL——消息框上显示3个按钮,“是”、“否”和“取消”。

2)要在消息框中显示图标,用下面的某一个标志:

MB_ICONWARNING——显示感叹号图标。

MB_ICONINFORMATION——显示消息图标。

MB_ICONASTERISK——显示危险图标。

MB_ICONQUESTION——显示问号图标。

MB_ICONSTOP——显示停止图标。

若把11.3.2节中“HelloWorld!”例子程序中调用语句改为:

INVOKE MessageBox, NULL, OFFSET  szTextOFFSET sz-

Caption    MB_ICONINFORMATION or MB_OK

再编译执行,屏幕上出现了一个不一样的消息框,如图11-10所示。

这些大写的符号实际上对应一个数值,每个值表示的意义不同,相当于前面讲的符号数。

11-10  运行结果