0

I have looked around this site to try to figure out if my use of casting to different unions is violating strict aliasing or otherwise UB.

I have packets coming in on a serial line and I store/get them like:

union uart_data {
  struct {
    uint8_t start;
    uint8_t addr;
    uin16_t length;
    uint8_t data[];
  };
  uint8_t bytes[BUFFER_SIZE];
};

void store_byte(uint8_t byte) {
  uart_data->start = byte;
  /* and so on with the other named fields. */
}

uint8_t * get_buffer() {
  return uart_data->bytes;
}

My understanding is that this is, at least with GCC and GNU extensions an valid way to do type punning.

However, I then want to cast the return value from get_buffer() to a more specific type of packet that the uart doesn't need to know the details about.

union spec_pkt {
  struct {
    uint8_t start;
    uint8_t addr;
    uin16_t length;
    uint8_t command;
    uint8_t some_field;
    uint16_t data_length;
    uint8_t data[];
  };
  uint8_t bytes[BUFFER_SIZE];
};

void process(uint8_t *data) {
  union specific_pkt *pkt = (union specific_pkt *)data;
}

I recall having read somewhere that this is valid since I'm casting from a type that exists in the union but I can't find the source.

My rationale for doing this it this way is that I can have a uart driver that only needs to know about the lowest level details. I'm on an MCU so I only have access to pre-allocated buffers to data and this way I don't have to memcpy between buffers, wasting space. And in my application code I can handle the packet in a nicer way than:

uint8_t data[BUFFER_SIZE];

data[START_POS];
data[LEN_POS];
data[DATA_POS];

If this is violating the SA rule or is UB I'd love some alternatives to achieve the same.

I'm using GCC on a target that supports unaligned access and GCC allows type punning through unions.

evading
  • 3,032
  • 6
  • 37
  • 57
  • Isn't the padding in structs entirely up to the compiler? That means that different builds could fill `bytes` differently. – ikegami Dec 18 '19 at 13:06
  • 1
    Why not just add all possible mutations of the data you require as different structs inside your union? – Ishay Peled Dec 18 '19 at 13:06
  • I'm not sure about the alignment of your `union spec_pkt`. Maybe in your case it is only byte-aligned, but in case someone reuses your code with a `struct` that needs a stricter alignment, you may get problems. Instead of reinterpreting a `uint8_t*`buffer pointer as a structure pointer, I suggest to define your buffer to be of this union type and pass the `bytes` field to the UART functions. – Bodo Dec 18 '19 at 13:09
  • Converting from an `uint8_t*` to a `struct` or `union` is undefined. The simple reason is that some members may have stronger alignment. The other way around is possible, because then you are relaxing constraints. – Jens Gustedt Dec 18 '19 at 13:13
  • What matters is how the actual data is stored for the first time, i.e what "effective type" it got. You need to show that part of the code or there's no telling if this is a strict aliasing violation. – Lundin Dec 18 '19 at 13:50
  • 2
    If you're worried about strict aliasing, you can always compile with `-fno-strict-aliasing` – dbush Dec 18 '19 at 13:58
  • 1
    Since this is for an embedded system, then yes always compile with `-fno-strict-aliasing -ffreestanding`. gcc wasn't really designed with embedded systems in mind. – Lundin Dec 18 '19 at 14:03
  • 1
    @JensGustedt: A lot of confusion stems from the fact that the Standard uses the same terminology for actions that should be expected to behave predictably under some circumstances but not all, based upon implementation-specific factors, as for those which shouldn't generally be expected to behave predictably under any circumstances. For some reason, people seem to think the Standard uses the term "Implementation-Defined" for the former, but it doesn't; Implementation-Defined is reserved for actions which *all* implementations should *always* handle predictably. – supercat Dec 18 '19 at 17:39
  • @ikegami: The Standard was written after C had become popular. Just about every C implementation that doesn't go out of its way to be weird will place each structure member at the first offset that follows earlier members and satisfy its alignment, unless something unusual about the target platform would make that impractical. Such behavior would be sufficiently universal as to justify standardizing it, except that doing so would require a means of identifying which target platforms are sufficiently "unusual" as to justify different behavior, and implementations for normal platforms... – supercat Dec 20 '19 at 18:49
  • ...consistently behave in the stated fashion without regard for whether the Standard requires them to do so. – supercat Dec 20 '19 at 18:49
  • @supercat, Re "*Just about every C implementation that doesn't go out of its way to be weird will place each structure member at the first offset that follows earlier members and satisfy its alignment,*", That's not the case for gcc on an x86. x86 has no alignment restrictions, yet `struct { char ch; int i; }` includes padding on that system – ikegami Dec 20 '19 at 18:55
  • @ikegami: The x86 can only perform single-operation loads and stores of data which does not cross alignment boundaries. Although the processor includes logic to split other loads and stores into multiple operations, the underlying bus interface is limited to accesses that don't straddle alignment boundaries. Further, while a 16-bit object could be placed at e.g. offset 9 within a 32-bit aligned structure without ever crossing a 32-bit boundary, alignment requirements are typically expressed as the smallest power of two for which *no* multiple will cross an alignment boundary. – supercat Dec 21 '19 at 16:40
  • @supercat, I didn't say there was no reason for adding padding. – ikegami Dec 21 '19 at 16:51
  • @ikegami: Perhaps I should have made clear that "satisfies its alignment" doesn't necessarily mean "satisfies the minimum alignment necessary to avoid a trap", but "satisfies the alignment specified by the implementation". I don't think any implementations add padding without documenting that they do so, but some do provide options to control what alignment they assume for various types. On a 68000-based system, for example, 32-bit objects must be 16-bit aligned but there can be accessed equally fast at any location satisfying that alignment; some other processors in that family... – supercat Dec 21 '19 at 18:32
  • ...no longer require 16-bit alignment, but will perform slower when using 32-bit objects that aren't 32-bit aligned. In any case, structure layouts where every object is aligned to a multiple of the smallest power of two less than its will be devoid of padding except on weird implementations. – supercat Dec 21 '19 at 18:34

1 Answers1

1

The Standard completely fails to specify the circumstances under which a structure or union object may be accessed via a non-character lvalue whose type is not that of the structure or union. If one recognizes that the purpose of the Standard is to purely indicate when a compiler must recognize that an object is being accessed by a seemingly-unrelated lvalue, but is not meant to apply to situations where a compiler would be able to see that an lvalue or pointer of one type is used to derive another which is then used to access storage associated with the first, without any intervening conflicting action on that storage, this omission would make sense. For example, given:

struct sizedPointer { int length,size; int *dat; };
void storeThing(struct sizedPointer *dest, int n)
{
  if (dest->length < dest->size)
  {
    dest->dat[dest->length] = n;
    dest->length++;
  }
}

such an interpretation would allow a compiler to assume that dest->length will not be written using dest->dat, since its value has been observed after dest->dat was formed, but would require that a compiler recognize that given:

union blob { uint16_t hh[8]; uint64_t oo[2]; } myBblob;

an operation like

sscanf(someString, "%4x", &myBlob.hh[1]);

might interact with any lvalues that are derived from myBlob after the function returns.

Unfortunately, gcc and clang instead interpret the rule as only mandating recognition in cases where failure to do so would completely gut the language. Because the Standard doesn't mandate that member-type lvalues be usable in any fashion whatsoever, and gcc and clang have explicitly stated that they should not be relied upon to do anything beyond what the Standard requires, support for anything useful should be viewed as being at the whim of the maintainers of clang and gcc.

supercat
  • 77,689
  • 9
  • 166
  • 211
  • What is the conclusion then? Does the OP's code violate the rules, or not? Or are the rules so vague that this cannot be determined? – th33lf Dec 19 '19 at 10:34
  • @th33lf: The rules are meant to say when lvalues of different types must be *presumed* capable of aliasing despite the lack of direct evidence that they do so. They make no effort to say when compilers should recognize *direct* evidence that two lvalues access the same storage, because they recognized that different compilers should use slightly different rules based upon their design and intended purpose. For example, a compiler that does no optimizations wouldn't need to recognize any accesses as potentially affecting the same storage, one that only does intra-procedural... – supercat Dec 19 '19 at 15:08
  • ...optimizations should be able to focus only on the current function, one that does intra-procedural optimizations should be able to recognize evidence that's split between the functions being optimized, etc. Unfortunately, the authors of C89 deliberately tried to avoid any statements about how compilers *should* process non-portable code, and such avoidance has been interpreted as a judgment that compilers should make no effort to process non-portable code usefully despite the authors' explicit statements to the contrary in the published Rationale. – supercat Dec 19 '19 at 15:12
  • In the OP's example, within the function `process()`, there are only two pointers involved, one of which is a struct and other is a char. The compiler has cannot assume, as per the standard, that they do not alias. So where is the scope for optimizations that do not account for aliasing breaking anything here? Also, is the rationale document available online? It is proving surprisingly difficult to find for C11! – th33lf Dec 20 '19 at 09:47
  • @th33lf: I haven't seen the Rationale document for C11, but N1570 6.5p6 and 6.5p7 are identical to the corresponding portions in C99, whose published Rationale is available. The Standard does not recognize generally function boundaries, or even compilation-unit boundaries, as optimization barriers, so given something like `T1 *p1, T2 *p2;... f1(p1); f2(p2); f3(p1);` a compiler might reasonably assume that any load performed in `f3()` will retrieve the same value as in `f1`. The problem is that gcc makes no effort to notice any actions between the calls that would derive `p1` and `p2` from... – supercat Dec 20 '19 at 18:12
  • ...lvalues that might legitimately identify the same object, but wouldn't always do so, e.g. `f1(&arrayOfUnions[i].m1); f2(&arrayOfUnions[j].m2); f3(&arrayOfUnions[i].m1);` If code were to create a pointer to `arrayOfUnions[i].m1` before the call to `f2()`, any use of *that* pointer in `f3`, without re-derivation, would alias any use of the pointer to `arrayOfUnions[j].m2` to access the same storage, but if code re-derives the pointer a quality compiler should have no trouble recognizing that. Unfortunately, neither clang nor gcc make any effort to do so. – supercat Dec 20 '19 at 18:18
  • @th33lf: What's needed to make things work is to separate the data-dependency or sequencing implications of actions from the "physical" actions themselves. A statement like `p1 = &arrayOfUnions[i].m1;` should be recognized as not only loading `p1` with an address, but also creating a data dependency between anything that might be done with `arrayOfUnions[i]` before the call and anything done with `*p1` afterward. If `&arrayOfUnions[i].m1` is re-evaluated, a compiler might omit the repeated address calculation, but doing so would not entitle it to ignore the second data dependency. – supercat Dec 20 '19 at 18:34
  • @th33lf: From what I can tell, clang and gcc both infer data dependencies from actions that generate code, which means that if a compiler decides that it can omit the repeated address calculation, it will no longer have any record of the required data dependency that had been implied by its computation. – supercat Dec 20 '19 at 18:36