C에서 정의되지 않은 일반적인 동작에 대해 질문 할 때 때때로 엄격한 앨리어싱 규칙을 참조합니다.
그들은 무엇에 대한 이야기?
C에서 정의되지 않은 일반적인 동작에 대해 질문 할 때 때때로 엄격한 앨리어싱 규칙을 참조합니다.
그들은 무엇에 대한 이야기?
답변:
엄격한 앨리어싱 문제가 발생하는 일반적인 상황은 구조체 (예 : 장치 / 네트워크 메시지)를 시스템의 단어 크기 버퍼 ( uint32_t
s 또는 uint16_t
s에 대한 포인터)에 오버레이하는 경우 입니다. 포인터를 캐스팅하여 이러한 버퍼 또는 구조체에 버퍼를 오버레이하면 엄격한 앨리어싱 규칙을 쉽게 위반할 수 있습니다.
따라서 이런 종류의 설정에서 메시지를 보내려면 동일한 메모리 청크를 가리키는 두 개의 호환되지 않는 포인터가 있어야합니다. 그런 다음 순진하게 다음과 같은 시스템을 코딩 할 수 있습니다 sizeof(int) == 2
.
typedef struct Msg
{
unsigned int a;
unsigned int b;
} Msg;
void SendWord(uint32_t);
int main(void)
{
// Get a 32-bit buffer from the system
uint32_t* buff = malloc(sizeof(Msg));
// Alias that buffer through message
Msg* msg = (Msg*)(buff);
// Send a bunch of messages
for (int i =0; i < 10; ++i)
{
msg->a = i;
msg->b = i+1;
SendWord(buff[0]);
SendWord(buff[1]);
}
}
엄격한 앨리어싱 규칙은이 설정을 불법으로 만듭니다. 호환 가능한 유형 이 아니 거나 C 2011 6.5 단락 7 1에서 허용하는 다른 유형 중 하나 가 아닌 개체의 별칭을 지정하는 포인터 는 정의되지 않은 동작입니다. 불행히도, 당신은 여전히 이런 식으로 코딩 할 수 있습니다. 어쩌면 경고를 받고, 잘 컴파일되도록하고, 코드를 실행할 때 이상한 예기치 않은 동작이 발생합니다.
(GCC는 앨리어싱 경고를 제공하는 기능이 다소 일관성이없는 것으로 보이며 때로는 친숙한 경고를 제공하지만 때로는 그렇지 않습니다.)
이 동작이 정의되지 않은 이유를 확인하려면 엄격한 앨리어싱 규칙이 컴파일러를 구입하는 것에 대해 생각해야합니다. 기본적으로이 규칙을 사용하면 buff
모든 루프 실행 내용을 새로 고치기 위해 명령어 삽입에 대해 생각할 필요가 없습니다 . 대신, 앨리어싱에 대해 성가 시게 강요되지 않는 가정을 사용하여 최적화 할 때 , 루프가 실행되기 전에 해당 명령어를 생략 하고 CPU 레지스터에 로드 buff[0]
및 buff[1
]하고 루프 본문을 가속화 할 수 있습니다. 엄격한 앨리어싱이 도입되기 전에 컴파일러는 그 내용이 buff
언제 어디서나 변경 될 수 있는 편집증 상태에 있어야했습니다 . 따라서 성능을 높이고 대부분의 사람들이 포인터를 입력하지 않는다고 가정하면 엄격한 앨리어싱 규칙이 도입되었습니다.
예제가 고안되었다고 생각하면 버퍼를 대신 보내는 다른 함수에 버퍼를 전달하는 경우에도 발생할 수 있습니다.
void SendMessage(uint32_t* buff, size_t size32)
{
for (int i = 0; i < size32; ++i)
{
SendWord(buff[i]);
}
}
이 편리한 기능을 활용하기 위해 이전 루프를 다시 작성했습니다.
for (int i = 0; i < 10; ++i)
{
msg->a = i;
msg->b = i+1;
SendMessage(buff, 2);
}
컴파일러는 SendMessage를 인라인하려고 시도하기에 충분하거나 현명하지 않을 수 있으며, 다시 버프를로드하거나로드하지 않기로 결정할 수도 있습니다. SendMessage
별도로 컴파일 된 다른 API의 일부인 경우 에는 버프 내용을로드하라는 지침이있을 수 있습니다. 그런 다음 C ++에있을 수 있으며 컴파일러가 인라인 할 수 있다고 생각하는 템플릿 화 된 헤더 전용 구현입니다. 또는 자신의 편의를 위해 .c 파일에 작성한 것일 수도 있습니다. 어쨌든 정의되지 않은 동작이 계속 발생할 수 있습니다. 우리가 어떤 일이 벌어지고 있는지 아는 경우에도 여전히 규칙을 위반하므로 잘 정의 된 동작이 보장되지 않습니다. 따라서 단어 구분 버퍼를 취하는 함수를 래핑한다고해서 반드시 도움이되는 것은 아닙니다.
어떻게이 문제를 해결할 수 있습니까?
노조를 사용하십시오. 대부분의 컴파일러는 엄격한 앨리어싱에 대해 불평하지 않고이를 지원합니다. 이것은 C99에서 허용되고 C11에서 명시 적으로 허용됩니다.
union {
Msg msg;
unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
};
컴파일러에서 엄격한 앨리어싱을 비활성화 할 수 있습니다 ( gcc에서 f [no-] strict-aliasing ))
char*
시스템 단어 대신 앨리어싱에 사용할 수 있습니다 . 규칙은 char*
( signed char
및 포함 unsigned char
)에 대한 예외를 허용합니다 . 항상 char*
다른 유형의 별명으로 가정합니다 . 그러나 이것은 다른 방식으로는 작동하지 않습니다. 구조체 별칭에 문자 버퍼가 있다고 가정하지 않습니다.
초보자 조심
이것은 두 가지 유형을 서로 오버레이 할 때 하나의 잠재적 인 지뢰밭입니다. 엔디안 , 단어 정렬 및 구조체를 올바르게 패킹 하여 정렬 문제를 처리하는 방법 에 대해서도 배워야 합니다.
1 C 2011 6.5 7에서 lvalue가 액세스 할 수있는 유형은 다음과 같습니다.
unsigned char*
있다 .. char*
대신에 멀리 사용될 지도 모른다 ? 내가 사용하는 경향 unsigned char
보다는 char
위한 기본 유형으로 byte
내 바이트가 서명하지 않기 때문에 내가 (특히 오버 플로우 WRT) 서명 행동의 불확실성을 원하지 않는다
unsigned char *
것이 좋습니다.
uint32_t* buff = malloc(sizeof(Msg));
와 후속 통합 unsigned int asBuffer[sizeof(Msg)];
버퍼 선언의 크기는 다르며 정확하지 않습니다. malloc
전화는 버그 나 없음-the- (그것을하지 않는 것) 노조가 필요 이상 4 배 더 큰 것입니다 ... 나는 그것이 명확성을 위해 것을 이해하지만, 후드 4 바이트 정렬에 의존 less ...
내가 찾은 가장 좋은 설명은 Mike Acton의 엄격한 앨리어싱 이해 입니다. PS3 개발에 중점을 두었지만 기본적으로 GCC입니다.
기사에서 :
"엄격한 앨리어싱은 C (또는 C ++) 컴파일러에 의해 만들어진 것으로, 다른 유형의 객체에 대한 포인터를 역 참조하는 것은 결코 같은 메모리 위치를 참조하지 않을 것이라고 가정합니다."
기본적으로 만약 당신이 int*
어떤 메모리를 가리키는 메모리를 가리키고 있다면 int
a float*
를 그 메모리 를 가리키고 float
규칙을 어길 때 사용하십시오 . 코드가 이것을 존중하지 않으면 컴파일러의 최적화 프로그램이 코드를 손상시킬 가능성이 높습니다.
규칙에 대한 예외는이며 char*
모든 유형을 가리킬 수 있습니다.
이것은 C ++ 03 표준 의 3.10 섹션에있는 엄격한 앨리어싱 규칙입니다 (다른 답변은 좋은 설명을 제공하지만 규칙 자체는 제공하지 않았습니다).
프로그램이 다음 유형 중 하나 이외의 lvalue를 통해 오브젝트의 저장된 값에 액세스하려고하면 동작이 정의되지 않습니다.
- 객체의 동적 유형
- 객체의 동적 유형의 cv-qualified 버전
- 객체의 동적 유형에 해당하는 부호있는 유형 또는 부호없는 유형 인 유형
- cv-qualified 버전의 객체 동적 유형에 해당하는 부호있는 또는 부호없는 유형 인 유형
- 상기 구성원들 중 상기 유형들 중 하나를 포함하는 집합 또는 연합 유형 (재귀 적으로 하위 집합 또는 포함 된 연합의 구성원 포함)
- 객체의 다이나믹 타입의 (아마도 cv-qualified) 기본 클래스 타입 인 타입
char
또는unsigned char
유형입니다.
C ++ 11 및 C ++ 14 문구 (변경 사항 강조) :
프로그램 이 다음 유형 중 하나 이외 의 glvalue 를 통해 객체의 저장된 값에 액세스하려고 하면 동작이 정의되지 않습니다.
- 객체의 동적 유형
- 객체의 동적 유형의 cv-qualified 버전
- 객체의 동적 유형과 유사한 유형 (4.4에 정의 된대로)
- 객체의 동적 유형에 해당하는 부호있는 유형 또는 부호없는 유형 인 유형
- cv-qualified 버전의 객체 동적 유형에 해당하는 부호있는 또는 부호없는 유형 인 유형
- 그 중에서, 상기 종류 중 하나를 포함하는 전체 또는 조합 형태 요소 또는 비 정적 데이터 멤버 (포함한 재귀 원소 또는 비 - 정적 데이터 부재 subaggregate 또는 연합 포함)
- 객체의 다이나믹 타입의 (아마도 cv-qualified) 기본 클래스 타입 인 타입
char
또는unsigned char
유형입니다.
lvalue 대신 glvalue 와 집계 / 연합 사례의 명확화 라는 두 가지 변경 사항이 작았습니다 .
세 번째 변경은보다 강력한 보증을 제공합니다 (강한 앨리어싱 규칙을 완화 함). 이제 별칭에 안전한 유사한 유형 의 새로운 개념 .
또한 C 문구 (C99; ISO / IEC 9899 : 1999 6.5 / 7; ISO / IEC 9899 : 2011 §6.5 ¶7에서 정확히 동일한 문구가 사용됨) :
객체는 다음 유형 73) 또는 88) 중 하나를 가진 lvalue 표현식으로 만 저장된 값에 액세스해야합니다 .
- 객체의 유효 유형과 호환되는 유형
- 객체의 유효 유형과 호환되는 유형의 정규화 된 버전
- 객체의 유효 유형에 해당하는 부호있는 유형 또는 부호없는 유형 인 유형
- 객체의 유효 유형의 정규화 된 버전에 해당하는 부호있는 유형 또는 부호없는 유형 인 유형
- 상기 구성원들 중 상기 유형들 중 하나를 포함하는 집합 또는 연합 유형 (재귀 적으로 하위 집합의 구성원 또는 포함 된 연합을 포함) 또는
- 문자 유형
73) 또는 88) 이 목록의 목적은 개체의 별칭이 될 수도 있고 그렇지 않을 수도있는 환경을 지정하는 것입니다.
wow(&u->s1,&u->s2)
이라면 포인터를 수정하는 데 사용되는 경우에도 합법적이어야하며 u
, 이로 인해 대부분의 최적화가 무효화됩니다. 앨리어싱 규칙은 용이하게 설계되었습니다.
이것은 "엄격한 앨리어싱 규칙은 무엇이며 왜 우리는 신경 쓰는가?" 에서 발췌 한 것 입니다. 쓰기.
C 및 C ++에서 앨리어싱은 저장된 값에 액세스 할 수있는 표현식 유형과 관련이 있습니다. C 및 C ++에서 표준은 어떤 표현식 유형이 어떤 유형의 별명을 지정할 수 있는지 지정합니다. 컴파일러와 옵티마이 저는 앨리어싱 규칙을 엄격하게 따른다고 가정 할 수 있으므로 엄격한 앨리어싱 규칙 이라는 용어가 사용 됩니다. 허용되지 않는 유형을 사용하여 값에 액세스하려고하면 정의되지 않은 동작 ( UB ) 으로 분류됩니다 . 우리가 정의되지 않은 행동을 취하면 모든 베팅이 해제되고, 프로그램 결과는 더 이상 신뢰할 수 없습니다.
불행히도 엄격한 앨리어싱 위반으로 인해 우리는 종종 예상 결과를 얻습니다. 새로운 최적화 기능을 갖춘 컴파일러의 향후 버전은 우리가 유효하다고 생각한 코드를 손상시킬 가능성을 남겨 둡니다. 이는 바람직하지 않으며 엄격한 앨리어싱 규칙과이를 위반하지 않는 방법을 이해하는 것이 좋습니다.
우리가 왜 관심을 갖는지에 대해 더 이해하기 위해, 엄격한 별칭 지정 규칙을 위반할 때 발생하는 문제, 유형 정리에 사용되는 일반적인 기술이 종종 엄격한 별칭 지정 규칙을 위반하기 때문에 유형 펀칭 및 펀칭 유형을 올바르게 입력하는 방법에 대해 논의합니다.
몇 가지 예를 살펴보면 표준이 말하는 내용에 대해 자세히 이야기하고 몇 가지 추가 예를 검토 한 다음 엄격한 앨리어싱을 피하고 우리가 놓친 위반을 포착하는 방법을 볼 수 있습니다. 다음은 놀랍지 않아야 할 예제입니다 ( live example ).
int x = 10;
int *ip = &x;
std::cout << *ip << "\n";
*ip = 12;
std::cout << x << "\n";
우리는이 * INT 메모리에 포인팅가에 의해 점유 지능 이 유효한 앨리어싱이다. 옵티마이 저는 ip를 통한 할당 이 x가 차지하는 값을 업데이트 할 수 있다고 가정해야합니다 .
다음 예제는 정의되지 않은 동작으로 이어지는 앨리어싱을 보여줍니다 ( live example ).
int foo( float *f, int *i ) {
*i = 1;
*f = 0.f;
return *i;
}
int main() {
int x = 0;
std::cout << x << "\n"; // Expect 0
x = foo(reinterpret_cast<float*>(&x), &x);
std::cout << x << "\n"; // Expect 0?
}
foo 함수에서 우리는 int * 와 float *를 취합니다 .이 예제에서 우리는 foo 를 호출 하고이 예제에서 int 를 포함하는 동일한 메모리 위치를 가리 키도록 두 매개 변수를 설정합니다 . 은 참고 reinterpret_cast는 그것의 템플릿 매개 변수에 의해 지정하신 유형을 가지고있는 것처럼 표현을 치료하기 위해 컴파일러을 말하고있다. 이 경우 표현식 & x 가 float * 유형 인 것처럼 취급하도록 지시합니다 . 우리는 순진하게 두 번째 cout 의 결과 가 0 이 될 것으로 기대할 수 있지만 gcc와 clang 모두 -O2를 사용하여 최적화를 활성화 하면 다음과 같은 결과가 나타납니다.
0
1
정의되지 않은 동작을 호출했기 때문에 예상되지 않았지만 완벽하게 유효한 것입니다. 플로트 하지 유효 별명 할 수 INT의 객체입니다. 따라서 f를 통한 저장 이 int 객체에 유효하게 영향을 줄 수 없기 때문에 i 는 역 참조 할 때 i 가 반환 값이 될 때 저장된 상수 1을 가정 할 수 있습니다 . 컴파일러 탐색기에서 코드를 연결하면 이것이 정확히 무슨 일인지 알 수 있습니다 ( live example ).
foo(float*, int*): # @foo(float*, int*)
mov dword ptr [rsi], 1
mov dword ptr [rdi], 0
mov eax, 1
ret
TBAA (Type-Based Alias Analysis)를 사용하는 옵티마이 저는 1 이 리턴되고 상수 값을 리턴 값을 전달하는 레지스터 eax 로 직접 이동 한다고 가정 합니다. TBAA는로드 및 저장을 최적화하기 위해 별명을 지정할 수있는 유형에 대한 언어 규칙을 사용합니다. 이 경우 TBAA는 float 가 별칭과 int를 별칭으로 지정할 수 없다는 것을 알고 i 의로드를 최적화합니다 .
이 표준은 정확히 우리가 허용되고 허용되지 않는다고 무엇을 말합니까? 표준 언어는 간단하지 않으므로 각 항목마다 의미를 보여주는 코드 예제를 제공하려고합니다.
C11의 표준은 구역에 다음을 말한다 6.5 표현 제 7 항 :
객체는 다음 유형 중 하나를 갖는 lvalue 표현식으로 만 저장된 값에 액세스해야합니다. 88) — 객체의 유효 유형과 호환되는 유형,
int x = 1;
int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type int which is compatible with int
— 유효 객체 유형과 호환되는 유형의 정규화 된 버전
int x = 1;
const int *p = &x;
printf("%d\n", *p); // *p gives us an lvalue expression of type const int which is compatible with int
— 객체의 유효 유형에 해당하는 부호있는 유형 또는 부호없는 유형 인 유형
int x = 1;
unsigned int *p = (unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type unsigned int which corresponds to
// the effective type of the object
GCC / 그 소리는 확장자가 와 도 할당 허용하는 부호없는 INT *을 에 * INT 가 호환 유형이없는 경우에도 불구하고.
— 객체의 유효 유형의 정규화 된 버전에 해당하는 부호있는 유형 또는 부호없는 유형 인 유형
int x = 1;
const unsigned int *p = (const unsigned int*)&x;
printf("%u\n", *p ); // *p gives us an lvalue expression of type const unsigned int which is a unsigned type
// that corresponds with to a qualified verison of the effective type of the object
-구성원 중 상기 언급 된 유형 중 하나를 포함하는 집합 또는 조합 유형 (재귀 적으로 하위 집합 또는 포함 된 연합의 구성원 포함) 또는
struct foo {
int x;
};
void foobar( struct foo *fp, int *ip ); // struct foo is an aggregate that includes int among its members so it can
// can alias with *ip
foo f;
foobar( &f, &f.x );
— 문자 유형.
int x = 65;
char *p = (char *)&x;
printf("%c\n", *p ); // *p gives us an lvalue expression of type char which is a character type.
// The results are not portable due to endianness issues.
[basic.lval] 11 항의 C ++ 17 표준 초안 은 다음과 같습니다.
프로그램이 다음 유형 중 하나 이외의 glvalue를 통해 객체의 저장된 값에 액세스하려고하면 동작이 정의되지 않습니다. 63 (11.1) — 객체의 동적 유형,
void *p = malloc( sizeof(int) ); // We have allocated storage but not started the lifetime of an object
int *ip = new (p) int{0}; // Placement new changes the dynamic type of the object to int
std::cout << *ip << "\n"; // *ip gives us a glvalue expression of type int which matches the dynamic type
// of the allocated object
(11.2) — 객체의 동적 유형의 cv-qualified 버전
int x = 1;
const int *cip = &x;
std::cout << *cip << "\n"; // *cip gives us a glvalue expression of type const int which is a cv-qualified
// version of the dynamic type of x
(11.3) — 객체의 동적 유형과 유사한 유형 (7.5에 정의 된대로)
(11.4) — 객체의 동적 유형에 해당하는 부호있는 유형 또는 부호없는 유형
// Both si and ui are signed or unsigned types corresponding to each others dynamic types
// We can see from this godbolt(https://godbolt.org/g/KowGXB) the optimizer assumes aliasing.
signed int foo( signed int &si, unsigned int &ui ) {
si = 1;
ui = 2;
return si;
}
(11.5) — 객체의 동적 유형의 cv-qualified 버전에 해당하는 부호있는 또는 부호없는 유형 인 유형
signed int foo( const signed int &si1, int &si2); // Hard to show this one assumes aliasing
(11.6)-요소 또는 비 정적 데이터 멤버 (재귀 적으로 하위 집합 또는 포함 된 유니온의 요소 또는 비 정적 데이터 멤버 포함) 중 위에서 언급 한 유형 중 하나를 포함하는 집계 또는 통합 유형
struct foo {
int x;
};
// Compiler Explorer example(https://godbolt.org/g/z2wJTC) shows aliasing assumption
int foobar( foo &fp, int &ip ) {
fp.x = 1;
ip = 2;
return fp.x;
}
foo f;
foobar( f, f.x );
(11.7) — 객체의 동적 유형에 대한 (cv cv-qualified) 기본 클래스 유형 인 유형
struct foo { int x ; };
struct bar : public foo {};
int foobar( foo &f, bar &b ) {
f.x = 1;
b.x = 2;
return f.x;
}
(11.8) — 문자, 부호없는 문자 또는 std :: byte 유형.
int foo( std::byte &b, uint32_t &ui ) {
b = static_cast<std::byte>('a');
ui = 0xFFFFFFFF;
return std::to_integer<int>( b ); // b gives us a glvalue expression of type std::byte which can alias
// an object of type uint32_t
}
부호있는 char에 주목할 가치 가있는 것은 위의 목록에 포함되어 있지 않습니다 . 이것은 문자 유형 이라고 하는 C 와의 현저한 차이점입니다 .
우리는이 시점에 도달했고 왜 우리가 별명을 밝히고 싶을 지 궁금 할 것입니다. 대답은 일반적으로 pun 을 입력 하는 것이며, 종종 사용되는 방법이 엄격한 앨리어싱 규칙을 위반하는 것입니다.
때때로 우리는 타입 시스템을 우회하고 객체를 다른 타입으로 해석하려고합니다. 이것은 메모리 세그먼트를 다른 유형으로 재 해석하기 위해 유형 punning 이라고 합니다. 유형 제거 는 객체의 기본 표현에 액세스하여 보거나 전송하거나 조작하려는 작업에 유용합니다. 우리가 사용하는 타입 제거 (punning)는 컴파일러, 직렬화, 네트워킹 코드 등입니다.
전통적으로 이것은 객체의 주소를 가져 와서 재 해석하려는 유형의 포인터로 캐스팅 한 다음 값에 액세스하거나 다른 말로 별칭을 지정하여 수행되었습니다. 예를 들면 다음과 같습니다.
int x = 1 ;
// In C
float *fp = (float*)&x ; // Not a valid aliasing
// In C++
float *fp = reinterpret_cast<float*>(&x) ; // Not a valid aliasing
printf( "%f\n", *fp ) ;
앞에서 보았 듯이 이것은 유효한 앨리어싱이 아니므로 정의되지 않은 동작을 호출합니다. 그러나 전통적으로 컴파일러는 엄격한 앨리어싱 규칙을 이용하지 않았으며 이러한 유형의 코드는 일반적으로 작동했습니다. 개발자는 불행히도 이런 식으로 일하는 데 익숙해졌습니다. 유형 정리의 일반적인 대체 방법은 공용체를 사용하는 것입니다. 공용체는 C에서는 유효하지만 C ++ 에서는 정의되지 않은 동작 입니다 ( 실례 참조 ).
union u1
{
int n;
float f;
} ;
union u1 u;
u.f = 1.0f;
printf( "%d\n”, u.n ); // UB in C++ n is not the active member
이것은 C ++에서는 유효하지 않으며 일부는 노조의 목적이 변형 유형을 구현하기위한 것만 고려하고 유형 punning에 노조를 사용하는 것이 악용이라고 생각합니다.
C 및 C ++에서 유형 punning 의 표준 방법 은 memcpy 입니다. 이 손으로 조금 무거운를 보일 수 있지만, 최적화의 사용을 인식해야 방어 적이기 에 대한 형의 말장난을 하고 도망을 최적화하고 이동을 등록하는 레지스터를 생성합니다. 예를 들어 int64_t 가 double 과 크기가 같다는 것을 알고 있다면 :
static_assert( sizeof( double ) == sizeof( int64_t ) ); // C++17 does not require a message
우리는 memcpy 를 사용할 수 있습니다 :
void func1( double d ) {
std::int64_t n;
std::memcpy(&n, &d, sizeof d);
//...
충분한 최적화 수준에서 현대의 모든 컴파일러는 이전에 언급 한 reinterpret_cast 메소드 또는 punning 유형의 공용체 메소드와 동일한 코드를 생성합니다 . 생성 된 코드를 살펴보면 register mov ( live Compiler Explorer Example ) 만 사용한다는 것을 알 수 있습니다.
C ++ 20에서는 bit_cast ( proposal에서 링크로 사용 가능한 구현)를 얻을 수 있습니다.이를 통해 typepun을 간단하고 안전하게 수행하고 constexpr 컨텍스트에서 사용할 수 있습니다.
다음은 사용하는 방법에 대한 예입니다 bit_cast 말장난 입력 부호 INT 에 플로트 ( 가 살고 참조 )
std::cout << bit_cast<float>(0x447a0000) << "\n" ; //assuming sizeof(float) == sizeof(unsigned int)
경우 에 과 에서 유형이 같은 크기를 가지고 있지 않습니다, 그것은 중간 struct15를 사용하는 우리를 필요로한다. 우리는 포함하는 구조체 사용합니다 를 sizeof (서명되지 않은 int)를 문자 배열 ( 가정 4 바이트 부호없는 INT를 로) 에서 유형 및 부호없는 INT 는 AS가 하기 입력 :
struct uint_chars {
unsigned char arr[sizeof( unsigned int )] = {} ; // Assume sizeof( unsigned int ) == 4
};
// Assume len is a multiple of 4
int bar( unsigned char *p, size_t len ) {
int result = 0;
for( size_t index = 0; index < len; index += sizeof(unsigned int) ) {
uint_chars f;
std::memcpy( f.arr, &p[index], sizeof(unsigned int));
unsigned int result = bit_cast<unsigned int>(f);
result += foo( result );
}
return result ;
}
불행히도이 중간 유형이 필요하지만 이것이 bit_cast 의 현재 제약 조건입니다 .
우리는 C ++에서 엄격한 앨리어싱을 잡기위한 좋은 도구가 많지 않습니다. 우리가 가지고있는 툴은 엄격한 앨리어싱 위반의 경우와 잘못 정렬 된로드 및 저장의 경우를 잡을 것입니다.
플래그 -fstrict-aliasing 및 -Wstrict-aliasing을 사용하는 gcc는 오 탐지 / 음수가 아닌 경우도 일부 경우를 포착 할 수 있습니다. 예를 들어 다음과 같은 경우 gcc에서 경고가 생성됩니다 ( 실제 참조 ).
int a = 1;
short j;
float f = 1.f; // Originally not initialized but tis-kernel caught
// it was being accessed w/ an indeterminate value below
printf("%i\n", j = *(reinterpret_cast<short*>(&a)));
printf("%i\n", j = *(reinterpret_cast<int*>(&f)));
이 추가 사례를 포착하지는 않지만 ( 실제 참조 ) :
int *p;
p=&a;
printf("%i\n", j = *(reinterpret_cast<short*>(p)));
clang은 이러한 플래그를 허용하지만 실제로 경고를 구현하지는 않습니다.
우리가 사용할 수있는 또 다른 도구는 잘못 정렬 된로드와 저장을 잡을 수있는 ASan입니다. 이는 직접적인 앨리어싱 위반이 아니지만 엄격한 앨리어싱 위반의 일반적인 결과입니다. 예를 들어 -fsanitize = address를 사용하여 clang으로 빌드하면 다음과 같은 경우 런타임 오류가 발생합니다.
int *x = new int[2]; // 8 bytes: [0,7].
int *u = (int*)((char*)x + 6); // regardless of alignment of x this will not be an aligned address
*u = 1; // Access to range [6-9]
printf( "%d\n", *u ); // Access to range [6-9]
내가 추천 할 마지막 도구는 C ++ 전용이며 엄격하게 도구는 아니지만 코딩 방식이므로 C 스타일 캐스트를 허용하지 않습니다. gcc와 clang은 모두 -Wold-style- cast를 사용하여 C 스타일 캐스트에 대한 진단을 생성합니다 . 이렇게하면 정의되지 않은 유형의 펑이 reinterpret_cast를 사용하게되며, 일반적으로 reinterpret_cast는 자세한 코드 검토를위한 플래그 여야합니다. 감사를 수행하기 위해 reinterpret_cast에 대한 코드베이스를 검색하는 것이 더 쉽습니다.
C의 경우 이미 다룬 모든 도구가 있으며 C 언어의 큰 부분 집합에 대한 프로그램을 철저히 분석하는 정적 분석기 인 tis-interpreter도 있습니다. -fstrict-aliasing을 사용하는 경우에 한 가지 사례가 누락 된 이전 예제의 C 버전이 주어짐 ( 실제 참조 )
int a = 1;
short j;
float f = 1.0 ;
printf("%i\n", j = *((short*)&a));
printf("%i\n", j = *((int*)&f));
int *p;
p=&a;
printf("%i\n", j = *((short*)p));
tis-interpeter는 세 가지를 모두 포착 할 수 있으며 다음 예제에서는 tis-kernal을 tis-interpreter로 호출합니다 (출력은 간결하게 편집 됨).
./bin/tis-kernel -sa example1.c
...
example1.c:9:[sa] warning: The pointer (short *)(& a) has type short *. It violates strict aliasing
rules by accessing a cell with effective type int.
...
example1.c:10:[sa] warning: The pointer (int *)(& f) has type int *. It violates strict aliasing rules by
accessing a cell with effective type float.
Callstack: main
...
example1.c:15:[sa] warning: The pointer (short *)p has type short *. It violates strict aliasing rules by
accessing a cell with effective type int.
마지막으로 현재 개발중인 TySan 이 있습니다. 이 소독제는 섀도 메모리 세그먼트에 유형 검사 정보를 추가하고 앨리어싱 규칙을 위반하는지 확인합니다. 이 도구는 잠재적으로 모든 앨리어싱 위반을 포착 할 수 있어야하지만 런타임 오버 헤드가 클 수 있습니다.
reinterpret_cast
할 것인지 또는 무엇 cout
을 의미 하는지 알지 못하는 나와 같은 사람들에게는 따르기가 어렵습니다 . (C ++를 언급하는 것은 괜찮지 만 원래 질문은 C와 IIUC에 관한 것이 었습니다.이 예제는 C로 올바르게 작성 될 수 있습니다.)
엄격한 앨리어싱은 포인터만을 가리키는 것이 아니며 참조에도 영향을 미칩니다. 부스트 개발자 위키에 대한 논문을 작성했으며 컨설팅 웹 사이트의 페이지로 전환하기에 충분히 받아 들였습니다. 그것은 그것이 무엇인지, 왜 사람들을 그렇게 혼란스럽게하는지, 그것에 대해 어떻게 해야하는지 완전히 설명합니다. 엄격한 앨리어싱 백서 . 특히 C ++에서 유니온이 위험한 행동 인 이유와 memcpy를 사용하는 것이 C와 C ++ 모두에서 유일한 이식 가능한 이유를 설명합니다. 이것이 도움이 되길 바랍니다.
Doug T.가 이미 쓴 것에 대한 부록으로서, 다음은 아마도 gcc로 트리거하는 간단한 테스트 사례입니다.
check.c
#include <stdio.h>
void check(short *h,long *k)
{
*h=5;
*k=6;
if (*h == 5)
printf("strict aliasing problem\n");
}
int main(void)
{
long k[1];
check((short *)k,k);
return 0;
}
로 컴파일하십시오 gcc -O2 -o check check.c
. 컴파일러는 "h"가 "check"함수에서 "k"와 같은 주소 일 수 없다고 가정하기 때문에 일반적으로 (대부분의 gcc 버전에서 시도한) "엄격한 앨리어싱 문제"를 출력합니다. 이 때문에 컴파일러는 if (*h == 5)
자리 비움을 최적화 하고 항상 printf를 호출합니다.
여기에 관심이있는 사람들은 x64 용 우분투 12.04.2에서 실행되는 gcc 4.6.3에 의해 생성 된 x64 어셈블러 코드입니다.
movw $5, (%rdi)
movq $6, (%rsi)
movl $.LC0, %edi
jmp puts
따라서 if 조건은 어셈블러 코드에서 완전히 사라졌습니다.
long long*
및 int64_t
*에 해당하는 일부 시스템에서 ). 하나는 제정신 컴파일러는이 있음을 인식해야한다고 기대할 수 long long*
및 int64_t*
그들이 동일하게 저장하는 경우 동일한 스토리지에 액세스 할 수 있지만 이러한 치료는 더 이상 유행이다.
포인터 캐스트를 통한 타입 제거 (유니온 사용과 반대)는 엄격한 앨리어싱을 해제하는 주요 예입니다.
fpsync()
가 fp로 쓰기와 int로 읽기 또는 그 반대로 읽기 사이 의 지시문을 실행 한 경우에만 제대로 작동하도록 지정할 수 있습니다 ( 별도의 정수 및 FPU 파이프 라인 및 캐시가있는 구현에서) 그러한 지시어는 비싸지 만 컴파일러가 모든 유니언 액세스에서 그러한 동기화를 수행하는 것만 큼 비싸지는 않습니다. 또는 구현시 공통 초기 시퀀스를 사용하는 상황을 제외하고 결과 값을 사용할 수 없도록 지정할 수 있습니다.
C89의 이론적 근거에 따르면, 표준 작성자는 다음과 같은 코드를 컴파일러에 요구하지 않아도됩니다.
int x;
int test(double *p)
{
x=5;
*p = 1.0;
return x;
}
을 가리킬 x
가능성을 허용하기 위해 대입 문과 return 문 사이 의 값을 다시로드해야 하고 대입 이 값을 변경할 수 있어야합니다 . 컴파일러가 위와 같은 상황에서 앨리어싱이 없을 것이라고 가정 할 수 있다는 개념은 논란의 여지가 없습니다.p
x
*p
x
불행하게도, C89의 저자는 문자 그대로 읽을 경우 다음 함수조차도 정의되지 않은 동작을 호출하도록하는 방식으로 규칙을 작성했습니다.
void test(void)
{
struct S {int x;} s;
s.x = 1;
}
그 형식의 좌변을 사용하므로 int
액세스 타입의 오브젝트 struct S
및 int
액세스 사용될 수있는 타입과는 다른 것이다 struct S
. 문자가 아닌 유형의 구조체 및 공용체 멤버의 모든 사용을 정의되지 않은 동작으로 취급하는 것은 터무니없는 일이기 때문에, 거의 모든 사람들은 한 유형의 lvalue가 다른 유형의 객체에 액세스하는 데 사용될 수있는 상황이 적어도 있음을 인식합니다. . 불행히도 C 표준위원회는 그러한 상황을 정의하지 못했습니다.
대부분의 문제는 결함 보고서 # 028의 결과이며 다음과 같은 프로그램의 동작에 대해 질문했습니다.
int test(int *ip, double *dp)
{
*ip = 1;
*dp = 1.23;
return *ip;
}
int test2(void)
{
union U { int i; double d; } u;
return test(&u.i, &u.d);
}
결함 보고서 # 28은 "double"유형의 공용체 멤버를 작성하고 "int"유형 중 하나를 읽는 조치가 구현 정의 동작을 호출하기 때문에 프로그램이 정의되지 않은 동작을 호출한다고 설명합니다. 이러한 추론은 무의미하지만, 원래 문제를 해결하기 위해 아무 것도하지 않고 언어를 불필요하게 복잡하게하는 유효 유형 규칙의 기초를 형성합니다.
원래 문제를 해결하는 가장 좋은 방법은 규칙의 목적에 대한 각주를 규범적인 것처럼 취급하고 실제로 별칭을 사용하여 액세스가 충돌하는 경우를 제외하고 규칙을 시행 할 수 없게 만드는 것입니다. 다음과 같은 것이 주어진다 :
void inc_int(int *p) { *p = 3; }
int test(void)
{
int *p;
struct S { int x; } s;
s.x = 1;
p = &s.x;
inc_int(p);
return s.x;
}
내 충돌도 없다 inc_int
저장에 대한 모든 액세스를 통해 액세스하기 때문에 *p
형식의 좌변과 완료는 int
,와의 충돌이 없습니다 test
때문에 p
가시적에서 파생은 struct S
, 다음 시간 s
, 이제까지 될 것이다 저장에 대한 모든 액세스를 사용 를 통해이 p
이미 일어난 것입니다.
코드가 약간 변경된 경우 ...
void inc_int(int *p) { *p = 3; }
int test(void)
{
int *p;
struct S { int x; } s;
p = &s.x;
s.x = 1; // !!*!!
*p += 1;
return s.x;
}
여기서, 실행중인 시점에 동일한 스토리지에 액세스하는 데 사용되는 다른 참조가 존재하기 때문에 표시된 행에 p
대한 액세스와 s.x
표시된 행에 대한 액세스 간에 앨리어싱 충돌이 발생 합니다 .
Defect Report 028은 원래 예제가 두 포인터의 생성과 사용 사이의 겹침으로 인해 UB를 호출했다고 말했는데, 이는 "유효 유형"또는 다른 복잡성을 추가하지 않고도 상황을 훨씬 더 명확하게 만들었습니다.
많은 답변을 읽은 후 무언가를 추가해야한다고 생각합니다.
다음과 같은 이유로 엄격한 앨리어싱 (나중에 설명하겠습니다) 이 중요합니다 .
메모리 액세스는 비용이 많이 들고 (성능면에서) 물리적 메모리에 다시 쓰기 전에 CPU 레지스터에서 데이터가 조작되는 이유 입니다.
서로 다른 두 CPU 레지스터의 데이터가 동일한 메모리 공간에 기록 되면 C로 코딩 할 때 어떤 데이터가 "생존"하는지 예측할 수 없습니다 .
CPU 레지스터의로드 및 언로드를 수동으로 코딩하는 어셈블리에서는 어떤 데이터가 손상되지 않았는지 알 수 있습니다. 그러나 C는 (고맙게도)이 세부 사항을 추상화합니다.
두 포인터가 메모리에서 동일한 위치를 가리킬 수 있기 때문에 충돌 가능성을 처리하는 복잡한 코드 가 생길 수 있습니다 .
이 추가 코드는 느리고 필요하지 않은 추가 메모리 읽기 / 쓰기 작업을 수행하므로 성능 이 저하되고 성능 이 저하 됩니다.
엄격한 앨리어싱 규칙은 우리가 중복 기계 코드를 피할 수 는있는 경우에 해야한다 (또한 참조 두 개의 포인터가 같은 메모리 블록을 가리 키지 않는 것으로 가정하는 것이 안전 restrict
키워드).
엄격한 앨리어싱은 다른 유형에 대한 포인터가 메모리의 다른 위치를 가리키는 것으로 가정하는 것이 안전하다고 말합니다.
컴파일러가 두 개의 포인터가 다른 유형 (예 : an int *
및 a float *
) 을 가리키는 것을 발견 하면 메모리 주소가 다르고 메모리 주소 충돌을 방지 하지 않으므로 기계 코드가 더 빨라집니다.
예를 들면 다음과 같습니다.
다음과 같은 기능을 가정 해 봅시다.
void merge_two_ints(int *a, int *b) {
*b += *a;
*a += *b;
}
a == b
(포인터가 동일한 메모리를 가리키는) 경우를 처리하려면 메모리에서 CPU 레지스터로 데이터를로드하는 방법을 주문하고 테스트해야하므로 코드는 다음과 같이 끝날 수 있습니다.
로드 a
하고 b
메모리에서.
추가 a
에 b
.
저장 b
하고 다시로드하십시오 a
.
(CPU 레지스터에서 메모리로 저장하고 메모리에서 CPU 레지스터로로드).
추가 b
에 a
.
저장 a
메모리에합니다 (CPU 레지스터에서).
3 단계는 실제 메모리에 액세스해야하므로 매우 느립니다. 그러나, 인스턴스를 방지하기 위해 필요한 것 a
과 b
같은 메모리 주소를 가리 킵니다.
엄격한 앨리어싱을 사용하면 컴파일러에 이러한 메모리 주소가 명확하게 다르다는 사실을 알 수 있습니다 (이 경우 포인터가 메모리 주소를 공유하는 경우 수행 할 수없는 추가 최적화가 가능함).
서로 다른 유형을 사용하여 두 가지 방법으로 컴파일러에 알릴 수 있습니다. 즉 :
void merge_two_numbers(int *a, long *b) {...}
restrict
키워드 사용 즉 :
void merge_two_ints(int * restrict a, int * restrict b) {...}
이제 엄격한 앨리어싱 규칙을 충족하면 3 단계를 피할 수 있으며 코드가 훨씬 빠르게 실행됩니다.
실제로 restrict
키워드 를 추가 하면 전체 기능을 다음과 같이 최적화 할 수 있습니다.
로드 a
하고 b
메모리에서.
추가 a
에 b
.
a
와에 결과를 모두 저장 합니다 b
.
이 최적화 때문에 가능한 충돌의 이전에 완료되지 않았을 수있다 (여기서 a
와 b
배 대신에 두 배가 될 것이다).
b
(다시로드하지 않고) 저장 하고 다시로드 중 a
입니다. 더 명확 해지기를 바랍니다.
restrict
있지만 후자는 대부분의 환경에서보다 효과적이며 일부 제약 조건을 완화 register
하면 restrict
도움이되지 않는 경우를 채울 수 있다고 생각합니다 . 프로그래머가 컴파일러가 앨리어싱의 증거를 인식해야하는 모든 경우를 설명하는 것으로, 표준에 대한 특별한 증거가없는 경우에도 컴파일러가 앨리어싱을 추정해야하는 위치를 설명하는 것으로 간주하는 모든 경우를 설명하는 것으로 표준을 취급하는 것이 "중요한"지 확실하지 않습니다 .
restrict
키워드는 작업 속도뿐만 아니라 그 수도 최소화합니다. 이는 의미가있을 수 있습니다. 결국 가장 빠른 작업은 전혀 작업하지 않습니다. :)
엄격한 앨리어싱은 동일한 데이터에 대해 다른 포인터 유형을 허용하지 않습니다.
이 기사 는 문제를 자세하게 이해하는 데 도움이됩니다.
int
및을 포함하는 구조체 int
).
기술적으로 C ++에서 엄격한 앨리어싱 규칙은 적용 할 수 없습니다.
간접 정의 ( * operator ) 의 정의에 유의하십시오 .
단항 * 연산자는 간접을 수행합니다. 적용되는 표현식은 객체 유형에 대한 포인터 또는 함수 유형에 대한 포인터 여야하며 결과는 표현식이 가리키는 객체 또는 함수 를 참조하는 lvalue 입니다.
glvalue는 평가가 객체의 신원을 결정하는 표현식입니다 (... 조각).
따라서 잘 정의 된 프로그램 추적에서 glvalue는 객체를 나타냅니다. 따라서 소위 엄격한 앨리어싱 규칙은 적용되지 않습니다. 이것은 디자이너가 원하는 것이 아닐 수도 있습니다.
int foo;
하면 lvalue 식으로 무엇에 액세스 *(char*)&foo
합니까? 그 유형의 개체 char
입니까? 그 물체는 동시에 존재 foo
합니까? foo
위에서 언급 한 유형의 객체의 저장된 값 을 변경하기 위해 글을 쓰시겠습니까 char
? 그렇다면 유형 char
의 lvalue를 사용하여 유형의 객체의 저장된 값에 액세스 할 수있는 규칙이 int
있습니까?
int i;
은 각 문자 유형 in addition to one of type
int ? I see no way to apply a consistent definition of "object" which would allow for operations on both
* (char *) & i` 및의 4 개의 객체를 만듭니다 i
. 마지막으로, 표준 volatile
에는 "객체"의 정의를 충족하지 않는 하드웨어 레지스터에 액세스 할 수있는 정규화 된 포인터 조차 없습니다.
c
하고c++faq
.