防止意外对象不兼容

Protection against accidental object incompatibility?

本文关键字:不兼容 对象 意外      更新时间:2023-10-16

TL;DR

防止由共享的、可能是模板化的标头的预处理器指令中的编译器参数拼写错误导致的二进制不兼容,这些指令控制不同编译单元中的条件编译?

前任。

g++ ... -DYOUR_NORMAl_FLAG ... -o libA.so
/**Another compilation unit, or even project. **/
g++ ... -DYOUR_NORMA1_FLAG ... -o libB.so
/**Another compilation unit, or even project. **/
g++ ... -DYOUR_NORMAI_FLAG ... main.cpp libA.so //The possibilities!

基本故事

最近,我遇到了一个奇怪的错误:症状是单个 SIGSEGV,重新编译后似乎总是发生在同一位置。这让我相信发生了某种内存损坏,实际的底层指针根本不是指针,而是某个数据部分。

我把你从漫长而艰苦的旅程中拯救出来,几乎需要两个原本非常好的工作日来追踪问题。足以说,Valgrind,GDB,nm,readelf,电围栏,GCC的堆栈粉碎保护,然后更多的措施/方法/方法失败了。

在彻底的破坏中,我的注意力转向了构建过程中最精细的细节,类似于:

  • 构建一个小库。
  • 构建一个大型库,该库使用小型库。
  • 构建大型库的测试套件。

仅在将大型库用作静态或动态库依赖项的情况下(即动态链接器自动加载它,没有 dlopen(有问题吗。库的所有代码都包含在测试中的测试用例中,一切都有效:这是最重要的线索。

"解决方案">

最后,事实证明这是最简单的事情:一个(!(错别字。

事实证明,编译标志在测试套件中与单个字符不同,并且大库:控制小库行为的定义拼写错误。关键信息:小型图书馆有一些模板。这些在每种情况下都直接使用,没有事先明确实例化。当标志被切换时,其中一个模板化类的内容发生了变化:在定义标志的情况下,一些数据字段根本不存在!链接器没有注意到这一点。(由于类是模板化的,因此生成的符号很弱。代码使用动态强制转换,受此问题影响的类继承自损坏的类 ->事情向南发展。

我的问题如下:您将如何防止此类问题?是否有任何工具或解决方案可以解决此特定问题?

面向未来

我想到了两件事,并且认为无法在目标文件级别构建任何保护:

  • 1:将作为预处理器符号实现的选项保存在某个明确定义的位置,最好通过单独的构建步骤提取。提供检查脚本,该脚本使用它来检查所有编译器定义,并在用户代码中定义。将此检查集成到生成过程中。可能使用Levenshtein距离或类似距离来检查拼写错误。昂贵,并且脚本/解决方案可能会变得复杂。类似标志可能存在问题(但为什么要有它们?(,编译的库代码必须附带其他文件。(好吧,也许在DWARF 2中,这是不正确的,但让我们假设我们不希望这样。
  • 2:集中构建选项:便宜,自定义选项保持开放(想想makefile.local(,但制造了整体怪物,强大的项目耦合。

我想继续熄灭一些可能引起火焰的余烬,可能会在一些读者中燃烧:"不要使用预处理器符号"在这里不是一个选项。

  • 条件编译确实在高性能代码中占有一席之地,使用模板和 enable_if-s 做所有事情都会不必要地使事情过于复杂。虽然上述解决方案通常是不希望的,但它可能会在开发过程中出现
  • 请假设你无法控制情况,假设你有遗留代码,假设你能强迫自己避免回避。
  • 如果这些都不起作用,请推广到 ABI 不兼容检测,尽管这可能会使问题的范围对 SO 来说过于扩大。

我知道:

  • http://gcc.gnu.org/onlinedocs/libstdc++/manual/abi.html
    • DT_SONAME不适用。
    • 其中的其他版本方案也不适用 - 它们旨在保护本身没有错误的软件包。
  • 混合C++ ABI 以针对旧库进行构建
  • 静态分析工具,用于检测C++中的 ABI 中断

如果很重要,请不要使用默认情况。

#ifdef YOUR_NORMAL_FLAG
  // some code
#elsif YOUR_SPECIAL_FLAG
  // some other code
#else
  // in case of a typo, this is a compilation error
#  error "No flag specified"
#endif

如果过度使用条件编译,这可能会导致大量编译器选项,但有一些方法可以解决这个问题,例如定义配置文件

flag=normal
flag2=special

它被构建脚本解析并生成选项,并可能检查拼写错误,或者可以直接从 Makefile 解析。