prosource

gcc, 엄격한 별칭 지정 및 조합을 통한 캐스팅

probook 2023. 7. 22. 10:10
반응형

gcc, 엄격한 별칭 지정 및 조합을 통한 캐스팅

여러분은 할 수 있는 공포 이야기가 있나요?GCC 매뉴얼은 최근 -fstrict-aliasing 및 조합을 통한 포인터 캐스팅과 관련된 경고를 추가했습니다.

[...] 주소를 가져와서 결과 포인터를 캐스팅하고 결과를 참조하지 않으면 캐스트가 다음과 같은 조합 유형을 사용하는 경우에도 정의되지 않은 동작[강조 추가됨]이 있습니다.

    union a_union {
        int i;
        double d;
    };

    int f() {
        double d = 3.0;
        return ((union a_union *)&d)->i;
    }

이 정의되지 않은 행동을 설명할 수 있는 예를 가진 사람이 있습니까?

이 질문은 C99 표준이 말하는 것과 그렇지 않은 것에 관한 것이 아닙니다.그것은 오늘날 gcc와 다른 기존 컴파일러의 실제 기능에 관한 것입니다.

추측할 뿐이지만 한 가지 잠재적인 문제는 다음과 같은 환경에 있을 수 있습니다.d3.으로 설정 이유는 다음과 같습니다.d는 직접적으로 읽히지 않는 임시 변수이며, '확장 호환' 포인터를 통해 읽히지 않는 임시 변수이므로 컴파일러는 이 변수를 설정하려고 하지 않을 수 있습니다.그런 다음 f()는 스택에서 일부 가비지를 반환합니다.

제 단순하고 순진한 시도는 실패합니다.예:

#include <stdio.h>

union a_union {
    int i;
    double d;
};

int f1(void) {
    union a_union t;
    t.d = 3333333.0;
    return t.i; // gcc manual: 'type-punning is allowed, provided...' (C90 6.3.2.3)
}

int f2(void) {
    double d = 3333333.0;
    return ((union a_union *)&d)->i; // gcc manual: 'undefined behavior' 
}

int main(void) {
    printf("%d\n", f1());
    printf("%d\n", f2());
    return 0;
}

CYGWIN에 제공되는 상태에서 잘 작동합니다.

-2147483648
-2147483648

어셈블러를 보면 gcc가 완전히 최적화되어 있음을 알 수 있습니다.t 원정:f1()미리 계산된 답변만 저장합니다.

movl    $-2147483648, %eax

하는 동안에f2()33333333.0을 부동 소수점 스택으로 밀어넣은 다음 반환 값을 추출합니다.

flds   LC0                 # LC0: 1246458708 (= 3333333.0) (--> 80 bits)
fstpl  -8(%ebp)            # save in d (64 bits)
movl   -8(%ebp), %eax      # return value (32 bits)

그리고 함수도 인라인화되어 있지만(이것이 일부 엄격한 별칭 버그의 원인인 것으로 보입니다), 여기서는 관련이 없습니다. (그리고 이 어셈블리어는 관련이 없지만, 확증적인 세부사항을 추가합니다.)

또한 주소를 지정하는 것은 분명히 잘못된 것입니다(또는 정의되지 않은 동작을 설명하려는 경우 옳습니다).예를 들어, 우리가 이것이 잘못되었다는 것을 알고 있는 것처럼:

extern void foo(int *, double *);
union a_union t;
t.d = 3.0;
foo(&t.i, &t.d); // undefined behavior

우리도 마찬가지로 이것이 잘못되었다는 것을 알고 있습니다.

extern void foo(int *, double *);
double d = 3.0;
foo(&((union a_union *)&d)->i, &d); // undefined behavior

이에 대한 배경 설명은 다음 예를 참조하십시오.

http://www..org/jtc1/sc22/wg14/www/docs/n1422.pdfhttp ://www.open-std.org/jtc1/sc22/wg14/www/docs/n1422.pdf
http://gcc.gnu.org/ml/gcc/2010-01/msg00013.://gcc.gnu.org/ml/gcc/2010-01/msg00013.html
http://davmac.wordpress.com/2010/02/26//http://davmac.wordpress.com/2010/02/26/c99-revisited/
http://cellperformance.beyond3d.com/articles/2006/06/.htmlhttp ://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html
( = Google에서 페이지 검색 후 캐시된 페이지 보기)

엄격한 별칭 지정 규칙은 무엇입니까?
C++의 C99 엄격한 별칭 규칙(GCC)

첫 번째 링크인 7개월 전 ISO 회의의 초안 회의록에서 한 참가자가 4.16절에 언급합니다.

규칙이 충분히 명확하다고 생각하는 사람이 있습니까?아무도 그것들을 해석할 수 없습니다.

기타 참고 사항:제 테스트는 gcc 4.3.4와 -O2, 옵션 -O2 및 -O3는 암시적으로 -fstrict-aliasing입니다.GCC 설명서의 예제에서는 크기가 (더블) > = 크기가 (int)라고 가정합니다. 크기가 동일하지 않더라도 상관 없습니다.

, 에서 Mike 또한마, 이액셀이퍼링스언이급듯했서크에먼,-Wstrict-aliasing=2하지만 아닙니다. =3warning: dereferencing type-punned pointer might break strict-aliasing rules여기 예를 들어 보겠습니다.

GCC가 노조에 대해 경고하고 있다는 사실이 반드시 노조가 현재 작동하지 않는다는 것을 의미하지는 않습니다.하지만 여기 당신의 것보다 약간 덜 간단한 예가 있습니다.

#include <stdio.h>

struct B {
    int i1;
    int i2;
};

union A {
    struct B b;
    double d;
};

int main() {
    double d = 3.0;
    #ifdef USE_UNION
        ((union A*)&d)->b.i2 += 0x80000000;
    #else
        ((int*)&d)[1] += 0x80000000;
    #endif
    printf("%g\n", d);
}

출력:

$ gcc --version
gcc (GCC) 4.3.4 20090804 (release) 1
Copyright (C) 2008 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ gcc -oalias alias.c -O1 -std=c99 && ./alias
-3

$ gcc -oalias alias.c -O3 -std=c99 && ./alias
3

$ gcc -oalias alias.c -O1 -std=c99 -DUSE_UNION && ./alias
-3

$ gcc -oalias alias.c -O3 -std=c99 -DUSE_UNION && ./alias
-3

따라서 GCC 4.3.4에서 조합은 "하루를 절약"합니다(출력 "-3"을 원한다고 가정).엄격한 앨리어싱에 의존하는 최적화를 비활성화하고 두 번째 경우에만 출력 "3"이 발생합니다(단,).-Wall을 사용하면 USE_UNION도 type-pun 경고를 비활성화합니다.

테스트할 gcc 4.4는 없지만 이 코드를 다시 주세요.는 당의코사다메음테다니합스트를모리의신실상드에 대한 합니다.d유니언을 통해 다시 읽기 전에 초기화됩니다. 지뢰는 수정 여부를 테스트합니다.

그나저나, 더블의 절반을 인트로 읽는 안전한 방법은 다음과 같습니다.

double d = 3;
int i;
memcpy(&i, &d, sizeof i);
return i;

GCC를 최적화하면 다음과 같은 결과를 얻을 수 있습니다.

    int thing() {
401130:       55                      push   %ebp
401131:       89 e5                   mov    %esp,%ebp
401133:       83 ec 10                sub    $0x10,%esp
        double d = 3;
401136:       d9 05 a8 20 40 00       flds   0x4020a8
40113c:       dd 5d f0                fstpl  -0x10(%ebp)
        int i;
        memcpy(&i, &d, sizeof i);
40113f:       8b 45 f0                mov    -0x10(%ebp),%eax
        return i;
    }
401142:       c9                      leave
401143:       c3                      ret

그래서 Memcpy에 대한 실제적인 전화는 없습니다.만약 당신이 이것을 하지 않는다면, 노조 출연자들이 GCC에서 일하는 것을 중단한다면 당신은 받을 자격이 있습니다 ;-)

음, 약간의 괴벽 게시물이지만, 여기 공포 이야기가 있습니다.네이티브 바이트 순서가 빅 엔디언이라는 가정하에 작성된 프로그램을 포팅합니다.이제 작은 엔디안에도 필요합니다.안타깝게도 데이터는 다양한 방식으로 액세스할 수 있기 때문에 어디에서나 기본 바이트 순서만 사용할 수 없습니다.예를 들어 64비트 정수는 두 개의 32비트 정수 또는 4개의 16비트 정수 또는 16개의 4비트 정수로 처리될 수 있습니다.설상가상으로, 소프트웨어가 어떤 종류의 바이트 코드에 대한 인터프리터이고 데이터는 그 바이트 코드에 의해 형성되기 때문에 메모리에 정확히 무엇이 저장되어 있는지 알아낼 방법이 없습니다.예를 들어, 바이트 코드는 16비트 정수 배열을 작성한 다음 32비트 부동소수로 쌍에 액세스하는 명령을 포함할 수 있습니다.그리고 그것을 예측하거나 바이트 코드를 변경할 방법이 없습니다.

따라서 네이티브 엔디언에 관계없이 빅 엔디언 순서로 저장된 값으로 작업할 래퍼 클래스 세트를 만들어야 했습니다.최적화 없이 Visual Studio와 Linux의 GCC에서 완벽하게 작동했습니다.하지만 gcc-O2로 지옥이 탈출했습니다.많은 디버깅을 통해 그 이유가 다음과 같다는 것을 알게 되었습니다.

double D;
float F; 
Ul *pF=(Ul*)&F; // Ul is unsigned long
*pF=pop0->lu.r(); // r() returns Ul
D=(double)F; 

이 코드는 32비트 정수에 저장된 플로트의 32비트 표현을 두 배로 변환하는 데 사용되었습니다.컴파일러는 D에 할당한 후 *pF에 할당하기로 결정한 것으로 보입니다. 결과적으로 코드가 처음 실행되었을 때 D의 값은 가비지였고 결과 값은 1회 반복하여 "늦었습니다".

기적적으로, 그 시점에서 다른 문제는 없었습니다.그래서 저는 원래 플랫폼인 HP-UX에서 새로운 코드를 테스트하기로 결정했습니다. HP-UX는 네이티브 빅 엔디언 순서의 RISC 프로세서에서 사용되었습니다.이번엔 새 수업에서 또 고장이 났습니다.

typedef unsigned long long Ur; // 64-bit uint
typedef unsigned char Uc;
class BEDoubleRef {
        double *p;
public:
        inline BEDoubleRef(double *p): p(p) {}
        inline operator double() {
                Uc *pu = reinterpret_cast<Uc*>(p);
                Ur n = (pu[7] & 0xFFULL) | ((pu[6] & 0xFFULL) << 8)
                        | ((pu[5] & 0xFFULL) << 16) | ((pu[4] & 0xFFULL) << 24)
                        | ((pu[3] & 0xFFULL) << 32) | ((pu[2] & 0xFFULL) << 40)
                        | ((pu[1] & 0xFFULL) << 48) | ((pu[0] & 0xFFULL) << 56);
                return *reinterpret_cast<double*>(&n);
        }
        inline BEDoubleRef &operator=(const double &d) {
                Uc *pc = reinterpret_cast<Uc*>(p);
                const Ur *pu = reinterpret_cast<const Ur*>(&d);
                pc[0] = (*pu >> 56) & 0xFFu;
                pc[1] = (*pu >> 48) & 0xFFu;
                pc[2] = (*pu >> 40) & 0xFFu;
                pc[3] = (*pu >> 32) & 0xFFu;
                pc[4] = (*pu >> 24) & 0xFFu;
                pc[5] = (*pu >> 16) & 0xFFu;
                pc[6] = (*pu >> 8) & 0xFFu;
                pc[7] = *pu & 0xFFu;
                return *this;
        }
        inline BEDoubleRef &operator=(const BEDoubleRef &d) {
                *p = *d.p;
                return *this;
        }
};

어떤 정말 이상한 이유로, 첫 번째 할당 연산자는 1-7바이트만 올바르게 할당했습니다.바이트 0에는 항상 넌센스가 포함되어 있었는데, 부호 비트와 순서의 일부가 있기 때문에 모든 것이 깨졌습니다.

저는 노조를 해결책으로 사용하려고 노력해 왔습니다.

union {
    double d;
    Uc c[8];
} un;
Uc *pc = un.c;
const Ur *pu = reinterpret_cast<const Ur*>(&d);
pc[0] = (*pu >> 56) & 0xFFu;
pc[1] = (*pu >> 48) & 0xFFu;
pc[2] = (*pu >> 40) & 0xFFu;
pc[3] = (*pu >> 32) & 0xFFu;
pc[4] = (*pu >> 24) & 0xFFu;
pc[5] = (*pu >> 16) & 0xFFu;
pc[6] = (*pu >> 8) & 0xFFu;
pc[7] = *pu & 0xFFu;
*p = un.d;

하지만 효과도 없었습니다.사실, 그것은 조금 더 나았습니다 - 그것은 단지 마이너스 숫자에 실패했습니다.

는 네이티브하고, 그에 시서저네이엔브티디위를스니한테간안다추음다단있, 든것다통것생니습고하는각을수하해행이음을모을가스한를점트에한는thing▁via▁at▁every▁end▁for다있▁then▁adding▁about▁a▁nativeian▁doing니▁simple를 통해 모든 을 수행하는 것을 생각하고 있습니다.char*가 있는 포인터if (LITTLE_ENDIAN)주위를 둘러보다.엎친 데 덮친 격으로, 그 프로그램은 모든 곳에서 노조를 많이 이용하는데, 이것은 현재로서는 잘 되는 것처럼 보이지만, 이 모든 혼란 후에 저는 그것이 명백한 이유 없이 갑자기 깨져도 놀라지 않을 것입니다.

다음 코드가 "잘못됨"이라는 귀하의 주장:

extern void foo(int *, double *);
union a_union t;
t.d = 3.0;
foo(&t.i, &t.d); // undefined behavior

틀렸습니다.두 조합원의 주소를 가져와서 외부 함수에 전달하는 것만으로 정의되지 않은 동작이 발생하는 것은 아닙니다. 잘못된 방법으로 포인터 중 하나를 다시 참조하는 것에서만 얻을 수 있습니다.예를 들어 함수 foo가 전달한 포인터를 다시 참조하지 않고 즉시 반환되는 경우 동작이 정의되지 않습니다.C99 표준의 엄격한 판독으로, 정의되지 않은 동작을 호출하지 않고 포인터를 참조할 수 있는 경우도 있습니다. 예를 들어, 두 번째 포인터에 의해 참조된 값을 읽은 다음 동적으로 할당된 객체(즉, 둘 다 가리키는 한 첫 번째 포인터)를 통해 값을 저장할 수 있습니다.하나("선택 유형"이 없는 경우).

앨리어싱은 컴파일러가 동일한 메모리 조각에 대해 두 개의 서로 다른 포인터를 가질 때 발생합니다.포인터를 타이핑하여 새 임시 포인터를 생성하는 것입니다.예를 들어 옵티마이저가 어셈블리 명령을 다시 정렬하는 경우 두 포인터에 액세스하면 완전히 다른 두 가지 결과를 얻을 수 있습니다. 즉, 동일한 주소에 쓰기 전에 읽기를 다시 정렬할 수 있습니다.이것이 정의되지 않은 동작인 이유입니다.

매우 간단한 테스트 코드에서는 문제를 볼 수 없지만, 많은 작업이 진행 중일 때 문제가 나타납니다.

노조가 특별한 경우가 아니라는 것을 분명히 하기 위한 경고라고 생각합니다.

별칭에 대한 자세한 내용은 다음 위키백과 문서를 참조하십시오. http://en.wikipedia.org/wiki/Aliasing_(computing)#Conflicts_with_optimization

당신은 이것을 보았습니까?엄격한 별칭 지정 규칙은 무엇입니까?

링크에는 gcc 예제와 함께 이 기사에 대한 보조 링크가 포함되어 있습니다.http://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html

이런 조합을 시도하는 것이 문제에 더 가까울 것입니다.

union a_union {
    int i;
    double *d;
};

그러면 같은 메모리를 가리키는 int와 double*의 두 가지 유형이 있습니다.이 경우 더블을 사용합니다.(*(double*)&i)문제를 일으킬 수 있습니다.

여기 내 것이 있습니다.저는 이것이 모든 GCC v5.x 이상의 버그라고 생각합니다.

#include <iostream>
#include <complex>
#include <pmmintrin.h>

template <class Scalar_type, class Vector_type>
class simd {
 public:
  typedef Vector_type vector_type;
  typedef Scalar_type scalar_type;
  typedef union conv_t_union {
    Vector_type v;
    Scalar_type s[sizeof(Vector_type) / sizeof(Scalar_type)];
    conv_t_union(){};
  } conv_t;

  static inline constexpr int Nsimd(void) {
    return sizeof(Vector_type) / sizeof(Scalar_type);
  }

  Vector_type v;

  template <class functor>
  friend inline simd SimdApply(const functor &func, const simd &v) {
    simd ret;
    simd::conv_t conv;

    conv.v = v.v;
    for (int i = 0; i < simd::Nsimd(); i++) {
      conv.s[i] = func(conv.s[i]);
    }
    ret.v = conv.v;
    return ret;
  }

};

template <class scalar>
struct RealFunctor {
  scalar operator()(const scalar &a) const {
    return std::real(a);
  }
};

template <class S, class V>
inline simd<S, V> real(const simd<S, V> &r) {
  return SimdApply(RealFunctor<S>(), r);
}



typedef simd<std::complex<double>, __m128d> vcomplexd;

int main(int argc, char **argv)
{
  vcomplexd a,b;
  a.v=_mm_set_pd(2.0,1.0);
  b = real(a);

  vcomplexd::conv_t conv;
  conv.v = b.v;
  for(int i=0;i<vcomplexd::Nsimd();i++){
    std::cout << conv.s[i]<<" ";
  }
  std::cout << std::endl;
}

주어야 합니다

c010200:~ peterboyle$ g++-mp-5 Gcc-test.cc -std=c++11 
c010200:~ peterboyle$ ./a.out 
(1,0) 

하지만 -O3 아래: 나는 이것이 틀리고 컴파일러 오류라고 생각합니다.

c010200:~ peterboyle$ g++-mp-5 Gcc-test.cc -std=c++11 -O3 
c010200:~ peterboyle$ ./a.out 
(0,0) 

g++4.9 이하

c010200:~ peterboyle$ g++-4.9 Gcc-test.cc -std=c++11 -O3 
c010200:~ peterboyle$ ./a.out 
(1,0) 

llvm xcode 아래

c010200:~ peterboyle$ g++ Gcc-test.cc -std=c++11 -O3 
c010200:~ peterboyle$ ./a.out 
(1,0) 

저는 당신의 문제를 잘 이해하지 못합니다.컴파일러는 당신의 예에서 해야 할 일을 정확히 수행했습니다.union변환은 당신이 했던 것입니다.f1f2그것은 일반적인 포인터 타입캐스트이며, 당신이 그것을 조합에 캐스팅한 것은 관련이 없으며, 그것은 여전히 포인터 캐스팅입니다.

언급URL : https://stackoverflow.com/questions/2906365/gcc-strict-aliasing-and-casting-through-a-union

반응형