gcc, строгое искажение, и бросающий через объединение

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

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

    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 

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/jtc1/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)

В первой ссылке спроектируйте минуты ISO, встречающегося семь месяцев назад, участвующих примечаний в разделе 4.16:

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

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

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

34
задан curiousguy 1 October 2017 в 15:51
поделиться

5 ответов

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

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

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

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

3
ответ дан 27 November 2019 в 17:14
поделиться

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

0
ответ дан 27 November 2019 в 17:14
поделиться

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

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

Попытка подобного объединения была бы ближе к решению проблемы.

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

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

2
ответ дан 27 November 2019 в 17:14
поделиться

Тот факт, что 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 перед чтением через объединение: мой проверяет, изменена ли она.

Кстати, безопасный способ прочитать половину двойки как int:

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 перестанут работать union casts ;-)

.
12
ответ дан 27 November 2019 в 17:14
поделиться

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

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

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

4
ответ дан 27 November 2019 в 17:14
поделиться
Другие вопросы по тегам:

Похожие вопросы: