GCC, строгое псевдонимы и приведение через союз - PullRequest
33 голосов
/ 25 мая 2010

У вас есть какие-нибудь страшные истории, чтобы рассказать? Руководство GCC недавно добавило предупреждение о -fstrict-aliasing и приведении указателя через объединение:

[...] Взятие адреса, приведение результирующего указателя и разыменование результата имеют неопределенное поведение [выделение добавлено], даже если приведение использует тип объединения, например :

    union a_union {
        int i;
        double d;
    };

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

У кого-нибудь есть пример, иллюстрирующий это неопределенное поведение?

Обратите внимание, что этот вопрос не о том, что говорит или не говорит стандарт C99. Сегодня речь идет о реальном функционировании gcc и других существующих компиляторов.

Я только догадываюсь, но одна потенциальная проблема может заключаться в настройке от d до 3,0. Поскольку 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() помещает 3333333.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.open -std.org / ОТК1 / SC22 / WG14 / WWW / Docs / n1422.pdf
http://gcc.gnu.org/ml/gcc/2010-01/msg00013.html
http://davmac.wordpress.com/2010/02/26/c99-revisited/
http://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html
(= поиск страницы в Google , затем просмотр кэшированной страницы)

Что такое строгое правило наложения имен?
C99 строгие правила псевдонимов в C ++ (GCC)

В первой ссылке, черновик протокола собрания ИСО семь месяцев назад, один участник отмечает в разделе 4.16:

Кто-нибудь считает, что правила достаточно ясны? Никто на самом деле не может их интерпретировать.

Другие примечания: Мой тест был с gcc 4.3.4, с -O2; опции -O2 и -O3 подразумевают -fstrict-aliasing. Пример из руководства GCC предполагает sizeof (double) > = sizeof (int); не имеет значения, неравны ли они.

Кроме того, как отметил Майк Актон в ссылке cellperformace, -Wstrict-aliasing=2, но not =3, выдает warning: dereferencing type-punned pointer might break strict-aliasing rules для примера здесь.

Ответы [ 7 ]

11 голосов
/ 02 июня 2010

Тот факт, что 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 также отключает предупреждение о каламбурах.

У меня нет 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; -)

4 голосов
/ 11 июля 2010

Ваше утверждение, что следующий код является "неправильным":

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

... неправильно. Просто взятие адреса двух членов объединения и передача их внешней функции не приводит к неопределенному поведению; вы получаете это только от разыменования одного из этих указателей недопустимым способом. Например, если функция foo немедленно возвращает значение без разыменования указателей, которые вы передали, то поведение не является неопределенным. При строгом чтении стандарта C99 даже в некоторых случаях указатели могут быть разыменованы без вызова неопределенного поведения; например, он мог бы прочитать значение, на которое ссылается второй указатель, и затем сохранить значение через первый указатель, при условии, что они оба указывают на динамически размещенный объект (то есть один без «объявленного типа»).

3 голосов
/ 27 октября 2011

Ну, это немного некропостинга, но вот ужасная история. Я портирую программу, которая была написана с допущением, что порядок байтов в нативном порядке является байтовым. Теперь мне нужно, чтобы он тоже работал на little endian. К сожалению, я не могу просто использовать собственный порядок байтов везде, так как данные могут быть доступны разными способами. Например, 64-разрядное целое число может рассматриваться как два 32-разрядных целых числа или как 4 16-разрядных целых числа, или даже как 16 4-разрядных целых числа. Что еще хуже, нет никакого способа выяснить, что именно хранится в памяти, потому что программное обеспечение является интерпретатором какого-то байт-кода, а данные формируются этим байт-кодом. Например, байт-код может содержать инструкции для записи массива 16-разрядных целых чисел, а затем обращаться к паре из них в виде 32-разрядного числа с плавающей запятой. И нет никакого способа предсказать это или изменить байтовый код.

Поэтому мне пришлось создать набор классов-оболочек для работы со значениями, хранящимися в порядке с прямым порядком байтов, независимо от собственного порядка байтов. Отлично работал в Visual Studio и в GCC на Linux без оптимизации. Но с 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-разрядном целом числе, в удвоение. Похоже, что компилятор решил выполнить присвоение * pF после присвоения D - в результате при первом выполнении кода значение D было мусором, а последующие значения были «запоздалыми» на 1 итерацию.

Чудом, в тот момент не было никаких других проблем. Поэтому я решил продолжить и протестировать мой новый код на исходной платформе 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;

но это тоже не сработало. На самом деле, это было немного лучше - это не удалось только для отрицательных чисел.

На данный момент я думаю о том, чтобы добавить простой тест для собственного порядка байтов, а затем выполнить все с помощью char* указателей с проверками if (LITTLE_ENDIAN). Что еще хуже, программа интенсивно использует союзы повсюду, что пока работает нормально, но после всего этого беспорядка я не удивлюсь, если он внезапно сломается без видимой причины.

3 голосов
/ 25 мая 2010

Псевдоним возникает, когда компилятор имеет два разных указателя на один и тот же фрагмент памяти. Типизируя указатель, вы генерируете новый временный указатель. Например, если оптимизатор переупорядочивает инструкции по сборке, доступ к двум указателям может дать два совершенно разных результата - он может изменить порядок чтения перед записью на один и тот же адрес. Вот почему это неопределенное поведение.

Вы вряд ли увидите проблему в очень простом тестовом коде, но она появится, когда многое произойдет.

Я думаю, что предупреждение состоит в том, чтобы прояснить, что профсоюзы не являются особым случаем, даже если вы ожидаете, что они будут.

См. Эту статью в Википедии для получения дополнительной информации о псевдонимах: http://en.wikipedia.org/wiki/Aliasing_(computing)#Conflicts_with_optimization

2 голосов
/ 01 июня 2010

Вы видели это? Что такое строгое правило наложения имен?

Ссылка содержит дополнительную ссылку на эту статью с примерами gcc. http://cellperformance.beyond3d.com/articles/2006/06/understanding-strict-aliasing.html

Попытка подобного союза была бы ближе к проблеме.

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

Таким образом, у вас есть 2 типа, int и double *, указывающие на одну и ту же память. В этом случае использование двойного (*(double*)&i) может вызвать проблему.

1 голос
/ 06 мая 2017

Вот мое: По-моему, это ошибка во всех 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) 

Под г ++ 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) 
0 голосов
/ 01 июня 2010

Я не очень понимаю вашу проблему. Компилятор сделал именно то, что должен был сделать в вашем примере. union преобразование - это то, что вы сделали в f1. В f2 это обычный тип указателя, то, что вы привели его к объединению, не имеет значения, это все еще указатель приведение

...