캐스팅 없이도 C의 엄격한 앨리어싱 위반?
어떻게 수 *i
와 u.i
,이 코드에 다른 번호를 인쇄 비록 i
로 정의된다 int *i = &u.i;
? 여기서 UB를 트리거한다고 가정 할 수는 있지만 정확히 어떻게되는지 알 수 없습니다.
( ideone 데모는 'C'를 언어로 선택하면 복제됩니다.하지만 @ 2501이 지적했듯이 'C99 strict'가 언어 인 경우가 아닙니다. 그러나 다시 문제가 발생합니다 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,와 함께 -fstrict-aliasing -std=c99 -O2
, 또한 함께 -std=c11
)
내 이론은 -lvalue를 100
통해 조합 구성원에 대한 쓰기가 (이 플랫폼 / 엔디안 / 무엇이든) 정의 되기 때문에 '정답' 입니다. 그러나 나는 옵티마이 저가 쓰기가 앨리어싱 할 수 있다는 것을 깨닫지 못한다고 생각합니다. 따라서 그것이 영향을 줄 수있는 유일한 라인 이라고 생각합니다 . 이것은 합리적인 이론입니까?short
*s
*s
u.i
*i=2;
*i
경우 *s
캔 별칭 u.i
및 u.i
별명이 수 *i
후 반드시 컴파일러는 생각해야 *s
캔 별칭을 *i
? 앨리어싱이 '전 이적'이어야하지 않습니까?
마지막으로, 저는 항상 엄격한 앨리어싱 문제가 잘못된 캐스팅으로 인해 발생한다고 가정했습니다. 그러나 이것에는 캐스팅이 없습니다!
(내 배경은 C ++입니다. 여기서 C에 대해 합리적인 질문을하고 싶습니다. 내 (제한된) 이해는 C99에서 한 조합원을 통해 작성한 다음 다른 조합원을 통해 읽는 것이 허용된다는 것입니다. 유형.)
불일치는 -fstrict-aliasing
최적화 옵션에 의해 발행됩니다 . 그 동작과 가능한 트랩은 GCC 문서에 설명되어 있습니다 .
다음과 같은 코드에 특별한주의를 기울이십시오.
union a_union { int i; double d; }; int f() { union a_union t; t.d = 3.0; return t.i; }
가장 최근에 쓴 조합원이 아닌 다른 조합원으로부터 읽는 관행 ( "타입 -punning"이라고 함)이 일반적입니다. 을 사용하더라도 공용체 유형을 통해 메모리에 액세스하는 경우
-fstrict-aliasing
type-punning이 허용 됩니다 . 따라서 위의 코드는 예상대로 작동합니다. 구조 공용체 열거 및 비트 필드 구현을 참조하십시오 . 그러나이 코드는 다음과 같지 않을 수 있습니다 .int f() { union a_union t; int* ip; t.d = 3.0; ip = &t.i; return *ip; }
두 번째 코드 예제는 정의되지 않은 동작을 나타내므로 준수 구현은이 최적화를 완벽하게 활용할 수 있습니다 . 참조를 위해 올라프 와 다른 사람들의 답변을 참조하십시오.
C 표준 (예 : C11, n1570), 6.5p7 :
객체는 다음 유형 중 하나를 가진 lvalue 표현식에 의해서만 액세스되는 저장된 값을 가져야합니다 .
- ...
- 멤버 (재귀 적으로, 하위 집계 또는 포함 된 공용체의 멤버 포함) 중에서 앞서 언급 한 유형 중 하나를 포함하는 집계 또는 공용체 유형 또는 문자 유형.
당신의 포인터의 좌변 표현은 하지 union
따라서이 예외가 적용되지 않는 유형. 컴파일러는이 정의되지 않은 동작을 악용하는 것이 정확합니다.
포인터의 유형을 유형에 대한 포인터로 union
만들고 각 멤버로 역 참조하십시오. 작동합니다.
union {
...
} u, *i, *p;
엄격한 앨리어싱은 C 표준에서 지정되지 않았지만 일반적인 해석은 유니온 멤버가 이름으로 직접 액세스되는 경우에만 유니온 앨리어싱 (엄격한 앨리어싱을 대체 함)이 허용된다는 것입니다.
이에 대한 근거를 위해 다음을 고려하십시오.
void f(int *a, short *b) {
규칙의 의도는 컴파일러가 별칭을 가정 a
하고 b
별칭을 사용하지 않고 f
. 컴파일러는 사실을 수 있도록했다하지만 a
및 b
조합원 중첩 될 수있다, 실제로 그 가정을 만들 수 없습니다.
두 포인터가 함수 매개 변수인지 아닌지 여부는 중요하지 않지만 엄격한 앨리어싱 규칙은이를 기반으로 구분하지 않습니다.
이 코드는 엄격한 앨리어싱 규칙을 따르지 않기 때문에 실제로 UB를 호출합니다. 6.5 표현식 §7에있는 C99 상태의 n1256 초안 :
객체는 다음 유형 중 하나를 가진 lvalue 표현식에 의해서만 액세스되는 저장된 값을 가져야합니다
.-객체
의 유효 유형과 호환되는 유형
- 객체의 유효 유형과 호환되는 유형의 정규화 된 버전 - 객체의 유효 유형에 해당하는 서명되거나 서명되지 않은 유형 인 유형
- 객체의 유효 유형의 정규화 된 버전에 해당하는 서명되거나 서명되지 않은 유형 인 유형
-하나를 포함하는 집계 또는 공용체 유형 멤버들 사이에서 앞서 언급 한 유형들 (재귀 적으로, 부분 집계 또는 포함 된 공용체의 멤버 포함), 또는
-문자 유형.
사이 *i = 2;
와 printf(" *i = %d\n", *i);
짧은 개체가 수정됩니다. 엄격한 앨리어싱 규칙의 도움으로 컴파일러는 가리키는 int 개체 i
가 변경되지 않았다고 가정 할 수 있으며 주 메모리에서 다시로드하지 않고도 캐시 된 값을 직접 사용할 수 있습니다.
정상적인 인간이 기대하는 것은 노골적으로는 아니지만 엄격한 앨리어싱 규칙은 컴파일러가 캐시 된 값을 사용하도록 최적화 할 수 있도록 정확하게 작성되었습니다.
두 번째 인쇄물의 경우 공용체는 6.2.6.1 형식 표현 / 일반 §7에서 동일한 표준으로 참조됩니다.
유니온 유형의 개체 멤버에 값이 저장되면 해당 멤버에 해당하지 않지만 다른 멤버에 해당하는 개체 표현의 바이트는 지정되지 않은 값을 사용합니다.
따라서 u.s
저장된 대로 표준에 의해 지정되지 않은u.i
값을 취했습니다.
그러나 나중에 6.5.2.3 구조 및 조합원 §3 참고 82에서 읽을 수 있습니다.
공용체 개체의 내용에 액세스하는 데 사용 된 멤버가 개체에 값을 저장하는 데 마지막으로 사용 된 멤버와 같지 않은 경우 값의 개체 표현 중 적절한 부분은 다음과 같이 새 형식의 개체 표현으로 재 해석됩니다. 6.2.6에 설명되어 있습니다 ( "유형 제거"라고도하는 프로세스). 이것은 트랩 표현 일 수 있습니다.
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
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.
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.
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.
'Programing' 카테고리의 다른 글
소스를 찾을 수 없지만 일부 또는 모든 이벤트 로그를 검색 할 수 없습니다. (0) | 2020.12.12 |
---|---|
클래스를 찾을 수 없음 : Android 스튜디오에서 단위 테스트를 실행할 때 빈 테스트 모음 (0) | 2020.12.12 |
광고 차단기가 앱에서 광고를 차단하지 못하도록하는 방법 (0) | 2020.12.12 |
크롬 확장 프로그램에 정보를 로컬로 저장하려면 어떻게해야합니까? (0) | 2020.12.12 |
applyBindings에서 Knockout 발생 클릭 바인딩 (0) | 2020.12.12 |