C++ 防止意外对象不兼容? TL;博士

C++ 防止意外对象不兼容? TL;博士,c++,linker,c-preprocessor,symbol-table,misspelling,C++,Linker,C Preprocessor,Symbol Table,Misspelling,在不同的编译单元中控制条件编译的共享(可能是模板化的)头的预处理器指令中,防止因编译器参数输入错误而导致的二进制不兼容 前 基本情况 最近,我遇到了一个奇怪的bug:症状是一个SIGSEGV,在重新编译后似乎总是发生在同一个位置。这让我相信有某种内存损坏正在发生,而实际的底层指针根本不是指针,而是某个数据段 我把你从漫长而艰苦的旅程中解救出来,花了几乎两天的时间来追踪这个问题。可以说,Valgrind、GDB、nm、readelf、电子围栏、GCC的堆栈破坏保护以及其他一些措施/方法/方法都失败

在不同的编译单元中控制条件编译的共享(可能是模板化的)头的预处理器指令中,防止因编译器参数输入错误而导致的二进制不兼容

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

我把你从漫长而艰苦的旅程中解救出来,花了几乎两天的时间来追踪这个问题。可以说,Valgrind、GDB、nm、readelf、电子围栏、GCC的堆栈破坏保护以及其他一些措施/方法/方法都失败了

在彻底的毁灭中,我的注意力转向了构建过程中最好的细节,这类似于:

  • 建一个小图书馆
  • 建造一个大图书馆,使用小图书馆
  • 构建大型库的测试套件
仅当大型库用作静态或动态库依赖项时(即动态链接器自动加载它,无dlopen) 有什么问题吗。在测试用例中,库的所有代码都被简单地包含在测试中,一切都正常工作:这是最重要的线索

“解决方案” 最后,它变成了最简单的事情:一个(!)打字错误

事实证明,编译标志与测试套件中的单个字符不同,而大型库:控制小型库行为的define拼写错误。 关键信息:这个小图书馆有一些模板。在每种情况下都直接使用了它们,没有预先进行显式实例化。切换标志时,其中一个模板类的内容发生了更改:如果定义了标志,则某些数据字段根本不存在! 链接器没有注意到这一点。(因为类是模板化的,所以生成的符号很弱。) 代码使用动态强制转换,受此问题影响的类继承自损坏的类->things south

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

未来证明 我考虑过两件事,认为不能在对象文件级别建立保护:

  • 1:将实现为预处理器符号的选项保存在一些定义良好的位置,最好通过单独的构建步骤提取。提供检查脚本,使用该脚本检查所有编译器定义,并在用户代码中定义。将此检查集成到构建过程中。可能使用Levenshtein距离或类似方法检查拼写错误。昂贵,而且脚本/解决方案可能会变得复杂。类似标志可能存在问题(但为什么有它们?),编译后的库代码必须附带其他文件。(好吧,也许对于矮人2来说,这是不真实的,但让我们假设我们不想要它。)
  • 2:集中构建选项:价格便宜,自定义选项保持打开状态(想想makefile.local),但会造成整体畸形,强大的项目耦合
我想继续,扑灭一些可能在一些读者中燃烧起来的可能引发火焰的余烬:“不要使用预处理器符号”在这里不是一个选项

  • 条件编译确实在高性能代码中占有一席之地,使用模板和enable_if-s做任何事情都会导致不必要的过度复杂。虽然上述解决方案通常不可取,但也可能在开发过程中出现
  • 请假设您无法控制这种情况,假设您有遗留代码,假设您可以尽一切努力强迫自己避免旁敲侧击
  • 如果这些都做不到,可以推广到ABI不兼容检测中,尽管这可能会使问题的范围扩大太多
我知道:

    • DT_SONAME不适用
    • 其中的其他版本方案也不适用——它们旨在保护本身没有故障的软件包

如果重要,不要有默认案例

#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进行解析。

make
是您的朋友。他们是我的朋友,但不幸的是,我朋友的朋友不一定是我的朋友,最终,在单独的项目中生成文件,彼此默默地憎恨。消除了缺乏信息就是信息的情况,这是非常成功的。这个解决方案演变成了上面的第1个解决方案——我不喜欢这个解决方案引入了额外的标志,这必须引起注意。如果我们涉及到构建系统,为什么不尝试消除引入额外复杂性的需要呢?此外,我忘了提到我希望保持“否定”行为尽可能简单。这与-U for unde不符-编译器没有关于标志实际上是二进制开关的信息,因此如果构建系统没有显式处理,该选项将失去意义->即时错误,这增加了另一层复杂性。
flag=normal
flag2=special