C의 엄격한 별칭 위반, 주물 없음에도 불구하고?
어떻게 그럴 수가 있지?*i
그리고u.i
이 코드의 다른 숫자를 인쇄한다.i
로 정의된다.int *i = &u.i;
내가 여기서 UB를 촉발시켰다고 추측할 수 있을 뿐인데 어떻게 해야 정확한지 모르겠어.
(언어로 'C'를 선택하면 하나의 데모가 복제된다.그러나 @2501이 지적했듯이 'C99 엄격한'이 언어라면 그렇지 않다.하지만 또 다시, 난 문제가 생겼어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
)
내 이론은 이다.100
'정답'이다, 왜냐하면 노조원에게 쓴 글이 '정답'이다.short
-lvalue*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; }
가장 최근에 작성된 조합원이 아닌 다른 조합원으로부터 읽는 관행('형식 퍼닝')이 일반적이다.와도
-fstrict-aliasing
, 조합 유형을 통해 메모리에 액세스할 경우 형식 변환이 허용된다.그래서 위의 코드는 예상대로 작동한다.구조 조합 열거 및 비트 필드 구현을 참조하십시오.그러나 이 코드는 다음이 아닐 수 있다.int f() { union a_union t; int* ip; t.d = 3.0; ip = &t.i; return *ip; }
두 번째 코드 예는 정의되지 않은 행동을 나타내기 때문에 이 최적화를 이용하기 위해 구현을 준수하는 것이 완벽하게 허용된다는 점에 유의하십시오.Olaf와 다른 사람들의 답변을 참고하십시오.
물체는 다음 유형 중 하나를 가진 l값 식으로만 저장된 값에 접근해야 한다.
- ...
- 앞에서 언급한 조합원들 사이의 유형 중 하나를 포함하는 집합 또는 조합 유형(하위집합 또는 포함된 조합원 포함) 또는 문자 유형.
포인터의 lvalue 표현은 union
따라서 이 예외는 적용되지 않는다.컴파일러는 정의되지 않은 이 행동을 올바르게 이용하고 있다.
포인터의 유형 포인터를 에 대한 포인터로 만드십시오.union
각 구성원과 함께 타이핑 및 불합격.이 방법이 효과적임:
union {
...
} u, *i, *p;
엄격한 앨리어싱은 C표준에서 과소평가되지만, 통상적인 해석은 (엄격한 앨리어싱을 대체하는) 조합원이 이름으로 직접 접근할 때만 허용된다는 것이다.
이에 대한 근거를 위해 다음 사항을 고려하십시오.
void f(int *a, short *b) {
규칙의 목적은 컴파일러가a
그리고b
별칭을 지정하지 않고 다음에서 효율적인 코드를 생성f
. 그러나 만약 컴파일러가 그 사실을 허락해야만 한다면.a
그리고b
조합원이 중복될 수도 있어, 사실 그런 가정은 할 수 없었다.
두 포인터가 함수 매개변수인지 아닌지는 중요하지 않다. 엄격한 앨리어싱 규칙은 그것에 근거하여 차별화되지 않는다.
이 코드는 당신이 엄격한 앨리어싱 규칙을 존중하지 않기 때문에 실제로 UB를 호출한다. C99의 n1256 초안은 6.5 표현 §7:
물체는 다음 유형 중 하나를 가진 l값 식으로만 저장된 값에 접근해야 한다.
— 개체의 유효 유형과 호환되는 유형,
— 개체의 유효 유형과 호환되는 유형의 정규화된 버전,
— 개체의 유효 유형에 해당하는 서명 또는 서명되지 않은 유형,
— 개체의 유효 유형의 정규화된 버전에 해당하는 서명 또는 서명되지 않은 유형,
— 조합원 중 앞에서 언급한 유형 중 하나를 포함하는 골재 또는 조합 유형(하위집합 또는 포함 조합원 포함) 또는
— 문자 유형
그사이에*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("타입 펀닝"이라고도 하는 프로세스)에서 설명한 새로운 타입의 오브젝트 표현으로 재해석된다.이것은 함정 표현일 수도 있다.
비록 주식이 규범적이지는 않지만, 표준에 대한 더 나은 이해를 허용한다.언제u.s
을 통해 저장되어 있다*s
포인터, 짧은 값에 해당하는 바이트가 2 값으로 변경되었다.작은 엔디안 시스템을 가정하면, 100이 짧은 값보다 작기 때문에, int로서의 표현은 이제 고차 바이트가 0이었으므로 2가 되어야 한다.
TL/DR: 규범적이지는 않더라도, 노트 82는 x86 또는 x64 계열의 작은 엔디안 시스템에서는 그러한 것을 요구해야 한다.printf("u.i = %d\n", u.i);
인화하다 2.그러나 엄격한 앨리어싱 규칙에 따르면 컴파일러는 여전히 다음과 같은 값이 가리키는 것으로 가정할 수 있다.i
변경되지 않았으므로 인쇄할 수 있음100
당신은 C 표준의 다소 논란의 여지가 있는 영역을 조사하고 있다.
이것은 엄격한 앨리어싱 규칙이다.
물체는 다음 유형 중 하나를 가진 l값 식으로만 저장된 값에 접근해야 한다.
- 개체의 유효 유형과 호환되는 유형,
- 개체의 유효 유형과 호환되는 형식의 정규화된 버전,
- 개체의 유효 유형에 해당하는 서명 또는 서명되지 않은 유형,
- 개체의 유효 유형의 정규화된 버전에 해당하는 서명 또는 서명되지 않은 유형,
- 조합원들 사이에서 앞서 언급한 유형들 중 하나를 포함하는 골재 또는 조합 유형(하위집합 또는 포함된 조합원 포함,
- 성격 유형
(2011년, 6.5/7)
lvalue 식*i
활자를 가지고 있다.int
. lvalue 식*s
활자를 가지고 있다.short
. 이러한 유형들은 서로 호환되지 않으며, 다른 특정 유형과 양립할 수 없으며, 포인터들이 앨리어싱된 경우 두 가지 접근 모두를 준수할 수 있는 다른 대안이 엄격한 앨리어싱 규칙에도 없다.
적어도 하나 이상의 액세스가 부적합할 경우 동작이 정의되지 않으므로 보고한 결과 또는 실제로 다른 결과는 전적으로 허용된다.실제로 컴파일러는 다음 명령어를 사용하여 할당 순서를 변경하는 코드를 생성해야 한다.printf()
통화 또는 이전에 로드된 값을 사용하는*i
기억에서 다시 읽는 대신 레지스터에서 읽거나 비슷한 것들로 말이야
앞서 언급한 논쟁은 때때로 사람들이 95번 각주를 가리키기 때문에 발생한다.
조합 오브젝트의 내용을 읽을 때 사용한 멤버가 오브젝트에 값을 저장하기 위해 마지막으로 사용한 멤버와 동일하지 않을 경우, 해당 값의 오브젝트 표현 중 적절한 부분을 6.2.6('형 펀닝'이라고도 하는 프로세스)에서 설명한 새로운 타입의 오브젝트 표현으로 재해석한다.이것은 함정 표현일 수도 있다.
그러나 각주는 정보적인 것이 아니라 규범적인 것이 아니기 때문에 만약 그것들이 충돌한다면 어떤 텍스트가 승리하는지는 정말 의심의 여지가 없다.개인적으로 나는 이 각주를 단순히 시행지침으로 받아들이며 조합원들을 위한 창고가 겹친다는 사실의 의미를 명확히 한다.
옵티마이저가 마법을 부린 결과로 보인다.
와 함께-O0
, 두 선 모두 예상대로 100을 인쇄한다.와 함께-O2
, 약간의 재주문이 진행되고 있다.
gdb는 다음과 같은 출력을 제공한다.
(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 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.
이 문제에 대한 합리적인 해결책은 C가 다중 컴파일 모드가 있어야 하는 충분히 다양한 목적을 위해 사용된다는 것을 인식하는 것이다. 하나의 필수 모드는 기본 스토리지를 직접 읽고 쓰는 것처럼 주소가 취해진 모든 접속을 다루며, 예상되는 코드와 호환될 것이다.모든 수준의 포인터 기반 펀칭 지원다른 모드는 C11보다 더 제한적일 수 있다. 단, 한 유형으로 사용된 저장소를 언제 어디서 재해석해야 하는지를 나타내기 위해 코드를 명시적으로 사용하는 경우를 제외하고는 C11보다 더 제한적일 수 있다.다른 모드에서는 일부 최적화를 허용하지만 더 엄격한 방언으로 세분되는 일부 코드를 지원할 수 있다. 특정 방언에 대한 특정 지원이 없는 컴파일러는 보다 정의된 앨리어싱 동작으로 하나를 대체할 수 있다.
'programing' 카테고리의 다른 글
Vuetify Select 입력으로 어레이 데이터를 로드하는 방법 (0) | 2022.04.23 |
---|---|
자바에서 null이란 무엇인가? (0) | 2022.04.23 |
유사한 기능 구성요소에 대한 기존 Vue 구조(Vuex 포함) (0) | 2022.04.23 |
C구조와 C++구조 (0) | 2022.04.23 |
vue-properties가 중첩된 경로에 템플릿을 렌더링하지 않음 (0) | 2022.04.22 |