16

This code prints different values after compiling with -O1 and -O2 (both gcc and clang):

#include <stdio.h>

static void check (int *h, long *k)
{
  *h = 5;
  *k = 6;
  printf("%d\n", *h);
}

union MyU
{
    long l;
    int i;
};

int main (void)
{
  union MyU u;
  check(&u.i, &u.l);
  return 0;
}

I think it should be undefined behavior, because of the pointer aliasing, but I cannot pinpoint exactly what part of the code is forbidden.

It does write to one union element and then read from the other, but according to Defect Report #283 that is allowed. Is it UB when the union elements are accessed through pointers rather than directly?

This question is similar to Accessing C union members via pointers, but I think that one was never fully answered.

Community
  • 1
  • 1
Tor Klingberg
  • 4,790
  • 6
  • 41
  • 51
  • 2
    Not that I'm an expert, but reading the link you provided in your last paragraph, it appears like you are violating the rule given in the accepted answer there: "The value of a union member other than the last one stored into (6.2.6.1)." Specifically, you write a 6 into `u.l` then read from `*h`, which points to `u.i`, making it what that post argues is "unspecified" behavior. – Turix Apr 06 '14 at 16:59
  • @Tor: It was, and your example is amply explained by the accepted answer. – Deduplicator Apr 06 '14 at 17:08
  • possible duplicate of [Accessing C union members via pointers](http://stackoverflow.com/questions/16804650/accessing-c-union-members-via-pointers) – Deduplicator Apr 06 '14 at 17:08
  • 1
    6.2.6.1.7 of the ISO standard says: "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." – mfro Apr 06 '14 at 17:14
  • In C++ writing to one union member and then reading from an other is undefined, but in C99 it seems to be merely unspecified (or implementation defined). Naturally, the result of type punning must be implementation defined as it depends on the bit formats, but it should not differ between optimization levels. – Tor Klingberg Apr 06 '14 at 17:20
  • The higher optimization level will probably force the compiler to keep the value of *h in a register or to reorder the assignment of *h closer to the `printf()` (since it's value is needed there again anyway). You can't tell for sure (since you are relying on undefined behaviour) as long as you don't inspect the generated assembly. – mfro Apr 06 '14 at 17:26
  • 2
    I found [this blog post](http://davmac.wordpress.com/2010/02/26/c99-revisited/) and an other defect report [#236](http://www.open-std.org/jtc1/sc22/wg14/www/docs/dr_236.htm). I think the answer is that a union has an "active member" (the member last written to) and the active member may only be changed through the union. Trying to change the active member by writing to a pointer to a union member is undefined behavior. – Tor Klingberg Apr 06 '14 at 17:44
  • @TorKlingberg: The optimization in question here is `-fstrict-aliasing`, which is enabled with `-O2`, `-O3` and `-Os`. You can turn it off with `-fno-strict-aliasing` in order to test. – rici Apr 06 '14 at 17:45
  • 1
    @Deduplicator, I think the suggested duplicate is different. The aliasing rules specify that it's OK to use `char` to alias other types , however in this post it is `int` and `long` being aliased, so "unspecified" is no longer the right answer IMO; violation of aliasing rules trumps this. – M.M Apr 06 '14 at 23:43

5 Answers5

2

It took me a while to realize what the crux of the issue is here. DR236 discusses it. The issue is actually about passing pointers to a function which point to overlapping storage; and whether the compiler is allowed to assume that such pointers may alias each other or not.

If we are just discussing aliasing of union members then it would be simpler. In the following code:

u.i = 5;
u.l = 6;
printf("%d\n", u.i);

the behaviour is undefined because the effective type of u is long; i.e. the storage of u contains a value that was stored as a long. But accessing these bytes via an lvalue of type int violates the aliasing rules of 6.5p7. The text about inactive union members having unspecified values does not apply (IMO); the aliasing rules trump that, and that text comes into play when aliasing rules are not violated, for example, when accessed via an lvalue of character type.

If we exchange the order of the first two lines above then the program would be well-defined.

However, things all seem to change when the accesses are "hidden" behind pointers to a function.

The DR236 addresses this via two examples. Both examples have check() as in this post. Example 1 mallocs some memory and passes h and k both pointing to the start of that block. Example 2 has a union similar to this post.

Their conclusion is that Example 1 is "unresolved", and Example 2 is UB. However, this excellent blog post points out that the logic used by DR236 in reaching these conclusions is inconsistent. (Thanks to Tor Klingberg for finding this).

The last line of DR236 also says:

Both programs invoke undefined behavior, by calling function f with pointers qi and qd that have different types but designate the same region of storage. The translator has every right to rearrange accesses to *qi and *qd by the usual aliasing rules.

(apparently in contradiction of the earlier claim that Example 1 was unresolved).

This quote suggests that the compiler is allowed to assume that two pointers passed to a function are restrict if they have different types, however I cannot find any wording in the Standard to this effect, or even addressing the issue of the compiler re-ordering accesses through pointers.

It has been suggested that the aliasing rules allow the compiler to conclude that an int * and a long * cannot access the same memory. However, Examples 1 and 2 flatly contradict this.

If the pointers had the same type, then I think we agree that the compiler cannot reorder the accesses, because they might both point to the same object. The compiler has to assume the pointers are not restrict unless specifically declared as such.

Yet, I fail to see the difference between this case, and the cases of Example 1 and 2.

DR236 also says:

Common understanding is that the union declaration must be visible in the translation unit.

which again contradicts the claim that Example 2 is UB, because in Example 2 all of the code is in the same translation unit.

My conclusion: it seems to me that the C99 wording indicates that the compiler should not be allowed to re-order *h = 5; and *k = 6; in case they alias overlapping storage. Notwithstanding the fact that the DR236 contradicts the C99 wording and does not clarify matters. But reading *h after that should cause undefined behaviour, so the compiler is allowed to generate output of 5 or 6 , or anything else.

In my reading, if you modify check() to be *k = 6; *h=5; then it should be well-defined to print 5. It'd be interesting to see whether a compiler still does something else in this case, and also the compiler's rationale if it does.

M.M
  • 138,810
  • 21
  • 208
  • 365
  • "But accessing these bytes via an lvalue of type int violates the aliasing rules of 6.5p7" The value is accessed via the union object which is one of the cases explicitly allowed by 6.5p7. 6.2.6.1p7 absolutely applies, in fact there's no other situation where it could apply. – tab Apr 07 '14 at 02:22
  • 1
    `u` has "aggregate or union type" as in 6.5p7, but `u.l` does not. As discussed in detail in the linked blog post, it's not very clear what that bullet point is trying to say, but the purpose could be to ensure that it is well-defined to write `union MyU v; v = u;`. – M.M Apr 07 '14 at 02:27
  • This is sooo complicated, but I think your answer isn't quite right (I'm the author of that blog post you link to). First, footnote 82 of C99 (95 in C11) specifically allows your example which you claim has undefined behavior. Being a footnote it's non-normative; the (contorted) reasoning that supports it is that the union member access operator operates on the union object (and extracts the member value from it) rather than accessing the member object directly (6.5.2.3p3). This is a different case than trying to access the members via pointers, in which case 6.5p7 comes into play. – davmac Apr 27 '15 at 09:32
  • (The footnote 82 I refer to was added in Technical Corrigendum #3, and isn't part of the original, uncorrected C99 spec. Also, I think you should ignore DR236 - it's inconsistent with both itself and with the standard, and although it calls for amendment of 6.5p6/p7 such amendment was never made. I honestly believe the committee members involved in that discussion just did not properly understand the issue. Anyway, compilers that I've tested seem to follow with my interpretation here). – davmac Apr 27 '15 at 09:45
  • @davmac I agree that DR236 seems muddled and should be disregarded. However I'm not sure if we are closer to a solution. I think the *intent* of the aliasing rules is that the OP code is UB if `h` and `k` point to overlapping storage. However I cannot see any text in C11 to support this for the union case, unless we (somewhat arbitrarily) decide that C11 footnote 95 only applies to accessing the union directly by the named member, and not by a pointer to that member or otherwise. (in which case strict aliasing would again trump the union aliasing) – M.M Apr 27 '15 at 10:32
  • Well, 6.5.2.3p3 defines the member access operator - "A postfix expression followed by the . operator and an identifier designates a member of a structure or union object." - therefore `u.i` accesses the containing union object but does not access the member object; rather it interprets the union contents as per the member type (and generates an lvalue that can be used to set the "active" member). This contrasts with access via a pointer. This interpretation explains why footnote 95 applies only to accessing the union directly via the named member. – davmac Apr 27 '15 at 11:31
  • @davmac: I wish the Standard had been written more like a protocol document, and had indicated what a assumptions a compiler was allowed to make and what it was allowed to do on the basis of those assumptions, rather than as a bunch of sometimes-ambiguous requirements for programs. Allowing a compiler to make assumptions about what will and won't alias can make some useful optimizations possible, but if some people reading a standard would conclude that a particular program is strictly-conforming while others would conclude that the same program invokes Undefined Behavior, that would imply... – supercat Jul 06 '15 at 17:06
  • ...that the Standard is defective. A protocol document could avoid such problems by having separate boundary-conditions by splitting "Compilers must handle cleanly programs conforming to X" and "Strictly-compliant code must conform to Y", such that code which abides by any plausible interpretation of Y would abide by all plausible interpretations of X, and code which would fail any plausible interpretation of X would fail all plausible interpretations of Y. – supercat Jul 06 '15 at 17:13
  • @supercat Indeed. While I believe that in general the consensus understanding of the standard by compiler vendors is fine, this understanding relies on occasional deviation from or addendum to the actual wording in the standard. There's a lot of inconsistent use of terms which is something you shouldn't see in a formal document defining a standard. I have a complete list of (IMO) some of the more serious issues here: https://davmac.wordpress.com/c99-errata/ – davmac Jul 07 '15 at 09:47
  • @davmac: A more general problem I think is that some people seem unaware that C's popularity stems from the fact that while the Standard doesn't recognize distinct roles for compilers and underlying platforms, common historical practice has been for compilers to refrain from engaging in overly wacky behavior when the platform doesn't. Further, many forms of Undefined Behavior offer behavioral guarantees on many platforms which are quite weak but nonetheless very useful. For example, some platforms may guarantee that `u>>N` with will arbitrarily yield `u>>(N & 31) or... – supercat Jul 07 '15 at 17:47
  • ...`N > 31 ? 0 : u >> n`. Not as nice perhaps as guaranteeing one or the other form, but often useful nonetheless. Many programs have two requirements: (1) Given valid input, produce valid output; (2) Don't launch nuclear missiles even when given invalid input". IMHO, a good language standard should make it easy to write programs which will satisfy #1 as quickly as possible while upholding #2. Doing so, however, requires that a language provide ways by which programmers can give a compiler broad freedom without giving it total freedom--something that C lacks, but that could be added... – supercat Jul 07 '15 at 17:53
  • ...in ways which would not break the meaning of existing code--even [especially] code which presently relies upon actual or de-facto weak behavioral guarantees. – supercat Jul 07 '15 at 17:54
  • @davmac: I was just looking through that linked document; one of the biggest problems is that C89's rules were written in a way that didn't make sense when applied to heap storage, and compilers in 1989 didn't need aliasing rules for heap storage, but the authors of C99 wanted to pretend that they were "clarifying" rules rather than adding new ones. The net effect is a bunch of rules whose crippling effects extend far beyond any plausible benefit, since there's no plausible workaround except massive overuse of memcpy. I find it ironic that twenty years ago I found myself irked at... – supercat May 03 '16 at 06:59
  • ...the fact that C seemed to rely an awful lot on memcpy, but hoped the situation would improve. As it is, however, the need for memcpy has only increased with time. – supercat May 03 '16 at 07:00
  • 1
    @supercat these rules are being revised for C2X, see [N2012](http://www.open-std.org/JTC1/SC22/WG14/www/docs/n2012.htm) – M.M May 03 '16 at 07:19
  • @M.M: Some good stuff in there. I think there need to be more means via which implementations can promise constrained behaviors for things that are now UB, especially since such promises could allow code to be written more concisely and efficiently than if it has to be written as `if (safe_case) some_code; else safer_code;` but the generated machine code for "some_code" would have met the requirements for "safer_code" even though the source code would not. I also think the concept of pointer provenance is a good one, but there need to be some "escape hatches". – supercat May 03 '16 at 21:26
  • @M.M: IMHO, what C should really strive toward is to have a definition of "conditional-conformance" where if program X is conditionally conformant, any compiler Y may at its leisure either refuse compilation or run it in such fashion as to meet requirements, but Y must do one or the other. There is no way to define a dialect of C which would be supportable by all implementations but would correctly execute the entire corpus of C code, but it should be possible to define one which would allow programs to be cleanly rejected by all compilers that can't support them. – supercat May 03 '16 at 21:31
  • @M.M: One defect I see in the proposed document is that it fails to recognize the possibility that on platforms where the compiler doesn't know everything about the runtime environment (almost anything other than a VM) a program may receive pointers from the outside world, via I/O or other means, which identify storage the compiler can't possibly know anything about. – supercat May 05 '16 at 05:23
2

The relevant quote from the standard is the relevant aliasing rules which are violated. Violation of a normative shall always results in Undefined Behavior, so everything goes:

6.5 Expressions §7
An object shall have its stored value accessed only by an lvalue expression that has one of the following types:88)
— 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.

While main() does use a union, check() does not.

Deduplicator
  • 44,692
  • 7
  • 66
  • 118
  • What's your opinion of the alternate version of `check`, with `*k = 6; *h = 5; printf("%d\n", *h);` ? – M.M Apr 07 '14 at 01:18
  • @MattMcNabb: Still UB as far as I can see. Though I don't think anything catastrophic will happen in either case. – Deduplicator Apr 07 '14 at 01:20
  • 1
    There's no aliasing violation in this alternate version though (if the writes are not re-ordered) - `*h` is only read immediately after `*h` was written. Also, OP was being charitable by using `int` and `long`; if one type was `double` or a pointer type, then things could get hairy. – M.M Apr 07 '14 at 01:22
  • 2
    @MattMcNabb: Hm. Cannot find anything I'm sure would make the alternate version UB, so it might be ok. But I would not bet on it, nor on any specific compiler correctly following that understanding. Just too dangerous. – Deduplicator Apr 07 '14 at 01:30
  • @MattMcNabb I actually believe there is an aliasing violation in the alternate version (because the member present in the union after the store via `*k` has a declared type of `long`, the write via `*h` which refers to the same object via an incompatible type constitutes an aliasing violation. See my comment on your answer for more. However you are unlikely to ever see this cause a problem in practice). – davmac Apr 27 '15 at 09:52
  • (also it's not clear the initial store via `*k` would be legal, with similar reasoning). – davmac Apr 27 '15 at 09:59
  • @davmac I now agree with you – M.M Apr 27 '15 at 10:34
1

I have compiled your code with -O1 and -O2 and ran a gdb session, here is the output:

(gdb) r
Starting program: /home/sheri/test 
Breakpoint 1, main () at test.c:17
17  {
(gdb) s
19          check(&u.i, &u.l);
(gdb) p u
$1 = <optimized out>
(gdb) p u.i
$2 = <optimized out>
(gdb) p u.l
$3 = <optimized out>`

I am not a gdb expert but here are the things to note. 1. the union is not present in the stack but it's kept in a register, and that's why it prints when you print it, or it's i or l

I have disassembled the executable and looked at main, and here is what I found: 0000000000400440 :

400440: 48 83 ec 08             sub    $0x8,%rsp
400444: ba 06 00 00 00          mov    $0x6,%edx
400449: be 3c 06 40 00          mov    $0x40063c,%esi
40044e: bf 01 00 00 00          mov    $0x1,%edi
400453: 31 c0                   xor    %eax,%eax
400455: e8 d6 ff ff ff          callq  400430 <__printf_chk@plt>

So in line 2 the compiler pushed 0x6 into %edx register directly, and it didn't create the function check at first place, as it already figured out that the value that is passed to printf will always be 6.

May be u should try the same and see what output did you got in your machine.

silentnights
  • 813
  • 3
  • 11
  • 21
  • UB can be the expected behaviour or it can be something completely different. Just because it works with compiler on running doesn't mean it will have the same behaviour with all configurations of those. – dave Apr 07 '14 at 01:13
  • @dave Machine code can, however, show that something doesn't work in a reasonable way, which points to it either being undefined or there being a compiler bug. – Kaz Apr 07 '14 at 04:23
  • 1
    @Kaz my point was that showing that it does work however doesn't point to it not being undefined behaviour. And that's the problem with your answer. You can show that it isn't working in a reasonable way but can't show that it IS working reasonably. – dave Apr 26 '14 at 04:19
0

In C89, the code is perfectly legitimate unless one reads the Standard in such a way as to say that while taking the address of a struct or union member yields a pointer of the member type, the storage can't actually be accessed using that pointer unless it is first converted to a character type or passed to memcpy. If it's legal to use a pointer to a union member at all, nothing in the standard would suggest that it would be illegal to use it as you do above.

The C99 Standard wanted to allow compilers to be more aggressive with type-based aliasing, despite the fact that its "restrict" qualifier eliminates much of the need for it, but couldn't pretend that the above code wasn't legal, so it adds a requirement that if the compiler can see that the two pointers could possibly be members of the same union it must allow for that possibility. In the absence of whole-program optimization, this would allow most C89 programs to be made C99 compliant by ensuring that suitable union type definitions are visible in any functions that would see both pointer types. For your code to be valid under C99, you'd have to move the union type declaration above the function that receives the two pointers. That still won't make the code work for gcc, because the authors of gcc don't want to let details like correct standard-compliant behavior get in the way of generating "efficient" code.

supercat
  • 77,689
  • 9
  • 166
  • 211
-1

Taking the addresses is absolutely fine.

What's not fine: Reading an object using a different type than was used for writing it. So after writing to the int*, reading the long* is undefined behaviour and vice versa. Writing to the int*, then writing to the long* etc. is defined behaviour (the union now has its long member with a defined value).

gnasher729
  • 51,477
  • 5
  • 75
  • 98