DLL(DynamicLinkLibrary,动态链接库)是一个可以被其他应用程序调用的程序模块,它封装了可以调用的资源或函数。动态链接库的扩展名一般是DLL,但有时也可能是其他扩展名。DLL文件是可执行文件,符合Windows系统的PE文件格式,但依附于EXE文件创建的进程执行,不能独立运行。一个DLL文件可以被多个进程加载和调用。Windows操作系统下有很多DLL文件,有的是操作系统的DLL文件,有的是应用程序的DLL文件。使用DLL文件有什么好处?DLL是动态链接库,对应的,还有静态链接库。动态链接库是在EXE文件运行时加载执行的,而静态链接库是在连接OBJ文件时同时保存到程序中的。动态链接库可以减小可执行文件的大小,在需要的时候进入内存;软件分为多个模块,可以按模块开发,发布升级也很方便。在某些情况下,必须使用DLL来完成一些工作。本文通过一个简单的DLL程序来初步了解DLL程序的编写。1.编写一个简单的DLL程序从一个简单的DLL程序开始,在DLL程序中加入导出功能。所谓导出函数就是DLL提供给外部EXE或其他类型的可执行文件调用的函数。当然,DLL本身也可以调用自己。DLL程序的入口函数不是main()函数,也不是WinMain()函数,而是DllMain()函数,其定义如下:BOOLWINAPIDllMain(HINSTANCEhinstDLL,//handletotheDLLmoduleDWORDfdwReason,//reasonforcallingfunctionLPVOIDlpvReserved//reserved);参数说明如下。hinstDLL:这个参数是当前DLL模块的句柄,即这个动态链接库模块的实例句柄。fdwReason:该参数表示调用DllMain()函数的原因。这个参数有4个值,也就是说调用DllMain()函数有4种情况。这4个值分别是DLL_PROCESS_ATTACH(当DLL被进程加载时,DllMain()函数被调用),DLL_PROCESS_DETACH(当DLL被进程卸载时,DllMain()函数被调用),DLL_THREAD_ATTACH(当一个DLL被进程卸载时,DllMain()函数被调用)进程中的线程被创建,调用DllMain()函数)和DLL_THREAD_DETACH(当进程中的线程结束时,调用DllMain()函数)。lpvReserved:保留参数,即不被程序员使用的参数。启动VC6集成开发环境,创建DLL工程。创建一个“AsimpleDLLProject”类型的工程,VC生成的代码如下:BOOLAPIENTRYDllMain(HANDLEhModule,DWORDul_reason_for_call,LPVOIDlpReserved){returnTRUE;}在生成的代码中,函数定义处有APIENTRY的函数修饰符。该修饰符是一个宏,其定义如下:#defineAPIENTRYWINAPI由于多次调用DllMain()函数,因此需要根据调用情况执行不同的代码。例如,当进程加载DLL文件时,可能需要在DLL中申请一些资源;并且在卸载DLL的时候,需要释放之前申请的资源。由于种种原因,在编写DLL程序时,需要将DllMain()函数的结构写成如下形式:}caseDLL_TH:{break;}caseDLL_THREAD_DETACH:{break;}}returnTRUE;}这是一个switch/case结构,可以根据不同的调用原因执行不同的代码。2、为DLL添加简单的导出功能以上代码只是一个简单的DLL程序的开始,没有实际意义。对于DLL文件,不需要DllMain()。根据DLL文件的本质作用是提供其他可执行文件的使用,那么DLL程序需要编写可供其他程序使用的函数,而这些公开供其他程序使用的函数称为导出功能。在上述代码的基础上添加导出函数,定义如下:extern"C"__declspec(dllexport)VOIDMsgBox(char*szMsg);extern"C"表示函数以C方式导出。由于源码是一个.CPP文件,如果以C++的方式导出,编译后函数名会被名字给砸了,动态调用函数时会极其不便。__declspec(dllexport)的作用是声明一个导出函数,并从这个DLL中打开该函数供其他模块使用。MsgBox()函数的实现如下:VOIDMsgBox(char*szMsg){charszModuleName[MAX_PATH]={0};GetModuleFileName(NULL,szModuleName,MAX_PATH);MessageBox(NULL,szMsg,szModuleName,MB_OK);}这个函数被调用时,其所在进程的进程名会显示在MessageBox窗口的标题栏上。这样,第一个DLL文件的准备就完成了。编译连接代码,查看编译连接的输出,会发现VC生成了两个文件,分别是“FirstDll.dll”和“FirstDll.lib”。前者是其他可执行程序使用的DLL文件,里面包含程序员写的代码,导出函数,后者是库文件,里面包含导出函数的一些信息,供调用的程序员使用编译时DLL文件中的导出函数。在DLL中导出函数有两种方法,这是其中一种。另一种方法是创建一个.DEF文件来定义要导出的函数。函数除了通过函数名导出外,还可以通过序号导出。创建一个.DEF文件可以方便的管理DLL工程中导出的函数(比在代码中一个一个的找__declspec(dllexport)方便多了)。由于这里的代码比较短,所以使用了__declspec(dllexport)的定义方式。3、DLL程序的调用方法——一个DLL程序不能单独运行,需要编写一个EXE程序(当然也可以在另一个DLL程序中调用)来调用这个DLL文件中导出的函数。在VC集成开发环境中添加一个测试工程,在工作区中右击“Workspace'FirstDll':1project(s)”,在弹出的菜单中选择“AddNewProjecttoWorkspace”,如图图1显示。图1添加项目测试DLL添加控制台项目,然后编写调用DLL的测试代码,如下:#include#pragmacomment(lib,"FirstDll")extern"C"VOIDMsgBox(char*szMsg);intmain(intargc,char*argv[]){MsgBox("HelloFirstDll!");return0;}#pragmacomment(lib,"FirstDll")告诉链接器在FirstDll.lib文件中找到DLL有关导出函数的信息。编译并连接上面的代码,VC会产生一个连接错误,如图2所示。图2连接错误信息这个错误是因为链接器找不到“FirstDll.lib”文件。将“FirstDll.lib”复制到测试工程目录下,然后添加到测试工程中,再次编译连接即可成功。运行编写好的测试程序,会弹出一个错误对话框,如图3所示。图3运行测试程序时的错误信息根据错误信息可以看出缺少要测试的DLL文件,即“FirstDll.dll”文件。将其复制到与可执行文件相同的目录下,然后再次运行,程序就可以顺利执行了。一般在释放一个DLL文件时,需要同时释放DLL文件、Lib文件和.h文件。当然,如果有说明文件或说明书会更专业。4、调用DLL程序的第二种方法是静态调用,即通过链接器将DLL函数的导出函数写入可执行文件。现在使用第二种方法调用DLL中的函数,相对于前面的方法是动态调用。动态调用不是在链接时完成的,而是在运行时完成的。动态调用不会在可执行文件中写入DLL相关信息。下面我们来写一个关于动态调用的测试程序。该程序的创建方法与静态调用的方法相同,这里不再赘述。动态调用DLL函数的代码如下:if(hModule==NULL){MessageBox(NULL,"FirstDll.dll文件不存在","DLL文件加载失败",MB_OK);return-1;}PFUNMSGpFunMsg=(PFUNMSG)GetProcAddress(hModule,"MsgBox");pFunMsg("HelloFirstDll!");return0;}代码正常编译连接。但请注意,本程序中没有使用#pragmacomment()指令,相关导入信息也没有通过lib留在程序中。运行编译链接的程序,程序会提示“FirstDll.dll文件不存在”。按照前面的方法,将FirstDll.dll文件复制到与测试程序相同的目录下,运行测试程序,程序执行成功。DLL动态加载调用非常有用。在第一个测试程序中,如果测试系统的加载器找不到DLL文件,系统会直接报错退出。在第二个测试程序中,如果测试程序找不到DLL文件,程序会给出错误提示,程序其实可以继续执行,不影响其他代码的运行(当然,因为DLL无法加载可能会丢失一些功能)。了解了动态加载调用和静态加载调用的区别,那么它们的优缺点就很清楚了。静态加载调用使用方便,动态加载调用更灵活。在某些情况下,需要使用动态加载调用的方法来使用DLL中导出的函数。例如函数OpenThread(),该函数在VC6自带的PSDK中没有提供LIB文件和函数原型定义,没有LIB文件无法连接成功(该函数对应的LIB文件在新版PSDK)。这种情况下只能使用LoadLibrary()和GetProcAddress()这两个函数来动态加载和调用OpenThread()函数(其实很多时候在DLL中使用导出的函数是找不到对应的函数的)file.lib文件,例如ntdll.dll中的很多函数虽然导出了,但是系统并没有提供对应的lib文件)。现在查看LoadLibrary()函数和GetProcAddress()函数的定义。LoadLibrary()函数的定义如下:HMODULELoadLibrary(LPCTSTRlpFileName);这个函数只有一个参数,就是要加载的DLL文件的文件名。如果函数调用成功,它返回一个模块句柄。GetProcAddress()函数的定义如下:FARPROCGetProcAddress(HMODULEhModule,LPCSTRlpProcName);这个函数有两个参数,分别如下。hModule:该参数为模块句柄,通常通过LoadLibrary()函数或GetModuleHandle()函数获取;lpProcName:该参数指定获取函数地址的函数名。如果函数调用成功,则返回lpProcName指向的函数名的函数地址。五、查看DLL程序导出函数的工具介绍在前面的DLL编程介绍中,提到了导出函数。这里有两个查看DLL程序导出函数的工具。其中一个是VC自带的工具“Depends”,另一个是功能比较强大的工具“PEID”,可以用来查看PE结构,识别打包信息。先用“Depends”查看DLL的导出函数。这个工具可以在VC6的安装菜单下找到。具体位置是“开始”→“程序”→“MicrosoftVisualStudio6.0”→“MicrosoftVisualStudio6.0工具”→“依赖”。打开程序,依次点击菜单项“文件”→“打开”,在“打开”对话框中找到编写好的FirstDll.dll文件,选中并打开(也可以直接拖动),出现工作窗口FirstDll.dll的信息如图4所示。图4Depends显示界面在图4右下角区域显示了DLL文件导出的函数。从图4可以看出,FirstDll.dll文件仅导出一个MsgBox函数。Depends的介绍就这些了,现在我们来看另一个工具“PEID”。本工具用于识别软件“指纹”信息(开发环境、版本、打包信息等)。将FirstDll.dll文件拖到PEID界面,PEID会自动解析DLL文件的PE结构信息,如图5所示。图5PEID显示界面从图5可以看出,只读编辑框PEID底部显示FirstDll.dll文件是VC6开发的,版本为Debug版本。点击“Subsystem”右侧的“Greaterthan”按钮,会显示PE结构的详细信息,如图6所示。图6PE结构详图图6下部PE结构详图,有一个“目录信息”,第一个目录信息是导出表信息,点击“导出表”按钮最右边的“大于”号,出现“导出查看器”界面,如图图7图7ExportViewer从图7可以看出,FirstDll.dll文件只有一个导出函数MsgBox(),并且只有一个导出项,导出函数的信息和Depends一样.