문제

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.

도움이 되었습니까?

해결책

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.

다른 팁

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.

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.

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.

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

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top