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 21 Functions:
00——Program termination
01——Keyboard input
02——Display output
03——AUX input
04——AUX output
05——Printer output
06——Direct console I/O
07——Direct STDIN input, no echo
08——Keyboard input, no echo
09——Print string
0B——Check standard input status
再进入09号功能看使用方法:
Print string (Func 09)
AH = 09h
DS:DX -> string terminated by "$"
这就是DOS时代汇编程序员都有一厚本《中断大全》的原因,因为所有的功能编号包括使用的参数定义仅从字面上看,是看不出一点头绪来的。
另外,80x86系列处理器能处理的中断最多只能有256个,不同的系统服务程序使用了不同的中断号,这少得可怜的中断数量就显得太少了,结果到最后是中断挂中断,大家抢来抢去的,把好好的一个系统搞得像接力赛跑一样。
对于这些弱点,程序员们都有个愿望:系统功能如果能以功能名称作为子程序名直接调用就好了,参数也最好定义的有意义一点,这样一来写程序就会方便得多,编系统扩展模块也就不必老是担心往哪个中断上面挂了,最好能把上面int 21h/ah=9的调用写成下面这副样子:
call PrintString,addr szHello
终于,好消息出来了,Win32环境中的编程接口就是这个样子,这就是API,它实际上是以一种新的方法代替了DOS中用软中断的方式。和DOS的结构相比,Win32的系统功能模块放在Windows的动态链接库(DLL)中,DLL是一种Windows的可执行文件,采用的是和 .exe文件同样的PE格式,在PE格式文件头的导出表中,以字符串形式指出了这个DLL能提供的函数列表。应用程序使用字符串类型的函数名指定要调用的函数。
应用程序在使用的时候由Windows自动装入DLL程序并调用相应的函数。
实际上,Win32的基础就是由DLL组成的。Win32 API的核心由3个DLL提供,它们是:
● KERNEL32.DLL——系统服务功能。包括内存管理、任务管理和动态链接等。
● GDI32.DLL——图形设备接口。利用VGA与DRV之类的显示设备驱动程序完成显示文本和矩形等功能。
● USER32.DLL——用户接口服务。建立窗口和传送消息等。
当然,Win32 API还包括其他很多函数,这些也是由DLL提供的,不同的DLL提供了不同的系统功能。如使用TCP/IP协议进行网络通信的DLL是Wsock32.dll,它所提供的API称为Socket API;专用于电话服务方面的API称为TAPI(Telephony API),包含在Tapi32.dll中。所有的这些DLL提供的函数组成了现在所用的Win32编程环境。
和在DOS中用中断方式调用系统功能一样,用API方式调用存放在DLL中的函数必须同样约定一个规范,用来定义函数的调用方法、参数的传递方法和参数的定义,洋洋洒洒几百MB的Windows系统比起才几百KB规模的DOS,其系统函数的规模和复杂程度都上了一个数量级,所以在使用一个API时,带的参数数量多达十几个是常有的事,在DOS下用寄存器来传递参数的方法显然已经不能胜任了。
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
);
最后还有一句说明:
Library: Use User32.lib.
上述函数声明说明了MessageBox有4个参数,它们分别是HWND类型的窗口句柄(hWnd),LPCTSTR类型的要显示的字符串地址(lpText)和标题字符串地址(lpCaption),还有UINT类型的消息框类型(uType)。这些数据类型看起来很复杂,但有一点是很重要的,对于汇编语言来说,Win32环境中的参数实际上只有一种类型,那就是一个32位的整数,所有这些HWND,LPCTSTR和UINT实际上就是汇编中的dword(double word),之所以定义为不同的模样,是用来说明用途。由于Windows是用C写成的,世界上的程序员好像也是用C语言的最多,所以Windows所有编程资料发布的格式也是C格式。
上面的声明用汇编的格式来表达就是:
MessageBox Proto hWnd:dword,lpText:dword,lpCaption:dword,uType:dword
上面最后一句Library: Use User32.lib则说明了这个函数包括在User32.dll中。
有了函数原型的定义以后,就是调用的问题了,Win32 API调用中要把参数放入堆栈,顺序是最后一个参数最先进栈,在汇编中调用MessageBox函数的方法是:
push uType
push lpCaption
push lpText
push hWnd
call MessageBox
在源程序编译链接成可执行文件后,call MessageBox语句中的MessageBox会被换成一个地址,指向可执行文件中的导入表,导入表中指向MessageBox函数的实际地址会在程序装入内存的时候,根据User32.dll在内存中的位置由Windows系统动态填入。
1. 使用invoke语句
API是可以调用了,另一个烦人的问题又出现了,Win32的API动辄就是十几个参数,整个源程序一眼看上去基本上都是把参数压入堆栈的push指令,参数的个数和顺序很容易搞错,由此引起的莫名其妙的错误源源不断,源程序的可读性看上去也很差。如果写的时候少写了一句push指令,程序在编译和链接的时候都不会报错,但在执行的时候必定会崩溃,原因是堆栈对不齐了。
有没有解决的办法呢?最好是像C语言一样,能在同一句中打入所有的参数,并在参数使用错误的时候能够提示。
好消息又来了,Microsoft终于做了一件好事,在MASM中提供了一个伪指令实现了这个功能,那就是invoke伪指令,它的格式是:
invoke 函数名[,参数1][,参数2]……
对MessageBox的调用在MASM中可以写成:
invoke MessageBox,NULL,offset szText,offset szCaption,MB_OK
注意,invoke并不是80386处理器的指令,而是一个MASM编译器的伪指令,在编译的时候它把上面的指令展开成我们需要的4个push指令和1个call指令,同时,进行参数数量的检查工作,如果带的参数数量和声明时的数量不符,编译器会报错:
error A2137: too few arguments to INVOKE
编译时看到这样的错误报告,首先要检查的是有没有少写了一个参数。对于不带参数的API调用,invoke伪指令的参数检查功能可有可无,所以既可以用call API_Name这样的语法也可以用invoke API_Name这样的语法。
2. 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 字符集的,另一类是处理 Unicode 字符集的。前一类函数名字的尾部带一个“A”字符,处理Unicode的则带一个“W”字符。我们比较熟悉的ANSI字符串是以 NULL 结尾的一串字符数组,每一个ANSI字符占一个字节宽。对于欧洲语言体系,ANSI 字符集已足够了,但对于有成千上万个不同字符的几种东方语言体系来说,Unicode 字符集更有用。每一个Unicode字符占两个字节的宽度,这样一来就可以在一个字符串中使用65 336个不同的字符了。
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呢?实际上在程序的头文件user32.inc中有一句:
MessageBox equ <MessageBoxA>
它把MessageBox偷梁换柱变成了MessageBoxA。在源程序中继续沿用MessageBox是 为了程序的可读性以及保持和手册的一致性,但对于编译器来说,实际是在使用MessageBoxA。
由于并不是每个Win32系统都支持W系列的API,在Windows 9x系列中,对Unicode是不支持的,很多的API只有ANSI版本,只有Windows NT系列才对Unicode完全支持。为了编写在几个平台中通用的程序,一般应用程序都使用ANSI版本的API函数集。
if UNICODE
MessageBox equ <MessageBoxW>
else
MessageBox equ <MessageBoxA>
endif
所有涉及版本问题的API都可以按此方法定义,然后在源程序的头部指定UNICODE=1或UNICODE=0,重新编译后就能产生不同的版本。
4.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语句的语法是:
include 文件名
或 include <文件名>
当遇到要包括的文件名和MASM的关键字同名等可能会引起编译器混淆的情况时,可以用<>将文件名括起来。
5. 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文件插入到源程序中,它只是告诉链接器在链接的时候到指定的库文件中去找而已。
再回过头来看显示消息框的语句:
invoke MessageBox,NULL,offset szText,offset szCaption,MB_OK
在uType这个参数中使用了MB_OK,这个MB_OK是什么意思呢,先来看《Microsoft Win32 Programmer's Reference》中的说明:
uType —— 定义对话框的类型,这个参数可以是以下标志的合集:
要定义消息框上显示按钮,用下面的某一个标志:
MB_ABORTRETRYIGNORE —— 消息框有三个按钮:“终止”,“重试”和“忽略”
MB_HELP —— 消息框上显示一个“帮助”按钮,按下后发送WM_HELP消息
MB_OK —— 消息框上显示一个“确定”按钮,这是默认值
MB_OKCANCEL —— 消息框上显示两个按钮:“确定”和“取消”
MB_RETRYCANCEL —— 消息框上显示两个按钮:“重试”和“忽略”
MB_YESNO —— 消息框上显示两个按钮:“是”和“否”
MB_YESNOCANCEL —— 消息框上显示三个按钮:“是”、“否”和“取消”
要在消息框中显示图标,用下面的某一个标志:
MB_ICONWARNING —— 显示惊叹号图标
MB_ICONINFORMATION —— 显示消息图标
MB_ICONASTERISK —— 显示危险图标
MB_ICONQUESTION —— 显示问号图标
MB_ICONSTOP —— 显示停止图标
……
这些是uType参数说明中的一小半,可以看出,参数中可以用的值有很多种,让我们换一个值试试看,把语句改为:
invoke MessageBox,NULL,offset szText,offset szCaption, MB_ICONWARNING or MB_YESNO
再编译执行看,屏幕上出现了一个不一样的消息框,如图3.3所示。
和参数说明中的一样!消息框中出现了一个惊叹号图标,按钮也变成了“是”和“否”两个按钮!MB_ICONWARNING和MB_YESNO等参数究竟是什么意思呢,MASM中显然没有这样的预定义,让我们先来找Visual C++的头文件,在WinUser.h中可以找到下面一段:
/*
* MessageBox() Flags
*/
#define MB_OK Ox
#define MB_OKCANCEL Ox
#define MB_ABORTRETRYIGNORE Ox
#define MB_YESNOCANCEL Ox
#define MB_YESNO Ox
#define MB_RETRYCANCEL Ox
#define MB_ICONHAND Ox
#define MB_ICONQUESTION Ox
#define MB_ICONEXCLAMATION Ox
#define MB_ICONASTERISK Ox
#if(WINVER >= Ox0400)
#define MB_USERICON Ox
#define MB_ICONWARNING MB_ICONEXCLAMATION
#define MB_ICONERROR MB_ICONHAND
#endif /* WINVER >= 0x0400 */
#define MB_ICONINFORMATION MB_ICONASTERISK
#define MB_ICONSTOP MB_ICONHAND
……
显然,MB_YESNO就是4,MB_ICONWARNING就是30h,默认的MB_OK就是0,Win32 API的参数使用这样的定义方法显然是为了免除程序员死记数值定义的麻烦。在编写Win32汇编程序的时候,MASM32工具包中的Windows.inc也包括了所有这些参数的定义,只要在程序的开头包含这个定义文件:
include windows.inc
就可以方便地完全按照API手册来使用Win32函数。
打开\masm32\include目录下的Windows.inc查看一下,可以发现整个文件总共有两万六千多行,包括了几乎所有的Win32 API参数中的常量和数据结构定义。正是有了这个文件中详尽的定义,Win32ASM才得以流行起来,试想一下,哪个程序员愿意每使用一个API语句,就到函数手册中去看参数,然后到Microsoft发布的Visual C++的头文件中去找对应的数值,再应用到汇编源程序中?这样会有80%以上的时间花在做无用功上(最后还是要骂Microsoft为什么不提供汇编格式的头文件,毕竟MASM32工具包不是Microsoft出的)。
有时候由于版本的原因,当使用最新的API手册时,会发现有些参数使用的常量在Windows.inc中并没有定义,这下惨了,谁都不知道类似于MB_XXXYYY的东西代表什么数值,Microsoft的《Microsoft Programmer's Reference》手册中从来就不会把参数对应的数值写进去。遇到这种情况,只有拿出最原始的办法了,就是到最新的Visual C++或SDK的include目录中去,在C语言格式的 .h头文件中把定义找出来,然后自行增补到Windows.inc中去。如果这样也找不到定义值的话,那只好放弃使用这个API了。