58

How can *i and u.i print different numbers in this code, even though i is defined as int *i = &u.i;? I can only assuming that I'm triggering UB here, but I can't see how exactly.

(ideone demo replicates if I select 'C' as the language. But as @2501 pointed out, not if 'C99 strict' is the language. But then again, I get the problem with gcc-5.3.0 -std=c99!)

// gcc       -fstrict-aliasing -std=c99   -O2
union
{   
    int i;
    short s;
} u;

int     * i = &u.i;
short   * s = &u.s;

int main()
{   
    *i  = 2;
    *s  = 100;

    printf(" *i = %d\n",  *i); // prints 2
    printf("u.i = %d\n", u.i); // prints 100

    return 0;
}

(gcc 5.3.0, with -fstrict-aliasing -std=c99 -O2, also with -std=c11)

My theory is that 100 is the 'correct' answer, because the write to the union member through the short-lvalue *s is defined as such (for this platform/endianness/whatever). But I think that the optimizer doesn't realize that the write to *s can alias u.i, and therefore it thinks that *i=2; is the only line that can affect *i. Is this a reasonable theory?

If *s can alias u.i, and u.i can alias *i, then surely the compiler should think that *s can alias *i? Shouldn't aliasing be 'transitive'?

Finally, I always had this assumption that strict-aliasing problems were caused by bad casting. But there is no casting in this!

(My background is C++, I'm hoping I'm asking a reasonable question about C here. My (limited) understanding is that, in C99, it is acceptable to write through one union member and then reading through another member of a different type.)

Aaron McDaid
  • 26,501
  • 9
  • 66
  • 88
  • Cannot reproduce on ideone: https://ideone.com/SUw1di. It probably uses an older version of gcc. – 2501 Sep 28 '16 at 21:11
  • I'm not sure if this would cause this, but couldn't the compiler think it can move the first print statement before `*s = 100;`? Not a language lawyer, but maybe check for that in the assembled code? – Adam Martin Sep 28 '16 at 21:12
  • I can reproduce on ideone, @2501. I've just added such a link to the question – Aaron McDaid Sep 28 '16 at 21:12
  • See also the analogous C++ question [Violating strict aliasing without any casting](http://stackoverflow.com/questions/39757246/violating-strict-aliasing-even-without-any-casting). – Jonathan Leffler Sep 28 '16 at 21:16
  • 1
    Can reproduce with gcc 6.2 and clang 3.8 using -O2. – Baum mit Augen Sep 28 '16 at 21:19
  • 1
    I've just tweaked the start of the question to point out that the behaviour on ideone depends on whether 'C' or 'C99 strict' is selected as the language – Aaron McDaid Sep 28 '16 at 21:19
  • @AdamMartin, I'm looking at the assember now, but it's a mystery to me :-). I'll try to simplify the C a little further, in the hope that the assember will seem more reasonable – Aaron McDaid Sep 28 '16 at 21:23
  • @AaronMcDaid dbush's answer seems to indicate that that is the problem. – Adam Martin Sep 28 '16 at 21:27
  • @AdamMartin, on second thoughts, I guess that's it. The two lines you refer to involve different types of lvalue, `int` and `short`, and there is no union in sight in either of those two lines. Therefore, it can swap them – Aaron McDaid Sep 28 '16 at 21:27
  • 2
    [This question](http://stackoverflow.com/questions/38798140/is-the-strict-aliasing-rule-incorrectly-specified/) might interest you, it comes to the same conclusion as the accepted answer here. – alain Sep 28 '16 at 21:35
  • Thanks @alain, I think I saw that recently too. And I was always happy to see, when the pointers are passed into another function, that the compiler can't be expected to have perfect knowledge of what might alias. I find this question here interesting because it all happens within one fucntion – Aaron McDaid Sep 28 '16 at 21:46
  • This happens simply because the two pointers may not alias. It breaks strict aliasing. Even though they point to valid objects of correct respective types, they still alias, and they should not. – 2501 Sep 28 '16 at 21:59
  • Does `volatile int * i = &u.i;` prevent the undesired behavior? – infixed Sep 29 '16 at 13:43
  • @infixed No. The keyword volatile does not change/avoid aliasing rules. – 2501 Sep 29 '16 at 16:06

7 Answers7

56

The disrepancy is issued by -fstrict-aliasing optimization option. Its behavior and possible traps are described in GCC documentation:

Pay special attention to code like this:

      union a_union {
        int i;
        double d;
      };

      int f() {
        union a_union t;
        t.d = 3.0;
        return t.i;
      }

The practice of reading from a different union member than the one most recently written to (called “type-punning”) is common. Even with -fstrict-aliasing, type-punning is allowed, provided the memory is accessed through the union type. So, the code above works as expected. See Structures unions enumerations and bit-fields implementation. However, this code might not:

      int f() {
        union a_union t;
        int* ip;
        t.d = 3.0;
        ip = &t.i;
        return *ip;
      }

Note that conforming implementation is perfectly allowed to take advantage of this optimization, as second code example exhibits undefined behaviour. See Olaf's and others' answers for reference.

Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
Grzegorz Szpetkowski
  • 36,988
  • 6
  • 90
  • 137
  • 9
    "... provided the memory is accessed through the union type" I guess that's the crucial portion, in this context. – Aaron McDaid Sep 28 '16 at 21:32
  • 4
    This is not a problem of the compiler, but the code invoking undefined behaviour. The lvalues of the dereferenced pointers are **not** `union`s, thus violate the _effective type rule_ required by the standard. Using a compiler-option to work-around this is not the correct approach, as that can degrade optimisation quality. The correct way is to fix the actual fault in the code to become compliant. There is no reason not to make the pointers pointers to the `union`. – too honest for this site Sep 29 '16 at 13:24
  • @Olaf: The Standard makes no provision for an aggregate to be accessed using any lvalue of a non-character member type, *even if the lvalue in question happens to be a member-access expression*. It relies upon quality compilers making at least some effort to support the use of derived lvalues, but leaves the amount of effort as a Quality of Implementation issue. Given `union { uint32_t words[4]; uint16_t shorts[8];} u;` gcc can't even recognize the possibility of aliasing between `*(u.words+i)` and `*(u.shorts+j)` even though both lvalues are directly derived from the same union lvalue `u`. – supercat Jun 28 '18 at 16:55
18

C standard (i.e. C11, n1570), 6.5p7:

An object shall have its stored value accessed only by an lvalue expression that has one of the following types:

  • ...
  • an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union), or a character type.

The lvalue expressions of your pointers are not union types, thus this exception does not apply. The compiler is correct exploiting this undefined behaviour.

Make the pointers' types pointers to the union type and dereference with the respective member. That should work:

union {
    ...
} u, *i, *p;
too honest for this site
  • 12,050
  • 4
  • 30
  • 52
  • I guess the point is that `*i` and `u.i` are not really the same. Both are lvalue `int`s, and we know they refer to the same location. But the latter 'remembers' that it is inside a union. – Aaron McDaid Sep 28 '16 at 21:50
  • 1
    @AaronMcDaid: Problem is not the `union`, but the pointers. You declared them with distinct types, so the lvalues are **not** the same. And they are not `union`s. The exception in the standard only applies to `union`s. Not sure, but as you write you know C++: I strongly doubt this is allowed in C++ either. – too honest for this site Sep 28 '16 at 21:52
  • By your last statement.. do you mean `int * i = &u; short * s = &u`? And then you're free to assign an `int` or `short` to either pointer which you could then deref in the print statements? If you could provide example code, I'd be very grateful :) –  Sep 30 '16 at 15:59
  • 1
    @MattGalaxy: That would apparently be even worse and should generate a diagnostic for assigning the wrong type to the pointers. The lvalue for the read would be the same as in the original code. The pointers have to be declared pointers **to** that `union`. For this, of course the `union` needs a tag to be usable as distinct type, the pointers be declared with the union or a `typedef` for that `union` before any variable declaration. – too honest for this site Sep 30 '16 at 16:10
12

Strict aliasing is underspecified in the C Standard, but the usual interpretation is that union aliasing (which supersedes strict aliasing) is only permitted when the union members are directly accessed by name.

For rationale behind this consider:

void f(int *a, short *b) { 

The intent of the rule is that the compiler can assume a and b don't alias, and generate efficient code in f. But if the compiler had to allow for the fact that a and b might be overlapping union members, it actually couldn't make those assumptions.

Whether or not the two pointers are function parameters or not is immaterial, the strict aliasing rule doesn't differentiate based on that.

M.M
  • 138,810
  • 21
  • 208
  • 365
  • The strict aliasing rule also doesn't differentiate based upon whether the lvalues used for access are of the form `someAggregate.member`. What is *should* recognize as a distinction is whether an lvalue used to access an object has an active association. If code takes the address of one union member and passes it to a function, *and the function accesses the union exclusively via that pointer*, the pointer should be associated with the union. If it then takes the address of a different member, that should break the association of the first pointer with anything that will be accessed... – supercat Jul 04 '18 at 17:27
  • ...in conflicting fashion via the second. In the DR#028 example, it would be impossible for a function to receive pointers of distinct types that could both be actively associated with the same storage for purposes of conflicting accesses, so a compiler would be entitled to assume that wouldn't happen. It may be reasonable for the Standard to process a dialect where the addresses of union members are essentially useless, but quality implementations of such a dialect should treat the address-of operator as yielding a pointer type that is not compatible with a pointer-to-member type. – supercat Jul 04 '18 at 17:30
7

This code indeed invokes UB, because you do not respect the strict aliasing rule. n1256 draft of C99 states in 6.5 Expressions §7:

An object shall have its stored value accessed only by an lvalue expression that has one of the following types:
— a type compatible with the effective type of the object,
— a qualified version of a type compatible with the effective type of the object,
— a type that is the signed or unsigned type corresponding to the effective type of the object,
— a type that is the signed or unsigned type corresponding to a qualified version of the effective type of the object,
— an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union), or
— a character type.

Between the *i = 2; and the printf(" *i = %d\n", *i); only a short object is modified. With the help of the strict aliasing rule, the compiler is free to assume that the int object pointed by i has not been changed, and it can directly use a cached value without reloading it from main memory.

It is blatantly not what a normal human being would expect, but the strict aliasing rule was precisely written to allow optimizing compilers to use cached values.

For the second print, unions are referenced in same standard in 6.2.6.1 Representations of types / General §7:

When a value is stored in a member of an object of union type, the bytes of the object representation that do not correspond to that member but do correspond to other members take unspecified values.

So as u.s has been stored, u.i have taken a value unspecified by standard

But we can read later in 6.5.2.3 Structure and union members §3 note 82:

If the member used to access the contents of a union object is not the same as the member last used to store a value in the object, the appropriate part of the object representation of the value is reinterpreted as an object representation in the new type as described in 6.2.6 (a process sometimes called "type punning"). This might be a trap representation.

Although notes are not normative, they do allow better understanding of the standard. When u.s have been stored through the *s pointer, the bytes corresponding to a short have been changed to the 2 value. Assuming a little endian system, as 100 is smaller that the value of a short, the representation as an int should now be 2 as high order bytes were 0.

TL/DR: even if not normative, the note 82 should require that on a little endian system of the x86 or x64 families, printf("u.i = %d\n", u.i); prints 2. But per the strict aliasing rule, the compiler is still allowed to assumed that the value pointed by i has not changed and may print 100

Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
Serge Ballesta
  • 143,923
  • 11
  • 122
  • 252
6

You are probing a somewhat controversial area of the C standard.

This is the strict aliasing rule:

An object shall have its stored value accessed only by an lvalue expression that has one of the following types:

  • a type compatible with the effective type of the object,
  • a qualified version of a type compatible with the effective type of the object,
  • a type that is the signed or unsigned type corresponding to the effective type of the object,
  • a type that is the signed or unsigned type corresponding to a qualified version of the effective type of the object,
  • an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union),
  • a character type.

(C2011, 6.5/7)

The lvalue expression *i has type int. The lvalue expression *s has type short. These types are not compatible with each other, nor both compatible with any other particular type, nor does the strict aliasing rule afford any other alternative that allows both accesses to conform if the pointers are aliased.

If at least one of the accesses is non-conforming then the behavior is undefined, so the result you report -- or indeed any other result at all -- is entirely acceptable. In practice, the compiler must produce code that reorders the assignments with the printf() calls, or that uses a previously loaded value of *i from a register instead of re-reading it from memory, or some similar thing.

The aforementioned controversy arises because people will sometimes point to footnote 95:

If the member used to read the contents of a union object is not the same as the member last used to store a value in the object, the appropriate part of the object representation of the value is reinterpreted as an object representation in the new type as described in 6.2.6 (a process sometimes called ‘‘type punning’’). This might be a trap representation.

Footnotes are informational, however, not normative, so there's really no question which text wins if they conflict. Personally, I take the footnote simply as an implementation guidance, clarifying the meaning of the fact that the storage for union members overlaps.

John Bollinger
  • 160,171
  • 8
  • 81
  • 157
  • 1
    As the lvalues are not `union`s, the member apparently is not used. **Where** they point to is not really relevant here, but how (i.e. through which type) they are accessed (that is typical for C). There is some parallel to `malloc`ed memory which also gets its type defined by the lvalue of the first write. – too honest for this site Sep 28 '16 at 22:02
  • @Olaf: The type of lvalue of the form `aggregate.member` is the declared type of the member. The authors of C89 likely thought compiler writers would have the common sense to recognize that applying a member-access operator to an aggregate would cause quality compilers to make a bona fide effort to associate actions on the lvalue with actions on the aggregate, nothing in the Standard indicates that it is required in cases where that lvalue is used directly, nor that quality implementations should be limited to handling that exact case. – supercat Jul 04 '18 at 17:22
5

Looks like this is a result of the optimizer doing its magic.

With -O0, both lines print 100 as expected (assuming little-endian). With -O2, there is some reordering going on.

gdb gives the following output:

(gdb) start
Temporary breakpoint 1 at 0x4004a0: file /tmp/x1.c, line 14.
Starting program: /tmp/x1
warning: no loadable sections found in added symbol-file system-supplied DSO at 0x2aaaaaaab000

Temporary breakpoint 1, main () at /tmp/x1.c:14
14      {
(gdb) step
15          *i  = 2;
(gdb)
18          printf(" *i = %d\n",  *i); // prints 2
(gdb)
15          *i  = 2;
(gdb)
16          *s  = 100;
(gdb)
18          printf(" *i = %d\n",  *i); // prints 2
(gdb)
 *i = 2
19          printf("u.i = %d\n", u.i); // prints 100
(gdb)
u.i = 100
22      }
(gdb)
0x0000003fa441d9f4 in __libc_start_main () from /lib64/libc.so.6
(gdb)

The reason this happens, as others have stated, is because it is undefined behavior to access a variable of one type through a pointer to another type even if the variable in question is part of a union. So the optimizer is free to do as it wishes in this case.

The variable of the other type can only be read directly via a union which guarantees well defined behavior.

What's curious is that even with -Wstrict-aliasing=2, gcc (as of 4.8.4) doesn't complain about this code.

dbush
  • 205,898
  • 23
  • 218
  • 273
1

Whether by accident or by design, C89 includes language which has been interpreted in two different ways (along with various interpretations in-between). At issue is the question of when a compiler should be required to recognize that storage used for one type might be accessed via pointers of another. In the example given in the C89 rationale, aliasing is considered between a global variable which is clearly not part of any union and a pointer to a different type, and nothing in the code would suggest that aliasing could occur.

One interpretation horribly cripples the language, while the other would restrict the use of certain optimizations to "non-conforming" modes. If those who didn't to have their preferred optimizations given second-class status had written C89 to unambiguously match their interpretation, those parts of the Standard would have been widely denounced and there would have been some sort of clear recognition of a non-broken dialect of C which would honor the non-crippling interpretation of the given rules.

Unfortunately, what has happened instead is since the rules clearly don't require compiler writers apply a crippling interpretation, most compiler writers have for years simply interpreted the rules in a fashion which retains the semantics that made C useful for systems programming; programmers didn't have any reason to complain that the Standard didn't mandate that compilers behave sensibly because from their perspective it seemed obvious to everyone that they should do so despite the sloppiness of the Standard. Meanwhile, however, some people insist that since the Standard has always allowed compilers to process a semantically-weakened subset of Ritchie's systems-programming language, there's no reason why a standard-conforming compiler should be expected to process anything else.

The sensible resolution for this issue would be to recognize that C is used for sufficiently varied purposes that there should be multiple compilation modes--one required mode would treat all accesses of everything whose address was taken as though they read and write the underlying storage directly, and would be compatible with code which expects any level of pointer-based type punning support. Another mode could be more restrictive than C11 except when code explicitly uses directives to indicate when and where storage that has been used as one type would need to be reinterpreted or recycled for use as another. Other modes would allow some optimizations but support some code that would break under stricter dialects; compilers without specific support for a particular dialect could substitute one with more defined aliasing behaviors.

supercat
  • 77,689
  • 9
  • 166
  • 211