《Windows PE》4.3 延迟加载导入表

延迟加载导入表(Delayed Import Table)是PE文件中的一个数据结构,用于实现延迟加载(Lazy Loading)外部函数的机制。

延迟加载是指在程序运行时,只有当需要使用某个外部函数时才进行加载和绑定,而不是在程序启动时就进行全部的动态链接。这个机制可以减少程序启动时间和内存占用。

本节必须掌握的知识点:

        延迟加载导入表数据结构

        延迟加载DLL的意义及实现

        实例分析

4.3.1 延迟加载导入表数据结构

■延迟加载版本1的结构如下:

typedef struct _IMAGE_DELAYLOAD_DESCRIPTOR {

    union {

        DWORD AllAttributes;

        struct {

            DWORD RvaBased : 1; // 如果为1,表示延迟加载版本2的thunk

            DWORD ReservedAttributes : 31; // 保留字段

        } DUMMYSTRUCTNAME;

    } Attributes;

    DWORD DllNameRVA; // 目标库名称的RVA,以NULL结尾的ASCII字符串

    DWORD ModuleHandleRVA; // HMODULE的缓存位置的RVA(相对虚拟地址)

DWORD ImportAddressTableRVA; // IAT(Import Address Table)起始位置的RVA

// 名称表起始位置的RVA,对应PIMAGE_THUNK_DATA::AddressOfData

    DWORD ImportNameTableRVA;

    DWORD BoundImportAddressTableRVA; // 可选的绑定IAT的RVA

    DWORD UnloadInformationTableRVA; // 可选的卸载信息表的RVA

    DWORD TimeDateStamp; // 如果未绑定,则为0;否则为目标DLL的日期/时间戳

} IMAGE_DELAYLOAD_DESCRIPTOR, *PIMAGE_DELAYLOAD_DESCRIPTOR;

●延迟加载版本1的thunk数据结构

延迟加载版本1(Delayed Load Version 1)的thunk数据结构是一种用于存储被延迟加载的外部函数的地址的数据结构。延迟加载版本1的thunk数据结构相对简单,下面是延迟加载版本1的thunk数据结构:

typedef struct _IMAGE_THUNK_DATA {

    union {

        DWORD ForwarderString; // 如果是转发函数,指向转发字符串的RVA

        DWORD Function; // 延迟加载函数的地址

        DWORD Ordinal; // 延迟加载函数的序号

// 对应PIMAGE_IMPORT_BY_NAME或PIMAGE_THUNK_DATA

        DWORD AddressOfData;

    } u1;

} IMAGE_THUNK_DATA, *PIMAGE_THUNK_DATA;

延迟加载版本1的thunk数据结构中的u1联合包含了几个字段,具体的含义取决于延迟加载的方式:

ForwarderString:如果外部函数是一个转发函数,则指向转发字符串的RVA(相对虚拟地址)。

Function:延迟加载函数的地址。该字段保存外部函数的地址。

Ordinal:延迟加载函数的序号。

AddressOfData:指向IMAGE_IMPORT_BY_NAME或另一个IMAGE_THUNK_DATA结构的地址。在延迟加载版本1中,这个字段用于保存函数的名称表的RVA(相对虚拟地址)。

延迟加载版本1的thunk数据结构用于在需要时获取外部函数的地址,并将其更新到IAT(Import Address Table)中,以便程序可以直接调用外部函数。

■延迟加载导入表的使用方法如下:

●在PE文件的导入表中,找到包含延迟加载导入的DLL(动态链接库)项。这通常是在导入表中找到被标记为延迟加载的DLL名称。

●当需要使用延迟加载的外部函数时,在代码中调用该函数之前,需要先检查外部函数是否已经加载和绑定。

●如果外部函数尚未加载和绑定,可以使用LoadLibrary函数来加载DLL,并通过GetProcAddress函数获取外部函数的地址。

●将获取到的外部函数地址更新到IAT(Import Address Table)中,以便程序可以直接调用外部函数。

●重复步骤3和步骤4,直到所有需要延迟加载的外部函数都被加载和绑定。

 注意

1.延迟加载导入表通常需要操作系统或动态链接库的支持来实现延迟加载的机制。在Windows环境下,可以使用delayimp库和设置相应的编译选项来实现延迟加载导入表。

2.此外,可以使用一些特定的宏和函数来简化延迟加载的操作,例如__declspec(delay_load)宏用于标记延迟加载的函数,并提供了一些辅助函数(如__FUnloadDelayLoadedDLL)来卸载延迟加载的DLL。

3.延迟加载导入表的使用可以优化程序的启动时间和内存占用,只有在实际需要使用外部函数时才进行加载和绑定。

4.请记住,延迟加载不是操作系统功能。它完全由链接器和运行时库添加的其他代码和数据实现。因此,在 WINNT.H 中找不到很多对延迟加载的引用。但是,您可以看到延迟加载数据和常规导入数据之间的明显相似之处。

5.延迟加载数据由 DataDirectory 中的 IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 条目(数据目录项第13项)指向。这是 ImgDelayDescr 结构数组的 RVA,在 Visual C++ 的 DelayImp.H 中定义。每个延迟加载导入的 DLL 都有一个 ImgDelayDescr(延迟加载导入描述符)。

6.从 ImgDelayDescr 中收集的关键是它包含 DLL 的 IAT 和 INT 的地址。这些表的格式与常规导入的格式相同,只是它们被写入运行时库代码而不是操作系统并读取。首次从延迟加载的 DLL 调用 API 时,运行时会调用 LoadLibrary(如有必要),然后调用 GetProcAddress。生成的地址存储在延迟加载 IAT 中,以便将来的调用直接转到 API。

7.延迟加载数据有点愚蠢,需要解释。在 Visual C++ 6.0 的原始化身中,所有包含地址的 ImgDelayDescr 字段都使用虚拟地址,而不是 RVA。也就是说,它们包含可以找到延迟加载数据的实际地址。这些字段是 DWORD,大小为 x86 上的指针。

8.现在快进到 IA-64 支持。突然之间,4 个字节不足以容纳一个完整的地址。在这一点上,Microsoft做了正确的事情,并将包含地址的字段更改为RVA

9.仍然存在确定 ImgDelayDescr 使用的是 RVA 还是虚拟地址的问题。该结构具有用于保存标志值的字段。当 Attributes 字段的RvaBased位处于打开状态时,结构成员应被视为 RVA。这是从 Visual Studio® .NET 和 64 位编译器开始的唯一选项。如果 Attributes中的该位处于关闭状态,则 ImgDelayDescr 字段是VA虚拟地址。

■延迟加载版本2(Delayed Load Version 2)

延迟加载版本2(Delayed Load Version 2)是一种改进的延迟加载机制,用于在程序运行时按需加载外部函数。

延迟加载版本2相对于旧的延迟加载机制有以下改进:

●延迟加载导入表中的Attributes字段的RvaBased位被用于标识是否使用延迟加载版本2的thunk。如果该位为1,表示使用延迟加载版本2的thunk。

●延迟加载版本2的thunk是一种特殊的数据结构,用于存储外部函数的地址。每个延迟加载版本2的thunk对应一个被延迟加载的外部函数。

●延迟加载版本2的thunk中包含了一个回调函数,用于在外部函数被加载和绑定后进行通知。这样可以在加载和绑定外部函数之后执行一些额外的操作,例如初始化相关数据或执行其他逻辑。

●延迟加载版本2的结构:

typedef struct _IMAGE_DELAYLOAD_DESCRIPTOR {

    union {

        DWORD AllAttributes;

        struct {

            DWORD RvaBased : 1; // 如果为1,表示延迟加载版本2的thunk

            DWORD ReservedAttributes : 31; // 保留字段

        } DUMMYSTRUCTNAME;

    } Attributes;

    DWORD DllNameRVA; // 目标库名称的RVA,以NULL结尾的ASCII字符串

    DWORD ModuleHandleRVA; // HMODULE的缓存位置的RVA

DWORD ImportAddressTableRVA; // IAT起始位置的RVA

// 名称表起始位置的RVA,对应PIMAGE_THUNK_DATA::AddressOfData

    DWORD ImportNameTableRVA;

    DWORD BoundImportAddressTableRVA; // 可选的绑定IAT的RVA

    DWORD UnloadInformationTableRVA; // 可选的卸载信息表的RVA

DWORD TimeDateStamp; // 如果未绑定,则为0;否则为目标DLL的日期/时间戳

// 延迟加载导入描述符

    PIMAGE_DELAYLOAD_IMPORT_DESCRIPTOR DelayLoadImportDescriptor;

    DWORD DelayLoadInfoSize; // 延迟加载信息的大小

    DWORD DelayLoadInfoTable; // 延迟加载信息表的RVA(相对虚拟地址)

    DWORD BoundDelayLoadTable; // 可选的绑定延迟加载信息表的RVA(相对虚拟地址)

    DWORD UnloadDelayLoadTable; // 可选的卸载延迟加载信息表的RVA(相对虚拟地址)

    DWORD Timestamp; // 时间戳,表示延迟加载导入描述符的版本

} IMAGE_DELAYLOAD_DESCRIPTOR, *PIMAGE_DELAYLOAD_DESCRIPTOR;

●延迟加载版本2的结构相对于延迟加载版本1添加了以下字段:

1.DelayLoadImportDescriptor:指向延迟加载导入描述符的指针。该描述符包含了延迟加载版本2的thunk的信息。

2.DelayLoadInfoSize:延迟加载信息的大小,即IMAGE_DELAYLOAD_IMPORT_DESCRIPTOR结构的大小。

3.DelayLoadInfoTable:延迟加载信息表的RVA(相对虚拟地址),包含了延迟加载版本2的thunk的详细信息。

4.BoundDelayLoadTable:可选的绑定延迟加载信息表的RVA(相对虚拟地址),用于绑定延迟加载版本2的thunk。

5.UnloadDelayLoadTable:可选的卸载延迟加载信息表的RVA(相对虚拟地址),用于卸载延迟加载版本2的thunk。

6.Timestamp:时间戳,表示延迟加载导入描述符的版本。

延迟加载版本2的结构中的DelayLoadImportDescriptor和DelayLoadInfoTable字段提供了更详细的延迟加载信息,使得可以更灵活地控制和管理延迟加载的外部函数。

●延迟加载版本2的thunk数据结构

延迟加载版本2(Delayed Load Version 2)的thunk数据结构是一种特殊的数据结构,用于存储被延迟加载的外部函数的地址。延迟加载版本2的thunk相对于延迟加载版本1的thunk有一些改进和扩展。下面是延迟加载版本2的thunk数据结构:

typedef struct _IMAGE_DELAYLOAD_THUNK {

    union {

        PVOID AddressOfData;// PIMAGE_IMPORT_BY_NAME或PIMAGE_THUNK_DATA

        DWORD ForwarderString; // 如果是转发函数,指向转发字符串的RVA

        DWORD Function; // 延迟加载函数的地址

        DWORD Ordinal; // 延迟加载函数的序号

        DWORD AddressTable; // IAT(Import Address Table)中的地址

// 延迟加载函数的名称表的RVA,对应PIMAGE_THUNK_DATA::AddressOfData

        DWORD NameTable;

    } u1;

    PIMAGE_DELAYLOAD_DESCRIPTOR DelayloadDescriptor; // 延迟加载导入描述符

    PVOID CallbackFunction; // 回调函数的地址

} IMAGE_DELAYLOAD_THUNK, *PIMAGE_DELAYLOAD_THUNK;

延迟加载版本2的thunk数据结构相对于延迟加载版本1的thunk添加了以下字段:

1.CallbackFunction:回调函数的地址。回调函数用于在外部函数被加载和绑定之后进行通知,可以执行一些额外的操作。

延迟加载版本2的thunk数据结构中的AddressOfData字段(在u1联合中)用于保存外部函数的地址。具体的含义取决于延迟加载的方式,可以是函数地址、函数序号、IAT中的地址、延迟加载函数的名称表等。

DelayloadDescriptor字段指向延迟加载导入描述符,该描述符包含了与该thunk相关的延迟加载信息。

使用延迟加载版本2的thunk数据结构,可以更灵活地控制和管理被延迟加载的外部函数,并在需要时执行回调函数进行额外的操作。

●使用延迟加载版本2的步骤如下:

1.在PE文件的延迟加载导入表中找到被标记为延迟加载版本2的DLL(动态链接库)项。

2.当需要使用延迟加载的外部函数时,在代码中调用该函数之前,检查相关的thunk是否已经被加载和绑定。

3.如果thunk尚未加载和绑定,会触发加载和绑定操作,并执行相关的回调函数。

4.在回调函数中,可以执行一些额外的操作,例如初始化相关数据或执行其他逻辑。

5.获取外部函数的地址,并将其更新到IAT(Import Address Table)中,以便程序可以直接调用外部函数。

6.重复步骤3至步骤5,直到所有需要延迟加载的外部函数都被加载和绑定。

延迟加载版本2相对于旧的延迟加载机制提供了更灵活的控制和更强大的功能。通过使用回调函数,可以在外部函数加载和绑定后进行额外的操作,以满足特定的需求。

       ●延迟加载导入表与导入表的区别:

延迟加载导入表和导入表是相互分离的。一个PE文件中可以同时存在这两种数据,也可以单独存在一种。

1.导入表

一个应用程序要调用动态链接库的某个函数,需要先在程序中静态引入该动态链接库,编译器在编译时会分解调用该引入函数的call指令,并将其调用最终指向IAT表。PE加载器要完成的任务就是根据导入表的描述,将IAT中的地址修正为函数在进程地址空间的真实VA地址,这样就能保证该函数被正确调用。

在以上描述中,程序要正确运行,必须保证该动态链接库能够在进程环境变量指定的 PATH中找到,并且将其加载到与程序相同的用户空间,才可以调用来自DLL中的导入函数。

2.延迟加载导入表是一种特殊类型的导入表,同导入表一样,它记录了应用程序要导入的部分或全部动态链接库及相关的函数信息。与导入表不同的是,它所记录的这些DLL动态链接库并不会被操作系统的PE加载器加载,只有等到由其登记的相关函数被应用程序调用时,PE中注册的延迟加载函数才会根据延迟加载导入表中对该函数的描述,动态加载相关链接库并修正函数的VA地址,实现对函数的调用。即首次从延迟加载的 DLL 调用 API 时,运行时会调用 LoadLibrary(如有必要),然后调用 GetProcAddress。生成的地址存储在延迟负载 IAT 中,以便将来的调用直接转到 API函数。

4.3.2 延迟加载DLL的意义及实现

Microsoft Visual C++ 6.0提供了一个出色的新特性,它能够使DLL的操作变得更加容易。这个特性称为延迟加载DLL。延迟加载的DLL是个隐含链接的DLL,它实际上要等到你的代码试图引用DLL中包含的一个符号(例如函数名或全局变量)时才进行加载。

延迟加载的DLL在下列情况下是非常有用的:

●如果一个应用程序使用若干个DLL,那么它的初始化时间就比较长,因为加载程序要将所有需要的DLL映射到进程的用户空间中。解决这个问题的方法之一是在进程运行的时候分开加载各个DLL。延迟加载的DLL能够更容易地完成这样的加载。 

●如果调用代码中的一个新函数,然后试图在老版本的系统上运行你的应用程序,而该系统中没有该函数,那么加载程序就会报告一个错误,并且不允许该应用程序运行。你需要一种方法让你的应用程序运行。如果(在运行时)发现该应用程序在老的系统上运行,那么你将不调用遗漏的函数。

例如,一个应用程序在Windows 2000上运行时想要使用PSAPI函数,而在Windows 98上运行想要使用ToolHelp函数(比如Process32Next)当该应用程序初始化时,它调用GetVersionEx函数来确定主操作系统,并正确地调用相应的其他函数。如果试图在Windows 98上运行该应用程序,就会导致加载程序显示一条错误消息,因为Windows 98上并不存在PSAPI.dll模块。同样,延迟加载的DLL能够使你非常容易地解决这个问题。

延迟加载DLL的实现

●首先象平常那样创建一个DLL。也要象平常那样创建一个可执行模块,但是必须修改两个链接程序开关,并且重新链接可执行模块。下面是需要添加的两个链接程序开关:

/Lib:DelayImp.lib

/DelayLoad:Mydll.dll

Lib开关告诉链接程序将一个特殊的函数__delayLoadHelper嵌入你的可执行模块。

DelayLoad开关将下列事情告诉链接程序:

1.从可执行模块的输入节中删除MyDll.dll,这样,当进程被初始化时,操作系统的加载程序就不会显式加载DLL。

2.将新的Delay Import(延迟导入.didata)节嵌入可执行模块,以指明哪些函数正在从MyDll.dll导入。 

3. 通过转移到对__delayLoadHelper函数的调用,转换到对延迟加载函数的调用。当应用程序运行时,对延迟加载函数的调用实际上是对__delayLoadHelper函数的调用。该函数引用特殊的Delay Import节,并且调用LoadLibrary之后再调用GetProcAddress。一旦获得延迟加载函数的地址,__delayLoadHelper就要安排好对该函数的调用(更新到IAT表中),这样,将来的调用就会直接转向对延迟加载函数的调用。注意,当第一次调用同一个DLL中的其他函数时,必须同样将它们更新到IAT表中。另外,可以多次设定/delayLoad链接程序的开关,为想要延迟加载的每个DLL设定一次开关。 

●延迟加载异常处理

1.通常情况下,当操作系统的加载程序加载可执行模块时,它将设法加载必要的DLL。如果一个DLL无法加载,那么加载程序就会显示一条错误消息。如果是延迟加载的DLL,那么在进行初始化时将不检查是否存 在DLL。如果调用延迟加载函数时无法找到该DLL, __delayLoadHelper函数就会引发一个软件异常条件。可以使用结构化异常处理(SEH)方法来跟踪该异常条件。如果不跟踪该异常条件,那么你的进程就会终止运行。

2.当__delayLoadHelper确实找到你的DLL,但是要调用的函数不在该DLL中时,将会出现另一个问题。比如,如果加载程序找到一个老的DLL版本,就会发生这种情况。在这种情况下,__delayLoadHelper也会引发一个软件异常条件,对这个软件异常条件的处理方法与上面相同。

Visual C++ 开发小组定义了两个软件异常条件代码,即VcppException(ERROR_SEVERITY_ERROR,ERROR_MOD_NOT_FOUND)和VcppException(ERROR_SEVERITY_ERROR,ERROR_PROC_NOT_FOUND)。这些代码分别用于指明DLL模块没有找到和函数没有找到。 

实验三十:延迟加载异常处理

       如果调用延迟加载的函数时无法找到DLL,函数_delayLoadHeaper就会引发一个软件异常。该异常可以使用结构化异常处理(SHE)方法捕获。以下是SHE异常处理代码示例:

/*------------------------------------------------------------------------

 FileName:Exception.cpp

 实验30:延迟加载DLL的SEH异常处理

 (c) bcdaren, 2024

-----------------------------------------------------------------------*/

#include <iostream>

#include <stdexcept>

#include <windows.h>

int main() {

    try {

        // 抛出一个std::runtime_error异常

        throw std::runtime_error("An error occurred.");

    }

    catch (const std::exception& e) {

        // 捕获std::exception及其派生类型的异常

        std::cout << "Caught exception: " << e.what() << std::endl;

    }

    catch (...) {

        // 捕获其他类型的异常

        std::cout << "Caught unknown exception." << std::endl;

    }

    DWORD errorCode1 = ERROR_MOD_NOT_FOUND;

    // 使用FormatMessage函数获取错误消息

    LPSTR errorMessage = nullptr;

    DWORD result = FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM |

FORMAT_MESSAGE_ALLOCATE_BUFFER, nullptr, errorCode1,

        0, reinterpret_cast<LPSTR>(&errorMessage), 0, nullptr);

    if (result != 0) {

        std::cout << "Error severity: " << ERROR_SEVERITY_ERROR << std::endl;

        std::cout << "Error code: " << errorCode1 << std::endl;

        std::cout << "Error message: " << errorMessage << std::endl;

        // 释放错误消息的缓冲区

        LocalFree(errorMessage);

    }

    else {

        std::cout << "Failed to retrieve error message." << std::endl;

    }

    DWORD errorCode2 = ERROR_PROC_NOT_FOUND;

    // 使用FormatMessage函数获取错误消息

    result = FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM |

FORMAT_MESSAGE_ALLOCATE_BUFFER, nullptr, errorCode2,

        0, reinterpret_cast<LPSTR>(&errorMessage), 0, nullptr);

    if (result != 0) {

        std::cout << "Error severity: " << ERROR_SEVERITY_ERROR << std::endl;

        std::cout << "Error code: " << errorCode2 << std::endl;

        std::cout << "Error message: " << errorMessage << std::endl;

        // 释放错误消息的缓冲区

        LocalFree(errorMessage);

    }

    else {

        std::cout << "Failed to retrieve error message." << std::endl;

    }

    return 0;

}

运行:

Caught exception: An error occurred.

Error severity: 3221225472

Error code: 126

Error message: 找不到指定的模块。

Error severity: 3221225472

Error code: 127

Error message: 找不到指定的程序。

下面给出一段微软MSDN给出的示例代码及说明:

https://learn.microsoft.com/zh-cn/cpp/build/reference/linker-support-for-delay-loaded-dlls?view=msvc-170

可以在 MSVC include 目录中的文件 delayhlp.cpp 中找到定义的 __HrLoadAllImportsForDll 函数指示链接器从使用 /delayload 链接器选项指定的 DLL 加载所有导入。

一次加载所有导入时,可以在一个位置集中处理错误。 可以避免围绕对导入的所有实际调用进行结构化异常处理。 这还避免了应用程序在某个过程期间失败的情况:例如,如果帮助程序代码在成功加载其他导入后无法加载某个导入。

调用 __HrLoadAllImportsForDll 不会更改挂钩和错误处理的行为。 有关详细信息,请参阅错误处理和通知。

__HrLoadAllImportsForDll 对 DLL 本身中存储的名称进行区分大小写的比较。

下面是在称为 TryDelayLoadAllImports 的函数中使用 __HrLoadAllImportsForDll 尝试加载命名 DLL 的示例。 它使用函数 CheckDelayException 来确定异常行为。

int CheckDelayException(int exception_value)

{

If (exception_value == VcppException(ERROR_SEVERITY_ERROR,

ERROR_MOD_NOT_FOUND) ||       

exception_value == VcppException(ERROR_SEVERITY_ERROR,

ERROR_PROC_NOT_FOUND))

    {

        // This example just executes the handler. (系统将控制权转给异常处理程序)

        return EXCEPTION_EXECUTE_HANDLER;

    }

    // Don't attempt to handle other errors

    return EXCEPTION_CONTINUE_SEARCH;

}

bool TryDelayLoadAllImports(LPCSTR szDll)

{

    __try

    {

        HRESULT hr = __HrLoadAllImportsForDll(szDll);

        if (FAILED(hr))

        {

            // printf_s("Failed to delay load functions from %s\n", szDll);

            return false;

        }

    }

    __except (CheckDelayException(GetExceptionCode()))

    {

        // printf_s("Delay load exception for %s\n", szDll);

        return false;

    }

    // printf_s("Delay load completed for %s\n", szDll);

    return true;

}

卸载延迟加载的DLL

假如你的应用程序需要一个特殊的DLL来打印一个文档,那么这个DLL就非常适合作为一个延迟加载的DLL,因为大部分时间它是不用的。不过,如果用户选择了Print命令,你就可以调用该DLL中的一个函数,然后它就能够自动进行DLL的加载。这确实很好,但是,当文档打印后,用户可能不会立即打印另一个文档,因此可以卸载这个DLL,释放系统的资源。如果用户决定打印另一个文档,那么DLL就可以根据用户的要求再次加载,若要卸载延迟加载的DLL,必须执行两项操:

●首先,当创建可执行文件时,必须设定另一个链接程序开关(/delay:unload)。

●其次,必须修改源代码,并且在你想要卸载DLL时调用__FunloadDelayLoaded DLL函数。

/delay:unload 链接程序开关告诉链接程序将另一个节放入文件中。该节包含了你清除已经调用的函数时需要的信息,这样它们就可以再次调用__delayLoadHelper函数。

当调用__FunloadDelayLoaded DLL时,你将想要卸载的延迟加载的DLL的名字传递给它。该函数进入文件中的未卸载节,并清除DLL的所有函数地址,然后__FunloadDelayLoaded DLL调用FreeLibrary,以便卸载该DLL。 

●有关卸载延迟加载的 DLL 的重要说明:

可以在 MSVC include 目录中的文件 delayhlp.cpp 中找到 __FUnloadDelayLoadedDLL2 函数的实现。 有关详细信息,请参阅了解延迟加载帮助程序函数。

__FUnloadDelayLoadedDLL2 函数的 name 参数必须与导入库包含的内容完全匹配(包括大小写)。 (该字符串也位于映像中的导入表中。)可以使用 DUMPBIN /DEPENDENTS 查看导入库的内容。 如果首选不区分大小写的字符串匹配,可以更新 __FUnloadDelayLoadedDLL2 以使用其中一个不区分大小写的 CRT 字符串函数或使用 Windows API 调用。

 注意

下面要指出一些重要的问题。 

1.千万不要自己调用FreeLibrary,来卸载DLL,否则函数的地址将不会被清除,这样,当下次试图调用DLL中的函数时,就会导致访问违规。手动卸载dll时,必须通过__FUnloadDelayLoadedDLL2(“xxx.dll”)卸载,不能使用FreeLibray()。

2.当调用__FunloadDelayLoaded DLL时,传递的DLL名字不应该包含路径,名字中的字母必须与你将DLL名字传递给/DelayLoad链接程序开关时使用的字母大小写相同,否则,__FUnloadDelayLoaded DLL的调用将会失败。

3.如果永远不打算卸载延迟加载的DLL,那么请不要设 定/delay:unload链接程序开关,并且你的可执行文件的长度应该比较小。

4.如果你不从用/delay:unload开关创建的模块中调用__FunloadDelayLoaded DLL,那么什么也不会发生,__FunloadDelayLoaded DLL什么操作也不执行,它将返回FALSE。 

5.延迟加载的DLL具备的另一个特性是,按照默认设置,调用的函数可以与一些内存地址相链接,在这些内存地址上,系统认为函数将位于一个进程的地址中。由于创建可链接的延迟加载的DLL节会使你的可执行文件变得比较大,因此链接程序也支持一个/Delay:nobind开关。因为人们通常都喜欢进行链接,因此大多数应用程序不应该使用这个链接开关。

6.延迟加载的DLL的最后一个特性是供高级用户使用的,它真正显示了Microsoft的注意力之 所在。当__delayLoadHelper函数执行时,它可以调用你提供的挂钩函数。这些函数将接收__delayLoadHelper函数的进度通知和错误通知。此外,这些函数可以重载DLL如何加载的方法以及如何获取函数的虚拟内存地址的方法。 

4.3.3 实例分析

在延迟加载版本1中,我们使用的是IAT(Import Address Table)来实现延迟加载。在编译和链接阶段,编译器和链接器会自动生成IAT,并将外部函数的地址填充到IAT中。在程序运行时,当第一次调用外部函数时,会触发操作系统加载和绑定外部函数,并将其更新到IAT中。

实验三十一:延迟加载版本1示例

       以下是一个使用延迟加载版本1的C语言示例代码:

dll.h

#pragma once

#include <windows.h>

#ifdef _cplusplus //如果C++模式编译

    #ifdef API_EXPORT

        #define EXPORT   extern "C" __declspec(dllexport)

    #else

        #define EXPORT    extern "C" __declspec(dllimport

    #endif

#else

    #ifdef API_EXPORT

        #define EXPORT   __declspec(dllexport

    #else

        #define EXPORT   __declspec(dllimport

    #endif

#endif

EXPORT void  CALLBACK ExampleFunction();

dll.c

/*------------------------------------------------------------------------

 FileName:dll.c

 实验31:延迟加载版本1示例(DLL)

 (c) bcdaren, 2024

-----------------------------------------------------------------------*/

#include <Windows.h>

#include <stdio.h>

#define API_EXPORT

#include "dll.h"

//入口和退出点

int WINAPI DllMain(HINSTANCE hInstance, DWORD fdwReason, PVOID pvReserved)

{

    return TRUE;

}

EXPORT void CALLBACK ExampleFunction()

{

    printf("DLL was loaded——delayload1.c!\n");

    return ;

}

delayload1.c

/*------------------------------------------------------------------------

 FileName:delayload1.c

 实验31:延迟加载版本1示例(可执行文件)

 (c) bcdaren, 2024

-----------------------------------------------------------------------*/

// link with /link /DELAYLOAD:DLL.dll /DELAY:UNLOAD

#include <windows.h>

#include <delayimp.h>

#include <stdio.h>

#include "dll.h"

#pragma comment(lib, "delayimp")

#pragma comment(lib,"DLL")

#pragma comment(linker, "/delay:unload")

#pragma comment(linker, "/Lib:Delayimp.lib")

// 使用外部函数

void UseExampleFunction()

{

    // 调用外部函数

    ExampleFunction();

}

int main()

{

    BOOL TestReturn;

    // 调用外部函数DLL.DLL will load at this point

    UseExampleFunction();

    //显式卸载MyDLL.dll will unload at this point

    TestReturn = __FUnloadDelayLoadedDLL2("DLL.dll");

    if (TestReturn)

        printf_s("\nDLL was unloaded\n");

    else

        printf_s("\nDLL was not unloaded\n");

    return 0;

}

延迟加载DLL配置方法一:

在解决方案的该项目“属性”->“配置属性”->“链接器”->“输入”->“延迟加载的Dll”, 写入DLL名。需要注意的是扩展名是 dll 不是 lib。

在解决方案的该项目“属性”->“配置属性”->“链接器”->“高级”->“卸载延迟加载的Dll”, 设置为”是”。如图4-6所示。

图4-6 延迟加载DLL配置属性

延迟加载DLL配置方法二:

       直接在代码中写入链接配置选项

#pragma comment(linker, "/delay:unload")

#pragma comment(linker, "/Lib:Delayimp.lib")

总结

在上述示例代码中,我们创建了一个DLL.dll动态链接库,包含了一个外部函数ExampleFunction。

然后,在UseExampleFunction函数中,我们调用外部函数ExampleFunction。

在main函数中,我们调用UseExampleFunction函数来使用外部函数。在第一次调用ExampleFunction时,操作系统将加载动态链接库DLL.dll,并加载和绑定外部函数,并将其更新到IAT中。

在延迟加载版本1中,使用的是IAT(Import Address Table)来实现延迟加载,操作系统会自动创建和维护IAT。

最后使用显式的方法调用__FUnloadDelayLoadedDLL2函数手动卸载DLL.dll动态链接库。

 注意

在VS编译环境中,真实的情况是,只有Debug版本才支持延迟加载DLL,而Release版本自定优化掉了延迟加载,仍然是在加载PE到内存的时刻加载了所有DLL动态链接库。

下面是Debug版本的PE文件静态分析。

将DelayLoad1_debug.exe拖入WinHex,找到数据目录项的第13项,如下所示:

00000160   00 00 00 00 10 00 00 00  00 00 00 00 00 00 00 00   ................

00000170   CC B1 01 00 50 00 00 00  00 F0 01 00 3C 04 00 00   瘫..P....?.<...

00000180   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

00000190   00 00 02 00 30 04 00 00  60 86 01 00 38 00 00 00   ....0...`?.8...

000001A0   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

000001B0   00 00 00 00 00 00 00 00  E0 7B 01 00 40 00 00 00   ........鄘..@...

000001C0   00 00 00 00 00 00 00 00  00 B0 01 00 CC 01 00 00   .........?.?..

000001D0   00 C0 01 00 40 00 00 00  00 00 00 00 00 00 00 00   .?.@...........

000001E0   00 00 00 00 00 00 00 00  2E 74 65 78 74 62 73 73   .........textbss

延迟导入表的RVA:0001C000H,大小为40H。位于.didat节区,FOA文件偏移地址为00009400H。

00009400   01 00 00 00 D0 7B 01 00  5C A1 01 00 70 C0 01 00   ....衶..\?.p?.

00009410   40 C0 01 00 B8 C1 01 00  C0 C2 01 00 00 00 00 00   @?.噶..缆......

00009420   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

00009430   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

00009440   A0 C0 01 00 00 00 00 00  00 00 00 00 00 00 00 00   犂..............

00009450   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

00009460   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

00009470   45 11 41 00 00 00 00 00  00 00 00 00 00 00 00 00   E.A.............

00009480   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

00009490   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

000094A0   00 00 5F 45 78 61 6D 70  6C 65 46 75 6E 63 74 69   .._ExampleFuncti

000094B0   6F 6E 40 30 00 00 00 00  00 00 00 00 00 00 00 00   on@0............

000094C0   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ................

typedef struct _IMAGE_DELAYLOAD_DESCRIPTOR {

    union {

        DWORD AllAttributes;

        struct {

            DWORD RvaBased : 1; // 如果为1,表示延迟加载版本2的thunk

            DWORD ReservedAttributes : 31; // 保留字段

        } DUMMYSTRUCTNAME;

    } Attributes;

    DWORD DllNameRVA; // 目标库名称的RVA:00017BD0H

    DWORD ModuleHandleRVA; // HMODULE的缓存位置的RVA:0001A15CH

DWORD ImportAddressTableRVA; // IAT起始位置的RVA:0001C070H

// 名称表起始位置的RVA,对应PIMAGE_THUNK_DATA::AddressOfData

    DWORD ImportNameTableRVA; 延迟加载导入名称表的RVA:0001C040H

    DWORD BoundImportAddressTableRVA; // 可选的绑定IAT的RVA:0001C1B8H

    DWORD UnloadInformationTableRVA; // 可选的卸载信息表的RVA:0001C2C0H

    DWORD TimeDateStamp; // 如果未绑定,则为0;否则为目标DLL的日期/时间戳

} IMAGE_DELAYLOAD_DESCRIPTOR, *PIMAGE_DELAYLOAD_DESCRIPTOR;

IAT起始位置的RVA:0001C070H对应的FOA地址为:00009470

ImportNameTableRVA; 0001C040H对应的FOA地址为:00009440

目标库名称的RVA:00017BD0H,对应的FOA地址为00006FD0,如下所示:

00006FD0   44 4C 4C 2E 64 6C 6C 00  00 00 00 00 00 00 00 00   DLL.dll.........

将使用OD调试器打开DelayLoad1_debug.exe,单步跟踪到DLL.dll中的ExampleFunction函数地址处,如图4-7所示。0x30C070即IAT表起始位置,存储的函数地址00411145H被替换为真实的函数地址0FEA1050H

图4-7 外部导入函数的跳转过程

 注意

延迟加载 DLL 导入有几个限制。

1.不支持数据导入。 解决方法是使用 LoadLibrary(或者在知道延迟加载帮助程序已加载 DLL 后使用 GetModuleHandle)和 GetProcAddress 自行显式处理数据导入。

2.不支持延迟加载 Kernel32.dll。 必须加载此 DLL 才能使延迟加载帮助程序例程正常工作。

3.不支持绑定转发的入口点。

4.如果 DLL 加载延迟,而不是在启动时加载,则进程可能会有不同的行为。 你可以看到在延迟加载 DLL 的入口点中是否存在按进程的初始化。 其他情况包括静态 TLS(线程本地存储),它使用通过 LoadLibrary 加载 DLL 时不处理的 __declspec(thread) 来声明。 使用 TlsAlloc、TlsFree、TlsGetValue 和 TlsSetValue 的动态 TLS 仍可在静态或者延迟加载的 DLL 中使用。

5.初次调用每个函数后,应将静态全局函数指针重新初始化为导入的函数。 这是必需的,因为第一次使用函数指针会指向 thunk,而不是加载的函数。

6.目前还没有办法在使用正常导入机制时,只延迟加载 DLL 中的特定过程。

7.不支持自定义调用约定(例如在 x86 体系结构上使用条件代码)。 此外,任何平台上都不保存浮点寄存器。 如果自定义帮助程序例程或挂钩例程使用浮点类型,请注意:这些例程必须在具有浮点参数的寄存器调用约定的计算机上保存和恢复完整浮点状态。 延迟加载 CRT DLL 时请小心谨慎,尤其是在帮助程序函数中调用 CRT 函数的情况,这些 CRT 函数采用数值数据处理器 (NDP) 堆栈上的浮点参数。

实验三十二:延迟加载版本2示例

       以下是一个使用延迟加载版本2的C语言示例代码:

dll2.h:

#pragma once

#include <windows.h>

#ifdef _cplusplus //如果C++模式编译

#ifdef API_EXPORT

#define EXPORT   extern "C" __declspec(dllexport)  

#else

#define EXPORT    extern "C" __declspec(dllimport

#endif

#else

#ifdef API_EXPORT

#define EXPORT   __declspec(dllexport

#else

#define EXPORT   __declspec(dllimport

#endif

#endif

EXPORT void CALLBACK MyFunction();

DLL2.c:

/*------------------------------------------------------------------------

 FileName:DLL2.c

 实验31:延迟加载版本1示例(DLL)

 (c) bcdaren, 2024

-----------------------------------------------------------------------*/

#include <Windows.h>

#include <stdio.h>

#define API_EXPORT

#include "dll2.h"

//入口和退出点

int WINAPI DllMain(HINSTANCE hInstance, DWORD fdwReason, PVOID pvReserved)

{

    return TRUE;

}

EXPORT void CALLBACK  MyFunction()

{

    printf("DLL was loaded——delayload1.c!\n");

    return ;

}

delayload2.c

/*------------------------------------------------------------------------

 FileName:delayload2.c

 实验32:延迟加载版本2示例(可执行文件)

 (c) bcdaren, 2024

-----------------------------------------------------------------------*/

#include <windows.h>

#include <delayimp.h>

#include <stdio.h>

#pragma comment(linker, "/DelayLoad:DLL2.dll")

#pragma comment(linker, "/delay:unload")

#pragma comment(linker, "/Lib:Delayimp.lib")

// 定义回调函数类型

typedef FARPROC(WINAPI *PfnDliHook)(unsigned int dliNotify, PDelayLoadInfo pdli);

// 定义延迟加载DLL的回调函数

FARPROC WINAPI DelayLoadFailureHook(unsigned int dliNotify, PDelayLoadInfo pdli)

{

    // 检查通知类型

    if (dliNotify == dliFailLoadLib)

    {

        // 如果是无法加载DLL的通知,则可以在此处进行处理

        // 例如,输出错误信息或采取其他适当的操作

        printf("Failed to load DLL: %s\n", pdli->szDll);

    }

    else if (dliNotify == dliFailGetProc)

    {

        // 如果是无法获取DLL导出函数的通知,则可以在此处进行处理

        // 例如,输出错误信息或采取其他适当的操作

        printf("Failed to get function: %s\n", pdli->dlp.szProcName);

    }

    // 返回一个替代函数或NULL

    // 如果返回一个替代函数,可以在此处实现替代函数的逻辑

    // 如果返回NULL,则表示继续使用缺失的函数将导致运行时错误

    return NULL;

}

// 定义一个延迟加载函数指针

typedef void(*MyFunctionPtr)();

volatile MyFunctionPtr myFunction;

// 定义一个全局变量保存回调函数指针

extern PfnDliHook __pfnDliNotifyHook2 = DelayLoadFailureHook;

int main()

{

    // 延迟加载DLL并获取函数指针-VS2017无法实现(被优化掉了)

    //MyFunctionPtr myFunction = (MyFunctionPtr)GetProcAddress(GetModuleHandle(NULL), "_MyFunction@0");

    HMODULE hModule =

LoadLibrary(TEXT("D:\\code\\winpe\\ch04\\DLL2\\DLL2.dll"));

    myFunction = (MyFunctionPtr)GetProcAddress(hModule, "_MyFunction@0");

    // 检查函数指针是否有效

    if (myFunction != NULL)

    {

        // 调用延迟加载的函数

        myFunction();

    }

    else

    {

        // 处理函数加载失败的情况

        printf("Failed to get function pointer.\n");

    }

    return 0;

}

 

总结

在上述示例中,我们首先定义了一个延迟加载DLL的回调函数DelayLoadFailureHook,用于处理无法加载DLL或无法获取导出函数的情况。在回调函数中,我们可以输出相应的错误信息或采取其他适当的操作。

然后,我们定义了一个延迟加载函数指针MyFunctionPtr,用于指向延迟加载的函数。

在main函数中,我们设置了延迟加载版本2的回调函数__pfnDliFailureHook2为我们定义的回调函数DelayLoadFailureHook。

接下来,我们使用GetProcAddress函数延迟加载DLL并获取函数指针。如果成功获取函数指针,我们可以调用延迟加载的函数;如果获取函数指针失败,我们可以处理函数加载失败的情况。

       【注意】VS编译环境中虽然可以设置延迟加载DLL,但是编译时自动优化掉了延迟加载,导致延迟加载失败。替代的方法就是直接使用LoadLibrary函数手动加载DLL。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值