【问题标题】:Does memory layout in ABI specifications apply only across ABI boundaries?ABI 规范中的内存布局是否仅适用于 ABI 边界?
【发布时间】:2020-03-31 05:40:48
【问题描述】:

ABI 标准中与内存布局相关的规范是否通常仅适用于 ABI 边界,或者也适用于例如 ABI 边界。在翻译单元内,或者如果不是这种情况,编译器通常会做出这样的额外保证吗?

如果“一般”过于宽泛,请考虑例如GCC/Clang 与 System V x64 和 Itanium C++ ABI。

这里有两个例子说明我的意思:

  1. System V x64 ABI 指定大小至少为 16 字节的数组具有至少 16 字节的对齐,即使元素类型的对齐更小,因此对齐比alignof 建议的更严格。它还指定long double 的对齐方式为16。那么如果调用以下在 C++ 标准下具有未定义行为的函数,是否可以在 System V x86 ABI 下安全使用,即使 storage 数组从未跨翻译单元边界公开?

    void f() {
        char storage[16]; // Only guaranteed to have alignment `1` by the C++ standard.
        using T = long double;
        auto p = new(storage) T;
    }
    
  2. Itanium C++ ABI 指定类的布局。例如:

    #include<new>
    
    struct A {
        int i;
        virtual ~A() {}
    };
    
    struct B : A {
        int j;
    };
    
    void f() {
        B b;
        std::launder(reinterpret_cast<A*>(&b))->i = 1;
    }
    

    f 在 C++ 标准下被调用时具有未定义的行为,因为 BA 不是标准布局,因此未指定 A 子对象是否位于与 b 相同的地址,如果没有,这会导致 std::launder 上出现未定义的行为。然而,在 Itanium C++ ABI 下,保证A 子对象与b 具有相同的地址,因此std::launder 将成功。那么在 Itanium C++ ABI 下,即使b 从未越过翻译单元边界,这是否安全?

我假设我的两个示例都是安全的,但是这是在引用的标准中还是在编译器的策略中指定的?

【问题讨论】:

  • 来自Itanium C++ ABI:“在本文档中,我们为 C++ 程序指定了应用程序二进制接口,即用户 C++ 代码与实现提供的系统和库之间的目标代码接口。” .对我来说,这并不能说明仅在二进制文件中使用的数据对象。因此,“内部”类的内存布局可能不同,但实际上我怀疑任何编译器“内部”和“外部”使用不同的二进制布局。

标签: c++ abi


【解决方案1】:

是的,根据我的阅读,这两个实例都是安全的。

我无法将您指向 Itanium C++ ABI 的某个部分,但无论如何您似乎对它要表达的内容持坚定的态度。

但我知道:

根据 C++ 标准未定义的行为的一种可能表现形式是该语言的某些实现,例如 Itanium C++ ABI,保证该构造的特定行为。

也就是说,如果一个标准说“未定义”,而另一个标准说“已定义为执行 Y”,那么,如果您的实现符合这两个标准,您应该能够假设“Y”发生。

(旁注:另一方面,如果一个标准说“这被定义为做 X”,而另一个标准说“那被定义为做 Y”,那么,如果“X”!=“Y”,你的实现不能同时符合这两个标准。)

【讨论】:

  • "但你似乎对它要说的话很坚定":我不是真的。我认为引言中相关的部分不够清晰,我无法确定它是否打算仅适用于图书馆之间或也适用于单个翻译单元。这基本上就是我问的原因。
  • "那么,如果您的实现符合这两个标准,您应该能够假设“Y”发生了。":是的,这很清楚,我认为它适用于此,因为否则很难在程序中使用此类实现细节,但我不确定标准是否自己指定它,或者编译器是否只是扩展这些 ABI 标准中的规范,或者我是否弄错了,不安全毕竟使用这些构造。我曾多次阅读 cmets,声称在问题的场景中依赖 ABI 规范是安全的。
  • C++ 标准对程序文本的行为方式设置了某些限制。附加标准不会“扩展”行为,而是对程序文本的行为方式施加附加限制。例如,C++ 对名为getuid() 的函数的功能没有任何限制;但如果实现also 符合POSIX,则该函数具有非常特殊的含义。那是另一个限制!考虑到这一点,您可以说您的构造仅在 C++ 下安全,但在 C++ 和 Itanium ABI 下,构造是安全的。
【解决方案2】:

你问了两个问题:

[...],编译器通常会做出这样的额外保证吗?

否则它们将几乎无法使用,但我认为不可能对此给出明确的答案。我们必须证明不存在不做出此类保证的编译器。

因此,“一般” 太宽泛了。

在查看特定编译器/平台时,您的示例引出了第二个问题:

我的未定义行为示例是否适用于某些平台?

您已经知道这些示例具有 UB。如果编译器检测到这一点,它可以对该代码执行任何操作,例如

  1. 放下整件事
  2. 生成格式化 SSD 的代码
  3. 生成符合您期望的代码

UB 的问题是:即使今天你的编译器选择第三个选项,也不能保证它明天会再次这样做。

另见cppreference

所以第二个问题的答案是:嗯,也许,但不能保证。

【讨论】:

  • 我在我的问题中写道:“如果“一般”过于宽泛,请考虑使用 System V x64 和 Itanium C++ ABI 的 GCC/Clang。”。对于第二部分:代码是 C++ 标准的 UB,但是如果编译器遵循的另一个标准对其进行了定义,那么编译器将不得不输出具有该效果的代码。我要询问的其他标准是 ABI 规范,至少 GCC 和 Clang 明确遵守。但我不知道 ABI 规范本身(使用 C++ 标准)是否真的定义了我提到的程序的行为。这就是问题所在。
  • @walnut 明白了,我确认(至少恕我直言),“一般”太宽泛了 :-) 至于具体情况,你用例子来解释你的意图/希望。对于那些 "The code is UB by the C++ standard" 是你的答案。允许编译器做一些明智的事情,但不能保证。稍微修改了我的答案。
  • @walnut 换个说法,我认为您的示例要求我从第一个问题中不会读到的东西。您似乎希望 C++ 标准是一个根据目标平台运行的模板。不过,那将是超级可怕的。怎么会有人编写任何平台无关的代码?因此:如果按照标准是UB,那么就是UB。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-05
  • 1970-01-01
  • 1970-01-01
  • 2013-02-19
  • 2019-05-26
相关资源
最近更新 更多