TL;博士
防止在不同编译单元中控制条件编译的共享的、可能是模板化的头文件的预处理器指令中的编译器参数拼写错误导致二进制不兼容?
前任。
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!
基本故事
最近遇到一个奇怪的bug:症状是单个SIGSEGV,重新编译后似乎总是出现在同一个位置。这让我相信发生了某种内存损坏,而实际的底层指针根本不是指针,而是一些数据部分。
我将您从漫长而艰苦的旅程中拯救出来,这几乎需要两个原本非常好的工作日来追踪问题。说够了,Valgrind、GDB、nm、readelf、电栅栏、GCC的栈砸保护,然后再一些措施/方法/途径都失败了。
在彻底的毁灭中,我的注意力转向了构建过程中最精细的细节,这类似于:
- 建一个小图书馆。
- 建立一个大型图书馆,使用小型图书馆。
- 构建大型库的测试套件。
只有当大型库用作静态库或动态库依赖项(即动态链接器自动加载它,没有 dlopen)时才会出现问题。库的所有代码都简单地包含在测试中的测试用例,一切正常:这是最重要的线索。
解决方案”
最后,结果证明这是最简单的事情:一个(!)错字。
事实证明,编译标志在测试套件中只有一个字符,而大型库:控制小型库行为的定义拼写错误。关键信息点:小型图书馆有一些模板。这些在每种情况下都直接使用,无需事先明确实例化。切换标志时,其中一个模板类的内容发生了变化:在定义标志的情况下,某些数据字段根本不存在!链接器没有注意到这一点。(由于类是模板化的,因此生成的符号很弱。)代码使用了动态强制转换,受此问题影响的类继承自损坏的类 -> 事情发生了变化。
我的问题如下:您将如何防范此类问题?是否有任何工具或解决方案可以解决这个特定问题?
未来证明
我想到了两件事,并且相信无法在目标文件级别上建立任何保护:
- 1:将实现为预处理器符号的选项保存在某个定义明确的位置,最好通过单独的构建步骤提取。提供检查脚本,使用它来检查所有编译器定义和用户代码中的定义。将此检查集成到构建过程中。可能使用 Levenshtein distance 或类似的方法来检查拼写错误。昂贵,脚本/解决方案可能会变得复杂。类似标志可能存在问题(但为什么有它们?),附加文件必须伴随已编译的库代码。(好吧,也许对于 DWARF 2,这是不真实的,但我们假设我们不希望这样。)
- 2:集中构建选项:便宜,自定义选项保持打开状态(想想 makefile.local),但会产生单体怪物,强大的项目耦合。
我想继续熄灭一些可能在某些读者中燃烧的可能引发火焰的余烬:“不要使用预处理器符号”在这里不是一个选项。
- 条件编译确实在高性能代码中占有一席之地,并且使用模板和 enable_if-s 做所有事情都会不必要地使事情变得过于复杂。虽然上述解决方案通常是不可取的,但它可能会出现在开发过程中。
- 请假设你无法控制这种情况,假设你有遗留代码,假设你可以强迫自己避免回避。
- 如果这些都不起作用,请概括为 ABI 不兼容性检测,尽管这可能会过度扩大问题的范围。
我知道:
- http://gcc.gnu.org/onlinedocs/libstdc++/manual/abi.html
- DT_SONAME 不适用。
- 其中的其他版本方案也不适用——它们旨在保护本身没有故障的包。
- 混合 C++ ABI 以针对遗留库进行构建
- 用于检测 C++ 中 ABI 中断的静态分析工具