一、引言:
汇编语言是一种运行速度最快,能使用所有机器特殊硬件功能的语言。对速度要求很高的程序,如实时响应处理,图形图象处理等都离不开汇编语言。目前,在计算机系统中,无论是操作系统、编译系统、图形处理系统及大量应用系统中都还不能完全离开汇编语言编制的程序模块。众所周知,汇编语言的特点在于其速度和与硬件打交道的能力,而高级语言则通常具有编程容易、方便、调试快速的特点。因此,两者的结合将发挥各自的优势,扬长避短,从而发挥更大的效益。在Windows环境下一般采用动态连接库(DLL)技术使两者相结合,即用汇编语言来建立DLL,再在高级语言中调用DLL。本文将首先详细介绍用汇编建立DLL的方法,然后作为一个例子,在VB程序中调用所建立的DLL。
二、DLL的建立
1、调用约定
调用约定是在语言中为实现调用而建立的一种协议。应用程序采用调用约定来规定应用程序以某种顺序把变量或参数传递给DLL;DLL采用调用约定来决定以何种顺序接收所传递给它的参数。16位的DLL的参数传递遵从PASCAL的调用约定,即对函数参数采用与参数在参数表中出现的顺序"从左到右"压入堆栈的方式进行(见图1),参数在堆栈中所占的字节与调用类型,参数类型等有关;且被调用函数已经明确获知参数的数目,因此在函数结束时,会一并恢复堆栈的状态。
调用约定使应用程序知道哪一些过程、函数是外部的,从而在连接时能找到这些程序模块;同时,也在DLL中说明哪些过程、函数是可以为其它程序模块所使用。在用汇编语言建立的DLL中,调用约定由过程定义语句中的关键字PASCAL指出。
高 地 址
低 地 址
参 数1
参 数2
返 回 地 址
BP 保 护
BP, SP
图1.PASCAL调用堆栈
2、参数传递
应用程序与DLL之间的通讯是经过参数传递来完成的,当DLL中的过程或函数定义之后,就可以在应用程序中对它进行调用,而调用与被调用之间的信息传递和交换可以通过系统堆栈进行参数传递来完成。在定义或说明时用形式参数,调用时则替换成实际参数。如何把实际参数传递给相应的形式参数,而且当过程完结后又如何把所得结果送回应用程序,这是编程中要解决的问题。 DLL中接收应用程序传递过来的参数由过程定义语句中的EXPORT关键字给出。常用的参数传递方式有传值和传址两种:传值是一种最简单的参数传递方法,它把实际参数的值传递给相应的形式参数。DLL中的库函数可用如下方法来接收应用程序传递过来的值(假设应用程序用传值方式传递过来两个整型值):
SUM1 PROC FAR PASCAL EXPORT I:PTR WORD,J:PTR WORD
… …
MOV AX,I ; 将参数I的值送入AX
MOV BX,J ; 将参数J的值送入BX
… …
SUM1 ENDP
传址是一种用得最多的参数传递方式,它把实际参数的地址传递给相应的形式参数。假设应用程序用传址方式传递过来两个整型值,则DLL库函数可用下列方法来接收地址:
SUM2 PROC FAR PASCAL EXPORT I:FAR PTR WORD
… …
LES SI,I ; 将 参 数I 的 地 址 送 入ES:[SI]
MOV AX,ES:[SI]
… …
SUM2 ENDP
在以后的处理过程中,将针对SI所指的单元进行间接访问。这种参数传送方法接口关系较简单,适用于大量参数的传送。DLL结果和值的返回,此部分可选择。当返回值的数据类型是简单类型时(即不含数组或结构等类型),且返回值长度不超过四个字节时,返回值如表1所示。
表1:
数据大小返回值所放寄存器
1 字 节 AL
2 字 节 AX
3 字 节 高 字( 或 段 地 址) 放DX
4 字 节 低 字( 或 位 移 量) 放AX
当返回值长度超过四个字节时,可参考文献[1]。从表1可看出,库函数可返回值也可返回地址。当返回值时,应用程序调用库函数的结果即为返回值。当返回地址时,应用程序调用库函数的结果得到的是结果的地址指针。
当返回值含有数组或结构等类型时,可把数组或结构的地址赋给以传址方式定义的形式参数,相应地应用程序以数组或结构的形式从相应地址取得结果即可。 3、DLL的源代码的结构
用汇编语言编写的DLL源代码的结构由入口函数、出口函数、模块定义文件和公共函数四部分组成。
(1)入口函数和出口函数
入口函数是当Windows系统把一个模块装入内存时要调用的函数,可执行程序必须拥有自己的入口点。如同WinMain函数是Windows应用程序的入口点一样,LibMain是16位DLL的入口函数,也是DLL的主函数,此函数在WIN.INC文件中声明。每当DLL被加载时Windows会调用该函数完成一些初始化工作。如果DLL不用初始化,则此函数什么也不做,仅作为DLL的入口点。
LibMain函数返回"1"以通知WindowsDLL初始化已正常执行完毕。若返回"0",Windows认为DLL初始化失败,从而终止应用程序的执行。
当一个应用程序调用完DLL时,要把它从内存释放,就需用到出口函数。16位DLL的出口函数是WEP,与LibMain函数一样,当DLL调用成功时,WEP函数返回True,否则,返回False。
总之,DLL源代码中的LibMain函数和WEP函数是由Windows调用的,而其它函数则是由应用程序调用。
(2)公共函数
公共函数或库函数是可被应用程序调用的完成一定功能的子程序或函数,为DLL的主体部分。汇编语言建立的库函数由过程语句定义,其格式见参数传递。在进入库函数时,应先保护一些寄存器的值,如DS、ES、SP、SI、DI等。退出返回前应恢复被保护的寄存器的值。最后,用RETn语句退出并恢复堆栈。在DLL的结构中,公共函数必须先声明后才能定义。声明格式如下:
SUM1 PROTO FAR PASCAL EXPORT :PTR WORD,:PTR WORD
其 中:SUM1: 库 函 数 名;
PROTO: 库 函 数 声 明 关 键 字;
FAR: 远 程 调 用;
PASCAL: 调 用 约 定;
EXPORT: 说 明 参 数 传 递 的 类 型,
此 处 说 明 传 递 两 个 双 字 节 参 数。
(3) 模块定义文件
为了开放DLL中的公共函数,就要用到模块定义文件,以告诉连接器DLL中哪些函数是公用的。通常DLL是通过将函数列在模块定义文件中开放其公共函数的,用关键字EXPORT列出可为应用程序所用的函数名。使用.DEF文件的DLL至少应拥有一个公共函数。
mydll.def的文件结构:
LIBRARY mydll ; 动 态 连 接 库 的 名 字
DESCRIPTION 'DLL FOR WINDOWS3.x & WIN95 '
EXETYPE WINDOWS
CODE PRELOAD MOVEABLE DISCARDABLE
DATA PRELOAD MOVEABLE SINGLE
SEGMENTS CODE2 PRELOAD FIXED
EXPORTS WEP @1 RESIDENTNAME ; 出 口 函 数
SUM1 @2 ; 公 共 函 数
SUM2 @3 ; 公 共 函 数
(4) DLL 举 例
mydll.asm 文 件
.model medium,pascal,farstack ;
用 中 模 式 进 行 编 译, 采 用pascal 调 用 约定,
; 用 系 统 堆 栈 传 递 参 数
.386
include mydll.inc ;mydll.inc 中 定 义 了Prolog 和
Epilog 这 二 个 保 护 现 场 和 恢 复 现 场 宏
.data
.stack
.code
Libmain proc far pascal
ret
Libmain endp
SUM1 proto far pascal export :ptr word,:ptr word
SUM2 proto far pascal export :far ptr dword
; 传值传送两个双字节数,
相加后将结果(双字节)用AX返回
SUM1 proc far pascal export i:ptr word,j:ptr word
Prolog ; 保 护 现 场 的 宏
mov ax, i
mov bx, j
add ax, bx
Epilog ; 恢 复 现 场 的 宏
ret 4 ; 恢 复 堆 栈
SUM1 endp
; 传址传送三个双字节参数,将前两个参数
相加后用第三个参数返回(返回值为两个字节),
;过程本身无返回值
SUM2 proc far pascal export x:far ptr dword
Prolog
mov ax, 0
les di, x
mov ax, es:[di]
mov bx, es:[di-2]
add ax , bx
mov es:[di-4], ax
Epilog
ret 4
SUM2 endp
CODE2 segment word 'code'
assume cs:CODE2
wep proc far pascal export ; 出 口 函 数
Prolog
mov ax, 1
Epilog
ret
wep endp
CODE2 ends
end
4、 编译与连接
MasmV6.11版本提供了把DLL的源代码编译连接成16位DLL文件的工具。对于Windows3.x,可在MASMV6.11提供的PWB集成环境中把DLL源代码编译连接成DLL。但在Windows95中,须用行汇编连接程序ML.EXE和LINK.EXE将DLL的源代码编译连接成16位的DLL文件,其方法如下:
1)ML/Cmydll.asm
命令开关/C的作用是指示编译连接器ML只编译不连接,即只把mydll.asm文件编译成mydll.obj文件。
2)LINKmydll.obj,mydll.dll,mydll.map,libw,mydll.def
即用连接器LINK把mydll.obj,mydll.def和libw连接生成mydll.dll文件。其中libw是MasmV6.11提供的连接生成Windows下的文件所需的库,mydll.map是在连接生成DLL过程中附带产生的文件,此项可省略。
三.调用DLL
为了调用DLL中的函数,程序有两种选择方法。隐含(或装载)连接和运行连接。在此我们只讨论运行连接,即在运行时,程序在需要时将明确地发命令来装载和释放DLL。下面以Windows下最流行的开发软件VB为例来说明调用所建的DLL的方法。
16位的DLL只能被16位的VB调用,即只能在VB3.0或VB4.0的16位版本中调用所建的DLL。VB在调用DLL前须先声明。
1.VB中DLL的声明
假设所建立的DLL已被放到Windows的system子目录下了。如果DLL是在别的目录下,则在DLL函数的声明中mydll.dll前必须带有路径。
VB中对DLL的调用实现是通过DECLARE语句来引入的。
(1)DLL函数的声明
当DLL中的库函数有返回值时,VB中应把库函数声明为Function,声明格式如下所示:
Private(Public)Declare Function
SUM1 Lib "mydll.dll"(ByVal I as Integer,_
ByVal J as Integer) as Integer
其中:SUM1:要调用的库函数名;
ByVal:说明用传值方式传送整型参数;
Lib:指出需调用的DLL的名字。
(2)DLL过程的声明
当DLL中的库函数无返回值时,VB中应把库函数声明为Sub,声明格式如下所示:
Private(Public) DeclareSub SUM2 Lib "mydll.dll"(ByRef I as Integer)
其中:SUM2:要调用的库函数名;
ByRef:说明用传址方式传送整型参数,因VB中参数的缺省传送方式是传址传送,故可省略关键字ByRef。
2.VB中DLL的调用
DLL声明完后,就可以调用了。如果库函数声明为私有的(Private),则DLL只能在声明的窗体代码中被调用;否则,就可在模块中的任何窗体代码中调用它。对于已声明过的库函数,VB可象自己的子过程或子函数一样使用它。
(1)函数的调用
Private Sub Add1()
Dim X , Y ,Z as Integer
… …
X=10
Y=20
Z=SUM1(X,Y) ' 调 用 后Z=30
… …
End Sub
(2) 过 程 的 调 用
Private Sub Add2()
Dim X , Y ,Z as Integer
X=10
Y=20
Z=0
CALL SUM2(X) ' 调 用 后Z=30
… …
End Sub