Does access through pointer change strict aliasing semantics?
Asked Answered
N

3

6

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.

Narva answered 14/8, 2018 at 16:34 Comment(13)
Hm. I don't see any aliasing here...Vehement
@EugeneSh. The aliasing is implied. Writing headers through foo, reading them through bar, not seeing updates.Narva
Pointing to members of structs is not breaking any rules; you are still accessing a My_Header through a pointer to a My_Header, so you haven't really aliased anything.Blinnie
@HostileFork Are you asking what will happen if a single object is accessed via two different pointers? You could ask it in a simpler way...Vehement
@HostileFork Can you give a more specific example of this reading/writing?Miss
In any case, any compliant compiler will generate the very same results for both unless some restrict or volatile is coming into a play.Vehement
As I understand it from C11 6.5.2.3p6, if a Bar_Type and Foo_Type are not accessed via union member access, the compiler can behave as though the Bar_Type and Foo_Type do not overlap in memory even if they do in fact overlap.Smoothspoken
@Miss I've fleshed out the example (...though honestly I feel it's just a distraction over the core yes-or-no question, which remains at the end, and it's sounding like people are saying "no" but I'm wondering how it is that passing a pointer to a datatype that can appear in multiple structures would ever work otherwise.) :-/Narva
@HostileFork Passing a pointer to a type is not a problem, regardless of whether that type is contained in others. The problem is when you pass a pointer to one type where a pointer to another type is expected.Miss
@Miss Okay, but the question is...can you beat the "passing a pointer to one type where a pointer to another type is expected" by extracting a pointer to a contained common type...and does whether that common type is written (or read) through a pointer matter. I found a case where it made a difference (e.g. optimized build, wrote to a bar->header.bits = ..., registers didn't sync on reading a foo->header.bits at the same address). Used alternative assignment method above and it did sync in that optimized build. Quirk/glitch or emergent guarantee of pointer-based access?Narva
@HostileFork Now that's very relevant. If you can come up with a minimal reproducible example for both cases (along with compiler/version), that can go a long way to getting a definitive answer.Miss
@Miss Perhaps supercat's answer is sufficient explanation?Narva
@HostileFork: A compiler's ability to recognize cases where a pointer which is visibly derived from an lvalue of another type might be used to access objects of the latter type is a Quality-of-Implementation issue. Nothing in the rationale would suggest that they thought compiler writers would use the rules as an excuse to willfully ignore cases where such derivation is visible. From the point of view of the Standard, code which uses the structure pointer and code which accesses the member directly have equivalent (undefined) behavior, but quality compilers should handle both.Haulm
E
4

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.

Etude answered 15/8, 2018 at 2:1 Comment(18)
"you would think", but I had a real-world case in 2016 where changing the code from case one to case two caused a synchronization where there wasn't one previously. The origin of this question is that I'd tweaked things to use a byte access through char* and thus (I think) made the question irrelevant...but...at the time I justified the modification by saying the reason it worked was that I thought the by-pointer access made it work...because I couldn't figure out why otherwise you could ever guarantee mutations via pointer would work. It was reducing it to this question that gave me pause.Narva
@HostileFork It seems that some compiler vendors also take the view that Q.80 has the answer "No" (and that both of your code forms are UB)Etude
@HostileFork: Many tricks cause obtuse compilers to miss "optimizations" for a little while, until the authors "fix" them, but the only reliable way to use a higher-quality compiler like icc which supports low-level constructs.Haulm
@M.M: So far as I can tell, the "Effective Type" rules have never accurately described real compiler behaviors in all corner cases. What's important are places where a compiler would want to reorder an access to either consolidate with another access, or hoist it out of a loop or function, and needs to know if it will conflict with anything else, which in turn depends upon whether a compiler looking at the two points of interest would see any evidence of a conflict.Haulm
@M.M: Given struct foo { int x; } *p;, if a byte is accessed twice using p->xand no intervening operation has used an lvalue that could identifies the same structure or an element of the same array, that would suggest that consolidating the accesses is unlikely to pose a problem--even if intervening accesses dereference an int*. If, however, code between the two accesses derives a pointer from a struct foo*, consolidating the accesses to p->x is likely to pose a problem. Looking for evidence of conflict is simpler than worrying about effective type, but can actually allow more...Haulm
...useful optimizations. Note, for example, that very few programs would need blanket permission to use a char* to access things of every type if compilers treat a cast from struct foo* to char* that occurs between two accesses to a struct foo as evidence that the storage would be accessed via "unusual" means.Haulm
@Haulm I agree that it would be desirable for optimization purposes that Q.80 answer is "No", however the current text of the standard doesn't actually specify that (IMO).Etude
@M.M: Given e.g. void test(struct foo *p, struct bar *q, int mode) { p->x=1; q->y=2; if (mode) p->x=1;}, a compiler should be allowed to unconditionally write p->x` and q->y in either order (and such action would be reasonable even for a quality compiler in a non-conforming mode). That does not, however, mean that such behavior would be reasonable for a quality compiler given void test(struct foo *p, struct foo *q, int mode) { p->x=1; ((struct bar*)q)->x=2; if (mode) p->x=1;}, with a conversion from struct foo* to struct bar* occurring between the two operations on p->x.Haulm
@M.M: Actually, the C89 rules would be pretty reasonable if the footnote were amplified to say that quality compilers should only apply the rule in cases that would actually involve aliasing as written, noting that the following acts don't involve aliasing: (1) if a pointer or lvalue is derived from another, any use of the former which occurs in the context where it is derived and precedes the next operation using the original, is an operation on the original; (2) if a derived pointer is passed to a function, and the function accesses its storage exclusively using the derived type...Haulm
@Haulm What about void q(struct foo *p, struct foo *q) { test(p, (struct bar *)q, 1); } (using the first test from your last comment). You agree that test should be able to re-order the writes; how would a compiler building q cope with that possibility? Or do you intend to declare that this q causes UB? If so , what rule exactly would you propose?Etude
...or pointers derived from others of that type, accesses made by the function are accesses to the original [a compiler that is in a position to care about how a function would interact with operations in calling code would be in a position to know about the pointer derivation; a compiler that isn't in a position to know about the derivation should have no reason to care]. The only tricky issue is how a compiler should treat a void* of unknown provenance. It's too bad the Standard didn't distinguish "pointer to any type" and "pointer to unknown type", since it would be clear how...Haulm
...those should be handled. Unfortunately, it's harder to tell when void* should be treated as a pointer to any type, versus a pointer to a type which is unknown to the code holding it, but will be next used as its original type.Haulm
@M.M: The latter function would have defined behavior iff p->x and ((struct bar*)q)->y occupy disjoint storage, because it would generally be impractical for the compiler processing test to know that the pointers might be related. Under the present Standard, however, a compiler would be required to presume that they might be used to write the same storage because each write would change the Effective Type as needed, but IMHO that's more likely to result in needless pessimization than to actually be useful.Haulm
In the absence of things like volatile accesses to force synchronization, a function which receives pointers of different types should be entitled to presume that they will be used to access distinct regions of storage. Note, however, that such an assumption would merely say that p->x and ((struct bar*)q)->y won't alias; it would say nothing about whether q->y might identify storage associated with any other part of any other struct foo that might exist in the universe.Haulm
@M.M: Looking back at my comment and yours, I think I may have missed a phrase: "...or pointers derived within the execution of that called function from pointers of that type. If your q invokes the first test in such a way that p->x and ((struct bar*)q)->y alias, that would cause the storage accessed via the derived pointer of type bar to be accessed in conflicting fashion using a pointer of type bar, which is not derived within test from anything of type foo.Haulm
@Etude I feel you've answered my question, which I think was better in the original form before a bunch of people in comments acted like I was crazy and made me change it. Do you agree that reverting to what I asked originally and accepting your answer would be acceptable?Narva
@M.M: BTW, I think the biggest mis-step in the history of the C Standard was probably the resolution to Defect Report 28; it correctly identified that the given code invokes UB if the pointers alias, but the only reason it gave for that conclusion was that the Standard did not specify any particular behavior of writing one union member and reading another. That led to the conclusion which got incorporated in the Effective Type rules--that pointers of any type may be used to write an object, provided a pointer of the matching type is used to read it.Haulm
@HostileFork I did indeed answer the original form and it's preferable to not make major edits to a question after an answer was posted, so I would agree with revertingEtude
P
2

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.
}
Peerage answered 14/8, 2018 at 16:50 Comment(6)
Well, the issue is not that I am passing around pointers to My_Header, I'm passing pointers to Foo_Type (extracted from malloc'd regions) which may actually be to something that was initialized through a header pointer in a Bar_Type. I'd like the write of the bits to be guaranteed seen when reading through a foo header. My intuition was that the optimizer would not honor the access through pointer as being different in a reduced example, and you're saying the lack of honoring is due to an analysis of what it knows and doesn't know...pointer rules aside.Narva
@HostileFork I... don't really follow you. You could post a question with your real use case. It's difficult to understand what your code is exactly and what problem are you trying to avoid.Peerage
The requirement from a dereferenced pointer in C is to point to a valid object, otherwise it is UB. That is exactly to avoid issues with optimizing these accesses (well, one of the reasons).Vehement
@Peerage I've tried to update it with what I thought was obvious, but apparently was not. Hopefully clearer.Narva
I think that pattern is one where the authors of the Standard would have expected that some compilers might optimize, and didn't intend to forbid it (note that there is no blanket permission to use arbitrary lvalues of an aggregate's member type to access the aggregate. An lvalue which is derived from another is generally not regarded as "aliasing" the latter unless the latter is used within the former's active lifetime, and the footnote to N1570 6.5p7 indicates the rules are intended to indicate when compilers must allow for aliasing. In your last example...Haulm
...there are four ways global_p could refer to the same storage as foo: global_p could be derived from foo and used after foo->header.bits is accessed [aliasing], foo could be derived from global_p and used after global_p.bits is written [aliasing], foo might be derived from something else after global_p had been derived from it [aliasing], or global_p might have been derived after foo [aliasing]. If global_p had been derived from something of type struct Foo_Type after the read of header.bits, however, that would have been another story.Haulm
H
2

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.

Haulm answered 14/8, 2018 at 20:29 Comment(7)
I take it you're offering an example you're sure about (and have checked with gcc and clang), that seems less controversial--but still does not work. Hence what I'm describing, which is similar, probably can't be taken for granted in gcc or clang, either...?Narva
@HostileFork: The indicated test2 function will work correctly in most contexts, but it is not reliable. If it is called twice, both gcc and clang will optimize out the re-derivation of p from unionArr[i], and thus not recognize that it may interact with code that takes the address of already-active member v2 of unionArr[j] and writes to it.Haulm
@HostileFork: Added test1b() and test2b() which should work analogous to test1a and test2a. Both gcc and clang will get tripped up with the sequence test2a(i); test2b(j); test2a(i); in cases where i==j.Haulm
Unions bypass strict aliasing so this has nothing to do with the questionEtude
@M.M: The Standard does not recognize a difference between using an aggregate member directly, versus taking its address and using that, nor do its aliasing rules distinguish between structs and unions. Instead, support for almost any kind of member access is a Quality of Implementation issue. As it happens, present versions of gcc and clang support more flexible semantics with pointers to struct members than with struct-member-access lvalues, but less flexible semantics with pointers to union members than union-member-access lvalues.Haulm
@MM: As written, the Standard (deliberately, IMHO) would allow a compiler to assume that a struct will not be accessed using a seemingly-unrelated lvalue of member type. Taking the address of the member and using the resulting pointer might cause today's version of clang/gcc to miss the "optimization", but quality implementations should be able to access Common Initial Sequence members using a freshly-cast structure pointers, and I wouldn't trust low-quality implementations that can't handle that to refrain from "optimizing" code that takes the address of struct members and assuming...Haulm
...it won't be used to inspect Common Initial Sequence members of the "wrong" structure type.Haulm

© 2022 - 2024 — McMap. All rights reserved.