首页 > 代码库 > DLL 基础

DLL 基础

1. 在 可执行文件 能够调用 DLL 之前,必须先把 DLL 载入到进程的地址空间中。
    有两种载入 DLL 的方式: 隐式载入时链接、显式运行时链接
   
2. DLL 函数创建的任何对象都为调用线程或进程所有——DLL绝对不会拥有任何对象。
    举个例子,如果 DLL 中一个函数调用了 VirtualAlloc ,系统会从调用进程的地址空间中预定地址区域。如果稍后 DLL 从地址空间中撤销映射,VirtualAlloc 分配的地址区域并不会被释放。

3. 当一个 DLL 提供一个内存分配函数的时候,它必须同时提供一个用来释放内存的函数。
    这是因为 可执行文件 和 DLL 可能使用不同的 C 运行时库,比如 exe 和 dll 各自静态链接了不同的 C 运行时库的静态版本,那么 DLL 里面的 malloc 与 exe 里的 free 函数就可能不匹配。
   
4. DLL 编译完成后,会产生一个 .dll (动态链接库)文件和一个 .lib (导入库)文件
    .lib 中列出了 Dll 导出的符号
   
    可执行文件的构建:
    可执行文件执行链接时,链接器必须确定代码中引入的导入符号来自于哪个 DLL,不然的话,exe 运行起来的时候不知道去哪里找 dll 进行载入。
    所以必须把 dll 的 .lib 传给链接器。链接器会把 .lib 的信息写入到 .exe 里面,称为导入段。
    如果没有把 .lib 传给链接器的话,就会出现错误:
    error Link2091: 无法解析的外部符号 xxxFunc,因为链接器 不知道将来这个符号要从哪个 dll 里导入。
   
    可执行文件的执行:
    1. 分配地址空间;
    2. 解析 可执行文件 的导入段,了解有哪些 dll 需要被加载;
    3. 在各种路径中查找定位 要加载的 dll,如果无法定位到 dll 的话,将会出现“无法启动,因为计算机中缺失 xxx.dll”错误;
    4. 将 dll 映射到地址空间;
    5. 检查 可执行文件 的导入段与 dll 的导出段是否对应,有可能 dll 被替换后,dll 中没有 exe 需要的导入函数,就会出现这种情况。如果不对应,将出现 “程序入口点 xxxFunc 无法定位到动态链接库 xxx.dll 上”错误;

5. 显式运行时链接
    之前提过的“隐式载入时链接” DLL 的载入发生在可执行文件载入时;
    可执行文件载入时会把需要用到的 dll 全部载入地址空间;
    而 “显式运行时链接” 则是在程序运行时载入 dll,然后设法获取 dll 某个导出函数的地址,对其进行调用;
    因为是运行时载入,根据前面对 .lib(导入库) 的描述,.lib 是在链接时候用的,所以显式链接不需要 .lib 文件,也不会有导入段;
   
    显式载入 .dll 需要用到的函数:
    LoadLibrary(pszDllPathName);        // 载入 dll 到进程地址空间中
    FreeLibrary(hInstDll);              // 从进程地址空间中卸载 dll
    GetModuleHandle(pszModuleName);     // 可以用来检查一个 模块 是否被载入
    FARPROC GetProcAddress(hInstDll, pszSymbolName);    // 得到 dll 中的指定导出函数。
   
6. DllMain, dll 的入口点函数
    系统会在不同的时刻掉哟in个这个函数,这些调用是通知性质的,通常被 dll 用来执行一些与进程或线程有关的初始化和清理工作;
    DllMain(hInstDll, fdwReason, fImpload)
    {
        switch(fdwReason)
        {
        case: DLL_PROCESS_ATTACH:       // dll 被加载到进程地址空间
            break;
        case: DLL_PROCESS_DETACH:       // dll 从进程地址空间卸载
            break;
        case: DLL_THREAD_ATTACH:        // 新线程被创建,每个 dll 的 DLLMain 会收到这个消息,执行线程相关的初始化操作
            break;
        case: DLL_THREAD_DETACH:        // 有线程被销毁,每个 dll 的 DllMain 会收到这个消息
            break;
        }
    }
   
7. DllHell
    多个程序共享一个 .dll,如果某个程序升级时,把这个 .dll 也升级了(或者降级),另外的程序对升级后的 .dll 不能兼容,就会引发问题。
    假设有一个导出类 class D:
    class D
    {
    public:
        int GetInt() { return m_i; }
    private:
        int m_i;
    };
    后来对 .dll 升级了一下,给 class D 多加了一个成员 m_ii;
    class D
    {
    public:
        int GetInt() { return m_i; }
    private:
        int m_ii;
        int m_i;
    };   
    当在 exe 里调用 D.GetInt() 的时候,会发现升级后的返回值,不正常了。
    为啥呢? 首先在编译 exe 的时候,我们会先创建一个 D 的实例: D* d = new D: 在编译的时候, D 的内存大小就已经固定了。
    当调用 D.GetInt() 的时候,this 指针传给 GetInt, return m_i; 等价于 return this+2个偏移量,可实际上,分配的内存大小只有一个偏移量那么大。
   
    出现这种问题的原因:
    1. 类的大小变化;
    2. 类成员偏移地址变化;
    3. 虚函数顺序变化;
   
    避免这种问题的方法:
    1. 不直接生成类的实例。在 dll 中提供 GreateInstance 函数,并且让导出类 的构造函数为私有;
    2. 不直接访问成员变量,因为是通过偏移地址访问的;
    3. 不使用虚函数,如果升级 dll 的时候多加了一个虚函数,虚表长度就会发生变化;
   
    同时,维护一个有导出类的 dll 时,不要改动成员变量,不要改动虚函数;
    导出类的 dll 不要导出 函数 以外的任何内容!
   
    参考: blog.csdn.net/anycell/article/details/6924568
   
8. 静态库、DLL、导入库
    DLL 和 导入库 .lib 前面已经说明白了;
    静态库的后缀也是 .lib 并且跟 .lib 有一样的使用方法;
    不同的是,静态库最后会直接和 可执行文件 合并在一起,在链接的时候就合并;
    静态库其实就是一堆 .obj 文件的集合,跟 .exe 编译之前的 .cpp 没什么区别;
   
    所以,静态库不存在什么加载,导入的问题,编写时也和正常的 .h,.cpp 没什么区别:
    int foo()        // 不需要 __declspec 标识,跟平常 cpp 文件一致就行
    {
        return 0;
    }