《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。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/888581.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Llama系列上新多模态!3.2版本开源超闭源,还和Arm联手搞了手机优化版,Meta首款多模态Llama 3.2开源!1B羊驼宝宝,跑在手机上了

Llama系列上新多模态&#xff01;3.2版本开源超闭源&#xff0c;还和Arm联手搞了手机优化版&#xff0c;Meta首款多模态Llama 3.2开源&#xff01;1B羊驼宝宝&#xff0c;跑在手机上了&#xff01; 在多模态领域&#xff0c;开源模型也超闭源了&#xff01; 就在刚刚结束的Met…

VSCode运行QT界面

VSCode用久了,感觉Qt Creator的写起代码来还是不如VSCode得心应手,虽然目前还是存在一些问题,先把目前实现的状况做个记录,后续有机会再进一步优化。 当前方式 通过QtCreator创建一个CMake项目,然后使用CMake的方式在VSCode中进行编译。 claude给出的建议 左上角的名字会…

C++ 算法学习——1.6 前缀和与二维前缀和算法

前缀和算法&#xff08;Prefix Sum Algorithm&#xff09;&#xff1a; 概念&#xff1a;前缀和算法通过在遍历数组时计算前缀和&#xff08;从数组的第一个元素开始累加到当前元素的和&#xff09;&#xff0c;可以在O(1)时间内得到任意区间的子数组和&#xff0c;而不需要重复…

详解 PDF 转 JPG:简单操作,高效转换

如今&#xff0c;众多软件都已具备将PDF转换为JPG的功能&#xff0c;所以pdf怎么转换成jpg图片已经不难解决了吧。接下来&#xff0c;我想分享几款依然保存在我电脑中&#xff0c;且非常实用的PDF转JPG工具给大家。 1.福昕PDF转换大师 链接一下>>https://www.pdf365.cn…

【2024年10月测试通过】conda下使用虚拟环境安装最新版pytorch2.4+cuda12.4

开头先说重点&#xff1a; 1.采用conda的虚拟环境&#xff0c;会在沙盒环境下安装好所有所需包&#xff0c;而且该虚拟环境拷贝给其他人员可以直接用&#xff0c;很方便。 2.pytorch官网访问不了&#xff0c;有一个国内镜像推荐&#xff0c;地址为PyTorch - PyTorch 中文 3.…

OXO:一款针对Orchestration框架的安全扫描引擎

关于OXO OXO是一款针对Orchestration框架的安全扫描引擎&#xff0c;该工具可以帮助广大研究人员检测Orchestration安全问题&#xff0c;并执行网络侦查、 枚举和指纹识别等操作。 值得一提的是&#xff0c;OXO还提供了数十种其他的协同工具&#xff0c;包括网络扫描代理&…

erlang学习:Linux命令学习10

从百度网盘下载文件 共享百度网盘获得链接 https://pan.baidu.com/s/1iUOTAWr1SRlL2fBZ7lIV拿到链接之后在浏览器中进行下载&#xff0c;可以查看下载链接 右键这些文件即可得到下载链接 类似于长这样 https://bdbl-cm01.baidupcs.com/file/b02f72906b3d0d07130be625eabc76…

出海快报 | “三消+短剧”手游横空出世,黄油相机“出圈”日本市场,从Q1看日本手游市场趋势和机会

编者按&#xff1a;TopOn出海快报栏目为互联网出海从业者梳理出海热点&#xff0c;供大家了解行业最新发展态势。 1.“三消短剧”横空出世&#xff0c;融合创新手游表现亮眼 随着竞争的加剧&#xff0c;新产品想要突出重围&#xff0c;只能在游戏中加入额外的元素。第一次打开…

java复制查询数组-cnblog

java数组 复制数组 copyOf(待复制数组,复制后新数组的长度) 如果复制后数组的长度&#xff0c;长于原来数组&#xff0c;多出来的元素会被补0&#xff0c;如果新数组元素少会从第一个元素&#xff0c;取到指定元素长度 package nb;import java.util.Arrays;public class co…

行业预测 60TB 硬盘将于 2028 年到来

在硬盘容量增长停滞了一段时间后&#xff0c;在短短四年内从目前的 30TB 增长到 60TB 将是一个巨大的增长。 然而&#xff0c;这正是 IEEE 最新发布的《海量数据存储设备和系统国际路线图》报告所预测的。 该路线图预计 2028 年市场上将出现 60TB 的硬盘驱动器。 这一增长将由一…

Flet介绍:平替PyQt的好用跨平台Python UI框架

随着Python在各个领域的广泛应用&#xff0c;特别是在数据科学和Web开发领域&#xff0c;对于一个简单易用且功能强大的用户界面&#xff08;UI&#xff09;开发工具的需求日益增长。传统的Python GUI库如Tkinter、PyQt虽然功能强大&#xff0c;但在易用性和现代感方面略显不足…

计算机毕业设计 | SpringBoot 房屋租赁网 租房买房卖房平台(附源码)

1&#xff0c;绪论 1.1 背景调研 在房地产行业持续火热的当今环境下&#xff0c;房地产行业和互联网行业协同发展&#xff0c;互相促进融合已经成为一种趋势和潮流。本项目实现了在线房产平台的功能&#xff0c;多种技术的灵活运用使得项目具备很好的用户体验感。 这个项目的…

Authentication Lab | Client Side Auth

关注这个靶场的其它相关笔记&#xff1a;Authentication Lab —— 靶场笔记合集-CSDN博客 0x01&#xff1a;Client Side Auth 前情提要 有些时候&#xff0c;开发人员会将身份验证的逻辑写于前端&#xff0c;这样写是十分不安全的&#xff0c;因为前端的代码几乎全部都是可见的…

C#多线程基本使用和探讨

线程是并发编程的基础概念之一。在现代应用程序中&#xff0c;我们通常需要执行多个任务并行处理&#xff0c;以提高性能。C# 提供了多种并发编程工具&#xff0c;如Thread、Task、异步编程和Parallel等。 Thread 类 Thread 类是最基本的线程实现方法。使用Thread类&#xff0…

快递查询软件:实现单号识别与批量物流查询的高效工具

随着网络购物的普及&#xff0c;快递物流行业迎来了前所未有的发展机遇&#xff0c;同时也面临着巨大的挑战。跟踪物流信息成为一个难题&#xff0c;因此&#xff0c;快递查询软件的核心功能之一便是单号识别。传统的快递单号输入方式繁琐且易出错在此背景下&#xff0c;快递查…

代码随想录day22:回溯part4

491.递增子序列 class Solution {List<List<Integer>> result new ArrayList<>();List<Integer> path new ArrayList<>();public List<List<Integer>> findSubsequences(int[] nums) {backTracking(nums, 0);return result;}priv…

如何基于 RLHF 来优化 ChatGPT 类型的大语言模型

&#x1f6b4;前言 对于ChatGPT来说&#xff0c;RLHF是其训练的核心。所谓RLHF&#xff0c;即Reinforcement Learning with Human Feedback&#xff0c;基于人类反馈的强化学习。这项技术通过结合模型自身的生成能力和人类专家的反馈&#xff0c;为改进文本生成质量提供了新的…

计算机网络-------重传、TCP流量控制、拥塞控制

重传、滑动窗口、流量控制、拥塞避免 重传机制 超时重传 发送方在发送数据时会启动一个定时器&#xff0c;当超过指定的时间之后&#xff0c;还没接收到接收方的ACK确认应答报文&#xff0c;就会重传该数据 快重传 当发送方收到接收方三个连续的ack之后说明发送方发送的报…

关于Amazon Linux 2023的版本及包管理器

在亚马逊上创建EC2实例时&#xff0c;会看到有一个Amazon Linux镜像。 那这个镜像与其他Linux有什么关系和区别呢&#xff1f; 网站是介绍&#xff1a;Amazon Linux 2023 是基于 Linux 的现代化通用操作系统&#xff0c;提供 5 年的长期支持。它针对 AWS 进行了优化&#xff0…

《Linux从小白到高手》理论篇:深入理解Linux的计划任务/定时任务

值此国庆佳节&#xff0c;深宅家中&#xff0c;闲来无事&#xff0c;就多写几篇博文。本篇详细深入介绍Linux的计划任务/定时计划。 Linux的计划任务 在很多时候为了自动化管理系统&#xff0c;我们都会用到计划任务&#xff0c;比如关机&#xff0c;重启&#xff0c;备份之类…