Page 1 of 1

针对某些类别的 IFNDR 的常见解决方案:让链接器验证相同的功能

Posted: Sat Jan 25, 2025 10:29 am
by jrineakter
前段时间,我曾举过一个 C++ 概念中格式错误的无需诊断(IFNDR)的例子,即内联函数被不同的 .cpp 文件编译为不同的定义。标准要求内联函数的所有定义必须相同。(还有其他原因会导致 IFNDR,但现在我们先关注内联函数。)如果您违反此规则,则编译器无需告诉您您搞砸了,并且生成的程序无需以任何特定方式运行。

实际上,这些问题对于编译器来说很难诊断,因为它需要跨翻译单元(.cpp 文件)匹配实体,并且传统上,编译器只对单个翻译单元进行操作,因此它无法“看到”来自其他翻译单元的定义。

为了让这个问题可诊断,至少对于内联函数来说,一个常见的建议是将检测推迟到链接器。编译器可以给所有内联函数添加一个标签,上面写着“必须相同”。当链接器发现相同内容的两个副本时,它会在丢弃其中一个之前验证这两个副本是否相同。

这个解决方案确实需要付出一些代价。编译器现在需要为所有内联函数生成代码。即使编译器决定在调用点内联它们,它们仍然必须生成“不合时宜”的内联版本以供检测。

现在,为内联函数生成“外部”版本实际上是相当正常的。具有外部链接的函数会为其生成代码,以防它们的地址被另一个翻译单元占用。所以编译器无论如何都会为许多类别的函数执行此操作。

问题在于逐字节比较很脆弱。最轻微的干扰都可能改变结果。如果您使用不同的优化级别或不同的编译器开关编译函数,您可能会得到不同的结果。您将无法链接使用不同编译器供应商编译的库,甚至无法链接同一编译器的不同版本编译的库,因为代码生成可能不完全匹配。

要求函数的每个实例都产生相同的代 合作伙伴电子邮件列表 码生成是比可重复性更强的要求。可重复构建(也称为确定性构建)从相同的输入产生相同的输出:如果不更改任何代码行,则结果相同。这意味着,诸如选择将哪个寄存器用于特定目的之类的决定不能留给外部因素,例如可用内存量或随机数生成器。

要求内联函数的每个编译版本都相同是一个更严格的要求。例如,假设编译器按照翻译单元中第一次遇到的顺序为每个命名实体分配一个递增的序列号。



这次,a和 的标识符编号b被交换,这可能会改变 的代码生成,因为编译器决定使用相反的寄存器来加载和。plus_a_and_bab

这两个函数实现在功能上是等效的,但在字节级别上并不相同。根据莱斯定理,确定两个函数在功能上是否等效是无法判定的。plus_a_and_b

您可能会说,“好吧,那么不需要要求两个版本逐字节相同,只需让编译器记录函数体,然后比较函数体。”

现在,你不能逐字比较函数体,因为尽管函数体相同,但事物的含义可能会发生变化。



在第一个版本中,get_a返回 的值::a,而第二个版本返回 的值::Sample::a。 的定义get_a在源代码级别上是相同的,但是 符号a在两个版本中的含义不同。

因此实际上,编译器必须记录函数体,以及函数体中每个标识符解析的信息。

但是,即使链接器验证了所有这些信息,链接完成后仍可能会引入不匹配的情况!

如果代码被编译成可重用模块(如 Windows 上的 DLL 或基于 Unix 的系统上的共享库),则可能存在使用不兼容的内联函数定义编译的客户端。编译该客户端时未检测到不匹配,因为它们使用了具有其他定义的可重用模块版本。只有当两个模块(您的和他们的)在运行的系统上相遇时才会发生不匹配。

您使用具有某些定义 X 的内联函数编译 DLL。
客户端编译其模块并链接到具有不同定义 Y 的内联函数的 DLL 的早期版本。
客户端模块运行并加载使用定义 X 的 DLL。
因此,现在您要要求操作系统参与检测:每当加载模块时,验证其所有类型和内联实体是否与加载到同一进程中的所有其他模块的类型和内联实体相匹配。这也意味着链接器无法删除有关内联函数的所有信息:它需要将这些信息保留在最终产品中,以便操作系统可以进行运行时验证。

但你真的不希望这样。这意味着一个进程加载的所有 DLL 必须同意所有实体的定义,即使这些实体从不交互。

ONE.DLL有一个名为 的内部类。此类永远不会与任何其他 DLL 共享。internal::MyObject
TWO.DLL还有一个名为 的内部类。此类永远不会与任何其他 DLL 共享。internal::MyObject
如果ONE.DLL和TWO.DLL都加载到同一个进程中,验证器会抱怨“不匹配:和具有不同的定义”。是的,它们具有不同的定义,但它们都是各自模块的内部定义,因此不匹配并不重要。您需要某种命名策略,以便 DLL 不会意外地在其内部类型上发生名称冲突。例如,也许所有内部名称都必须放在唯一的命名空间中。(祝那些不支持命名空间的语言好运。)One!internal::MyObjectTwo!internal::MyObject

此外,您经常需要不匹配。如果ONE.DLL和TWO.DLL都是 COM 提供程序,那么它们都将导出一个名为的函数Dll­Get­Class­Object,并且这些函数必然会在不同的 DLL 中执行不同的操作。或者可能ONE.DLL是使用 boost 版本 1.84.0 构建的,但TWO.DLL使用 boost 版本 1.86.0 构建的。它们都不会跨 ABI 边界传递 boost 类,因此它们各自使用不同版本的 boost 这一事实只是一个实现细节,不应成为声明不匹配的原因。

听起来我们需要在每个实体上添加注释,以表明它是否应该参与跨模块一致性检查。跨模块边界的对象将被注释为“确保一致性”,而模块内部的对象将被注释为“不打扰”。