Win32汇编--使用 MASM
经过上一讲的准备工作,相信大家已经搭建好了 Win32 汇编的工作环境,并已经知道编译、链接一个程序的过程和原理了。
现在,我们让例子回归到经典:
include <stdio.h>
int main(void)
{
Printf(“Hello, world\n”);
} // 事实上想想,这不正是初生的婴儿?!
麻雀虽小,五脏俱全。刚刚那个C语言的”Hello, world”程序包含了C语言中的最基本的格式。
在C语言的源程序中,我们不需要为堆栈段、数据段和代码段的定义而烦恼,编译器会自己解决。
回顾一下,在DOS 下的汇编这段代码会变成什么样? Follow me!
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 堆栈段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
stack segment
db 100 dup (?) ;定义100个字节的内存存储单元空间,默认值为?
stack ends
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 数据段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
data segment
szHello db ‘Hello, world’,0dh,0ah,’$’
data ends
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 代码段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
code segment
assume cs:code,ds:data,ss:stack
start:
mov ax,data
mov ds,ax
mov ah,9
mov dx,offset szHello
int 21h
mov ah,4ch
int 21h
code ends
end start
在例子中我们看到,stack、data、code都找到了自己的小窝。
回归主题,在Win32 汇编语言下,小麻雀”Hello World” 又会变成什么样子呢? Follow me!
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Sample code for < Win32ASM Programming 3rd Edition>
; by 罗云彬, http://www.win32asm.com.cn
; 视频讲座 by 小甲鱼, http://www.fishc.com
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Hello.asm
; 使用 Win32ASM 写的 Hello, world 程序
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 使用 nmake 或下列命令进行编译和链接:
; ml /c /coff Hello.asm
; Link /subsystem:windows Hello.obj
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.386
.model flat,stdcall
option casemap:none
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Include 文件定义
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
include windows.inc
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 数据段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.data
szCaption db '来自鱼C工作室', 0
szText db 'Hello, World !', 0
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 代码段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.code
start:
invoke MessageBox,NULL,offset szText,offset szCaption,MB_OK
invoke ExitProcess,NULL
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
end start
是不是又不同了?但是,我们怎么就发觉Win32 汇编其实是前边两种形态的集大成者?!
接下来,小甲鱼带大家逐段来理解和接受这个新先的语言!
模式定义
程序的第一部分是模式和源程序格式的定义语句
.386
.model flat,stdcall
option casemap:none
这些指令定义了程序使用的指令集、工作模式和格式。
1)指定使用的指令集
.386语句是汇编语句的伪指令,类似的指令还有:.8086、.186、.286、.386/.386p、.486/.486p和.586/.586p等,用于告诉编译器在本程序中使用的指令集。
在DOS的汇编中默认使用的是8086指令集,那时候如果在源程序中写入80386所特有的指令或使用32位的寄存器就会报错。
Win32环境工作在80386及以上的处理器中,所以这一句.386是必不可少的。
另外,后面带p的伪指令则表示程序中可以使用特权指令,如:mov cr0,eax
这一类指令必须在特权级0上运行,如果只指定.386,那么使用普通的指令是可以的,编译时到这一句就会报错。
如果我们要写的程序是VxD等驱动程序,中间要用到特权指令,那么必须定义.386p,在应用程序级别的Win32编程中,程序都是运行在优先级3上,不会用到特权指令,只需定义.386就够了。
80486和Pentium处理器指令是80386处理器指令的超集,同样道理,如果程序中要用80486处理器或Pentium处理器的指令,则必须定义.486或.586。
另外,Intel公司的80x86系列处理器从Pentium MMX开始增加了MMX指令集,为了使用MMX指令,除了定义.586之外,还要加上一句.mmx伪指令:
.386
.mmx
2)model语句
.model语句在低版本的宏汇编中已经存在,用来定义程序工作的模式,它的使用方法是:
.model 内存模式 [,语言模式] [,其他模式]
内存模式的定义影响最后生成的可执行文件,可执行文件的规模从小到大,可以有很多种类型。详见下表:
Windows 程序运行在保护模式下,系统把每一个Win32应用程序都放到分开的虚拟地址空间中去运行,也就是说,每一个应用程序都拥有其相互独立的4GB地址空间。
对Win32程序来说,只有一种内存模式,即flat(平坦)模式,意思是内存是很平坦地从0延伸到4GB,再没有64KB段大小限制。
对比一下DOS的Hello World和Win32的Hello World开始部分的不同,DOS程序中有这样语句
mov ax,data
mov ds,ax
意思是把数据段寄存器DS指向data数据段,data数据段在前面已经用data segment语句定义,只要DS不重新设置,那么从此以后指令中涉及的数据默认将从data数据段中取得。
所以下面的语句是从data数据段取出szHello字符串的地址后再显示:
mov ah,9
mov dx,offset szHello
int 21h
纵观Win32汇编的源程序,没有一处可以找到ds或es等段寄存器的使用,因为所有的4GB空间用32位的寄存器全部都能访问到了,不必在头脑中随时记着当前使用的是哪个数据段,这就是平坦内存模式带来的好处。
如果定义了.model flat,MASM自动为各种段寄存器做了如下定义:
ASSUME cs:FLAT, ds:FLAT, ss:FLAT, es:FLAT, fs:ERROR, gs:ERROR
也就是说,CS,DS,SS和ES段全部使用平坦模式,FS和GS寄存默认不使用,这时若在源程序中使用FS或GS,在编译时会报错。如果有必要使用它们,只需在使用前用下面的语句声明一下就可以了:
assume fs:nothing, gs:nothing 或者
assume fs:flat, gs:flat
在Win32汇编中,.model语句中还应该指定语言模式,即子程序和调用方式,例子中用的是stdcall,它指出了调用子程序或Win32 API时参数传递的次序和堆栈平衡的方法。
相对于stdcall,不同的语言类型还有C, SysCall, BASIC, FORTRAN 和PASCALL,虽然各种高级语言在调用子程序时都是使用堆栈来传递参数。
Windows的API调用使用是的stdcall格式,所以在Win32汇编中没有选择,必须在.model中加上stdcall参数。
(1) _stdcall调用
_stdcall是Pascal程序的缺省调用方式,参数采用从右到左的压栈方式,被调函数自身在返回前清空堆栈。WIN32 Api都采用_stdcall调用方式。
(2) _cdecl调用
_cdecl是C/C++的缺省调用方式,参数采用从右到左的压栈方式,传送参数的内存栈由调用者维护。_cedcl约定的函数只能被C/C++调用,每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小会比调用_stdcall函数的大。
3)option 语句
如例子中,我们定义了 option casemap:none 的意义是告诉编译器程序中的变量名和子程序名是否对大小写敏感。
由于Win32 API 的API函数名称本质是区分大小写的,所以必须指定这个选项,否则调用API函数就会出现问题。
段的定义
把上面的Win32的Hello World源程序中的语句归纳精简一下,再列在下面:
.386
.model flat,stdcall
option casemap:none
<一些include语句>
.data
<一些字符串、变量定义>
.code
<代码>
<开始标号>
<其他语句>
end 开始标号
模式定义中的模式、选项等定义并不会在编译好的可执行程序中产生什么东西,它们只是说明。
而真正的数据和代码是定义在各个段中的,如上面的.data段和.code段,考虑到不同的数据类型,还可以有其他种类的数据段,下面是包含全部段的源程序结构:
.386
.model flat,stdcall
option casemap:none
<一些include语句>
.stack [堆栈段的大小]
.data
<一些初始化过的变量定义>
.data?
<一些没有初始化过的变量定义>
.const
<一些常量定义>
.code
<代码>
<开始标号>
<其他语句>
end 开始标号
.stack、.data、.data?、.const和.code是分段伪指令,Win32中实际上只有代码和数据之分,
.data,.data?和.const这些都是指向数据段,.code是指向代码段。
和DOS汇编不同,Win32汇编不必考虑堆栈,系统会为程序分配一个向下扩展的、足够大的段作为堆栈段,所以.stack段定义常常被忽略。
前面我们不是说过Win32环境下不用段了吗?
是的,这些“段”,实际上并不是DOS汇编中那种意义的段,而是内存的“分段”。
上一个段的结束就是下一个段的开始,所有的分段,合起来,包括系统使用的地址空间,就组成了整个可以寻址的4GB线性地址空间。
我们接着往下看会更加容易理解。
Win32汇编的内存管理使用了80386处理器的分页机制,每个页(4KB大小)可以自由指定属性,所以上一个4KB可能是代码,属性是可执行但不可写,下一个4KB就有可能是既可读也可写但不可执行的数据。
再下面呢?有可能是可读不可写也不可执行的数据。(主要就看你放的是什么东西……)
嘿嘿,大家是否有点理解了?没关系,接着往下!
Win32汇编源程序中“分段”的概念实际上是把不同类型的数据或代码归类,再放到不同属性的内存页(也就是不同的“分段”)中,这中间不涉及使用不同的段选择器。(仅仅是配合分页机制搞捣鼓~)
虽然使用和DOS汇编同样的.code和.data语句来定义,意思可是完全不同了!
至此,相信大家和小甲鱼一样清晰啦,感谢老师,感谢拉登,感谢嫦娥_
数据段
.data、.data?和.const定义的是数据段,分别对应不同方式的数据定义,在最后生成的可执行文件中也分别放在不同的节区(Section)中。(这个在我们讲解PE结构的时候会很细致描述)
程序中的数据定义一段可以归纳为3类:
第一类是可读可写的已定义变量。
第二类是可读可写的未定义变量。
第三类数据是一些常量。
可读可写的已定义变量
这些数据在源程序中已经被定义了初始值,而且在程序的执行中有可能被更改。
如一些标志等,这些数据必须定义在.data段中,.data段是已初始化数据段,其中定义的数据是可读可写的,在程序装入完成的时候,这些值就已经在内存中了,.data段存放在可执行文件的_DATA节区内。
可读可写的未定义变量
这些变量一般是当做缓冲区或者在程序执行后才开始使用的,这些数据可以定义在.data段中,也可以定义在.data?段中,但一般把它放到.data?段中。
虽然定义在这两种段中都可以正常使用,但定义在.data?段中不会增大.exe文件的大小。
举例说明,如果要用到一个100KB的缓冲区,可以在数据段中定义:
szBuffer db 100 * 1024 dup (?)
如果放在.data段中,编译器认为这些数据在程序装入时就必须有效,所以它在生成可执行文件的时候保留了所有的100KB的内容,即使它们是全零!
如果程序其他部分的大小是50KB,那么最后的.exe文件就会是150KB大小,如果缓冲区定义为1MB,那么.exe文件会增大到1050KB。
.data?段则不同,其中的内容编译器会认为程序在开始执行后才会用到,所以在生成可执行文件的时候只保留了大小信息,不会为它浪费磁盘空间。
和上面同样的情况下,即使缓冲区定义为1MB,可执行文件同样只有50KB!总之,.data?段是未初始化数据段,其中的数据也是可读可写的,但在可执行文件中不占空间,.data?段在可执行文件中存放在_BSS节区中。
数据是一些常量
如一些要显示的字符串信息,它们在程序装入的时候也已经有效,但在整个执行过程中不需要修改,这些数据可以放在.const段中,.const段是常量段,它是可读不可写的。
一般为了方便起见,在小程序中常常把常量一起定义到.data段中,而不另外定义一个.const段。
在程序中如果不小心写了对.const段中的数据做写操作的指令,会引起保护错误,Windows会显示一个提示框并结束程序。
代码段
.code段是代码段,所有的指令都必须写在代码段中,在可执行文件中,代码段是放在_TEXT节区(区块)中的。
Win32环境中的数据段是不可执行的,只有代码段有可执行的属性。
对于工作在特权级3的应用程序来说,.code段是不可写的,在编写DOS汇编程序的时候,我们可以为非作歹,如果企图在Win32汇编下做同样的事情,结果就是和上面同样 “非法操作”!
当然事物总有两面性,在Windows95下,在特权级0下运行的程序对所有的段都有读写的权利,包括代码段。
另外,在优先级3下运行的程序也不是一定不能写代码段,代码段的属性是由可执行文件PE头部中的属性位决定的。
通过编辑磁盘上的.exe文件,把代码段属性位改成可写,那么在程序中就允许修改自己的代码段。
一个典型的应用就是一些针对可执行文件的压缩软件和加壳软件,如Upx和PeCompact等。
这些软件靠把代码段进行变换来达到解压缩和解密的目的,被处理过的可执行文件在执行时需要由解压代码来将代码段解压缩。
这就需要写代码段,所以这些软件对可执行文件代码段的属性预先做修改。
为了带大家更好认识这些花花绿绿的“段”到底是什么回事,小甲鱼带大家看一张图……
程序结束和程序入口
在C语言源程序中,程序不必显式地指定程序由哪里开始执行,编译器已经约定好从main() 函数开始执行了。
而在汇编程序中,并没有一个main函数,程序员可以指定从代码段的任何一个地方开始执行,这个地方由程序最后一句的end语句来指定:
end [开始地址]
这句语句同时表示源程序结束,所有的代码必须在end语句之前。
end start
上述语句指定程序从start这个标号开始执行。当然,start标号必须在程序的代码段中有所定义。
但是,一个源程序不必非要指定入口标号,这时候可以把开始地址忽略不写,这种情况发生在编写多模块程序的单个模块的时候。
当分开写多个程序模块时,每个模块的源程序中也可以包括.data、.data?、.const和.code段,结构就和上面的Win32 Hello World一样,只是其他模块最后的end语句必须不带开始地址。
当最后把多个模块链接在一起的时候,只能有一个主模块指定入口地址,在多个模块中指定入口地址或者没有一个模块指定了入口地址,链接程序都会报错。
注释
注释是源程序中不可忽略的一部分,汇编源程序的注释以分号(;)开始,注释既可以在一行的头部,也可以在一行的中间,一行中所有在分号之后的字符全部当做注释处理,但在字符串的字义中包含的引号内的分号不当做是注释的开始。
;这里是注释
call _PrintChar ;这里是注释
szChar db ‘Hello, world; ’,0dh,0ah
换行
当源程序的某一行过长,不利于阅读的时候,可以分行书写,分行的办法是在一行的最后用反斜杠(\)做换行符,如:
invoke MessageBox, NULL, offset szText, offset szCaption, MB_OK
可以写为:
invoke MessageBox,
NULL, \ ;父窗口句柄
offset szText, \ ;消息框中的文字
offset szCaption, \ ;标题文字
MB_OK
调用 API 函数
首先,API 是什么?
答:Win32程序是构筑在Win32 API基础上的。在Win32 API中,包括了大量的函数、结构和消息等,它不仅为应用程序所调用,也是Windows自身的一部分,Windows自身的运行也调用这些API函数。
在DOS下,操作系统的功能是通过各种软中断来实现的,如大家都知道int 21h是DOS中断,int 13h和int 10h是BIOS中的磁盘中断和视频中断。
当应用程序要引用系统功能时,要把相应的参数放在各个寄存器中再调用相应的中断,程序控制权转到中断中去执行,完成以后会通过iret中断返回指令回到应用程序中。
DOS汇编下的Hello World程序中有下列语句:
mov ah, 9
mov dx, offset szHello
int 21h
解释:这3条语句调用DOS系统模块中的屏幕显示功能,功能号放在ah中,9号功能表示屏幕显示,要输出到屏幕上的内容的地址放在dx中,然后去调用int 21h,字符串就会显示到屏幕上。
这个例子说明了应用程序调用系统功能的一般过程。首先,系统提供功能模块并约定参数的定义方法,同时约定调用的方式,同时约定调用的方式,应用程序按照这个约定来调用系统功能。
在这里,ah中放功能号9,dx中放字符串地址就是约定的参数,int 21h是约定的调用方式。
下面来看看这种方法的不便这处。首先,所有的功能号定义是冷冰冰的数字,int 21h的说明文档是这样的:int 21h说明文档
再进入09号功能看使用方法:
Print string (Func 09)
AH = 09h
DS:DX -> string terminated by “$”
这就是DOS时代汇编程序员都有一厚本《中断大全》的原因,因为所有的功能编号包括使用的参数定义仅从字面上看,是看不出一点头绪来的。
另外,80x86系列处理器能处理的中断最多只能有256个,不同的系统服务程序使用了不同的中断号,这少得可怜的中断数量就显得太少了,结果到最后是中断挂中断,大家抢来抢去的,把好好的一个系统搞得像接力赛跑一样。
调用 API 函数-细节
习惯工作于DOS汇编的程序员同志都有一个愿望:如果说,能够以功能名称作为子程序名直接调用,他们愿意以生命中的十年寿命作为交换……
随着Win32 的到来,他们的愿望实现了!这就是 API函数,它事实上就是以一种新的方法代替了 DOS 下的中断。
与DOS 中断相比,Win32 的系统功能模块放在 Windows 的动态链接库(DLL)中。
DLL 是一种Windows 的可执行文件,采用的是和我们熟悉的.exe 文件同样的PE(Portable Executable)约定格式。
关于 DLL
DLL 事实上只是一个大大的集装箱,装着各种系统的API 函数。应用程序在使用的时候由Windows自动载入DLL程序并调用相应的函数。
实际上,Win32的基础就是由DLL组成的。Win32 API的核心由3个DLL提供,它们是:
KERNEL32.DLL——系统服务功能。包括内存管理、任务管理和动态链接等。
GDI32.DLL——图形设备接口,处理图形绘制。
USER32.DLL——用户接口服务。建立窗口和传送消息等。
当然,Win32 API还包括其他很多函数,这些也是由DLL提供的,不同的DLL提供了不同的系统功能。如使用TCP/IP协议进行网络通信的DLL是Wsock32.dll,它所提供的API称为Socket API;专用于电话服务方面的API称为TAPI(Telephony API),包含在Tapi32.dll中,所有的这些DLL提供的函数组成了现在使用的Win32编程环境。
我们也经常自己打包自己的“集装箱”!
API函数的参数
在DOS下,我们演示过无数次,通过中断来调用系统“函数”,其中的“参数”是通过放在寄存器(ah)中。
Win32 API是用堆栈来传递参数的,调用者把参数一个个压入堆栈,DLL中的函数程序再从堆栈中取出参数处理,并在返回之前将堆栈中已经无用的参数丢弃。在Microsoft发布的《Microsoft Win32 Programmer’s Reference》中定义了常用API的参数和函数声明。
int MessageBox(
HWND hWnd, //handle to owner window
LPCTSTR lpText, //text in message box
LPCTSTR lpCaption, //message box title
UINT uType //message box style
); // 注意,上边是用C语言表示!
上述函数声明说明了MessageBox有4个参数,这些数据类型看起来很复杂,但有一点是很重要的,对于汇编语言来说,Win32环境中的参数实际上只有一种类型,那就是一个32位的整数,所以这些HWND,LPCTSTR和UINT实际上就是汇编中的dword(double word,双字型,4个字节,两个字,32位)
之所以定义为不同的模样,主要是用来说明了用途。由于Windows是用C写成的,世界上的程序员好像也是用C语言的最多,所以Windows所有编程资料发布的格式也是C格式。
上面的声明用汇编的格式来表达就是:
MessageBox Proto hWnd:dword, lpText:dword,
lpCaption:dword, uType:dword
在汇编中调用MessageBox函数的方法是:
push uType
push lpCaption
push lpText
push hWnd
call MessageBox
在源程序编译链接成可执行文件后,call MessageBox语句中的MessageBox会被换成一个地址,指向可执行文件中的导入表的一个索引(函数名或索引号)。
导入表中指向MessageBox函数的实际地址会在程序装入内存的时候,根据User32.dll在内存中的位置由Windows系统动态填入。
使用invoke语句
API是可以调用了,另一个烦人的问题又出现了,Win32的API动辄就是十几个参数,整个源程序一眼看上去基本上都是把参数压堆栈的push指令,参数的个数和顺序很容易搞错,由此引起的莫名其妙的错误源源不断,源程序的可读性看上去也很差。
如果写的时候少写了一句push指令,程序在编译和链接的时候都不会报错,但在执行的时候必定会崩溃,原因是堆栈对不齐了。
有木有解决的办法呢?那是必须得!最好是像C语言一样,能在同一句中打入所有的参数,并在参数使用错误的时候能够提示。
好消息又来了,Microsoft终于做了一件好事,在MASM中提供了一个伪指令实现了这个功能,那就是invoke伪指令,它的格式是:
invoke 函数名 [,参数1][,参数2]…[,参数n]
invoke MessageBox, NULL, offset szText,
offset szCaption, MB_OK
注意,invoke并不是80386处理器的指令,而是一个MASM编译器的伪指令,在编译的时候它把上面的指令展开成我们需要的4个push指令和一个call指令,同时,进行参数数量的检查工作,如果带的参数数量和声明时的数量不符,编译器报错:error A2137: too few arguments to INVOKE
编译时看到这样的错误报告,首先要检查的是有没有少写一个参数。
对于不带参数的API调用,invoke伪指令的参数检查功能可有可无,所以既可以用call API_Name这样的语法,也可以用invoke API_Name这样的语法
API函数的返回值
有的API函数有返回值,如MessageBox定义的返回值是int类型的数,返回值的类型对汇编程序来说也只有dword一种类型,它永远放在eax中。
如果要返回的内容不是一个eax所能容纳的,Win32 API采用的方法一般是返回一个指针,或者在调用参数中提供一个缓冲区地址,干脆把数据直接返回到缓冲区中去。
函数的声明
在调用API函数的时候,函数原型也必须预先声明,否则,编译器会不认这个函数。invoke伪指令也无法检查参数个数。声明函数的格式是:
函数名 proto [距离] [语言] [参数1]:数据类型, [参数2]:数据类型
句中的proto是函数声明的伪指令,距离可以是NEAR,FAR,NEAR16,NEAR32,FAR16或FAR32,Win32中只有一个平坦的段,无所谓距离,所以在定义时是忽略的;语言类型就是.model那些类型,如果忽略,则使用.model定义的默认值。
对Win32汇编来说只存在dword类型的参数,所以所有参数的数据类型永远是dword,另外对于编译器来说,它只关心参数的数量,参数的名称在这里是无用的,仅是为了可读性而设置的,可以省略掉,所以下面两句消息框函数的定义实际上是一样的:
MessageBox Proto hWnd:dword, lpText:dword,\ lpCaption:dword, uType:dword
MessageBox Proto :dword, :dword, :dword, :dword
在Win32环境中,和字符串相关的API共有两类,分别对应两个字符集:一类是处理ANSI字符集(1B)的,另一类是处理Unicode字符集(2B)的。前一类函数名字的尾部带一个A字符,处理Unicode的则带一个W字符。
我们比较熟悉的ANSI字符串是以NULL结尾的一串字符数组,每一个ANSI字符占一个字节宽。对于欧洲语言体系,ANSI字符集已足够了,但对于有成千上万个不同字符的几种东方语言体系来说,Unicode字符集更有用。
MessageBox和显示字符串有关,同样它有两个版本,严格地说,系统中有两个定义:
MessageBoxA Proto hWnd:dword, lpText:dword,\ lpCaption:dword, uType:dword
MessageBoxW Proto hWnd:dword, lpText:dword,\ lpCaption:dword, uType:dword
虽然《Microsoft Win32 Programmer’s Reference》中只有一个MessageBox定义,但User32.dll中确确实实没有MessageBox,而只有MessageBoxA和MessageBoxW,那么为什么还是可以使用MessageBox呢?
由于并不是每个Win32系统都支持W系统的API,例如在Windows 9x系列中,对Unicode是不支持的,很多的API只有ANSI版本,只有Windows NT系列才对Unicode完全支持。
为了编写在几个平台中通用的程序,一般应用程序都使用ANSI版本的API函数集。
这样的话,为了使程序更有移植性,在源程序中一般不直接指明使用Unicode还是ANSI版本,而是使用宏汇编中的条件汇编功能来统一替换。
如在源程序中使用MessageBox,但在头文件中定义:
if UNICODE
MessageBox equ
else
MessageBox equ
endif
所有涉及版本问题的API都可以按此方法定义,然后在源程序的头指定UNICODE=1或UNICODE=0,重新编译后就能产生不同的版本。
include语句
对于所有要用到的API函数,在程序的开始部分都必须预先声明,但这一个步骤显然是比较麻烦的,为了简化操作,可以采用各种语言通用的解决办法,就是把所有的声明预先放在一个文件中,在用到的时候再用include语句包含进来。
现在回到Win32 Hello World程序,这个程序用到了两个API函数:MessageBox和ExitProcess,它们分别在User32.dll和Kernel32.dll中。
在MASM32工具包中已经包括了所有DLL的API函数声明列表,每个DLL对应<DLL名.inc>文件(这些文件就是存放对应的函数声明),在源程序中只要使用include语句包含进来就可以了:
include user32.inc
include kernel32.inc
当用到其他的API函数时,只需相应增加对应的include语句。
编译器对include语句的处理仅是简单地把这一行用指定的文件内容替换掉而而已。
include语句的语法是:
include 文件名
或
include <文件名>
当遇到要包括的文件名和MASM的关键字同名等可能会引起编译器混淆的情况时,可以用<>将文件名括起来。
includelib语句
在DOS汇编中,使用中断调用系统功能是不必声明的,处理器自己知道到中断向量表中去取中断地址。
在Win32汇编中使用API函数,程序必须知道调用的API函数存在于哪个DLL中,否则,操作系统必须搜索系统中存在的所有DLL,并且无法处理不同DLL中的同名函数,这显然是不现实的,所以,必须有个文件包括DLL库正确的定位信息,这个任务是由导入库来实现的。
在使用外部函数的时候,DOS下有函数库的概念,那时的函数库实际上是静态库,静态库是一组已经编写好的代码模块,在程序中可以自由引用。
在源程序编译成目标文件,最后要链接可执行文件的时候,由link程序从库中找出相应的函数代码,一起链接到最后的可执行文件中。
DOS下C语言的函数库就是典型的静态库。库的出现为程序员节省了大量的开发时间,缺点就是每个可执行文件中都包括了要用到的相同函数的代码,占用了大量的磁盘空间,在执行的时候,这些代码同样重复占用了宝贵的内存。
Win32环境中,程序链接的时候仍然要使用函数库来定位函数信息,只不过由于函数代码放在DLL文件中,库文件中只留有函数的定位信息和参数数目等简单信息,这种库文件叫做导入库。
一个DLL文件对应一个导入库,如User32.dll文件用于编程的导入库是User32.lib,MASM32工具包中包含了所有DLL的导入库。
为了告诉链接程序使用哪个导入库,使用的语句是:
includelib 库文件名
或 includelib <库文件名>
和include的用法一样,在要包括让编译器混淆的文件名时加括号。
Win32 Hello World程序用到的两个API函数MessageBox和ExitProcess分别在User32.dll和Kernel32.dll中,那么在源程序使用的相应语句为:
includelib user32.lib
includelib kernel32.lib
和include语句的处理不同,includelib不会把.lib文件插入到源程序中,它只是告诉链接器在链接的时候到指定的库文件中去找而已。Dll文件中的函数没有包括声明,所以才需要将.inc文件插进去!
API参数中的等值定义(宏)
回过头来看显示消息框的语句:
invoke MessageBox, NULL, offset szText, offset \ szCaption, MB_OK
还是这个函数,不过这次我们关注的焦点有所改变:MB_OK
地球人都知道,MB_OK 就是使得程序弹出来的时候有个“确定”的选项!
我们这次来探索他背后的数字含义。
回顾一下原型:
int MessageBox( HWND hWnd,
LPCTSTR lpText,
LPCTSTR lpCaption,
UINT uType );
在uType这个参数中使用了MB_OK,这个MB_OK是什么意思?小甲鱼带大家着手来查找文档!
- MB_OK 事实上是 0
- 修改helloworld显示一个问号、一个确定按钮、一个取消按钮
- 在以上基础上当按下确定的时候弹出另一个对话框,说”您刚刚按下了确定按钮”,按下取消的时候同样要弹一个对话框提醒
- 事实上用 je, jmp 已经OUT 啦,在MASM下,我们可以用 if, elseif , else……
- 探究 .if 背后的真相!