不可不知的动态库

windows下有.lib/.dll文件,linux下有.a/.so文件,mac下还有别出心裁的.dylib文件,这些都是干嘛的?又是怎么生成的?

动态库和静态库

静态库:在链接步骤中,连接器将从库文件取得所需的代码,复制到生成的可执行文件中,这种库称为静态库,其特点是可执行文件中包含了库代码的一份完整拷贝;缺点就是被多次使用就会有多份冗余拷贝。即静态库中的指令都全部被直接包含在最终生成的可执行文件中了

动态库:动态链接库是一个包含可由多个程序同时使用的代码和数据的库。动态库不是可执行文件,动态链接提供了一种方法,使进程可以调用不属于其可执行代码的函数。函数的可执行代码位于一个库文件中,该文件包含一个或多个已被编译、链接并与使用它们的进程分开存储的函数。

mac

所谓dylib,就是bsd风格的动态库。基本可以认为等价于windows的dll和linux的so。mac基于bsd,所以也使用的是dylib。

在Mac OS X上.dylib.so之间的区别在于它们的编译方式。对于.so文件使用-shared;对于.dylib使用-dynamiclib。.so和.dylib都可以作为动态库文件互换,并且都具有DYLIB或BUNDLE的类型。

linux

linux静态库

Linux下的静态库以.a结尾

# 编译
gcc -c try1.c
gcc -c try2.c
ar cqs libtry.a try1.o try2.o(或 ar r libtry.a try1.o try2.o)

# 使用
gcc -c main.c -o main.o
gcc main.o -o name -L. -ltry (-l后面的名字就是我们上面生成的try库)

linux动态库

Linux下的动态库以.so 或 .so.y结尾,其中y代表版本号(Windows下为.dll),而且,Linux下的库必须以lib开头,用于系统识别(如:libjpeg.a libsdl.so)

gcc -shared -o libtry.so try.c

显示调用so

显式调用动态链接库必须包含动态链接库功能接口dlfcn.h(包含dl系列函数的声明)和被调函数的声明。

看起来还比较简单,但是当你对动态链接库函数使用比较频繁的时候,就知道他的麻烦了,所以,不推崇。

const char *dlerror(void);

当动态链接库操作函数执行失败时,dlerror可以返回出错信息,为NULL时表示操作函数执行成功。

void *dlopen(const char *filename, int flag); 

成功则返回为void*的句柄。flag可以为RTLD_LAZY(表明在动态链接库的函数代码执行时解决);RTLD_NOW(表明在dlopen返回前就解决所有未定义的符号,一旦未解决,dlopen将返回错误)。filename为动态库路径。

void *dlsym(void *handle, char *symbol);   

dlsym根据动态链接库操作句柄(handle)与符号(实际上就是欲调用的函数名),返回符号对应的函数的执行代码地址。由此地址,可以带参数执行相应的函数。

int dlclose (void *handle); 

参数为动态链接库的句柄。

隐式调用so

所谓隐式调用,就是调用的时候直接使用动态库中的函数,并不区别对待。

但是在隐式调用的时候必须要让程序能找到你所调用的函数的所属动态库。

ld.so.conf是系统对动态链接库进行查找的路径配置文件,也就是说该文件是系统链接工具/usr/bin/ld查找动态链接库的地图

# more /etc/ld.so.conf  你会看到以下内容:   

/usr/kerberos/lib
/usr/X11R6/lib
/usr/lib/sane
/usr/lib/qt-3.1/lib
/usr/lib/mysql
/usr/lib/qt2/lib
/usr/local/lib
/usr/local/BerkeleyDB.4.3/lib

所以,要达到我们的目的有以下几种方法:

  • 将自己的动态链接库文件拷到以上路径的目录下cp libwx.so.1 /usr/local/lib
  • 将自己动态链接库文件的路径加入到该文件中vi /etc/ld.so.conf, 然后加入自己的路径(或pwd >>/etc/ld.so.conf)
  • 把当前路径加入环境变量LD_LIBRARY_PATH,其实就是/usr/bin/ld的环境变量export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
# 编译的时候:   
gcc -o try main.c libtry.so.1
# 如果没有让/usr/bin/ld知道你的动态链接库在哪,编译的时候就要告诉它:
gcc -o try main.c /root/libtry.so.1
# 或者
gcc -L/root/wx -o qqq main.c libmy.so.1
# -L指定动态链接库所在的目录,有时候用gcc还会碰到-I,-l,他们分别指定头文件的目录和所链接的动态链接库

ldd :用来查看可执行文件qqq的动态链接库的依赖关系

ldd try
libjpeg.so.62 => /usr/lib/libjpeg.so.62 (0x00867000)
libSDL-1.2.so.0 => /usr/lib/libSDL-1.2.so.0 (0x0638a000)
libc.so.6 => /lib/tls/libc.so.6 (0x0046d000)
libm.so.6 => /lib/tls/libm.so.6 (0x00598000)
libdl.so.2 => /lib/libdl.so.2 (0x005bd000)
libasound.so.2 => /lib/libasound.so.2 (0x062e1000)
libX11.so.6 => /usr/X11R6/lib/libX11.so.6 (0x005d5000)
libXext.so.6 => /usr/X11R6/lib/libXext.so.6 (0x0069e000)
libpthread.so.0 => /lib/tls/libpthread.so.0 (0x006ae000)

瘟到死

静态库生成lib文件,动态库生成dll和lib文件

静态库中的lib:该LIB包含函数代码本身(即包括函数的索引,也包括实现),在编译时直接将代码加入程序当中

动态库中的lib:该LIB包含了函数所在的DLL文件和文件中函数位置的信息(索引),函数实现代码由运行时加载在进程空间中的DLL提供

总之,lib是编译时用到的,dll是运行时用到的。如果要完成源代码的编译,只需要lib;如果要使动态链接的程序运行起来,只需要dll。

DLL编译方法

用于指定存储类信息的扩展属性语法使用 __declspec 关键字,该关键字指定要将给定类型的实例与下面列出的特定于 Microsoft 的存储类属性一起存储。 其他存储类修饰符的示例包括 static 和 extern 关键字。 但是,这些关键字是 C 和 C++ 语言的 ANSI 规范的一部分,因此,扩展属性语法并未涵盖这些关键字。 扩展特性语法简化并标准化了 Microsoft 专用的 C 和 C ++ 语言扩展。

//新建生成dll的工程时,定义了宏DLL_EXPORT,因此DLL_API 是 __declspec(dllexport),用来导出
//当我们调用dll时,由于没有定义DLL_EXPORT,所以DLL_API是__declspec(dllimport),用来导入
#ifdef DLL_EXPORTS
#define DLL_API __declspec(dllexport)
#else
#define DLL_API __declspec(dllimport)
#endif

// 导出类
class DLL_API Cdll {
public:
Cdll(void);
// TODO: 在此添加您的方法。
};

//导出变量,变量在.cpp文件中定义
extern DLL_API int ndll;

//导出函数,加extern "C",是为了保证编译时生成的函数名不变,这样动态调用dll时才能
//正确获取函数的地址
extern "C" DLL_API int fndll(void);

编译的时候不用def文件就可以不生成lib文件,只要头文件就可以。 当然加载的时候要用显式加载,不能用隐式加载了。

DLL隐式调用

隐式链接 需要.h文件,dll文件,lib文件

(1)将dll放到工程的工作目录

(2)设置项目属性--vc++目录--库目录为lib所在的路径

(3)将lib添加到项目属性--链接器--输入--附加依赖项,或者直接在源代码中加入#pragma comment(lib, “**.lib”)

(4)在源文件中添加.h头文件

然后就像平常一样调用普通函数、类、变量

WIN 下 mingw32/gcc, cygwin/gcc 都不用 .lib 就可以链接 dll。

DLL显示调用

显示链接只需要.dll文件,但是这种调用方式不能调用dll中的变量或者类(c和c++无所不能,奇技淫巧总是有的)

显示调用主要使用WIN32 API函数LoadLibrary、GetProcAddress,举例如下:

typedef int (*dllfun)(void);//定义指向dll中函数的函数指针

HINSTANCE hlib = LoadLibrary(".\\dll.dll");
if(!hlib)
{
std::cout<<"load dll error\n";
return -1;
}
dllfun fun = (dllfun)GetProcAddress(hlib,"fndll");
if(!fun)
{
std::cout<<"load fun error\n";
return -1;
}
fun();

DLL地狱

DLL是Microsoft的共享库实现。共享库允许将公共代码捆绑到包装器DLL中,并由系统上的任何应用程序软件使用,而无需将多个副本加载到内存中。一个简单的例子可能是GUI文本编辑器,它被许多程序广泛使用。通过将此代码放在DLL中,系统上的所有应用程序都可以使用它而无需使用更多内存。这与静态库形成对比,静态库功能相似,但将代码直接复制到应用程序中。在这种情况下,每个应用程序都会增加它使用的所有库的大小,对于现代程序来说这可能非常大。

当计算机上的DLL版本与创建程序时使用的版本不同时,会出现问题。 DLL没有用于向后兼容的内置机制,甚至对DLL的微小更改使其内部结构与以前的版本不同,尝试使用它们通常会导致应用程序崩溃。静态库避免了这个问题,因为用于构建应用程序的版本包含在其中,因此即使系统上的其他位置存在较新的版本,这也不会影响应用程序。

版本不兼容的一个关键原因是DLL文件的结构。该文件包含DLL中包含的各个方法(过程,例程等)的目录以及它们获取和返回的数据类型。即使对DLL代码进行微小的更改也可能导致重新排列此目录,在这种情况下,调用特定方法的应用程序认为它是目录中的第4项可能最终会调用完全不同且不兼容的例程,这会通常会导致应用程序崩溃。

DLL经常遇到一些问题,特别是在系统上安装和卸载了许多应用程序之后。困难包括DLL版本之间的冲突,难以获得所需的DLL,以及具有许多不必要的DLL副本。

DLL不兼容的原因

  • 内存限制,以及16位版本的Windows中缺少进程内存空间的分离;
  • 缺乏针对DLL的强制标准版本控制,命名和文件系统位置模式;
  • 缺乏用于软件安装和删除的强制标准方法(包管理);
  • 缺乏对DLL应用程序二进制接口管理和安全措施的集中权威支持,允许发布具有相同文件名和内部版本号的不兼容DLL;
  • 过度简化的管理工具,防止用户和管理员识别已更改或有问题的DLL;
  • 开发人员破坏共享模块中函数的向后兼容性;
  • Microsoft发布对操作系统运行时组件的带外更新;
  • 早期版本的Windows无法运行同一个库的并行冲突版本;
  • 依赖于当前目录或%PATH%环境变量,它们随时间和系统而变化,以查找相关的DLL(而不是从显式配置的目录中加载它们);
  • 开发人员将示例应用程序中的ClassID重用于其应用程序的COM接口,而不是生成自己的新GUID。

DLL Hell在Windows NT之前版本的Microsoft操作系统上是一种非常普遍的现象,主要原因是16位操作系统没有将进程限制到自己的内存空间,因此不允许它们加载自己的版本的他们兼容的共享模块。在覆盖现有系统DLL之前,应用程序安装程序应该是好公民并验证DLL版本信息。 Microsoft和其他第三方工具供应商提供了简化应用程序部署的标准工具(始终涉及运送相关的操作系统DLL)。在授予使用Microsoft徽标之前,Microsoft甚至要求应用程序供应商使用标准安装程序并使其安装程序经过认证,以使其正常工作。良好的公民安装方法并没有缓解这个问题,因为互联网普及的增加为获得不合格的应用程序提供了更多的机会。

静态链接

应用程序中DLL Hell的一个简单解决方案是静态链接所有库,即包含程序中所需的库版本,而不是选择具有指定名称的系统库。这在C / C ++应用程序中很常见,其中不必担心安装了哪个版本的MFC42.DLL,而是将应用程序编译为静态链接到相同的库。这完全消除了DLL,并且可以在仅使用提供静态选项的库的独立应用程序中实现,就像Microsoft基础类库一样。然而,牺牲了DLL的主要目的,即程序之间的运行时库共享以减少内存开销。在多个程序中复制库代码会导致软件膨胀,并使安全修复程序或更新版本的相关软件的部署变得复杂。

Windows文件保护

使用Windows文件保护(WFP)在Windows 2000中引入了DLL覆盖问题(微软称为DLL Stomping)。这可以防止未经授权的应用程序覆盖系统DLL,除非它们使用允许此操作的特定Windows API。可能仍存在Microsoft的更新与现有应用程序不兼容的风险,但通过使用并排程序集,当前版本的Windows通常会降低此风险。 第三方应用程序无法踩踏操作系统文件,除非它们将合法的Windows更新与其安装程序捆绑在一起,或者如果它们在安装期间禁用了Windows文件保护服务,并且在Windows Vista或更高版本中也取得系统文件的所有权并授予自己访问权限。 SFC实用程序可以随时还原这些更改。

同时运行冲突的DLL

这里的解决方案包括为磁盘和内存中的每个应用程序提供相同DLL的不同副本。 一个简单的手动解决方案是将问题DLL的不同版本放入应用程序的文件夹中,而不是通用的系统范围文件夹。只要应用程序是32位或64位,并且DLL不使用共享内存,这通常可以正常工作。对于16位应用程序,这两个应用程序不能在16位平台上同时执行,也不能在32位操作系统下的同一个16位虚拟机中同时执行。 OLE在Windows 98 SE / 2000之前阻止了这一点,因为早期版本的Windows为所有应用程序都有一个COM对象注册表。Windows 98 SE / 2000引入了一种称为并行程序集的解决方案,它为每个需要它们的应用程序加载单独的DLL副本(从而允许需要冲突的DLL的应用程序同时运行)。这种方法通过允许应用程序将模块的唯一版本加载到其地址空间中来消除冲突,同时保留通过使用内存映射技术在仍然执行的不同进程之间共享公共代码来共享应用程序之间的DLL(即减少内存使用)的主要好处使用相同的模块。然而,在多个进程之间使用共享数据的DLL不能采用这种方法。一个负面影响是DLL的孤立实例可能无法在自动化过程中更新

便携式应用

根据应用程序体系结构和运行时环境,可移植应用程序可能是减少某些DLL问题的有效方法,因为每个程序都捆绑了自己所需的任何DLL的私有副本。该机制依赖于应用程序在加载它们时没有完全限定依赖DLL的路径,并且操作系统在任何共享位置之前搜索可执行目录。然而,这种技术也可以被恶意软件利用,如果私有DLL没有以与共享DLL相同的方式保持最新的安全补丁,则增加的灵活性也可能以牺牲安全性为代价。