DLL编写教程(绝对经典之作)
DLL编写教程
半年不能上⽹,最近⽹络终于通了,终于可以更新博客了,写点什么呢?决定最近写⼀个编程技术系列,其内容是⼀些通⽤的编程技术。例如DLL,COM,Socket,多线程等等。这些技术的特点就是使⽤⼴泛,但是误解很多;⽹上教程很多,但是⼏乎没有什么优质良品。我以近⼏个⽉来的编程经验发现,很有必要好好的总结⼀下这些编程技术了。⼀来对⾃⼰是总结提⾼,⼆来可以⽅便光顾我博客的朋友。
好了,废话少说,⾔归正传。第⼀篇就是《DLL编写教程》,为什么起这么⼟的名字呢?为什么不叫《轻轻松松写DLL》或者《DLL⼀⽇通》呢?或者更nb的《深⼊简出DLL》呢?呵呵,常常上⽹搜索资料的弟兄⾃然知道。
本⽂对通⽤的DLL技术做了⼀个总结,并提供了源代码打包下载,下载地址为:
DLL的优点
简单的说,dll有以下⼏个优点:
1)节省内存。同⼀个软件模块,若是以源代码的形式重⽤,则会被编译到不同的可执⾏程序中,同时运⾏这些exe时这些模块的⼆进制码会被重复加载到内存中。如果使⽤dll,则只在
内存中加载⼀次,所有使⽤该dll的进程会共享此块内存(当然,像dll中的全局变量这种东西是会被每个进程复制⼀份的)。
2)不需编译的软件系统升级,若⼀个软件系统使⽤了dll,则该dll被改变(函数名不变)时,系统升级只需要更换此dll即可,不需要重新编译整个系统。事实上,很多软件都是以这种⽅
式升级的。例如我们经常玩的星际、魔兽等游戏也是这样进⾏版本升级的。
3)Dll库可以供多种编程语⾔使⽤,例如⽤c编写的dll可以在vb中调⽤。这⼀点上DLL还做得很不够,因此在dll的基础上发明了COM技术,更好的解决了⼀系列问题。
最简单的dll
开始写dll之前,你需要⼀个c/c++编译器和链接器,并关闭你的IDE。是的,把你的VC和C++ BUILDER之类的东东都关掉,并打开你以往只⽤来记电话的记事本程序。不这样做的话,你可能⼀辈⼦也不明⽩dll的真谛。我使⽤了VC⾃带的cl编译器和link链接器,它们⼀般都在vc的bin⽬录下。(若你没有在安装vc的时候选择注册环境变量,那么就⽴刻将它们的路径加
⼊path吧)如果你还是因为离开了IDE⽽害怕到哭泣的话,你可以关闭这个页⾯并继续去看《VC++技术内幕》之类⽆聊的书了。
最简单的dll并不⽐c的helloworld难,只要⼀个DllMain函数即可,包含objbase.h头⽂件(⽀持COM技术的⼀个头⽂件)。若你觉得这个头⽂件名字难记,那么⽤windows.H也可以。源代码如下:dll_nolib.cpp
#include <objbase.h>
#include <iostream.h>
BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved)
{
HANDLE g_hModule;
switch(dwReason)
{
case DLL_PROCESS_ATTACH:
cout<<"Dll is attached!"<<endl;
g_hModule = (HINSTANCE)hModule;
break;
case DLL_PROCESS_DETACH:
cout<<"Dll is detached!"<<endl;
g_hModule=NULL;
break;
}
return true;
}
其中DllMain是每个dll的⼊⼝函数,如同c的main函数⼀样。DllMain带有三个参数,hModule表⽰本dll的实例句柄(听不懂就不理它,写过windows程序的⾃然懂),dwReason表
⽰dll当前所处的状态,例如DLL_PROCESS_ATTACH表⽰dll刚刚被加载到⼀个进程中,DLL_PROCESS_DETACH表⽰dll刚刚从⼀个进程中卸载。当然还有表⽰加载到线程中和从线程中卸载
的状态,这⾥省略。最后⼀个参数是⼀个保留参数(⽬前和dll的⼀些状态相关,但是很少使⽤)。
从上⾯的程序可以看出,当dll被加载到⼀个进程中时,dll打印"Dll is attached!"语句;当dll从进程中卸载时,打印"Dll is detached!"语句。
编译dll需要以下两条命令:
cl /c dll_nolib.cpp
这条命令会将cpp编译为obj⽂件,若不使⽤/c参数则cl还会试图继续将obj链接为exe,但是这⾥是⼀个dll,没有main函数,因此会报错。不要紧,继续使⽤链接命令。
Link /dll dll_nolib.obj
这条命令会⽣成dll_nolib.dll。
注意,因为编译命令⽐较简单,所以本⽂不讨论nmake,有兴趣的可以使⽤nmake,或者写个bat批处理来编译链接dll。
加载DLL(显式调⽤)
使⽤dll⼤体上有两种⽅式,显式调⽤和隐式调⽤。这⾥⾸先介绍显式调⽤。编写⼀个客户端程序:dll_nolib_client.cpp
#include <windows.h>
#include <iostream.h>
int main(void)
{
//加载我们的dll
HINSTANCE hinst=::LoadLibrary("dll_nolib.dll");
if (NULL != hinst)
{
cout<<"dll loaded!"<<endl;
}
return 0;
}
注意,调⽤dll使⽤LoadLibrary函数,它的参数就是dll的路径和名称,返回值是dll的句柄。使⽤如下命令编译链接客户端:
Cl dll_nolib_client.cpp
并执⾏dll_,得到如下结果:
Dll is attached!
dll loaded!
Dll is detached!
以上结果表明dll已经被客户端加载过。但是这样仅仅能够将dll加载到内存,不能到dll中的函数。
使⽤dumpbin命令查看DLL中的函数
Dumpbin命令可以查看⼀个dll中的输出函数符号名,键⼊如下命令:
Dumpbin –exports dll_nolib.dll
通过查看,发现dll_nolib.dll并没有输出任何函数。
如何在dll中定义输出函数
总体来说有两种⽅法,⼀种是添加⼀个def定义⽂件,在此⽂件中定义dll中要输出的函数;第⼆种是在源代码中待输出的函数前加上__declspec(dllexport)关键字。Def⽂件
⾸先写⼀个带有输出函数的dll,源代码如下:dll_def.cpp
#include <objbase.h>
#include <iostream.h>
void FuncInDll (void)
{
cout<<"FuncInDll is called!"<<endl;
}
BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved)
{
HANDLE g_hModule;
switch(dwReason)
{
case DLL_PROCESS_ATTACH:
g_hModule = (HINSTANCE)hModule;
break;
case DLL_PROCESS_DETACH:
g_hModule=NULL;
break;
}
return TRUE;
}
这个dll的def⽂件如下:dll_def.def
;
; dll_def module-definition file
;
LIBRARY dll_def.dll
DESCRIPTION '(c)2007-2009 Wang Xuebin'
EXPORTS
FuncInDll @1 PRIVATE
你会发现def的语法很简单,⾸先是LIBRARY关键字,指定dll的名字;然后⼀个可选的关键字DESCRIPTION,后⾯写上版权等信息(不写也可以);最后是EXPORTS关键字,后⾯写上dll中所有要输出的函数名或变量名,然后接上@以及依次编号的数字(从1到N),最后接上修饰符。
⽤如下命令编译链接带有def⽂件的dll:
Cl /c dll_def.cpp
Link /dll dll_def.obj /def:dll_def.def
再调⽤dumpbin查看⽣成的dll_def.dll:
Dumpbin –exports dll_def.dll
得到如下结果:
Dump of file dll_def.dll
File Type: DLL
Section contains the following exports for dll_def.dll
0 characteristics
46E4EE98 time date stamp Mon Sep 10 15:13:28 2007
0.00 version
1 ordinal base
1 number of functions
1 number of names
ordinal hint RVA name
1 0 00001000 FuncInDll
Summary
2000 .data
1000 .rdata
1000 .reloc
6000 .text
观察这⼀⾏
1 0 00001000 FuncInDll
会发现该dll输出了函数FuncInDll。
显式调⽤DLL中的函数
写⼀个dll_def.dll的客户端程序:dll_def_client.cpp
#include <windows.h>
#include <iostream.h>
int main(void)
{
/
/定义⼀个函数指针
typedef void (* DLLWITHLIB )(void);
//定义⼀个函数指针变量
DLLWITHLIB pfFuncInDll = NULL;
//加载我们的dll
HINSTANCE hinst=::LoadLibrary("dll_def.dll");
if (NULL != hinst)
{
cout<<"dll loaded!"<<endl;
}
//到dll的FuncInDll函数
pfFuncInDll = (DLLWITHLIB)GetProcAddress(hinst, "FuncInDll");
//调⽤dll⾥的函数
if (NULL != pfFuncInDll)
{
(*pfFuncInDll)();
}
return 0;
}
有两个地⽅值得注意,第⼀是函数指针的定义和使⽤,不懂的随便本c++书看看;第⼆是GetProcAddress的使⽤,这个API是⽤来查dll中的函数地址的,第⼀个参数是DLL的句柄,即LoadLibrary返回的句柄,第⼆个参数是dll中的函数名称,即dumpbin中输出的函数名(注意,这⾥的函数名称指的是编译后的函数名,不⼀定等于dll源代码中的函数名)。
编译链接这个客户端程序,并执⾏会得到:
dll loaded!
FuncInDll is called!
这表明客户端成功调⽤了dll中的函数FuncInDll。
__declspec(dllexport)
为每个dll写def显得很繁杂,⽬前def使⽤已经⽐较少了,更多的是使⽤__declspec(dllexport)在源代码中定义dll的输出函数。
Dll写法同上,去掉def⽂件,并在每个要输出的函数前⾯加上声明__declspec(dllexport),例如:
__declspec(dllexport) void FuncInDll (void)
这⾥提供⼀个dll源程序dll_withlib.cpp,然后编译链接。链接时不需要指定/DEF:参数,直接加/DLL参数即可,
Cl /c dll_withlib.cpp
Link /dll dll_withlib.obj
然后使⽤dumpbin命令查看,得到:
1 0 00001000 ?FuncInDll@@YAXXZ
可知编译后的函数名为?FuncInDll@@YAXXZ,⽽并不是FuncInDll,这是因为c++编译器基于函数重载的考虑,会更改函数名,这样使⽤显式调⽤的时候,也必须使⽤这个更改后的函数名,这显然给客户带来⿇烦。为了避免这种现象,可以使⽤extern “C”指令来命令c++编译器以c编译器的⽅式来命名该函数。修改后的函数声明为:
extern "C" __declspec(dllexport) void FuncInDll (void)
dumpbin命令结果:
1 0 00001000 FuncInDll
这样,显式调⽤时只需查函数名为FuncInDll的函数即可成功。
extern “C”
使⽤extern “C”关键字实际上相当于⼀个编译器的开关,它可以将c++语⾔的函数编译为c语⾔的函数名称。即保持编译后的函数符号名等于源代码中的函数名称。
隐式调⽤DLL
显式调⽤显得⾮常复杂,每次都要LoadLibrary,并且每个函数都必须使⽤GetProcAddress来得到函数指针,这对于⼤量使⽤dll函数的客户是⼀种困扰。⽽隐式调⽤能够像使⽤c函数库⼀样使⽤dll中的函数,⾮常⽅便快捷。
下⾯是⼀个隐式调⽤的例⼦:dll包含两个⽂件dll_withlibAndH.cpp和dll_withlibAndH.h。
代码如下:dll_withlibAndH.h
extern "C" __declspec(dllexport) void FuncInDll (void);
dll_withlibAndH.cpp
#include <objbase.h>
#include <iostream.h>
#include "dll_withLibAndH.h"//看到没有,这就是我们增加的头⽂件
extern "C" __declspec(dllexport) void FuncInDll (void)
{
cout<<"FuncInDll is called!"<<endl;
}
BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved)
{
HANDLE g_hModule;
switch(dwReason)
{
case DLL_PROCESS_ATTACH:
g_hModule = (HINSTANCE)hModule;
break;
case DLL_PROCESS_DETACH:
g_hModule=NULL;
break;
}
c语言编译器ide代码编辑return TRUE;
}
编译链接命令:
Cl /c dll_withlibAndH.cpp
Link /dll dll_withlibAndH.obj
在进⾏隐式调⽤的时候需要在客户端引⼊头⽂件,并在链接时指明dll对应的lib⽂件(dll只要有函数输出,则链接的时候会产⽣⼀个与dll同名的lib⽂件)位置和名称。然后如同调⽤api函数库中的函数⼀样调⽤dll中的函数,不需要显式的LoadLibrary和GetProcAddress。使⽤最为⽅便。客户端代码如下:dll_withlibAndH_client.cpp
#include "dll_withLibAndH.h"
//注意路径,加载 dll的另⼀种⽅法是 Project | setting | link 设置⾥
#pragma comment(lib,"dll_withLibAndH.lib")
int main(void)
{
FuncInDll();//只要这样我们就可以调⽤dll⾥的函数了
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论