动态链接库一些基础的概念:
动态链接库缩写为DLL。动态链接库提供了许多的通用函数,可以被多个程序调用。动态链接库,顾名思义,它只有在程序执行的时候才会装入到程序的地址空间中,程序不执行时就只保留一些库的信息在文件中。了解过PE文件结构的应该知道,PE文件不执行时,动态链接库的信息在PE文件的导入表中,导入表里面包含了这个PE文件要使用的所有的动态链接库的名称以及要从库中导入的函数的一些信息。
了解过Windows的分页内存机制应该知道,如果有多个程序用到同一个动态链接库,Windows在物理空间中只保留一份库的代码,系统通过分页机制将这份代码映射到不同的进程的地址空间中。因为Windows是一个分时系统,所以对于用户DLL,当时间片切换到了要用这个DLL的进程时,该DLL在内存中的代码就会被映射到这个进程的线性地址空间中。而对于系统DLL,因为所有程序都要用到,所以它会在所有时间片中都被映射。所以说,不管有多少程序正在使用DLL,库代码在物理内存中永远只有一份。但是库使用的数据段还是会被映射到不同的物理内存中,多少给程序在使用动态链接库就会有多少份数据段。
注意:动态链接库是被映射到其他应用程序的线性地址空间中执行的,它和应用程序可以看成是一体的,DLL可以使用应用程序的资源,它所拥有的资源也可以被应用程序使用,它所做的任何动作都是代表应用程序进行的,如果动态链接库进行打开文件,分配资源等操作,这些文件,资源都是归应用程序所拥有。
编写动态链接库:
我们先看一段动态链接库的源码:
.386
.model flat,stdcall
option casemap:none
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
include windows.inc
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.data?
dwCounter dd ?
.code
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;dll的入口函数
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
DllEntry proc _hInstance,_dwReason,_dwReserved
mov eax,TRUE
ret
DllEntry endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;私有函数。将数值限制在0到10之间
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_CheckCounter proc
mov eax,dwCounter
cmp eax,0
jge @F
xor eax,eax
@@:
cmp eax,100
jle @F
mov eax,100
@@:
mov dwCounter,eax
ret
_CheckCounter endp ;如果大于等于0,则保持原来的数字不变,如果大于十则将十填入,小于0则将0填入
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;dll的导入函数之一,数据自加一,且范围在0到10以内
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_IncCounter proc
inc dwCounter
call _CheckCounter
ret
_IncCounter endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;dll的导入函数之二,数据自减一,且范围在0到10以内
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_DecCounter proc
dec dwCounter
call _CheckCounter
ret
_DecCounter endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;dll的导入函数之三,取模运算
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_Mod proc uses ecx edx _dwNumber1,_dwNumber2
xor edx,edx
mov eax,_dwNumber1
mov ecx,_dwNumber2
.if ecx
div ecx
mov eax,edx
.endif
ret
_Mod endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
End DllEntry
乍一看好像和平常的汇编源代码没有区别,但是细看就会发现,它没有了start和end start 。而是换成了一个 End DllEntry,并且还加了一个入口函数。其他三个函数就是DLL的导出函数,导出函数就和我们平常自定义的函数一样写就行。
和可执行文件一样,动态链接库需要一个入口点,但是它的入口点是一个函数,函数的名字可以随意取,只要合法就行,但是函数的格式有规定。
入口函数的结构一般如下所示:
DllEntry proc hInstDLL,dwReason,dwReserved
mov eax,dwReason
.if eax == DLL_PROCESS_ATTACH
;保存hInstDll
;初始化库需要的各种资源
.if 初始化成功
mov eax,true
.else
mov eax,FALSE
.endif
.elseif eax == DLL_THREAD_ATTACH
;为新的线程分配资源
.elseif eax == DLL_THREAD_DETACH
;为线程释放资源
.elseif eax == DLL_PROCESS_DETACH
;释放库使用的资源
.endif
ret
DllEntry Endp
函数的第一个参数是该动态链接库的句柄,因为这个入口函数对调用该DLL的程序时不可见的,入口函数是供系统调用的,系统在装载,卸载动态链接库,以及进程中线程的创建和结束时都会调用这个入口函数。所以要在动态链接库里面获得它自己的DLL的话,就只能在这里获得了。
第二个参数是调用的原因,从上面代码可以看到原因有4种。
- DLL_PROCESS_ATTACH:这个原因表示动态链接库刚刚被映射到进程的地址空间,这里可以做一些初始化的工作。返回TRUE表示初始化成功,返回FALSE表示初始化失败,这样库的装入也会失败。
- DLL_THREAD_ATTACH:这个原因表示进程创建了一个新的线程,这样的话,就要为那个线程分配资源。
- DLL_THREAD_DETACH:这个原因表示要结束一个线程,所以要释放资源
- DLL_PROCESS_DETACH:这个原因表示动态链接库要被卸载了。
最后一个参数是系统保留参数,可以不加理会。
但是在我们的例子程序中,只返回应该一个true表示装入成功就可以了,因为我们也没有什么好初始化的。
编写动态链接库光有这个源文件还不够,我们还需要一个指出导出函数的文件,这个就是定义文件。比如,如果我们上面编写的那个DLL里面最后三个函数都导出的话,定义文件可以这样写:
EXPORTS _IncCounter
_DecCounter
_Mod
没错,就是这么一点,需要导出什么函数就把函数名写在EXPORTS后面就行了。而我们在库的代码中有写的函数但是没有在这里定义的的话,那么这个函数就是DLL的私有函数,对应用程序是不可见的。
生成应该动态链接库需要就是这两个文件,然后和之前编译链接汇编源代码成EXE文件不同的是连接选项,编译选项还是一样的。链接DLL的完整的链接选项:
Link /DLL /subsystem:windows /Def:filename.def filename.obj filename.res
DLL文件中也可以包含资源。
然后为了使用NMAKE,这里还是给出完整的makefile文件:
DLL = Sample
LINK_FLAG =/DLL /subsystem:windows
ML_FLAG = /c /coff
$(DLL).dll: $(DLL).obj $(DLL).def
LINK $(LINK_FLAG) /Def:$(DLL).def $(DLL).obj
.asm.obj:
ml $(ML_FLAG) $<
.rc.res:
rc $<
clean:
del*.obj
del*.exp
好,有了这三个文件,我们就可以直接用NAMKE编译链接了。
编译链接完了之后会发现文件夹下多了好几个文件:
原来文件中就只有Makefile,Sample.asm,Sample.def这三个文件。那么多出来的那几个文件是什么呢?
首先多出来的DLL文件就不用说什么了,这个就是我们最后要得到的动态链接库文件,然后下面还有个EXP文件是什么?这个其实就是一个连接过程中的副产品,叫输出库文件,没什么用,可以删掉。再下面是一个LIB文件,这个文件就很熟悉了,这个是一个导入库文件,我们编写程序的时候要导入什么DLL的话一般都是 :includelib xxxx.lib 。只有当我们指定了这个导入库文件,程序才知道到哪个库中寻找特定的函数。这个导入库文件是链接过程中自动帮我们生成的。最下面那个就不用多说了,就是编译后产生的OBJ文件。
DLL文件已经生成了,但是我们要在程序里面去调用的话这还不够。我们还需要编写一个INC文件,这个就是供程序使用的头文件,在程序中包含这个文件后我们就不需要再写函数声明了。
;***********************************************************************************
;作者: XXX
;E-mail: XXXXXXXXXX@qq.com
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;Version 1.0
; Data: 2020.01.31
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;Sample.dll 导出函数
;
;1、invoke _IncCounter
; 增加 DLL 内部计数器的值(最大到10)并返回计数
;2、invoke _DecCounter
; 减少DLL内部计数器的值(最小到0)并返回计数
;3、invoke _Mod,dwNumber1,dwNumber2
; 输入:dwNumber1 和 dwNumber2 为两个整数
; 输出: 两个数的模 dwNumber1 % dwNumber2
;**********************************************************************************
_IncCounter proto
_DecCounter proto
_Mod proto _dwNumber1:dword,_dwNumber2:dword
里面有用的部分也就是最后面三行,就是声明一下函数。如果只是自己用的话,前面那些东西就不用写了。
最后我们写个程序来调用这个DLL看看:
下面是资源文件:
#include <resource.h>
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#define ICO_MAIN 0x100
#define DLG_MAIN 0x1000
#define IDC_COUNT 0x1001
#define IDC_INC 0x1002
#define IDC_DEC 0x1003
#define IDC_NUM1 0x1004
#define IDC_NUM2 0x1005
#define IDC_MOD 0x1006
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
ICO_MAIN ICON "Main.ico"
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
DLG_MAIN DIALOG 186,132,173,79
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "自己写的DLL"
FONT 9,"宋体"
BEGIN
GROUPBOX "Dll内部计数器",-1,4,2,164,32,BS_GROUPBOX
EDITTEXT IDC_COUNT,10,15,73,12,ES_READONLY
PUSHBUTTON "增加(&A)",IDC_INC,86,13,36,14
PUSHBUTTON "减少(&D)",IDC_DEC,123,13,37,14
GROUPBOX "取模函数测试",-1,4,37,164,32,BS_GROUPBOX
EDITTEXT IDC_NUM1,11,50,43,12,ES_NUMBER
LTEXT "%",-1,57,52,8,8
EDITTEXT IDC_NUM2,64,50,43,12,ES_NUMBER
LTEXT "=",-1,110,52,8,8
EDITTEXT IDC_MOD,117,50,43,12,ES_READONLY
END
下面是汇编源文件:
.386
.model flat,stdcall
option casemap:none
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;Include文件
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
include windows.inc
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib
include Sample.inc
includelib Sample.lib
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;Equ
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
ICO_MAIN equ 100h
DLG_MAIN equ 1000h
IDC_COUNT equ 1001h
IDC_INC equ 1002h
IDC_DEC equ 1003h
IDC_NUM1 equ 1004h
IDC_NUM2 equ 1005h
IDC_MOD equ 1006h
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;代码段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
.data?
hInstance dd ?
.code
_ProcDlgMain proc uses ebx edi esi hWnd,wMsg,wParam,lParam
mov eax,wMsg
.if eax == WM_CLOSE
invoke EndDialog,hWnd,NULL
.elseif eax == WM_INITDIALOG
invoke LoadIcon,hInstance,ICO_MAIN
invoke SendMessage,hWnd,WM_SETICON,ICON_BIG,eax
.elseif eax == WM_COMMAND
mov eax,wParam
.if ax == IDC_INC
invoke _IncCounter
invoke SetDlgItemInt,hWnd,IDC_COUNT,eax,FALSE
.elseif ax == IDC_DEC
invoke _DecCounter
invoke SetDlgItemInt,hWnd,IDC_COUNT,eax,FALSE
.elseif ax == IDC_NUM1 || ax == IDC_NUM2
invoke GetDlgItemInt,hWnd,IDC_NUM1,NULL,FALSE
push eax
invoke GetDlgItemInt,hWnd,IDC_NUM2,NULL,FALSE
pop ecx
invoke _Mod,ecx,eax
invoke SetDlgItemInt,hWnd,IDC_MOD,eax,FALSE
.endif
.else
mov eax,FALSE
ret
.endif
mov eax,TRUE
ret
_ProcDlgMain endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
start:
invoke GetModuleHandle,NULL
mov hInstance,eax
invoke DialogBoxParam,eax,DLG_MAIN,\
NULL,offset _ProcDlgMain,NULL
invoke ExitProcess,NULL
end start
程序很简单,就是分别调用了DLL里面的三个函数,然后把结果显示再对话框中。来看看程序运行的结果:
当然要注意要把DLL文件与我们的程序放在一个目录下面,不然就会像这样:
系统查找DLL的顺序:程序所在目录,Windows系统目录,PATH环境变量指定目录。