developer tip

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

copycodes 2020. 9. 29. 07:58
반응형

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


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


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

따라서 이런 종류의 설정에서 메시지를 무언가로 보내려면 동일한 메모리 청크를 가리키는 두 개의 호환되지 않는 포인터가 있어야합니다. 그런 다음 다음과 같이 순진하게 코딩 할 수 있습니다.

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의 일부인 경우 buff의 내용을로드하라는 지침이있을 수 있습니다. 다시 말하지만, 아마도 당신은 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*다른 유형의 별칭을 사용 한다고 가정합니다 . 그러나 이것은 다른 방식으로 작동하지 않습니다. 구조체가 문자 버퍼를 별칭으로 지정한다는 가정이 없습니다.

초보자주의

이것은 두 유형을 서로 겹칠 때 하나의 잠재적 지뢰밭 일뿐입니다. 또한 endianness , 단어 정렬구조체를 올바르게 패킹 하여 정렬 문제를 처리하는 방법에 대해 배워야 합니다.

각주

1 C 2011 6.5 7에서 lvalue가 액세스 할 수 있도록 허용하는 유형은 다음과 같습니다.

  • 개체의 유효 유형과 호환되는 유형,
  • 객체의 유효 유형과 호환되는 유형의 정규화 된 버전
  • 개체의 유효 유형에 해당하는 서명 된 유형 또는 서명되지 않은 유형 인 유형
  • 객체의 유효 유형의 규정 된 버전에 해당하는 서명되거나 서명되지 않은 유형 인 유형
  • 멤버 (재귀 적으로, 하위 집계 또는 포함 된 공용체의 멤버 포함) 중에 앞서 언급 한 유형 중 하나를 포함하는 집계 또는 공용체 유형, 또는
  • 문자 유형.

내가 찾은 가장 좋은 설명은 Mike Acton ( Strict Aliasing 이해) 입니다. PS3 개발에 약간 초점을 맞추었지만 기본적으로 GCC에 불과합니다.

기사에서 :

"엄격한 앨리어싱은 C (또는 C ++) 컴파일러에 의해 만들어진 가정으로, 서로 다른 유형의 객체에 대한 포인터를 역 참조하는 것은 동일한 메모리 위치를 참조하지 않을 것입니다 (즉, 서로 앨리어싱)."

따라서 기본적으로 an int*을 포함하는 일부 메모리 int를 가리키고 a float*를 해당 메모리 를 가리키고 float규칙을 위반하는 것으로 사용하면 기본적으로 . 코드가이를 존중하지 않으면 컴파일러의 최적화 프로그램이 코드를 손상시킬 가능성이 높습니다.

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


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

프로그램이 다음 유형 중 하나가 아닌 다른 lvalue를 통해 객체의 저장된 값에 액세스하려고하면 동작이 정의되지 않습니다.

  • 객체의 동적 유형,
  • 객체의 동적 유형의 cv 한정 버전,
  • 객체의 동적 유형에 해당하는 서명되거나 서명되지 않은 유형 인 유형
  • 객체의 동적 유형의 cv 규정 버전에 해당하는 서명되거나 서명되지 않은 유형 인 유형
  • 멤버 사이에 앞서 언급 한 유형 중 하나를 포함하는 집계 또는 공용체 유형 (재귀 적으로 하위 집계 또는 포함 된 공용체의 멤버 포함),
  • 객체의 동적 유형의 (가능하게는 cv-qualified) 기본 클래스 유형 인 유형,
  • char또는 unsigned char유형입니다.

C ++ 11C ++ 14 문구 (변경 사항 강조) :

프로그램 이 다음 유형 중 하나가 아닌 다른 glvalue통해 객체의 저장된 값에 액세스하려고 하면 동작이 정의되지 않습니다.

  • 객체의 동적 유형,
  • 객체의 동적 유형의 cv 한정 버전,
  • 객체의 동적 유형과 유사한 유형 (4.4에서 정의 됨)
  • 객체의 동적 유형에 해당하는 서명되거나 서명되지 않은 유형 인 유형
  • 객체의 동적 유형의 cv 규정 버전에 해당하는 서명되거나 서명되지 않은 유형 인 유형
  • 그 중에서, 상기 종류 중 하나를 포함하는 전체 또는 조합 형태 요소 또는 비 정적 데이터 멤버 (포함한 재귀 원소 또는 비 - 정적 데이터 부재 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 ) 으로 분류됩니다 . 정의되지 않은 행동이 발생하면 모든 베팅이 해제되고 프로그램 결과는 더 이상 신뢰할 수 없습니다.

불행히도 엄격한 앨리어싱 위반으로 인해 우리는 종종 우리가 기대하는 결과를 얻을 수 있으며, 새로운 최적화를 사용하는 컴파일러의 향후 버전이 우리가 유효하다고 생각한 코드를 손상시킬 가능성을 남겨 둡니다. 이것은 바람직하지 않으며 엄격한 앨리어싱 규칙을 이해하고이를 위반하지 않는 방법을 이해하는 것이 가치있는 목표입니다.

우리가 관심을 갖는 이유에 대해 더 자세히 이해하기 위해 엄격한 앨리어싱 규칙을 위반할 때 발생하는 문제에 대해 논의 할 것입니다. 타입 punning에 사용되는 일반적인 기술은 종종 엄격한 앨리어싱 규칙을 위반하고 말장난을 올바르게 입력하는 방법을 위반하기 때문입니다.

예비 예

몇 가지 예를 살펴본 다음 표준이 말하는 내용에 대해 정확히 이야기하고 몇 가지 추가 예를 검토 한 다음 엄격한 앨리어싱을 피하고 놓친 위반을 포착하는 방법을 알아볼 수 있습니다. 다음은 놀라운 일이 아니어야하는 예입니다 ( 라이브 예 ).

int x = 10;
int *ip = &x;

std::cout << *ip << "\n";
*ip = 12;
std::cout << x << "\n";

우리는이 * INT 메모리에 포인팅가에 의해 점유 지능 이 유효한 앨리어싱이다. 옵티마이 저는 ip를 통한 할당 x가 차지하는 값을 업데이트 할 수 있다고 가정해야합니다 .

다음 예제는 정의되지 않은 동작으로 이어지는 앨리어싱을 보여줍니다 ( 라이브 예제 ) :

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 이 될 것이라고 순진하게 예상 할 수 있지만 -O2를 사용하여 최적화를 활성화하면 gcc와 clang 모두 다음과 같은 결과가 생성됩니다.

0
1

예상하지 못할 수도 있지만 정의되지 않은 동작을 호출했기 때문에 완벽하게 유효합니다. 플로트 하지 유효 별명 할 수 INT의 객체입니다. 따라서 옵티마이 저는 f를 통한 저장 int 객체에 유효하게 영향을 미칠 수 없기 때문에 i역 참조 할 때 저장된 상수 1 이 반환 값 이라고 가정 할 수 있습니다 . 컴파일러 탐색기에서 코드를 연결하면 이것이 정확히 무슨 일이 일어나고 있는지 보여줍니다 ( 라이브 예제 ) :

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 / clang에는 확장자가 있으며 호환되지 않는 유형이더라도 unsigned 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 Draft Standard의 내용

섹션 [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-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) — char, unsigned char 또는 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
}

부호있는 문자 가 위의 목록에 포함되어 있지 않다는 점에 주목할 필요 있습니다. 이것은 문자 유형 을 말하는 C 와의 주목할만한 차이점입니다 .

Type Punning이란?

우리는이 시점에 이르렀고 우리는 왜 별칭을 원할까요? 대답은 일반적으로 pun입력하는 것입니다 . 종종 사용되는 메서드는 엄격한 별칭 규칙을 위반합니다.

때때로 우리는 유형 시스템을 우회하고 객체를 다른 유형으로 해석하고 싶습니다. 메모리 세그먼트를 다른 유형으로 재 해석하는 유형 punning 이라고 합니다. 유형 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 ) ;

앞에서 보았 듯이 이것은 유효한 별칭이 아니므로 정의되지 않은 동작을 호출합니다. 그러나 전통적으로 컴파일러는 엄격한 앨리어싱 규칙을 이용하지 않았고 이러한 유형의 코드는 일반적으로 작동했지만, 개발자는 불행히도 이런 방식으로 일하는 데 익숙해졌습니다. 유형 punning의 일반적인 대체 방법은 공용체를 사용하는 것입니다. 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 ( 라이브 컴파일러 탐색기 예제 ) 만 사용됩니다.

C ++ 20 및 bit_cast

C ++ 20에서 우리는 constexpr 컨텍스트에서 사용할 수있을뿐만 아니라 type-pun에 간단하고 안전한 방법을 제공하는 bit_cast ( 제안의 링크에서 사용 가능한 구현 )를 얻을 수 있습니다.

다음은 bit_cast 를 사용 하여 pun a unsigned int to float 를 입력 하는 방법의 예입니다 ( live ) :

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 버전이 주어집니다 ( live ).

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. 일반적으로 (내가 시도한 대부분의 gcc 버전에서) 이것은 "엄격한 앨리어싱 문제"를 출력합니다. 컴파일러는 "h"가 "check"함수에서 "k"와 같은 주소가 될 수 없다고 가정하기 때문입니다. 그 때문에 컴파일러는 if (*h == 5)어웨이를 최적화 하고 항상 printf를 호출합니다.

여기에 관심이있는 사람들을 위해 x64 용 ubuntu 12.04.2에서 실행되는 gcc 4.6.3에서 생성 된 x64 어셈블러 코드가 있습니다.

movw    $5, (%rdi)
movq    $6, (%rsi)
movl    $.LC0, %edi
jmp puts

따라서 if 조건은 어셈블러 코드에서 완전히 사라졌습니다.


포인터 캐스트를 통한 유형 punning (union 사용과 반대)은 엄격한 앨리어싱을 깨는 주요 예입니다.


C89의 이론적 근거에 따르면 표준 작성자는 컴파일러에게 다음과 같은 코드를 제공하도록 요구하지 않았습니다.

int x;
int test(double *p)
{
  x=5;
  *p = 1.0;
  return x;
}

을 (를) 가리킬 수 x있는 가능성을 허용하기 위해 할당과 반환 문 사이 의 값을 다시로드해야하며 , 할당 은 결과적으로의 값을 변경할 수 있습니다 . 컴파일러가 위와 같은 상황에서 앨리어싱이 없을 것이라고 가정 할 자격이 있다는 개념은 논란의 여지가 없습니다.px*px

불행히도 C89의 작성자는 문자 그대로 읽는 경우 다음 함수조차도 정의되지 않은 동작을 호출하는 방식으로 규칙을 작성했습니다.

void test(void)
{
  struct S {int x;} s;
  s.x = 1;
}

그 형식의 좌변을 사용하므로 int액세스 타입의 오브젝트 struct Sint액세스 사용될 수있는 타입과는 다른 것이다 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유형의 lvalue로 수행 되기 때문에 int충돌이 없으며에서 가시적으로 파생되고 다음 에 사용될 해당 스토리지에 대한 모든 액세스 가 이루어 test지기 때문에 충돌이 없습니다. 통해 이미 일어난 것입니다.pstruct Ssp

코드가 약간 변경된 경우 ...

 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;
 }

여기에서 ps.x표시된 행에 대한 액세스 간에 별칭 충돌 이 있습니다 . 실행중인 해당 시점에 동일한 스토리지에 액세스하는 데 사용될 다른 참조가 존재하기 때문입니다 .

Had Defect Report 028은 원래 예제가 두 포인터의 생성과 사용이 겹치기 때문에 UB를 호출했다고 말했습니다. 그러면 "유효 유형"이나 기타 복잡성을 추가하지 않고도 상황이 훨씬 더 명확 해졌을 것입니다.


많은 답변을 읽은 후 뭔가 추가 할 필요가 있다고 느낍니다.

다음과 같은 이유로 엄격한 앨리어싱 (잠시 설명하겠습니다) 이 중요합니다 .

  1. 메모리 액세스는 비용이 많이들 수 있으며 (성능 측면에서) 데이터가 실제 메모리에 다시 기록되기 전에 CPU 레지스터에서 조작되는 이유 입니다.

  2. 두 개의 다른 CPU 레지스터에 있는 데이터가 동일한 메모리 공간에 기록되는 경우 C로 코딩 할 때 어떤 데이터가 "생존"할지 예측할 수 없습니다 .

    CPU 레지스터의 로딩과 언 로딩을 수동으로 코딩하는 어셈블리에서는 어떤 데이터가 그대로 유지되는지 알 수 있습니다. 그러나 C는 (고맙게도)이 세부 사항을 추상화합니다.

두 개의 포인터가 메모리의 동일한 위치를 가리킬 수 있으므로 충돌 가능성을 처리하는 복잡한 코드 가 생성 될 수 있습니다 .

이 추가 코드는 느리고 (아마도) 불필요한 추가 메모리 읽기 / 쓰기 작업을 수행하기 때문에 느리고 성능 이 저하 됩니다.

엄격한 앨리어싱 규칙은 우리가 중복 기계 코드를 피할 수 는있는 경우에 해야한다 (또한 참조 두 개의 포인터가 같은 메모리 블록을 가리 키지 않는 것으로 가정하는 것이 안전 restrict키워드).

Strict 앨리어싱은 다른 유형에 대한 포인터가 메모리의 다른 위치를 가리킨다 고 가정하는 것이 안전하다고 말합니다.

컴파일러가 두 포인터가 서로 다른 유형 (예 : an int *및 a float *) 을 가리키는 것을 감지하면 메모리 주소가 다른 것으로 간주하고 메모리 주소 충돌로부터 보호 하지 않으므로 기계 코드가 더 빨라집니다.

:

다음 기능을 가정 해 보겠습니다.

void merge_two_ints(int *a, int *b) {
  *b += *a;
  *a += *b;
}

a == b두 포인터가 동일한 메모리를 가리키는 경우를 처리하려면 메모리에서 CPU 레지스터로 데이터를로드하는 방법을 주문하고 테스트해야하므로 코드는 다음과 같이 끝날 수 있습니다.

  1. 로드 ab메모리에서.

  2. 추가 ab.

  3. 저장 b 하고 다시로드 a 합니다.

    (CPU 레지스터에서 메모리로 저장하고 메모리에서 CPU 레지스터로로드).

  4. 추가 ba.

  5. 저장 a메모리에합니다 (CPU 레지스터에서).

3 단계는 실제 메모리에 액세스해야하기 때문에 매우 느립니다. 그러나, 인스턴스를 방지하기 위해 필요한 것 ab같은 메모리 주소를 가리 킵니다.

엄격한 앨리어싱을 사용하면 이러한 메모리 주소가 분명히 다르다는 것을 컴파일러에 알려이를 방지 할 수 있습니다 (이 경우 포인터가 메모리 주소를 공유하는 경우 수행 할 수없는 추가 최적화를 허용합니다).

  1. 다른 유형을 사용하여 가리키는 두 가지 방법으로 컴파일러에이를 알릴 수 있습니다. 즉 :

    void merge_two_numbers(int *a, long *b) {...}
    
  2. restrict키워드 사용 . 즉 :

    void merge_two_ints(int * restrict a, int * restrict b) {...}
    

이제 Strict Aliasing 규칙을 충족하면 3 단계를 피할 수 있으며 코드가 훨씬 빠르게 실행됩니다.

실제로 restrict키워드 를 추가 하면 전체 기능을 다음과 같이 최적화 할 수 있습니다.

  1. 로드 ab메모리에서.

  2. 추가 ab.

  3. 결과를 a및에 모두 저장 합니다 b.

이 최적화 때문에 가능한 충돌의 이전에 완료되지 않았을 수있다 (여기서 ab배 대신에 두 배가 될 것이다).


엄격한 앨리어싱은 동일한 데이터에 대해 다른 포인터 유형을 허용하지 않습니다.

이 문서 는 문제를 자세히 이해하는 데 도움이됩니다.


기술적으로 C ++에서 엄격한 앨리어싱 규칙은 적용 할 수 없습니다.

간접 정의 ( * 연산자 )에 유의하십시오 .

단항 * 연산자는 간접을 수행합니다. 적용되는 표현식은 객체 유형에 대한 포인터이거나 함수 유형에 대한 포인터 여야하며 결과는 표현식이 가리키는 객체 또는 함수 참조하는 lvalue 입니다.

또한 glvalue의 정의에서

glvalue는 평가가 객체의 ID를 결정하는 표현식입니다. (... snip)

따라서 잘 정의 된 프로그램 추적에서 glvalue는 객체를 나타냅니다. 따라서 소위 엄격한 앨리어싱 규칙은 적용되지 않습니다. 이것은 디자이너가 원하는 것이 아닐 수도 있습니다.

참고 URL : https://stackoverflow.com/questions/98650/what-is-the-strict-aliasing-rule

반응형