25

Consider the following struct:

struct s {
  int a, b;
};

Typically1, this struct will have size 8 and alignment 4.

What if we create two struct s objects (more precisely, we write into allocated storage two such objects), with the second object overlapping the first?

char *storage = malloc(3 * sizeof(struct s));
struct s *o1 = (struct s *)storage; // offset 0
struct s *o2 = (struct s *)(storage + alignof(struct s)); // offset 4

// now, o2 points half way into o1
*o1 = (struct s){1, 2};
*o2 = (struct s){3, 4};

printf("o2.a=%d\n", o2->a);
printf("o2.b=%d\n", o2->b);
printf("o1.a=%d\n", o1->a);
printf("o1.b=%d\n", o1->b);

Is anything about this program undefined behavior? If so, where does it become undefined? If it is not UB, is it guaranteed to always print the following:

o2.a=3
o2.b=4
o1.a=1
o1.b=3

In particular, I want to know what happens to the object pointed to by o1 when o2, which overlaps it, is written. Is it still allowed to access the unclobbered part (o1->a)? Is accessing the clobbered part o1->b simply the same as accessing o2->a?

How does effective type apply here? The rules are clear enough when you are talking about non-overlapping objects and pointers that point to the same location as the last store, but when you start talking about the effective type of portions of objects or overlapping objects it is less clear.

Would anything change if the the second write was of a different type? If the members were say int and short rather than two ints?

Here's a godbolt if you want to play with it there.


1 This answer applies to platforms where this isn't the case too: e.g., some might have size 4 and alignment 2. On a platform where the size and alignment were the same, this question wouldn't apply since aligned, overlapping objects would be impossible, but I'm not sure if there is any platform like that.

BeeOnRope
  • 60,350
  • 16
  • 207
  • 386
  • 2
    I'm pretty sure it's UB, but I'll let a language lawyer provide chapter and verse. – Barmar Apr 07 '20 at 00:27
  • I think that the C compiler on the old Cray vector systems forced alignment and size to be the same, with an ILP64 model and forced 64-bit alignment (addresses are 64-bit words -- no byte addressing). Of course this generated plenty of other problems.... – John D McCalpin Apr 09 '20 at 21:46

1 Answers1

15

Basically this is all grey area in the standard; the strict aliasing rule specifies basic cases and leaves the reader (and compiler vendors) to fill in the details.

There have been efforts to write a better rule but so far they haven't resulted in any normative text and I'm not sure what the status of this is for C2x.

As mentioned in my answer to your previous question, the most common interpretation is that p->q means (*p).q and the effective type applies to all of *p, even though we then go on to apply .q .

Under this interpretation, printf("o1.a=%d\n", o1->a); would cause undefined behaviour as the effective type of the location *o1 is not s (since part of it has been overwritten).

The rationale for this interpretation can be seen in a function like:

void f(s* s1, s* s2)
{
    s2->a = 5;
    s1->b = 6;
    printf("%d\n", s2->a);
}

With this interpretation the last line could be optimised to puts("5"); , but without it, the compiler would have to consider that the function call may have been f(o1, o2); and therefore lose all benefits that are purportedly provided by the strict aliasing rule.

A similar argument applies to two unrelated struct types that both happen to have an int member at different offset.

M.M
  • 138,810
  • 21
  • 208
  • 365
  • 1
    With `f(s* s1, s* s2)`, without `restrict`, the compiler cannot assume `s1` and `s2` are different pointers. I _think_, again without `restrict`, it cannot even assume they do not partially overlap. IAC, I do not see that OP's concern is well demo'd by the `f()` analogy. Good luck unsnarling. UV for first half. – chux - Reinstate Monica Apr 07 '20 at 01:45
  • @chux-ReinstateMonica without restrict, `s1 == s2` would be allowed , but not partial overlap. (The optimization in my code example could still be performed if `s1 == s2`) – M.M Apr 07 '20 at 02:00
  • @chux-ReinstateMonica you could also consider the same issue with just `int` instead of structs (and a system with `_Alignof(int) < sizeof(int)`). – M.M Apr 07 '20 at 02:28
  • 3
    Status of this kind of question concerning effective type for C2x is pretty much open and still subject to debate in the study group. Be careful though with claiming equivalence of `p->q` and `(*p).q`. This might be true for the type interpreation as you state, but it is not true from an operational point of view. It is important for concurrent accesses to the same structure that an access of a member does not imply the access of any other member. – Jens Gustedt Apr 07 '20 at 07:50
  • Strict aliasing rule is about **access**. Left-hand side expression in the `E1.E2` expression doesn't perform access (I mean the whole `E1` expression. Some of its subexpressions may perform access. I.e. if `E1` is `(*p)`, then reading pointer value when evaluating `p` is access, but evaluation of `*p` or `(*p)` doesn't perform any access). Strict aliasing rule doesn't apply in case when there is no access. – Language Lawyer Apr 07 '20 at 11:54
  • @LanguageLawyer I agree the point is debatable, however major compiler vendors do consider it an access for effective type purpose, i.e. they perform TBAA as if there is never partial overlap of two objects (unless one is a member of the other of course), and a very good argument can be made for why this approach is taken (i.e. most cases of TBAA would not be possible otherwise because the function might have been called with pointers to subobjects of overlapping, unrelated structs). Feel free to write your own alternative answer – M.M Apr 07 '20 at 12:00
  • _major compiler vendors do consider it an access for effective type purpose_ But do not consider it for volatile semantics purpose. Is this consistent? – Language Lawyer Apr 07 '20 at 12:05
  • @LanguageLawyer It's inconsistent, which is a consequence of the fact that the current text falls far short of forming a rigorous specification. Compiler vendors do have to do something for all these situations and they are guided by the intent of the strict aliasing rule, not the inadequate text . – M.M Apr 07 '20 at 12:08
  • I think it would be correct to say that **read** happens when an lvalue is converted to a value as described in http://port70.net/~nsz/c/c11/n1570.html#6.3.2.1p2 and **write** happens through the left operand of the assignment operator (and in other equivalent cases like compound assignment or incr/decr). Strict aliasing rule apply only in these cases and not to the left operand in `E1.E2` or `E1->E2`. – Language Lawyer Apr 07 '20 at 14:56