网络安全编程:DLL编程

 [[384908]]

DLL(Dynamic Link Library,动态连接库)是一个可以被其他应用程序调用的程序模块,其中封装了可以被调用的资源或函数。动态连接库的扩展名一般是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()函数,该函数的定义如下: 

  1. BOOL WINAPI DllMain(  
  2.  HINSTANCE hinstDLL, // handle to the DLL module  
  3.  DWORD fdwReason, // reason for calling function  
  4.  LPVOID lpvReserved // reserved  
  5. ); 

参数说明如下。

hinstDLL:该参数是当前 DLL 模块的句柄,即本动态连接库模块的实例句柄。

fdwReason:该参数表示 DllMain()函数被调用的原因。

该参数的取值有4种,也就是说存在4种调用DllMain()函数的情况,这4个值分别是DLL_PROCESS_ATTACH(当DLL被某进程加载时,DllMain()函数被调用)、DLL_PRO CESS_DETACH(当DLL被某进程卸载时,DllMain()函数被调用)、DLL_THREAD_ATTACH(当进程中有线程被创建时,DllMain()函数被调用)和DLL_THREAD_DETACH(当进程中有线程结束时,DllMain()函数被调用)。

lpvReserved:保留参数,即不被程序员使用的参数。

启动VC6集成开发环境,创建一个DLL工程。创建一个“A simple DLL Project”类型的工程,VC生成代码如下: 

  1. BOOL APIENTRY DllMain( HANDLE hModule,DWORD ul_reason_for_call,LPVOID lpReserved)  
  2.  
  3.   return TRUE;  

在生成的代码中,函数定义处有一个APIENTRY的函数修饰符。该修饰符为一个宏,其定义如下:

  1. #define APIENTRY WINAPI 

由于DllMain()函数不止一次地被调用,根据调用的情况不同,需要执行不同的代码,比如当进程加载该DLL文件时,可能在DLL中要申请一些资源;而在卸载该DLL时,则需要将先前自身所申请的资源进行释放。出于种种原因,在编写DLL程序时,需要把DllMain()函数的结构写成如下形式: 

  1. BOOL APIENTRY DllMain( HANDLE hModule,DWORD ul_reason_for_call,LPVOID lpReserved)  
  2.  
  3.   switch ( ul_reason_for_call )  
  4.   {  
  5.   case DLL_PROCESS_ATTACH:  
  6.     {  
  7.       break;  
  8.     }  
  9.   case DLL_PROCESS_DETACH:  
  10.     {  
  11.       break;  
  12.     }  
  13.   case DLL_THREAD_ATTACH:  
  14.     {  
  15.       break;  
  16.     }  
  17.   case DLL_THREAD_DETACH:  
  18.     {  
  19.       break; 
  20.     }  
  21.   }  
  22.   return TRUE;  

这是一个switch/case结构,这样写可以达到根据不同的调用原因执行不同的代码。

2. 给DLL添加一个简单的导出函数

上面的代码只是一个简单的DLL程序的开始,并没有实际的意义。对于DLL文件来说,DllMain()并不是必需的。按照DLL文件的本质作用是为其他的可执行文件提供使用,那么DLL程序中需要编写能够提供其他程序使用的函数,这些公开提供给其他程序使用的函数被称为导出函数。在上面代码的基础上添加一个导出函数,定义如下: 

  1. extern "C" __declspec(dllexport) VOID MsgBox(char *szMsg); 

extern “C”表示该函数以 C 方式导出。由于源代码是.CPP 文件,因此,如果按照 C++的方式导出的话,那么在编译后函数名会被名字粉碎,导致在动态调用该函数时就会极为不方便。__declspec(dllexport)的作用是声明一个导出函数,将该函数从本 DLL 中开放提供给其他模块使用。

MsgBox()函数的实现如下: 

  1. VOID MsgBox(char *szMsg)  
  2.  
  3.   char szModuleName[MAX_PATH] = { 0 };  
  4.   GetModuleFileName(NULL, szModuleName, MAX_PATH);  
  5.   MessageBox(NULL, szMsg, szModuleName, MB_OK);  

该函数在被调用时会在MessageBox窗口的标题栏处显示其所在进程的进程名。

这样,第一个DLL文件的编写就完成了。编译连接该代码,查看编译和连接的输出情况会发现VC共生成了2个文件,分别是“FirstDll.dll”和“FirstDll.lib”,前者是供其他可执行程序使用的DLL文件,其中包含了程序员编写的代码、导出函数,而后者是一个库文件,其中包含一些导出函数的相关信息,供调用DLL文件中导出函数函数的程序员编译时使用。

导出DLL中的函数有两种方法,这是其中的一种。另外一种方式是建立一个.DEF的文件来定义导出哪些函数。函数除了可以通过函数名导出外,还可以通过序号进行导出。建立.DEF文件可以较为方便地管理DLL项目中的导出函数(总比在代码中逐个找__declspec(dllexport)要方便很多)。由于这里的代码比较短小,因此使用了__declspec(dllexport)这种定义方法。

3. 对DLL程序的调用方法一

DLL程序是无法单独运行的,它需要通过编写一个EXE程序(当然也可以在另外的DLL程序中调用)来调用这个DLL文件中的导出函数。在VC集成开发环境中添加一个测试项目,在工作区的“Workspace ‘FirstDll’:1 project(s)”上单击右键,在弹出的菜单中选择“Add New Project to Workspace”,如图1所示。

图1  添加对DLL进行测试的项目

添加一个控制台的项目,然后编写对DLL进行调用的测试代码,具体如下: 

  1. #include <windows.h>  
  2. #pragma comment (lib, "FirstDll")  
  3. extern "C" VOID MsgBox(char *szMsg);  
  4. int main(int argc, char* argv[])  
  5.  
  6.   MsgBox("Hello First Dll !");  
  7.   return 0; 

#pragma comment (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函数的代码如下: 

  1. #include <windows.h>  
  2. typedef VOID (*PFUNMSG)(char *);  
  3. int main(int argc, char* argv[])  
  4.  
  5.   HMODULE hModule = LoadLibrary("FirstDll.dll");  
  6.   if ( hModule == NULL )  
  7.   {  
  8.     MessageBox(NULL, "FirstDll.dll 文件不存在","DLL 文件加载失败", MB_OK);  
  9.     return -1;  
  10.   }  
  11.   PFUNMSG pFunMsg = (PFUNMSG)GetProcAddress(hModule, "MsgBox");  
  12.   pFunMsg("Hello First Dll !");  
  13.   return 0;  

对代码进行编译连接都正常通过。但是请注意,这个程序中并没有用到#pragma comment()指令,也没有通过lib在程序中留下相关的导入信息。运行编译连接好的程序,程序会给出提示“FirstDll.dll文件不存在”。按照前面的方法,将FirstDll.dll文件复制到与测试程序相同的目录下,运行测试程序,程序执行成功。

DLL的动态加载调用是非常有用的。在第一个测试程序中,如果测试系统的装载器无法找到DLL文件,那么系统会直接报错而退出。而在第二个测试程序中,如果测试程序无法找到DLL文件,则由程序给出一个错误的提示,同时程序其实可以继续往下执行,而不会影响其他代码的运行(当然,由于DLL无法加载可能会损失部分的功能)。明白了动态加载调用和静态加载调用的区别,那么它们的优缺点就很清楚了。静态加载调用使用方便,而动态加载调用灵活性较好。

在有些情况下,必须使用动态加载调用的方法来使用DLL中的导出函数。比如函数OpenThread(),该函数在VC6自带的PSDK中没有提供LIB文件和函数原型定义,没有LIB文件就无法连接成功(在新版的PSDK中有该函数对应的LIB文件)。在这种情况下,只能使用LoadLibrary()和GetProcAddress()这两个函数来动态加载调用OpenThread()函数(其实有很多情况下,在使用DLL文件中的导出函数时是找不到对应的LIB文件的,比如ntdll.dll中的很多函数虽然有导出,但是系统没有提供与其对应的LIB文件)。

现在了解一下LoadLibrary()函数和GetProcAddress()函数的定义。LoadLibrary()函数的定义如下: 

  1. HMODULE LoadLibrary( LPCTSTR lpFileName);  

该函数只有一个参数,即要加载的DLL文件的文件名。该函数调用成功,则返回一个模块句柄。

GetProcAddress()函数的定义如下: 

  1. FARPROC GetProcAddress( HMODULE hModule, LPCSTR lpProcName);  

该函数有两个参数,分别如下。

hModule:该参数是模块句柄,通常通过 LoadLibrary()函数或 GetModuleHandle()函数获得;

lpProcName:该参数指定要获得函数地址的函数名称。

该函数调用成功,则返回lpProcName指向的函数名的函数地址。

5. 查看DLL程序导出函数的工具介绍

前面介绍DLL编程时提到了导出函数,这里介绍两款查看DLL程序的导出函数的工具。其中一款是VC自带的工具“Depends”,另一款工具是一个功能更加强大的可以用来查看PE结构和识别加壳信息的工具“PEID”。

首先用“Depends”来查看DLL的导出函数,该工具可以在VC6的安装菜单下找到,具体位置为“开始”→“程序”→“Microsoft Visual Studio 6.0”→“Microsoft Visual Studio 6.0 Tools”→“Depends”。打开该程序,依次单击菜单项“File”→“Open”,在“打开”对话框中找到所写的FirstDll.dll文件,选中并打开(也可以直接进行拖曳),其工作窗口中显示了FirstDll.dll的信息,如图4所示。

图4  Depends显示界面

在图4的右下角区域范围显示的是该DLL文件导出的函数。从图4中可以看出,FirstDll.dll文件只导出一个MsgBox函数。

对于Depends的介绍就这么多,现在来看另外一个工具“PEID”。该工具是用来识别软件“指纹”信息(开发环境、版本、加壳信息等)的。将FirstDll.dll文件拖曳到PEID界面上,PEID会自动解析出该DLL文件的PE结构信息,界面如图5所示。

图5  PEID显示界面

从图5可以看出,PEID最下方的只读编辑框中显示了FirstDll.dll文件是由VC6开发的,并且版本是Debug版本。单击“子系统”右边的“大于号”按钮,会显示PE结构的详细信息,如图6所示。

图6  PE结构详情

在图6中的PE结构详细信息的下半部分有个“目录信息”,其中的第一个目录信息就是导出表信息,单击“导出表”最右侧的“大于号”按钮,出现“导出查看器”界面,如图7所示。

图7  导出查看器

从图7中可以看出,FirstDll.dll文件只有一个导出函数MsgBox(),只存在一个导出项。导出函数的信息与Depends相同。 

 

文章来源网络,作者:运维,如若转载,请注明出处:https://shuyeidc.com/wp/138791.html<

(0)
运维的头像运维
上一篇2025-03-03 04:29
下一篇 2025-03-03 04:30

相关推荐

  • 个人主题怎么制作?

    制作个人主题是一个将个人风格、兴趣或专业领域转化为视觉化或结构化内容的过程,无论是用于个人博客、作品集、社交媒体账号还是品牌形象,核心都是围绕“个人特色”展开,以下从定位、内容规划、视觉设计、技术实现四个维度,详细拆解制作个人主题的完整流程,明确主题定位:找到个人特色的核心主题定位是所有工作的起点,需要先回答……

    2025-11-20
    0
  • 社群营销管理关键是什么?

    社群营销的核心在于通过建立有温度、有价值、有归属感的社群,实现用户留存、转化和品牌传播,其管理需贯穿“目标定位-内容运营-用户互动-数据驱动-风险控制”全流程,以下从五个维度展开详细说明:明确社群定位与目标社群管理的首要任务是精准定位,需明确社群的核心价值(如行业交流、产品使用指导、兴趣分享等)、目标用户画像……

    2025-11-20
    0
  • 香港公司网站备案需要什么材料?

    香港公司进行网站备案是一个涉及多部门协调、流程相对严谨的过程,尤其需兼顾中国内地与香港两地的监管要求,由于香港公司注册地与中国内地不同,其网站若主要服务内地用户或使用内地服务器,需根据服务器位置、网站内容性质等,选择对应的备案路径(如工信部ICP备案或公安备案),以下从备案主体资格、流程步骤、材料准备、注意事项……

    2025-11-20
    0
  • 如何企业上云推广

    企业上云已成为数字化转型的核心战略,但推广过程中需结合行业特性、企业痛点与市场需求,构建系统性、多维度的推广体系,以下从市场定位、策略设计、执行落地及效果优化四个维度,详细拆解企业上云推广的实践路径,精准定位:明确目标企业与核心价值企业上云并非“一刀切”的方案,需先锁定目标客户群体,提炼差异化价值主张,客户分层……

    2025-11-20
    0
  • PS设计搜索框的实用技巧有哪些?

    在PS中设计一个美观且功能性的搜索框需要结合创意构思、视觉设计和用户体验考量,以下从设计思路、制作步骤、细节优化及交互预览等方面详细说明,帮助打造符合需求的搜索框,设计前的规划明确使用场景:根据网站或APP的整体风格确定搜索框的调性,例如极简风适合细线条和纯色,科技感适合渐变和发光效果,电商类则可能需要突出搜索……

    2025-11-20
    0

发表回复

您的邮箱地址不会被公开。必填项已用 * 标注