Violating of strict-aliasing in C, even without any casting?
Asked Answered
P

7

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.)

Plankton answered 28/9, 2016 at 21:6 Comment(14)
Cannot reproduce on ideone: ideone.com/SUw1di. It probably uses an older version of gcc.Lipchitz
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?Arkhangelsk
I can reproduce on ideone, @2501. I've just added such a link to the questionPlankton
See also the analogous C++ question Violating strict aliasing without any casting.Sanchez
Can reproduce with gcc 6.2 and clang 3.8 using -O2.Witcher
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 languagePlankton
@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 reasonablePlankton
@AaronMcDaid dbush's answer seems to indicate that that is the problem.Arkhangelsk
@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 themPlankton
This question might interest you, it comes to the same conclusion as the accepted answer here.Disaffirm
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 fucntionPlankton
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.Lipchitz
Does volatile int * i = &u.i; prevent the undesired behavior?Glenda
@Glenda No. The keyword volatile does not change/avoid aliasing rules.Lipchitz
E
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.

Emmerich answered 28/9, 2016 at 21:23 Comment(3)
"... provided the memory is accessed through the union type" I guess that's the crucial portion, in this context.Plankton
This is not a problem of the compiler, but the code invoking undefined behaviour. The lvalues of the dereferenced pointers are not unions, 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.Wagram
@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.Homosexuality
W
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;
Wagram answered 28/9, 2016 at 21:45 Comment(4)
I guess the point is that *i and u.i are not really the same. Both are lvalue ints, and we know they refer to the same location. But the latter 'remembers' that it is inside a union.Plankton
@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 unions. The exception in the standard only applies to unions. Not sure, but as you write you know C++: I strongly doubt this is allowed in C++ either.Wagram
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 :)Infielder
@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.Wagram
M
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.

Mcfarland answered 29/9, 2016 at 6:49 Comment(2)
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...Homosexuality
...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.Homosexuality
D
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

Davy answered 28/9, 2016 at 22:2 Comment(0)
D
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.

Dorise answered 28/9, 2016 at 21:45 Comment(2)
As the lvalues are not unions, 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 malloced memory which also gets its type defined by the lvalue of the first write.Wagram
@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.Homosexuality
N
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.

Nisbet answered 28/9, 2016 at 21:13 Comment(1)
UB is UB. Anything can happen.Wagram
H
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.

Homosexuality answered 3/10, 2016 at 17:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.