LTO 可见性¶
LTO 可见性 是实体的属性,它指定实体是否可以从当前 LTO 单位之外引用。链接单位 是链接在一起形成可执行文件或 DSO 的一组翻译单位,而链接单位的 LTO 单位 是链接单位中使用链接时优化链接在一起的子集;在不使用 LTO 的情况下,链接单位的 LTO 单位为空。每个链接单位只有一个 LTO 单位。
编译器使用类的 LTO 可见性来确定哪些类可以应用于全程序去虚拟化(-fwhole-program-vtables
)和控制流完整性(-fsanitize=cfi-vcall
和 -fsanitize=cfi-mfcall
)功能。这些功能使用全程序信息,因此它们需要整个类层次结构可见才能正常工作。
如果程序中的任何翻译单位使用全程序去虚拟化或控制流完整性功能中的任何一个,那么在多个链接单位中定义具有隐藏 LTO 可见性的类实际上就是 ODR 违规。具有公共 LTO 可见性的类可以在多个链接单位中定义,但权衡是全程序去虚拟化和控制流完整性功能只能应用于具有隐藏 LTO 可见性的类。类的 LTO 可见性被视为其定义的 ODR 相关属性,因此它在翻译单位之间必须保持一致。
在使用 LTO 构建的翻译单位中,LTO 可见性基于类在源代码级别表示的符号可见性(即 __attribute__((visibility("...")))
属性,或 -fvisibility=
标志),或者在 Windows 平台上,基于 dllimport 和 dllexport 属性。在针对非 Windows 平台时,具有除隐藏可见性以外可见性的类将接收公共 LTO 可见性。在针对 Windows 时,具有 dllimport 或 dllexport 属性的类将接收公共 LTO 可见性。所有其他类都将接收隐藏 LTO 可见性。具有内部链接的类(例如,在未命名命名空间中声明的类)也接收隐藏 LTO 可见性。
在 LTO 链接期间,所有具有公共 LTO 可见性但没有标记为 [[clang::lto_visibility_public]]
(见下文)的类将在应用 --lto-whole-program-visibility
lld 链接器选项时(对于 gold,为 -plugin-opt=whole-program-visibility
)被细化为隐藏 LTO 可见性。此标志可用于延迟指定类是否具有隐藏 LTO 可见性,直到链接时,以允许不同的 LTO 链接共享位代码对象。由于实现限制,与具有隐藏 LTO 可见性的类关联的符号在使用此标志时可能仍会从二进制文件中导出。引用这些符号是不安全的,并且它们的可见性可能会在将来的编译器版本中放松为隐藏。
在没有 LTO 构建的翻译单位中定义的类将接收公共 LTO 可见性,而不管其目标文件可见性、链接或其他属性如何。
此机制在大多数情况下都会产生正确的结果,但有两种情况可能会错误地推断出隐藏 LTO 可见性。
作为上述规则的推论,如果链接单位是由 LTO 目标文件和非 LTO 目标文件的组合产生的,那么在使用 LTO 构建的翻译单位和没有 LTO 构建的翻译单位中定义的任何隐藏可见性类都必须使用公共 LTO 可见性定义,以避免 ODR 违规。
一些 ABI 提供了在多个链接单位中定义没有可见性属性的抽象基类并使对其他链接单位中派生类的虚调用正常工作的能力。Windows 平台上的 COM 就是一个例子。如果 ABI 允许这样做,则以这种方式使用的任何基类都必须使用公共 LTO 可见性定义。
属于这两类中的任何一类的类可以使用 [[clang::lto_visibility_public]]
属性进行标记。为了专门处理 COM 案例,具有 __declspec(uuid())
属性的类接收公共 LTO 可见性。在 Windows 平台上,clang-cl 的 /MT
和 /MTd
标志会将程序静态链接到预构建的标准库;这些标志意味着在 std
和 stdext
命名空间中声明的每个类都具有公共 LTO 可见性。
示例¶
以下示例展示了在涉及两个链接单位(main
和 dso.so
)的几种情况下 LTO 可见性在实践中的工作方式。
+-----------------------------------------------------------+ +----------------------------------------------------+
| main (clang++ -fvisibility=hidden): | | dso.so (clang++ -fvisibility=hidden): |
| | | |
| +-----------------------------------------------------+ | | struct __attribute__((visibility("default"))) C { |
| | LTO unit (clang++ -fvisibility=hidden -flto): | | | virtual void f(); |
| | | | | } |
| | struct A { ... }; | | | void C::f() {} |
| | struct [[clang::lto_visibility_public]] B { ... }; | | | struct D { |
| | struct __attribute__((visibility("default"))) C { | | | virtual void g() = 0; |
| | virtual void f(); | | | }; |
| | }; | | | struct E : D { |
| | struct [[clang::lto_visibility_public]] D { | | | virtual void g() { ... } |
| | virtual void g() = 0; | | | }; |
| | }; | | | __attribute__((visibility("default"))) D *mkE() { |
| | | | | return new E; |
| +-----------------------------------------------------+ | | } |
| | | |
| struct B { ... }; | +----------------------------------------------------+
| |
+-----------------------------------------------------------+
我们现在将描述在这些链接单位中定义的每个类的 LTO 可见性。
类 A
没有在 main
的 LTO 单位之外定义,因此它可以具有隐藏 LTO 可见性。这是从命令行上指定的目标文件可见性推断出来的。
类 B
在 main
中定义,既在 LTO 单位内,也在 LTO 单位外。LTO 单位外的定义具有公共 LTO 可见性,因此 LTO 单位内的定义也必须具有公共 LTO 可见性,以避免 ODR 违规。
类 C
在 main
和 dso.so
中定义,因此必须具有公共 LTO 可见性。这是从 visibility
属性中正确推断出来的。
类 D
是一个抽象基类,在 dso.so
中定义了一个派生类 E
。这是一个 COM 场景的示例;main
的 LTO 单位中 D
的定义必须具有公共 LTO 可见性,以便与 dso.so
中 D
的定义兼容,后者可以通过调用函数 mkE
来观察。