AAPCS (ARM ABI) 下的 C/C++ 结构填充有多稳定?

nil*_*ilo 4 c c++ arm padding abi

C99标准告诉我们:

结构对象内可能有未命名的填充,但不是在其开头。

在结构或联合的末尾可能有未命名的填充。

我假设这也适用于任何 C++ 标准,但我没有检查它们。

让我们假设在 ARM Cortex-M 上运行的 C/C++ 应用程序(即在应用程序中使用两种语言)将一些持久数据存储在本地介质(例如串行 NOR 闪存芯片)上,并在上电后读取它循环,可能在将来升级应用程序本身之后。升级后的应用程序可能是用升级后的编译器(我们假设是 gcc)编译的。

让我们进一步假设开发人员很懒惰(当然不是我),并且直接将一些普通的 C 或 C++ 流式传输struct到闪存,而不是像任何偏执的有经验的开发人员那样首先将它们序列化。

事实上,有问题的开发人员很懒惰,但并非完全无知,因为他阅读了AAPCS(Arm 架构的过程调用标准)

除了懒惰之外,他的理由如下:

  • 他不想打包structs 以避免在应用程序的其余部分出现未对齐问题。
  • AAPCS 为每种基本数据类型指定了固定对齐方式。
  • 填充的唯一合理动机是实现正确对齐。
  • 因此,他认为,填充(以及因此 memberoffsetof和 total sizeof)完全struct由 AAPCS为任何 C 或 C++ 确定。
  • 因此,他进一步解释说,我的应用程序无法解释同一应用程序的早期版本会写入的某些回读数据(当然,假设闪存中数据的偏移量没有在写作和阅读之间变化)。

不过,开发者是有良心的,他有点担心:

  • C 标准没有提到任何填充的原因。实现正确对齐可能是填充的唯一合理原因,但根据标准,编译器可以随意填充尽可能多的内容。
  • 他怎么能确定他的编译器真的遵循AAPCS呢?
  • 他的假设是否会突然被他将开始使用的一些明显无关的编译器标志或编译器升级打破?

我的问题是:那个懒惰的开发人员的生活有多危险?换句话说,struct在上述假设下,填充在 C/C++ 中的稳定性如何?

结论

在提出这个问题两周后,收到的唯一答案并没有真正回答所提出的问题。我也在ARM 社区论坛上问过完全相同的问题,但根本没有答案。

然而,我选择接受3246135作为答案,因为:

  1. 我将没有正确答案视为与此案非常相关的信息。软件问题解决方案的正确性应该是显而易见的。在我的问题中所做的假设可能是正确的,但我无法轻易证明。此外,如果假设不正确,在一般情况下,后果可能是灾难性的。

  2. 相比风险,开发者在使用答案中暴露的策略时的负担似乎非常合理。假设一个恒定的字节序(这很容易强制执行),它是 100% 安全的(任何偏差都会在编译时产生错误),并且比完整的序列化轻得多。基本上,答案中公开的策略 是为了使 C/C++struct的持久性独立于任何 ABI 而必须付出的最低代价。

如果您是一名开发人员,问自己上述问题,请不要偷懒,而是使用已接受的答案中公开的策略,或保证跨软件版本的恒定填充的替代策略。

dbu*_*ush 8

您永远无法 100% 确定编译器不会在某些容量中引入填充。但是,您可以通过遵循一些规则来降低风险:

  • 使用固定大小类型所有成员,即uint32_tint64_t等等。
  • 从每个成员的偏移量开始,该偏移量是成员大小的倍数(或者如果成员是数组/结构,则为最大成员的大小)。
  • 避免位域

请注意,这样做可能会引入一些显式填充字段来满足对齐要求。

例如:

struct orig {
    int a;
    char b;
    int c[10];
    short d;
    char e[15];
    long f;
    int g;
};
Run Code Online (Sandbox Code Playgroud)

假设sizeof(short) == 2sizeof(int) == 4sizeof(long) == 8,此结构的成员的大小将为 74。 如果考虑可能的填充:

struct orig_padded {
    int a;
    char b;
    char pad1[3];
    int c[10];
    short d;
    char e[15];
    char pad2[7];
    long f;
    int g;
    char pad3[4];
};
Run Code Online (Sandbox Code Playgroud)

您的结构大小为 88。

通过一些重新排列,我们可以将大小减少到 74:

struct reordered {
    int64_t f;
    int32_t a;
    int32_t c[10];
    int32_t g;
    int16_t d;
    char b;
    char e[15];
};
Run Code Online (Sandbox Code Playgroud)

通过按大小降序对字段进行排序,我们基本上删除了字段之间的填充,只在末尾留下潜在的填充。还要注意使用固定大小以避免一些意外。然后作为保障,我们添加:

static_assert(sizeof(struct reordered) == 74);
Run Code Online (Sandbox Code Playgroud)

因此,如果结构的编译大小发生变化,您将在编译时知道。

有关更多详细信息,请查看结构包装的失落艺术