[c] 엄격한 앨리어싱 규칙은 무엇입니까?

C에서 정의되지 않은 일반적인 동작에 대해 질문 할 때 때때로 엄격한 앨리어싱 규칙을 참조합니다.
그들은 무엇에 대한 이야기?



답변

엄격한 앨리어싱 문제가 발생하는 일반적인 상황은 구조체 (예 : 장치 / 네트워크 메시지)를 시스템의 단어 크기 버퍼 ( uint32_ts 또는 uint16_ts에 대한 포인터)에 오버레이하는 경우 입니다. 포인터를 캐스팅하여 이러한 버퍼 또는 구조체에 버퍼를 오버레이하면 엄격한 앨리어싱 규칙을 쉽게 위반할 수 있습니다.

따라서 이런 종류의 설정에서 메시지를 보내려면 동일한 메모리 청크를 가리키는 두 개의 호환되지 않는 포인터가 있어야합니다. 그런 다음 순진하게 다음과 같은 시스템을 코딩 할 수 있습니다 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가 액세스 할 수있는 유형은 다음과 같습니다.

  • 객체의 유효 유형과 호환되는 유형
  • 유효 객체 유형과 호환되는 유형의 정규화 된 버전
  • 객체의 유효 유형에 해당하는 부호있는 유형 또는 부호없는 유형 인 유형
  • 오브젝트의 유효 유형의 규정 된 버전에 해당하는 부호있는 유형 또는 부호없는 유형 인 유형
  • 상기 구성원들 중 상기 유형들 중 하나를 포함하는 집합 또는 연합 유형 (재귀 적으로 하위 집합의 구성원 또는 포함 된 연합을 포함) 또는
  • 문자 유형

답변

내가 찾은 가장 좋은 설명은 Mike Acton의 엄격한 앨리어싱 이해 입니다. PS3 개발에 중점을 두었지만 기본적으로 GCC입니다.

기사에서 :

“엄격한 앨리어싱은 C (또는 C ++) 컴파일러에 의해 만들어진 것으로, 다른 유형의 객체에 대한 포인터를 역 참조하는 것은 결코 같은 메모리 위치를 참조하지 않을 것이라고 가정합니다.”

기본적으로 만약 당신이 int*어떤 메모리를 가리키는 메모리를 가리키고 있다면 inta float*를 그 메모리 를 가리키고 float규칙을 어길 때 사용하십시오 . 코드가 이것을 존중하지 않으면 컴파일러의 최적화 프로그램이 코드를 손상시킬 가능성이 높습니다.

규칙에 대한 예외는이며 char*모든 유형을 가리킬 수 있습니다.


답변

이것은 C ++ 03 표준 의 3.10 섹션에있는 엄격한 앨리어싱 규칙입니다 (다른 답변은 좋은 설명을 제공하지만 규칙 자체는 제공하지 않았습니다).

프로그램이 다음 유형 중 하나 이외의 lvalue를 통해 오브젝트의 저장된 값에 액세스하려고하면 동작이 정의되지 않습니다.

  • 객체의 동적 유형
  • 객체의 동적 유형의 cv-qualified 버전
  • 객체의 동적 유형에 해당하는 부호있는 유형 또는 부호없는 유형 인 유형
  • cv-qualified 버전의 객체 동적 유형에 해당하는 부호있는 또는 부호없는 유형 인 유형
  • 상기 구성원들 중 상기 유형들 중 하나를 포함하는 집합 또는 연합 유형 (재귀 적으로 하위 집합 또는 포함 된 연합의 구성원 포함)
  • 객체의 다이나믹 타입의 (아마도 cv-qualified) 기본 클래스 타입 인 타입
  • char또는 unsigned char유형입니다.

C ++ 11C ++ 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) 이 목록의 목적은 개체의 별칭이 될 수도 있고 그렇지 않을 수도있는 환경을 지정하는 것입니다.


답변

노트

이것은 “엄격한 앨리어싱 규칙은 무엇이며 왜 우리는 신경 쓰는가?” 에서 발췌 한 것 입니다. 쓰기.

엄격한 앨리어싱이란 무엇입니까?

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는 그것의 템플릿 매개 변수에 의해 지정하신 유형을 가지고있는 것처럼 표현을 치료하기 위해 컴파일러을 말하고있다. 이 경우 표현식 & xfloat * 유형 인 것처럼 취급하도록 지시합니다 . 우리는 순진하게 두 번째 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 표준은 무엇을 말합니까?

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.

C ++ 17 초안 표준의 내용

[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에 노조를 사용하는 것이 악용이라고 생각합니다.

우리는 어떻게 정확하게 Pun을 입력합니까?

C 및 C ++에서 유형 punning 의 표준 방법 은 memcpy 입니다. 이 손으로 조금 무거운를 보일 수 있지만, 최적화의 사용을 인식해야 방어 적이기 에 대한 형의 말장난을 하고 도망을 최적화하고 이동을 등록하는 레지스터를 생성합니다. 예를 들어 int64_tdouble 과 크기가 같다는 것을 알고 있다면 :

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

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 이 있습니다. 이 소독제는 섀도 메모리 세그먼트에 유형 검사 정보를 추가하고 앨리어싱 규칙을 위반하는지 확인합니다. 이 도구는 잠재적으로 모든 앨리어싱 위반을 포착 할 수 있어야하지만 런타임 오버 헤드가 클 수 있습니다.


답변

엄격한 앨리어싱은 포인터만을 가리키는 것이 아니며 참조에도 영향을 미칩니다. 부스트 개발자 위키에 대한 논문을 작성했으며 컨설팅 웹 사이트의 페이지로 전환하기에 충분히 받아 들였습니다. 그것은 그것이 무엇인지, 왜 사람들을 그렇게 혼란스럽게하는지, 그것에 대해 어떻게 해야하는지 완전히 설명합니다. 엄격한 앨리어싱 백서 . 특히 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 조건은 어셈블러 코드에서 완전히 사라졌습니다.


답변

포인터 캐스트를 통한 타입 제거 (유니온 사용과 반대)는 엄격한 앨리어싱을 해제하는 주요 예입니다.