CC++动态库崩溃处理方式

CC++动态库崩溃处理方式,第1张

Dll Hell产生原因

把Linux/Windows等 *** 作系统中共享库的版本兼容性困扰称为“Dll Hell”。产生的原因下面这种情况:比如,Windows的应用程序在发布release版本时会一次性将所有用到的Dll一起打包形成一个大的安装包,用户只需一键安装,无需关注具体Dll文件的配置问题,但是就是这种 *** 作导致某次安装将系统中已有的Dll文件换成其他版本的Dll文件,虽然被安装的程序可以运行,但是其他程序可能除问题。

再比如,AB两个人开发程序,A负责应用程序,B负责核心库,完成后将exe发送给用户。B修改某些内容后,重新编译Dll,将库发送给用户替换,那用户运行exe面临不兼容。

前人总结的经验,DLL HELL发生的三种主要场景:

1)使用旧版本的DLL替代了当前系统的新版本DLL,导致了其他程序无法正常运行;

2) 新版本DLL引入覆盖了旧版本DLL,因为新版本的DLL设计缺陷,没有做到完全的向后兼容;

3)新版本DLL本身就存在bug,导致程序运行崩溃,很少见。

解决Dll Hell的系统级方法

(1)静态连接:发布终极融合版本执行文件,从根本上杜绝了动态加载DLL的可能性,但也丧失了灵活升级的好处

(2)防止Dll覆盖:略,跟权限相关,大致就是限制第三方软件覆盖系统文件内的共享库。

(3)避免Dll冲突:解决不同应用程序依赖相同DLL不同版本的问题的一个方案是,让每个应用程序拥有一份自己依赖的DLL,并且把问题DLL的不同版本放到该应用程序的文件夹中,而不是系统DLL目录中,当应用程序需要安装DLL时候,首先从自己的文件夹下寻找所需要的DLL,然后再到系统文件夹中寻找。

(4).NET下DLL Hell的解决方案:详情略,与程序集清单manifest相关(Manifest文件描述了程序集的名字、版本号、本地文件资源以及程序集依赖的各种资源)。

(5)Linux共享库版本管理:为了解决这种共享库升级改动带来的代码管理困难,进行版本管理是好的解决方法。采用Libname.so.x.y.z方式命名共享库,x主版本号,y次版本号,z发布版本号。

Dll Hell本质原因

谈到共享库和DLL等库文件的兼容性,则不得不谈到ABI(相比于API对应于源代码级别的接口管理,二进制层次的ABI对应的则是 *** 作系统和底层机器码)对于不同的语言来说,主要包括一些诸如函数调用的堆栈结构、符号命名、参数规则、数据结构的内存分布等方面的规则。一般导致C语言的共享库ABI兼容性改变行为有4种:

1)导出函数的行为发生改变,即计算结果和之前产生的结果不一样;

2)导出函数被删除;

3)导出数据的结构发生变化,如共享库定义的结构体变量的结构发生改变:结构成员删除、顺序改变或其他引起结构体内存布局变化的行为(不过通常来讲,往结构体的尾部添加成员不会导致不兼容);

4)导出函数的接口发生变化,如函数返回值、参数被更改。

上面的说明非常抽象,可以用更本质的方式说明Dll和应用程序的调用关系,因此引发DLL Hell。应用程序在编译时就会确定调用Dll中的类的大小(用于申请空间)和类成员的偏移地址(用于访问),以及虚函数的顺序

解决Dll Hell的开发级方法

要想做一个可升级的DLL,必需避免以上三个问题。所以以下三点用来使DLL远离地狱。

1)不直接生成类的实例。对于类的大小,当我们定义一个类的实例,或使用new语句生成一个实例时,内存的大小是在编译时决定的。要使应用程序不依赖于类的大小,只有一个办法:应用程序不生成类的实例,使用DLL中的函数来生成。把导出类的构造函数定义为私有的(privated),在导出类中提供静态(static)成员函数(如NewInstance())用来生成类的实例。因为NewInstance()函数在新的DLL中会被重新编译,所以总能返回大小正确的实例内存。

2)不直接访问成员变量。应用程序直接访问类的成员变量时会用到该变量的偏移地址。所以避免偏移地址依赖的办法就是不要直接访问成员变量。把所有的成员变量的访问控制都定义为保护型(protected)以上的级别,并为需要访问的成员变量定义Get或Set方法。Get或Set方法在编译新DLL时会被重新编译,所以总能访问到正确的变量位置。

3)忘了虚函数,就算有也不要让应用程序直接访问它。因为类的构造函数已经是私有(privated)的了,所以应用程序也不会去继承这个类,也不会实现自己的多态。如果导出类的父类中有虚函数,或设计需要(如类工场之类的框架),一定要把这些函数声明为保护的(protected)以上的级别,并为应用程序重新设计调用该虑函数的成员函数。这一点也类似于对成员变量的处理。

如果导出的类能遵循以上三点,那么以后对DLL的升级将可以认为是安全的。

如果对一个已经存在的导出类的DLL进行维护,同样也要注意:不要改动所有的成员变量,包括导出类的父类,无论定义的顺序还是数量;不要动所有的虚函数,无论顺序还是数量。

总结起来,其实是一句话:导出类的DLL不要导出除了函数以外的任何内容。

终极武器:C语言共享库将保持ABI兼容,但是还有很多其他的因素都可能导致共享库的ABI兼容性丧失,比如不同版本的编译器、 *** 作系统和硬件平台等(使用不同版本的编译器或系统库可能会导致结构体的成员对齐方式不一致,从而导致ABI变化)。这也是为什么尽量不要使用C++编写共享库或DLL的原因,因为C++标准根本就没有规定ABI层级的标准,各家编译器都有自己的解读方式,所以往往使用C编写共享库更简单,而用C++编写则涉及到COM原理。

COM不是本文的重点,但是会稍微介绍一下。重点在于C语言的封装设计,让共享库避免Dll Hell。

C语言的封装设计 

在封装的设计下,代码上有了一些显著的变化:

1)数据结构的成员,对调用者不可见。调用者无法直接对数据结构的成员变量进行读写,只能通过特定的函数来 *** 作这些数据结构。

这些 *** 作函数是数据结构的设计者提供的,我们把这些 *** 作函数叫接口函数。

2)接口函数,不仅屏蔽掉数据结构的成员变量,还屏蔽掉数据结构之间的关系,甚至有些数据结构调用者根本就不知道其存在。

3)接口函数,是面向使用者进行设计,而非面向底层数据结构进行设计。对业务场景进行分析,提取共性,进行接口抽象,最终形成接口函数。

4)接口函数的函数名、参数类型和返回值,都要充分体现业务语义,屏蔽底层数据结构的具体实现细节。

核心是:向调用者隐藏数据结构的内部实现。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/1498724.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-06-25
下一篇 2022-06-25

发表评论

登录后才能评论

评论列表(0条)

保存