Does access through pointer change strict aliasing

2019-03-30 09:09发布

With these definitions:

struct My_Header { uintptr_t bits; }

struct Foo_Type { struct My_Header header; int x; }
struct Foo_Type *foo = ...;

struct Bar_Type { struct My_Header header; float x; }
struct Bar_Type *bar = ...;

Is it correct to say that this C code ("case one"):

foo->header.bits = 1020;

...is actually different semantically from this code ("case two"):

struct My_Header *alias = &foo->header;
alias->bits = 1020;

My understanding is that they should be different:

  • Case One considers the assignment unable to affect the header in a Bar_Type. It only is seen as being able to influence the header in other Foo_Type instances.

  • Case Two, by forcing access through a generic aliasing pointer, will cause the optimizer to realize all bets are off for any type which might contain a struct My_Header. It would be synchronized with access through any pointer type. (e.g. if you had a Foo_Type which was pointing to what was actually a Bar_Type, it could access through the header and reliably find out which it had--assuming that's something the header bits could tell you.)

This relies on the optimizer not getting "smart" and making case two back into case one.

3条回答
倾城 Initia
2楼-- · 2019-03-30 09:20

The way N1570 p5.6p7 is written, the behavior of code that accesses individual members of structures or unions will only be defined if the accesses are performed using lvalues of character types, or by calling library functions like memcpy. Even if a struct or union has a member of type T, the Standard (deliberately IMHO) refrains from giving blanket permission to access that part of the aggregate's storage using seemingly-unrelated lvalues of type T. Presently, gcc and clang seem to grant blanket permission for accessing structs, but not unions, using lvalues of member type, but N1570 p5.6p7 doesn't require that. It applies the same rules to both kinds of aggregates and their members. Because the Standard doesn't grant blanket permission to access structures using unrelated lvalues of member type, and granting such permission impairs useful optimizations, there's no guarantee gcc and clang will continue this behavior with with unrelated lvalues of member types.

Unfortunately, as can be demonstrated using unions, gcc and clang are very poor at recognizing relationships among lvalues of different types, even when one lvalue is quite visibly derived from the other. Given something like:

struct s1 {short x; short y[3]; long z; };
struct s2 {short x; char y[6]; };
union U { struct s1 v1; struct s2 v2; } unionArr[100];
int i;

Nothing in the Standard would distinguish between the "aliasing" behaviors of the following pairs of functions:

int test1(int i)
{
  return unionArr[i].v1.x;
}
int test2a(int j)
{
  unionArr[j].v2.x = 1;
}

int test2a(int i)
{
  struct s1 *p = &unionArr[i].v1;
  return p->x;
}
int test2b(int j)
{
  struct s2 *p = &unionArr[j].v2;
  p->x = 1;
}

Both of them use an lvalue of type int to access the storage associated with objects of type struct s1, struct s2, union U, and union U[100], even though int is not listed as an allowable type for accessing any of those.

While it may seem absurd that even the first form would invoke UB, that shouldn't be a problem if one recognizes support for access patterns beyond those explicitly listed in the Standard as a Quality of Implementation issue. According to the published rationale, the authors of the Standard thought compiler writers would to try to produce high-quality implementations, and it was thus not necessary to forbid "conforming" implementations from being of such low quality as to be useless. An implementation could be "conforming" without being able to handle test1a() or test2b() in cases where they would access member v2.x of a union U, but only in the sense that an implementation could be "conforming" while being incapable of correctly processing anything other than some particular contrived and useless program.

Unfortunately, although I think the authors of the Standard would likely have expected that quality implementations would be able to handle code like test2a()/test2b() as well as test1a()/test1b(), neither gcc nor clang supports them pattern reliably(*). The stated purpose of the aliasing rules is to avoid forcing compilers to allow for aliasing in cases where there's no evidence of it, and where the possibility of aliasing would be "dubious" [doubtful]. I've seen no evidence that they intended that quality compilers wouldn't recognize that code which takes the address of unionArr[i].v1 and uses it is likely to access the same storage as other code that uses unionArr[i] (which is, of course, visibly associated with unionArr[i].v2). The authors of gcc and clang, however, seem to think it's possible for something to be a quality implementation without having to consider such things.

(*) Given e.g.

int test(int i, int j)
{
  if (test2a(i))
    test2b(j);
  return test2a(i);
}

neither gcc nor clang will recognize that if i==j, test2b(j) would access the same storage as test2a(i), even when though both would access the same element of the same array.

查看更多
狗以群分
3楼-- · 2019-03-30 09:24

The code bar->header.bits = 1020; is exactly identical to struct My_Header *alias = &bar->header; alias->bits = 1020;.

The strict aliasing rule is defined in terms of access to objects through lvalues:

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

The only things that matter are the type of the lvalue, and the effective type of the object designated by the lvalue. Not whether you stored some intermediate stages of the lvalue's derivation in a pointer variable.


NOTE: The question was edited since the following text was posted. The following text applies to the original question where the space was allocated by malloc, not the current question as of August 23.

Regarding whether the code is correct or not. Your code is equivalent to Q80 effective_type_9.c in N2013 rev 1571, which is a survey of existing C implementations with an eye to drafting improved strict aliasing rules.

Q80. After writing a structure to a malloc’d region, can its members be accessed via a pointer to a different structure type that has the same leaf member type at the same offset?

The stumbling block is whether the code (*bar).header.bits = 1020; sets the effective type of only the int bits; or of the entire *bar. And accordingly, whether reading (*foo).header.bits reads an int, or does it read the entire *foo?

Reading only an int would not be a strict aliasing violation (it's OK to read int as int); but reading a Bar_Struct as Foo_Struct would be a violation.

The authors of this paper consider the write to set the effective type for the entire *bar, although they don't give their justification for that, and I do not see any text in the C Standard to support that position.

It seems to me there's no definitive answer currently for whether or not your code is correct.

查看更多
Evening l夕情丶
4楼-- · 2019-03-30 09:34

The fact that you have two structures which contain My_Header is a red herring and complicates your thinking without bringing anything new to the table. Your problem can be stated and clarified without any struct (other than My_Header ofcourse).

foo->header.bits = 1020;

The compiler clearly knows which object to modify.

struct My_Header *alias = &foo->header;
alias->bits = 1020;

Again the same is true here: with a very rudimentary analysis the compiler knows exactly which object the alias->bits = 1020; modifies.

The interesting part comes here:

void foo(struct My_Header* p)
{
   p->bits = 1020;
}

In this function the pointer p can alias any object (or sub-object) of type My_header. It really doesn't matter if you have N structures who contain My_header members or if you have none. Any object of type My_Header could be potentially modified in this function.

E.g.

// global:
struct My_header* global_p;

void foo(struct My_Header* p)
{
   p->bits = 1020;
   global_p->bits = 15;

   return p->bits;
   // the compiler can't just return 1020 here because it doesn't know
   // if `p` and `global_p` both alias the same object or not.
}

To convince you that the Foo_Type and Bar_Type are red herrings and don't matter look at this example for which the analysis is identical to the previous case who doesn't involve neither Foo_Type nor Bar_type:

// global:
struct My_header* gloabl_p;

void foo(struct Foo_Type* foo)
{
   foo->header.bits = 1020;
   global_p->bits = 15;

   return foo->header.bits;
   // the compiler can't just return 1020 here because it doesn't know
   // if `foo.header` and `global_p` both alias the same object or not.
}
查看更多
登录 后发表回答