How stable is C/C++ structure padding under the AAPCS (ARM ABI)?

Viewed 348

Question

The C99 standard tells us:

There may be unnamed padding within a structure object, but not at its beginning.

and

There may be unnamed padding at the end of a structure or union.

I am assuming this applies to any of the C++ standards too, but I have not checked them.

Let's assume a C/C++ application (i.e. both languages are used in the application) running on an ARM Cortex-M would store some persistent data on a local medium (a serial NOR-flash chip for instance), and read it back after power cycling, possibly after an upgrade of the application itself in the future. The upgraded application may have been compiled with an upgraded compiler (we assume gcc).

Let's further assume that the developer is lazy (that's not me, of course), and directly streams some plain C or C++ structs to flash, instead of first serializing them as any paranoid experienced developer would do.

In fact, the developer in question is lazy, but not totally ignorant, since he has read the AAPCS (Procedure Call Standard for the Arm Architecture).

His rationale, besides laziness, is the following:

  • He does not want to pack the structs to avoid misalignment problems in the rest of the application.
  • The AAPCS specifies a fixed alignment for every single fundamental data type.
  • The only rational motivation for padding is to achieve proper alignment.
  • Therefore, he thinks, padding (and therefore member offsetof and total sizeof) is fully determined for any C or C++ struct by the AAPCS.
  • Therefore, he further reasons, there is no way my application would not be able to interpret some read back data that an earlier version of the same application would have written (assuming, of course, that the offset of the data in flash memory has not changed between writing and reading).

However, the developer has a conscience and he is a little worried:

  • The C standard does not mention any reason for padding. Achieving proper alignment may be the only rational reason for padding, but compilers are free to pad as much as they want, according to the standard.
  • How can he be sure that his compiler really follows the AAPCS?
  • Could his assumptions suddenly be broken by some apparently unrelated compiler flag that he would start using, or by a compiler upgrade?

My question is: how dangerously does that lazy developer live? In other words, how stable is padding in C/C++ structs under the assumptions above?

Conclusion

Two weeks after this question was asked, the only answer that has been received does not really answer the asked question. I have also asked the exact same question on an ARM community forum, but got no answer at all.

I however choose to accept 3246135 as the answer because:

  1. I take the absence of proper answer as very relevant information for this case. The correctness of solutions to software problems should be obvious. The assumptions made in my question may be true, but I cannot easily prove it. Additionally, if the assumptions are incorrect, the consequences, in the general case, could be catastrophic.

  2. Compared to the risk, the burden on the developer when using the strategy exposed in the answer seems very reasonable. Assuming a constant endianness (which is quite easy to enforce), it is a hundred percent-safe (any deviation will generate an error at compile-time) and it is much lighter than a full-blown serialization. Basically, the strategy exposed in the answer is a mandatory minimum price to pay in order to make one's C/C++ structs persistent independently of any ABI.

If you are a developer asking yourself the question above, please do not be lazy, and use instead the strategy exposed in the accepted answer, or an alternative strategy that guarantees a constant padding across software releases.

1 Answers

You can never by 100% sure that the compiler won't introduce padding in some capacity. However, you can mitigate the risks by following a few rules:

  • Use fixed size types for all members, i.e. uint32_t, int64_t, etc.
  • Start each member at an offset that is a multiple of the member's size (or if the member is an array / struct, the size of the largest member).
  • Avoid bitfields

Note that doing this will likely introduce some explicit padding fields to satisfy alignment.

For example:

struct orig {
    int a;
    char b;
    int c[10];
    short d;
    char e[15];
    long f;
    int g;
};

The size of this struct's members, assuming sizeof(short) == 2, sizeof(int) == 4, and sizeof(long) == 8, would be 74. If you take into account likely padding:

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];
};

You have a struct size of 88.

With some rearranging we can reduce the size back to 74:

struct reordered {
    int64_t f;
    int32_t a;
    int32_t c[10];
    int32_t g;
    int16_t d;
    char b;
    char e[15];
};

By ordering the fields in descending order of size, we basically remove padding between the fields and only leave potential padding at the end. Note also the use of fixed sizes to avoid some surprises. Then as a safeguard, we add:

static_assert(sizeof(struct reordered) == 74);

So if the compiled size of the struct ever changes, you'll know at compile time.

For more details, take a look at The Lost Art of Structure Packing.

Related