首页 新闻 论坛 群组 Blog 文档 下载 读书 Tag 网摘 搜索 开源 FAQ 第二书店 博文视点 程序员
频道: 研发 数据库 中间件 信息化 视频 .NET Java 游戏 移动 服务: 人才 外包 培训
    图书品种:235680
       
热门搜索: ASP.NET Ajax Spring Hibernate Java

3.2.2  调用API

与在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这样的语法。

TASM中没有invoke伪指令,它直接使用call指令实现同样的功能,如在TASM源代码中写上:

call  MessageBox,NULL,offset szText,offset szCaption,MB_OK

TASM编译器会将其展开成我们需要的4个push指令和1个call指令。

2. API函数的返回值

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

3. 函数的声明

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

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

句中的proto是函数声明的伪指令,距离可以是NEAR,FAR,NEAR16,NEAR32,FAR16或FAR32,Win32中只有一个平坦的段,无所谓距离,所以在定义时是忽略的;语言类型就是 .model那些类型,如果忽略,则使用 .model定义的默认值。

后面就是参数的列表了,由于Win32 API仅仅使用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 536个不同的字符了。

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定义,从MSDN上查询也是如此,但User32.dll中确实没有MessageBox,而只有MessageBoxA和MessageBoxW,那么为什么在源代码中还是可以使用MessageBox呢?这是因为在程序的头文件user32.inc中有一句:

MessageBox      equ     <MessageBoxA>

它把MessageBox偷梁换柱变成了MessageBoxA。在源程序中继续沿用MessageBox是  为了程序的可读性,以及保持和手册的一致性,但对于编译器来说,实际是在使用MessageBoxA。

并不是每个Win32系统都支持W系列的API,在Windows 9x系列中,对Unicode是不支持的,绝大多数的API只有ANSI版本(为数不多的几个例外是存在MessageBoxW等函数,它们让Unicode版本的程序在检测到系统不支持的时候,能有机会用消息框提示用户并退出),只有Windows NT系列才对Unicode完全支持。为了编写在几个平台中通用的程序,一般应用程序都使用ANSI版本的API函数集。

为了使程序更有移植性,在源程序中一般不直接指明使用Unicode还是ANSI版本,而是使用宏汇编中的条件汇编功能来统一替换,如在源程序中使用MessageBox,但在头文件中定义:

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文件插入到源程序中,它只是告诉链接器在链接的时候到指定的库文件中去找API函数的位置信息而已。

查看所有评论(0)条】

最近评论



正在载入评论列表...
热点评论