分享

DLL高级技术

 royy 2013-07-25

1、DLL模块的显示载入和符号链接

1.1显式的载入DLL模块

1.2显式的卸载DLL模块

1.3显式的链接到导出符号


      上一节讨论了DLL链接的基本知识并集中讨论了隐式链接,这也是到目前为止最常用的DLL链接形式。对大多数应用程序来说,上一节的内容已经足够了。但是,我们还可以用DLL做更多的事情,这一部分我们将讨论与DLL相关的各种技术,虽然大多数应用程序不需要这些技术,但它们极其有用,因此我们还是应该对这些技术有所了解。

1、DLL模块的显示载入和符号链接

      为了让线程能够调用DLL模块中的一个函数,我们必须将DLL的文件映像 映射 到调用线程所在进程的地址空间中。我们可以通过2种方法来达到这一目的。第一种方法是直接让应用程序的源代码引用DLL中所包含的符号,这使得加载程序会在应用程序运行的时候隐式的载入并链接所需的DLL。第二种方法是让应用程序在运行的过程中,显式的载入所需的DLL并显式的与想要的输出符号进行链接。换句话说,当应用程序在运行的时候,其中的一个线程能够决定它想要调用一个DLL中的一个函数。该线程可以显式的将该DLL载入到进程的地址空间中,得到DLL所包含的一个函数的虚拟内存地址,然后用该内存地址来调用这个函数。这项技术的美妙之处在于,这一切都是在应用程序运行的时候完成的。

1.1显式的载入DLL模块

      在任何时候,进程中的一个线程可以调用下面的两个函数来将一个DLL映射到进程的地址空间中:

HMODULE LoadLibrary(LPCTSTR lpFileName);

HMODULE LoadLibraryEx(
                                       LPCTSTR lpFileName, // file name of module
                                       HANDLE hFile, // reserved, must be NULL
                                       DWORD dwFlags // entry-point execution option
                                       );

这两个函数会(根据上一部分介绍的搜索算法)在用户的系统中对DLL的文件映像进行定位,并试图将该文件映像 映射到 调用进程的地址空间中,这两个函数返回的HMODULE表示文件映像 被映射到的虚拟内存地址。注意:这个HMODULE类型等价于HINSTANCE类型,两者可以换用。本节后面介绍的DllMain入口点所接收的HINSTANCE参数 也同样是 文件映像 被映射到的虚拟内存地址,如果无法将DLL映射到进程的地址空间中,那么函数会返回NULL。为了得到相关的错误信息,我们可以调用GetLastError.

注意:LoadLibraryEx函数由两个额外的参数,hFile和dwFlags,参数hFile是为将来扩充保留的,现在必须将它设为NULL。参数dwFlags 可以被设为0,或下列标志的组合:DONT_RESOLVE_DLL_REFERENCES、LOAD_IGNORE_CODE_AUTHZ_LEVEL、LOAD_LIBRARY_AS_DATAFILE、LOAD_WITH_ALTERED_SEARCH_PATH。下面我们队这些标志做一个简要的介绍。

1)DONT_RESOLVE_DLL_REFERENCES标志告诉系统只需将DLL映射到调用进程的地址空间。正常情况下,当系统将一个DLL映射到进程的地址空间中的时候,系统会调用DLL中一个指定的函数来对DLL进行初始化。该函数通常是DllMain。DONT_RESOLVE_DLL_REFERENCES让 系统 只映射 文件映像,但不要调用DllMain。

      此外,一个DLL可能会导入一些包含在另一个DLL中的函数,当系统将一个DLL映射到进程的地址空间的时候,会检查该DLL是否还需要其它额外的DLL,并同时将他们自动载入。如果指定了DONT_RESOLVE_DLL_REFERENCES,那么系统不会将这些额外的DLL自动载入到进程的地址空间中。

      因此,在调用从该DLL导出的任何函数时,我们将面临很大的风险,代码所依赖的内部数据结构可能尚未初始化,或者代码所引用的DLL尚未载入。这些原因已经足以让我们避免使用这个标志了。

2)LOAD_LIBRARY_AS_DATAFILE标志告诉系统将DLL作为 数据文件 映射到进程的地址空间中,就只对文件进行映射这一点而言,它与DONT_RESOLVE_DLL_REFERENCES相似,系统不会花费额外的时间来准备执行 文件中的任何代码。例如:当系统将一个DLL映射到进程的地址空间中的时候,它会检查DLL中的一些信息来决定应该给文件中不同的段指定何种 页面保护属性。如果我们不指定LOAD_LIBRARY_AS_DATAFILE标志,那么系统会认为需要 执行 文件中的代码,并用相应的方式来设置页面保护属性。举个例子:如果一个DLL是用这个标志载入的,那么当我们对这个DLL调用GetProcAddress的时候,返回值将是NULL,而GetLastError将会返回ERROR_MOD_NOT_FOUND.

      出于几个原因,这个标志非常有用,首先,如果我们有一个DLL其中只包含资源而不包含函数,那么我们可以指定这个标志来将DLL的文件映像 映射到进程的地址空间中。然后我们可以使用LoadLibraryEx函数返回的HMODULE值,来调用载入资源的函数。另外,如果想要使用一个.exe文件中包含的资源,那么我们可以使用LOAD_LIBRARY_AS_DATAFILE标志,通常,载入一个.exe文件会启动一个新的进程,但我们可以使用LoadLibraryEx函数来将一个.exe文件的映像映射到进程的地址空间中。有了.exe文件映射后的HMODULE/HINSTANCE值,我们就可以访问其中的资源了。由于.exe文件没有DllMain函数,因此在调用LoadLibraryEx来载入.exe文件的时候,必须指定LOAD_LIBRARY_AS_DATAFILE标志。

3)LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE标志。这个标志与LOAD_LIBRARY_AS_DATAFILE相似,唯一的不同之处在于DLL文件是以独占访问模式打开的,从而禁止任何其它应用程序在当前应用程序使用该DLL文件的时候对其进行修改。与LOAD_LIBRARY_AS_DATAFILE标志相比,这个标志可以为我们的应用程序提供更好的安全性,正因为如此,我建议除非想让其它应用程序能够对DLL文件的内容进行修改,否则我们应该在应用程序中使用LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE标志。

4)LOAD_LIBRARY_AS_IMAGE_RESOURCE标志,该标志与LOAD_LIBRARY_AS_DATAFILE相似,但有一点略有不同,当系统载入DLL的时候,会对相对虚拟地址(Relative Virtual Address简称RVA)进行修复。RVA上一节有详细介绍,这样RVA就可以直接使用,而不必再根据DLL载入到的内存地址来对它们进行转换了。当需要对DLL进行解析来遍历其中的PE(Portable Executable)段时,这个标志特别有用。

5)LOAD_WITH_ALTERED_SEARCH_PATH标志。见Windows核心编程P545

6)LOAD_IGNORE_CODE_AUTHZ_LEVEL标志用来关闭WinSafer(又称Software Restriction Policies)所提供的验证,它是在Windows XP中引入的,其设计目的是为了对代码在执行过程中可以拥有的特权加以控制。

1.2显式的卸载DLL模块

      当进程不再需要引用DLL中的符号时,我们应该调用下面的函数来显式的将DLL从进程的地址空间中卸载:

BOOL FreeLibrary(HMODULE hInstDLL);

我们必须传入一个HMODULE值,用来标识我们想要卸载的DLL,这个值是先前调用LoadLibrary(Ex)时返回的。我们还可以调用下面这个函数来将一个DLL模块从进程的地址空间中卸载:

VOID FreeLibraryAndExitThread(HMODULE hInstDll,DWORD dwExitCode);

这个函数在Kernel32.dll中被实现如下:

VOID FreeLibraryAndExitThread(HMODULE hInstDll,DWORD dwExitCode)

{

      FreeLibrary(hInstDll);

      ExitThread(dwExitCode);

}

      乍一看,好像没什么大不了的,读者可能会奇怪Microsoft为什么要创建FreeLibraryAndExitThread函数。其原因是为了下面的情形:假设我们正在编写一个DLL,在一开始被映射到进程的地址空间中时,该DLL会创建一个线程,当线程完成了它的工作后,可以先后调用FreeLibrary和ExitThread,来从进程的地址空间中撤销对Dll的映射并终止线程。

      但如果分别调用FreeLibrary和ExitThread,那么会出现一个严重的问题。这个问题就是FreeLibrary会立即从进程的地址空间中撤销对DLL的映射。当FreeLibrary调用返回的时候,调用ExitThread的代码已经不复存在了,线程试图执行的是不存在的代码。这将引发访问违规,并导致整个进程被终止。

      但是,如果线程调用FreeLibraryAndExitThread,那么这个函数会调用FreeLibrary,这使得对DLL的映射会立即被撤销。但要执行的下一条指令仍在kernel32.dll中,而不是在已经被撤销的映射的DLL中。这意味着线程可以继续执行并调用ExitThread。ExitThread会使线程终止并且不再返回。

      实际上,每个DLL在进程中有一个与之对应的使用计数,LoadLibrary和LoadlibraryEx函数会递增该使用计数,而FreeLibrary和FreeLibraryAndExitThread会递减该使用计数。例如:当我们第一次调用LoadLibrary来载入一个DLL的时候,系统会将DLL的文件映像映射到调用进程的地址空间中,并将DLL的使用计数设为1。如果同一个进程的一个线程后来再调用LoadLibrary来载入同一个DLL文件映像的时候,系统不会再次将DLL的文件映像映射到进程的地址空间中。它只是将进程中与该DLL对应的使用计数递增。

      为了从进程的地址空间中撤销该DLL文件映像的映射,进程中的线程必须调用FreeLibrary两次,第一次只是将DLL的使用计数递减为1,第二次调用将DLL的使用计数递减为0,当系统发现DLL的使用计数递减为0时,会从进程的地址空间中撤销对该DLL文件映像的映射。如果任何线程再视图调用该DLL中的函数,那将引发访问违规,因为原来被映射到进程的地址空间中的代码已经不复存在了。

      系统会在每个进程中位每个DLL维护一个使用计数,也就是说,如果进程A中的一个线程执行了下面的代码,然后进程B中的一个线程执行了同样的代码,那么MyLib.dll会被映射到2个进程的地址空间中----该DLL在进程A和进程B中的使用计数都是1。

HMODULE hInstDll=LoadLibrary("MyLib.dll");

如何进程B中的一个线程后来执行了下面的代码,那么该DLL在进程B中的使用计数将变为0,系统会从进程B的地址空间中撤销对该DLL的映射。但是,这对映射到进程A的地址空间中的DLL丝毫没有影响,该DLL在进程A中的使用计数仍然是1。

FreeLibrary(hInstDll);

      线程可以通过调用GetModuleHandle函数来检测一个DLL是否已经被映射到了进程的地址空间中:

HMODULE GetModuleHandle(LPCTSTR pszModuleName);

例如:只有当MyLib.dll尚未被映射到进程的地址空间中时,下面的代码才会将它载入:

HMODULE hInstDll=GetModuleHandle("MyLib.dll");

if(hInstDll==NULL)

{

      hInstDll=LoadLibrary("MyLib.dll");

}

如果传NULL给GetModuleHandle,那么函数会返回应用程序的可执行文件的句柄。

      如果只有一个DLL(或.exe)的HINSTANCE/HMODULE,那么我们可以通过GetModuleFileName函数来得到该DLL的全路径。

WINAPI DWORD GetModuleFileName( HMODULE hModule,LPWSTR lpFilename, DWORD nSize);

第一个参数是该DLL(或.exe)的HMODULE。第二个参数是一个缓存的地址,函数会将文件映像的全路径保存到这个缓存中,第三个参数用来指定缓存的大小,以字符为单位。如果传NULL给第一个参数,那么该函数会在第二个参数中返回当前正在运行的应用程序的可执行文件的文件名。

      混用LoadLibrary和LoadLibraryEx可能会导致将同一个DLL映射到同一个地址空间中的不同位置,例如,我们看下面的代码:

HMODULE hDll1=LoadLibrary("MyLibrary.dll");

HMODULE hDll2=LoadLibraryEx("MyLibrary.dll",NULL,LOAD_LIBRARY_AS_IMAGE_RESOURCE);

HMODULE hDll3=LoadLibraryEx("MyLibrary.dll",NULL,LOAD_LIBRARY_AS_DATAFILE);

读者认为hDll1、hDll2和hDll3的值分别是什么?显然,如果载入的是同一个MyLibrary.dll,那么 它们的值应该相同。但如果改变代码的顺序,变成下面的这样,就不那么明显了:

HMODULE hDll1=LoadLibraryEx("MyLibrary.dll",NULL,LOAD_LIBRARY_AS_DATAFILE);

HMODULE hDll2=LoadLibraryEx("MyLibrary.dll",NULL,LOAD_LIBRARY_AS_IMAGE_RESOURCE);

HMODULE hDll3=LoadLibrary("MyLibrary.dll");

在这种情况下,hDll1、hDll2和hDll3的值各不相同!当我们用LOAD_LIBRARY_AS_DATAFILE、LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE或LOAD_LIBRARY_AS_IMAGE_RESOURCE标志调用LoadLibraryEx的时候,操作系统会先检测该DLL是否已经被LoadLibrary或LoadlibraryEx(没有使用这些标志)载入过。如果已经被载入,那么函数会返回地址空间中DLL原先已经被映射到的地址。但是,如果DLL尚未载入,那么Windows会将该DLL载入到地址空间中一个可用的地址,但并不认为它是一个完全载入的DLL。这时如果用这个模块句柄来调用GetModuleFileName,那么得到的返回值将为0。这是一种非常好的方法,可用让我们知道与一个模块句柄相对于的DLL并不包含动态函数,因此无法通过GetProcAddress来得到函数地址并对函数进行调用。

      始终应该记住的是,即便LoadLibrary和LoadLibraryEx载入的DLL是磁盘上的同一个文件,我们也不能将它们返回的映射地址互换使用。

1.3显式的链接到导出符号。

      一旦显式地载入一个DLL模块,线程必须通过调用下面的函数来得到它想要引用的符号的地址:

FARPROC GetProcAddress(HMODULE hInstDll, LPCSTR pszSymbolName);参数hInstDll用来指定包含符号的DLL的句柄,它是先前调用LoadLibrary(Ex)所返回的。参数pszSymbolName可以有2种形式。第1种形式是用符号名来指定我们想要得到哪个符号的地址,符号名通过一个 以字符零 为终止符的字符串来表示。第2种形式是用序号来指定我们想要得到哪个符号的地址:

GetProcAddress(hInstDll,MAKEINTRESOURCE(2));这种用法假定我们知道DLL的创建者给我们想要的符号指定的序号为2。再强调一次,Microsoft强烈发对使用序号,因此我们不会经常看到GetProcAddress的第二种用法。

      两种形式都能够从DLL中得到我们想要的符号的地址,如果DLL模块的导出段中不包含指定的符号,那么GetProcAddress会返回NULL,表示调用失败。

      应该意识到的是,调用GetProcAddress的第1种方法要比第二种方法慢,因为系统必须根据传入的符号名来执行字符串比较和搜索。如果使用第2种方法,即使传入的序号并没有任何导出函数与之相对应,GetProcAddress也可能会返回一个非NULL值。这个返回值会让我们的应用程序误以为我们得到了一个有效的地址,但事实上却并非如此。试图调用这个地址几乎肯定会引发访问违规。。这种行为也是我们应该优先使用符号名而避免使用序号的另一个原因。

      在能够使用GetProcAddress返回的函数指针来调用函数之前,我们需要将它转型为与函数类型相匹配的正确类型。

例如:下面的代码演示了任何从DLL中引用导出的符号

HINSTANCE hInst;

hInst=LoadLibrary("Dll.dll");//动态加载DLL

typedef int (*ADDPROC)(int a,int b);//定义函数指针 类型

ADDPROC Add=(ADDPROC)GetProcAddress(hInst,"add");//获取DLL的导出函数

if(Add!=NULL){//就可以正常使用了}

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多