《Windows核心编程》学习笔记(2)– DLL高级编程(显示运行时链接)

显式的载入DLL模块

HMODULE WINAPI LoadLibrary(

  __in          LPCTSTR lpFileName

);

 

HMODULE WINAPI LoadLibraryEx(
  __in          LPCTSTR lpFileName,
                 HANDLE    hFile,
  __in          DWORD dwFlags
);

hFile参数是为将来扩充所保留的,现在必须将它设为NULL

dwFlags 参数可以被设置为0,或者下列标志的组合:DONT_RESOLVE_DLL_REFERENCES

LOAD_IGNORE_CODE_AUTHZ_LEVEL LOAD_LIBRARY_AS_DATAFILE LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE LOAD_LIBRARY_AS_IMAGE_RESOURCE LOAD_WITH_ALTERED_SEARCH_PATH

这些标志有可以改变DLL文件的默认搜索路径等功能,具体参见MSDN.

 

上面这两个函数会在用户的系统中对DLL的文件映像进行定位,并试图将该文件映像映射到调用进程的地址空间中。两个函数返回的HMODULE表示文件映像被映 射到的虚拟内存地址DllMain入口点所接收的HINSTANCE参数也同样是文件映像被映射到的虚拟内存地址。

 

 

显式的卸载DLL模块

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

BOOL WINAPI FreeLibrary(
  __in          HMODULE hModule
);

 

我们还可以调用下面这个函数来将一个DLL模块从进程的地址空间中卸载:

VOID WINAPI FreeLibraryAndExitThread(
  __in          HMODULE hModule,
  __in          DWORD dwExitCode
);

这个函数适用的情形是:当我们编写了一个DLL,在一开始被映射到进程的地址空间中时,这个DLL会创建一个线程。当线程完成了它的工作后,我们先后调用FreeLibraryExitThread,来从进程的地址空间中撤销对DLL的映射并且终止进程。

但是如果线程分别调用FreeLibraryExitThread,那么会出现一个严重的错误。这个问题就是FreeLibrary会立即从进程的地址空间中撤销对DLL的映射。当FreeLibrary返回的时候,调用ExitThread的代码已经不复存在了,线程试图执行的是不存在的代码。

这将导致访问违规,并导致整个进程被终止。

此时,我们可以调用上述函数,就可以了。

 

检测一个DLL文件是否已经被映射到了进程的地址空间

HMODULE WINAPI GetModuleHandle(
  __in          LPCTSTR lpModuleName
);

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

例如:

HMODULE h = GetModuleHandle("Mylib.dll");

if (h == NULL)

{

         h = LoadLibrary(TEXT(“Mylib.dll”);

}

Else

{

         printf("Mylib.dll已经载入");

}

        

得到DLL的全路径

DWORD WINAPI GetModuleFileName(
  __in          HMODULE hModule,
  __out         LPTSTR lpFilename,
  __in          DWORD nSize
);

如果传NULL给第一个参数,那么函数会在lpFilename中返回当前正在运行的应用程序的可执行文件的文件名。

 

显式的链接到导出符号

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

FARPROC WINAPI GetProcAddress(
  __in          HMODULE hModule,
  __in          LPCSTR lpProcName
);

hModule参数是用来指定包含符号的DLL的句柄。它是先前调用LoadLibrary(Ex)GetModuleHandle所返回的值。

参数lpProcName是我们想要得到的符号名。

 

用法:

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

例如:Typedef void (CALLBACK *PFN_DUMPMODULE)(HMODULE hModule);

是与void DynamicDumpModule(HMODULE hModule)函数相对应的回调函数的类型签名。

 

例如:

PFN_DUMPMODULE pfnDumpModule =

(PFN_DUMPMODULE)GetProcAddress(hDll, “DynamicDumpModule”);

If(pfnDumpModule != NULL)

{

         PfnDumpModule(hDll);

}

 

DLL的入口点函数

一个DLL可以有一个入口点函数,系统会在不同时候调用这个入口点函数。这些调用是通用性质的,通常被DLL用来执行一些与进程或线程有关的初始化和清理工作。

如果DLL不需要这些通知,那么我们可以不必在源代码中实现这个入口点函数。

例如,如果要创建一个只包含资源的DLL,那么我们就不需要实现这个函数。

 

入口点函数:

BOOL APIENTRY DllMain( HANDLE hModule,

                     DWORD  ul_reason_for_call,

                     LPVOID lpReserved)

{

    switch (ul_reason_for_call)

         {

                   case DLL_PROCESS_ATTACH:

                   case DLL_THREAD_ATTACH:

                   case DLL_THREAD_DETACH:

                   case DLL_PROCESS_DETACH:

                            break;

    }

    return TRUE;

}

 

hModule参数包含该DLL实例的句柄。这个值表示一个虚拟内存地址,DLL的文件映像就被映射到进程地址空间的这个位置。通常我们将这个参数保存在一个全局变量中,这样在调用资源载入函数(比如DialogBoxLoadString)的时候,就可以使用它。

lpReserved如果DLL是隐式载入的,那么这个参数的值将不为0;如果DLL是显示载入的,那么这个参数的值将为0

 

函数转发器

函数转发器是DLL输出段中的一个条目,用来将一个函数调用转发到另一个DLL中的另一个函数。

我们可以在自己的DLL模块中使用函数转发器。最简单的方法是使用pragma指示符,如下所示:

 

#pragma comment(linker, “/export:SomeFunc = DllWork.SomeOtherFunc”)

 

这个pragma告诉链接器,正在编译的DLL应该输出一个名为SomeFunc的函数,但实际实现SomeFunc的是另一个名为SomeOtherFunc的函数,该函数被包含在另一个名为DllWork.dll

的模块中。

我们必须为每个想要转发的函数单独创建一行pragma

 

DLL重定向

MicrosoftWindows 2000开始新增了一项DLL重定向的特性。这个特性强制操作系统的加载程序首先从应用程序的目录中载入模块。只有当加载程序无法找到要找的文件时,才会在其他目录中搜索。

为了强制加载程序总是先检查应用程序的目录,我们所要做的就是将一个文件放到应用程序的目录中。这个文件的内容无关紧要,但他的文件名必须是AppName.local

举个例子,如果我们的程序为SuperApp.exe,那么重定向文件的名称必须是SuperApp.exe.local

对已注册的COM对象来说,这项特性极其有用。它允许应用程序将它的COM对象DLL放在自己的目录中,这样注册了同一个COM对象的其他应用程序就不会妨碍到我们的应用程序。

         注意,为了安全性的缘故,Windows Vista中这项特性默认是关闭的,因为它可能会使系统从应用程序的文件夹中载入伪造的系统DLL,而不是从Windows的系统文件夹下载入真正的系统DLL。为了打开这项特性,我们必须在HKLM\Software\Microsoft\WindowsNT\CurrentVersion\Image File Execution Options注册表项中增加一个条目DWORD DevOverrideEnable,并将它的值设为1.

 

模块的基地址重定位

每个可执行文件和DLL模块都有一个首选基地址,它表示在将模块映射到进程的地址空间中时的最佳内存地址。当我们在构建一个可执行模块的时候,链接器会将模块的首选基地址设为0x00400000。对DLL模块来说,链接器会将首选基地址设为0x10000000.

         假设有3个模块,一个user.exe,另外两个是A.dllB.dll。在编译链接各个模块时,我利用VS默认的base address,这样user.exe的默认基地址是0x00400000,AB的基地址是0x10000000。这样,当加载器加载 User.exe(它同时隐式链接A,B)。这样,A,B就会有一个被迫改变默认的基地址;从而导致映像文件里的机器代码指令(包含的硬编码地址)与加载 后的不一样,从而需要调整(效率就会降低)


        
当一个模块无法被加载到他的首选基地址的时候,存在以下两个主要缺点:

1.       加载程序必须遍历重定位段并修改模块中大量的代码。这个过程不仅是一大性能杀手,而且也确实会损害应用程序的初始化时间。

2.       当加载程序写入到模块的代码页面中时,系统的写时复制机制会强制这些页面以系统的页交换文件为后备存储器。由于页交换文件是所有模块的代码页面的后备存储器,因此这会减少可供系统中所有进程使用的存储器的数量。

 

为了要将多个模块载入到同一个地址空间中,那么我们必须给每个模块指定不同的首选基地址。

Visual studio提供了一个名为Rebase.exe的工具。如果在执行Rebase工具的时候传给它一组映像文件名,那么它会执行下面的操作。

1.  它会模拟创建一个进程地址空间。

2.  它会打开应该被载入到这个地址空间中的所有模块,并得到每个模块的大小以及他们的首选基地址。

3.  它会在模拟的地址空间中对模块重定位的过程进行模拟,使各模块之间没有交叠。

4.  对每个重定位过的模块,它会解析该模块的重定位段,并修改模块在磁盘文件中代码。

5.  为了反映新的首选基地址,它会跟新每个重定位过的模块的文件头。

 

用法:

Rebase.exe -b 0x400000 user.exe A.dll  B.dll

注意:我们应该在自己的构建过程的后期,等应用程序所有的模块都已经构建完成后运行它。

 

 

posted @ 2011-08-08 15:45 飞翔荷兰人 阅读(...) 评论(...) 编辑 收藏

I Love Lina~