添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
Publish Date:
Update Date:
2024-06-19 18:00
Creator:
Emacs 30.0.50 ( Org mode 9.6.15)
License:
This work is licensed under CC BY-SA 4.0
  • 如果同名的 DLL 已经载入到内存中,不论它在哪个目录,系统只会在检查已载入的 DLL 之前检查重定向。系统不会搜索该 DLL。
  • 如果 DLL 存在于应用程序运行的 Windows 版本的 已知 DLL 列表中,系统会使用已知 DLL 的副本而不是寻找 DLL。
  • 如果一个 DLL 存在依赖,系统会搜索它依赖的 DLL,并假设它们只有名字信息(没有路径信息)。即使第一个 DLL 以指定的绝对路径载入,其余依赖的 DLL 也只会使用名字进行搜索。
  • DLL 减少了代码的重复并节约了存储空间,一个 DLL 可以被多个应用程序使用,这些程序共享一个 DLL,不像静态库那样存在于每个应用程序内。
  • 在相同基地址使用相同 DLL 的多个进程在物理内存中共享 DLL 的一个副本。这样就节约了内存。
  • 当一个 DLL 中的函数改变时,只要函数的参数,调用约定(calling conventions)和返回值不发生变化的话,使用这个函数的应用程序就不必重新编译或重新链接。与之相比,函数发生变化时,使用静态链接的应用程序需要被重新链接。
  • 使用 不同编程语言 编写的程序可以调用 DLL 中的函数,只要它遵守与 DLL 中的函数相同的调用约定。 调用约定 决定了参数的压栈顺序,函数是否负责清理调用栈,和参数是否传递给寄存器。
  • 使用 DLL 的程序不是自洽(self-contained)的,它对与它本身分离的 DLL 模块存在依赖。如果载入时的动态链接没有找到它所需要的 DLL,系统会终止进程,并向用户发送错误信息。系统不会终止运行时的动态链接时的情况(没有在载入时检查 DLL 是否载入),但缺失 DLL 的导出函数对使用它的程序是不可用的。
  • 运行时载入动态库库会花费一定时间,所以比静态库的执行速度慢一点(差异很小)。
  • 一个进程载入了 DLL。对于使用载入时链接的进程,DLL 在进程初始化时就被载入。对于使用运行时链接的进程,DLL 的载入发生在 LoadLibrary LoadLibraryEx 返回之前。
  • 一个进程卸载了 DLL。当进程终止时,或调用 FreeLibrary 且引用计数归零时,DLL 会被卸载。如果进程是因为 TerminateProcess TerminateThread 而终止,系统则不会调用 DLL 的入口点函数。
  • 在进程中创建的新线程载入了 DLL。你可以在创建线程时使用 DisableThreadLibraryCalls 来使其无效化。
  • 一个载入了 DLL 的进程的线程正常终止了。对整个进程,入口点函数只会被调用一次,而不是对每个存在于进程中的线程。同样的,可以使用 DisableThreadLibraryCalls 来使其无效化。
  • class __declspec(dllexport) CExamplaExport : public CObject
    { ... class definition ... };
    创建 DLL 时,一般会创建一个包含导出函数的函数原型和导出的类的头文件(用于 DLL 的构建),并将在这些声明中加上 __declspec(dllexport) 。为了让代码的可读性更强,可以为 __declspec(dllexport) 定义一个宏,并使用这个宏来标注导出函数。
    #define DllExport __declspec(dllexport)
    4.2.1.1. 导出 C++ 函数供 C 程序使用
    如果你想在 C 代码中使用由 C++ 编写的 DLL,你就应该将这些函数声明为 C 的链接(linkage)而不是 C++ 的。如果没有指定的话,C++ 编译器会使用 C++ 的类型安全命名(也叫做名字修饰)和 C++ 的调用约定,这使得 C 很难对其进行调用。 要指定 C 链接,在函数声明中使用 extern "C" ,例如:
    extern "C" __declspec(dllexport) int MyFunc(long parm1);
    4.2.1.2. 导出 C 函数供 C/C++ 程序使用
    如果你想要在 C 或 C++ 中调用由 C 编写的 DLL,你需要使用 __cpluscplus 预处理宏来确定在使用哪种语言。如果正在使用 C++ 语言,应该将这些函数声明为 C 链接。如果你这样做了并为你的 DLL 提供了客户程序使用的头文件,这些函数可以不加改变地在 C 和 C++ 中使用。
    //MyFunc.h
    #ifdef __cplusplus
    extern "C" { //only need to export C interface if
                 // used by C++ source code
    #endif
    __declspec(dllimport) void MyCFunc();
    __declspec(dllimport) void AnotherCFunc();
    #ifdef __cplusplus
    #endif
    (dllimport 的使用对于函数声明不是必要的,但是这样做可以让编译器生成更好的代码) (此处的头文件仅作为头文件提供给其它模块使用,不参与编译,函数定义处应使用 dllexport) 如果你需要将 C 函数供 C++ 程序使用,而函数声明头文件又没有向上面那么做,那么可以这样做来避免 C++ 编译器对 C 函数名进行装饰:
    extern "C" {
    #include "MyCHeader.h"
        #define CLASS_DECLSPEC __declspec(dllexport)
    #else
        #define CLASS_DECLSPEC __declspec(dllimport)
    #endif
    class CLASS_DECLSPEC CExampleA : public CObject
    { ... class definition ...};

    4.3.1. __decpspec(dllimport) 对函数调用的影响

    假设 func1 是一个 DLL 中的函数,与包含 main 函数的 .exe 文件分离。 不使用 __declspec(dllimport) ,考虑下面的代码
    int main(void)
        func1();
    编译器会生成类似下面的代码:
    
    call func1
    链接器会将它翻译成类似这个样子:
    call 0x40000000       ; The address of 'func1'
    如果 func1 存在于其它 DLL 中(而不是 .exe 中),链接器不能直接对其进行分析,因为它无法知道 func1 的地址。在 16 位环境中,链接器将地址添加在 .exe 文件的表中,载入器可以通过它在运行时使用正确的地址进行相应的修补。在 32 位或 64 位环境中,链接器会生成一个 thunk ,它知道地址。在 32 位环境中看起来就像这样:
    call 0x40000000: jmp DWORD PTR __imp_func1
    在这里, imp_func1 是 .exe 文件中的导入地址表中 func1 的地址。这样,链接器就可以识别所有地址。载入器(loader)只需在载入时更新 .exe 文件的导入地址表即可使程序正常运行。 因此,使用 __declspec(dllimport) 更好,因为如果不需要的话,链接器就不会生成一个 thunk。Thunk 让代码更多并可能会降低 cache 性能。如果你告诉编译器该函数在 DLL 中,它能为你产生一个间接调用。 使用下面的代码:
    __decpspec(dllimport) void func1(void);
    int main(void)
        func1();
    会生成这样的指令:
    
    call DWORD PTR __imp_func1
    现在,thunk 和 jmp 指令就不存在了,代码变得更小更快。 另一方面,对于 DLL 内的函数调用,不需要使用间接调用。你已经知道了函数的地址。在间接调用之前,载入和存储函数地址需要时间和空间,因此直接调用总是更小更快。你只有在从 DLL 外部调用 DLL 函数时使用 __declspec(dllimport) 。不要在构建 DLL 时在 DLL 内使用 __declspec(dllimport) 。 对数据而言,使用 _declspec(dllimport) 可以消除一层间接。当你从 DLL 中导出数据时,你仍然需要浏览整个导入地址表,这意味着你当你访问 DLL 导出的数据时,你不得不记住额外的间接层,执行额外的间接寻址,就像这样:
    //project.h
    #ifdef _DLL //if accessing the data from inside the DLL
        ULONG ulDataInDll;
    #else       //if accessing the data from outside the DLL
        ULONG *ulDataInDll;
    当你使用 __declspec(dllimport) 来标出数据时,编译器会自动为你生成间接代码。你不必担心是否需要解引用,可以直接使用变量而不是指针。 不要在构建 DLL 时使用 __declspec(dllimport) ,在 DLL 内的函数不需要使用导入地址表来访问数据对象。 #ifdef DLL_BUILD //use macro to detect dll or client program #define DECL __declspec(dllexport) #else #define DECL __declspec(dllimport) #endif DECL int yyzero; DECL int add(int, int); DECL int multi(int, int); #ifdef __cplusplus #endif #endif //correspond to YYDLL_INCLUDE
    编写源文件时,可以使用 DEF 文件。下面的源文件是不使用 DEF 文件的情况。
    //yydll.c
    #include "yydll.h"
    // inner data and function
    int zero_in = 0;
    int add_in(int a, int b)
        return a + b;
    //exprot data and functions
    DECL int yyzero = 0;
    DECL int add(int a, int b)
        return a + b;
    DECL int multi(int a, int b)
        if (a == zero_in)
            return 0;
            return add_in(b, multi(a - 1, b));
    将 yydll.cyydll.h 放在同一目录下,就可以开始编译了。
    如果使用 MSVC 编译器,首先运行 vsdevcmd.bat 并 cd 到源代码目录,再使用如下命令:
    
    cl /LD /D DLL_BUILD yydll.c
    即可得到 yydll.lib 和 yydll.dll。 如果使用 MINGW 下的 gcc 编译器,则使用如下命令:
    gcc -c -D DLL_BUILD yydll.c
    gcc -shared -o yydll.dll yydll.o -Wl,--out-implib,yydll_dll.a
    即可得到 yylib.dll 和 yydll_dll.a 文件。.a 文件对应于 MSVC 中的 .lib 导入库。 接下来编写 main.c
    //main.c
    #include "yydll.h"
    #include <stdio.h>
    int main(void)
        int a = 2;
        int b = 31;
        int c = 1847;
        if ((a != yyzero) && (b != yyzero) && (c != yyzero))
            printf("all not zero\n");
        printf("%d\n", add(a, add(b, c)));
        printf("%d\n", multi(a, multi(b, c)));
        return 0;
    使用 MSVC 的话,使用如下命令生成 .exe 文件
    
    cl main.c yydll.lib
    若使用 gcc ,则使用如下命令,(因为没有使用 gcc 的库命名规范,所以不能使用 -l 的方法。)
    gcc -o main.exe main.c yydll_dll.a
    得到 main.exe 并运行,可以看到:
    all not zero
    114514
    如果将 yydll.dll 移走,运行时会直接弹窗,显示 dll 未找到。 若要在运行时进行库的载入,main 函数应改为:
    //main.c
    #include<stdio.h>
    #include<windows.h>
    typedef int (*myfun)(int, int);
    int main(void)
        HINSTANCE hinstLib;
        myfun myadd = NULL;
        myfun mymulti = NULL;
        int * pyyzero = NULL;
        BOOL fFreeResult, fRuntimeLinkSuccess = FALSE;
        int a = 2;
        int b = 31;
        int c = 1847;
        hinstLib = LoadLibrary(TEXT("yydll.dll"));
        if (hinstLib != NULL)
            myadd = (myfun)GetProcAddress(hinstLib, "add");
            mymulti = (myfun)GetProcAddress(hinstLib, "multi");
            pyyzero = (int*)GetProcAddress(hinstLib, "yyzero");
            if (myadd != NULL && mymulti != NULL && pyyzero != NULL);
                fRuntimeLinkSuccess = TRUE;
                if ((a != *pyyzero) && (b != *pyyzero) && (c != *pyyzero))
                    printf("all not zero\n");
                printf("%d\n", myadd(a, myadd(b, c)));
                printf("%d\n", mymulti(a, mymulti(b, c)));
            fFreeResult = FreeLibrary(hinstLib);
        if (!fRuntimeLinkSuccess)
            printf("link failed");
        return 0;
    生成 .exe 的 MSVC 和 gcc 命令分别是:
    
    cl main.c
    gcc -o main.exe main.c
    运行之,与使用导入库的结果一致。如果把 yydll.dll 从当前目录移走的话,程序会输出 link failed。 这里没有使用 DEF 文件,想要了解 DEF 文件的用法,可以参考文档:Module-Definition (.Def) Files 若要在 Visual Studio 中创建并使用 DLL,可以参考微软官方文档:Walkthrough: Create and use your own Dynamic Link Library (C++)
  • Programming Windows —— Charles Petzold (Windows 程序设计)
  • Dynamic-Link Libraries:https://docs.microsoft.com/en-us/windows/win32/dlls/dynamic-link-libraries
  • Building C/C++ DLLs in Visual Studio:https://docs.microsoft.com/en-us/cpp/build/dlls-in-visual-cpp?view=vs-2019
  •