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;
}
原文地址:http://www.cnblogs.com/zuibunan/p/3880856.html