Intereting Posts
Почему я могу изменить значение переменной const char *? printf () приводит к тарабарщине Как скомпилировать код C во время выполнения и получить указатель на соответствующую функцию? Назначение и точки последовательности: как это неоднозначно? c round double linked-list: rev traverse дает другой адрес указателя-указателя для того же узла Как определить массив указателей на функции и функции не имеют того же определения входных аргументов? fopen возвращает NULL-указатель, если файл уже открыт Как хранить ключи ассоциативного массива в C, реализованные через hcreate / hsearch по значению (не по ссылке)? Проблема при записи и чтении в двоичный файл Хеширование кукушки в C Извлечение бит с использованием манипуляции с битами Методы окончания файла (EOF), не работающие в консоли NetBeans Анализаторы статического кода для C Использование clock () для измерения времени выполнения Массив char * должен заканчиваться на ‘\ 0’ или “\ 0”?

Нарушение строгой сглаживания в C, даже без кастинга?

Как *i и ui печатать разные числа в этом коде, хотя i определяется как int *i = &u.i; ? Я могу только предположить, что я запускаю UB здесь, но я не понимаю, как именно.

( ideone demo replicates, если я выбираю «C» в качестве языка. Но, как указывал @ 2501, не если «C99 strict» – это язык. Но опять же, я получаю проблему с gcc-5.3.0 -std=c99 ! )

 // gcc -fstrict-aliasing -std=c99 -O2 union { int i; short s; } u; int * i = &u.i; short * s = &u.s; int main() { *i = 2; *s = 100; printf(" *i = %d\n", *i); // prints 2 printf("ui = %d\n", ui); // prints 100 return 0; } 

(gcc 5.3.0, с -fstrict-aliasing -std=c99 -O2 , также с -std=c11 )

Моя теория состоит в том, что 100 – это «правильный» ответ, потому что запись члену профсоюза через short значение *s определяется как таковое (для этой платформы / endianness / whatever). Но я думаю, что оптимизатор не понимает, что запись в *s может быть псевдонимом ui , и поэтому она думает, что *i=2; это единственная строка, которая может влиять на *i . Это разумная теория?

Если *s может быть псевдоним ui , а ui может быть псевдоним *i , то, безусловно, компилятор должен думать, что *s может псевдоним *i ? Не следует ли переходить на псевдонимы?

Наконец, у меня всегда было это предположение, что проблемы с жестким сглаживанием были вызваны плохим кастингом. Но в этом нет кастинга!

(Мой фон – C ++, я надеюсь, что задаю здесь разумный вопрос о C. Мое (ограниченное) понимание заключается в том, что на C99 допустимо писать через одного члена профсоюза, а затем читать через другого члена другого тип.)

Неисправность выдается -fstrict-aliasing оптимизации -fstrict-aliasing . Его поведение и возможные ловушки описаны в документации GCC :

Обратите особое внимание на код:

  union a_union { int i; double d; }; int f() { union a_union t; td = 3.0; return ti; } 

Практика чтения из другого члена профсоюза, чем тот, который был недавно написан (называемый «пуш-тип»), является обычным явлением. Даже с -fstrict-aliasing допускается использование типа -fstrict-aliasing при условии, что доступ к памяти осуществляется через тип объединения . Таким образом, приведенный выше код работает так, как ожидалось. См. Структуры перечислений объединений и реализация битовых полей . Однако этот код не может :

  int f() { union a_union t; int* ip; td = 3.0; ip = &t.i; return *ip; } 

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

C (например, C11, n1570), 6.5p7 :

Объект должен иметь сохраненное значение, к которому обращается только выражение lvalue, которое имеет один из следующих типов:

  • совокупность или тип объединения, который включает один из вышеупомянутых типов среди своих членов (включая рекурсивно, член субагрегата или содержащегося объединения) или тип символа.

Выражения lvalue ваших указателей не являются типами union , поэтому это исключение не применяется. Компилятор правильно использует это неопределенное поведение.

Сделать указатели типов указателей на тип union и разыменовать соответствующий член. Это должно работать:

 union { ... } u, *i, *p; 

Строгое сглаживание указывается в стандарте C, но обычная интерпретация заключается в том, что союзное сглаживание (которое заменяет строгий псевдоним) разрешено только в том случае, когда члены профсоюза получают доступ по имени.

Для обоснования этого следует рассмотреть:

 void f(int *a, short *b) { 

objectiveю правила является то, что компилятор может предположить, что a и b не являются псевдонимами и генерируют эффективный код в f . Но если компилятор должен был учитывать тот факт, что a и b могут быть перекрывающимися членами профсоюза, на самом деле они не могли бы сделать эти предположения.

Независимо от того, являются ли эти два указателя функциональными параметрами или нет, это не имеет значения, строгое правило псевдонимов не отличается от этого.

Этот код действительно вызывает UB, потому что вы не соблюдаете строгое правило псевдонимов. n1256 черновик состояний C99 в 6.5. Выражения §7:

Объект должен иметь сохраненное значение, к которому обращается только выражение lvalue, которое имеет один из следующих типов:
– тип, совместимый с эффективным типом объекта,
– квалифицированная версия типа, совместимая с эффективным типом объекта,
– тип, который является подписанным или неподписанным типом, соответствующим эффективному типу объекта,
– тип, который является подписанным или неподписанным типом, соответствующим квалифицированной версии эффективного типа объекта,
– совокупный или тип объединения, который включает один из вышеупомянутых типов среди его членов (включая рекурсивно, член субагрегата или содержащегося объединения) или
– тип символа.

Между *i = 2; и printf(" *i = %d\n", *i); изменяется только короткий объект. С помощью правила строгого псевдонима компилятор может предположить, что объект int, указанный i , не был изменен, и он может напрямую использовать кешированное значение без перезагрузки его из основной памяти.

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

Для второй печати объединения ссылаются на один и тот же стандарт в 6.2.6.1. Представления типов / Общие положения §7:

Когда значение хранится в члене объекта типа union, байты представления объекта, которые не соответствуют этому члену, но соответствуют другим членам, принимают неопределенные значения.

Так как us были сохранены, ui приняли значение, не определенное стандартным

Но мы можем прочитать далее в 6.5.2.3. Структура и члены профсоюза §3 примечание 82:

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

Хотя примечания не являются нормативными, они позволяют лучше понять стандарт. Когда us были сохранены *s указателя *s , байты, соответствующие короткому значению, были изменены на значение 2. Предполагая, что система немногочисленна, в 100 меньше, чем значение короткого, представление как int должно теперь быть 2, так как байты верхнего порядка равны 0.

TL / DR: даже если это не является нормативным, примечание 82 должно требовать, чтобы в маленькой системной системе семейства x86 или x64 printf("ui = %d\n", ui); отпечатки 2. Но в соответствии с правилом строгого сглаживания компилятору все же разрешено предполагать, что значение, указанное i , не изменилось и может печатать 100

Вы исследуете несколько противоречивую область стандарта C.

Это строгое правило псевдонимов:

Объект должен иметь сохраненное значение, к которому обращается только выражение lvalue, которое имеет один из следующих типов:

  • тип, совместимый с эффективным типом объекта,
  • квалифицированную версию типа, совместимую с эффективным типом объекта,
  • тип, который является подписанным или неподписанным типом, соответствующим эффективному типу объекта,
  • тип, который является подписанным или неподписанным типом, соответствующим квалифицированной версии эффективного типа объекта,
  • совокупный или союзный тип, который включает один из вышеупомянутых типов среди его членов (в том числе, рекурсивно, член субагрегата или содержащегося объединения),
  • тип символа.

(C2011, 6,5 / 7)

Выражение lvalue *i имеет тип int . Выражение lvalue *s имеет тип short . Эти типы несовместимы друг с другом и не совместимы ни с каким другим конкретным типом, и правило строгих правил псевдонимов не дает никакой другой альтернативы, которая позволяет обоим обращениям соответствовать, если указатели сглажены.

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

Вышеупомянутые споры возникают, потому что люди иногда указывают на сноску 95:

Если элемент, используемый для чтения содержимого объекта объединения, не совпадает с элементом, используемым последним для хранения значения в объекте, соответствующая часть представления объекта значения переинтерпретируется как представление объекта в новом типе как описанный в 6.2.6 (процесс, иногда называемый «type punning»). Это может быть ловушечное представление.

Сноски носят информационный характер, но не являются нормативными, поэтому нет никакого вопроса, какой текст победит, если они конфликтуют. Лично я использую сноску просто как руководство по внедрению, разъясняя смысл того факта, что хранение для членов профсоюза перекрывается.

Похоже, это результат того, что оптимизатор делает свою магию.

С -O0 обе строки печатают 100, как ожидалось (предполагая мало-endian). С -O2 происходит некоторая переупорядоченность.

gdb дает следующий результат:

 (gdb) start Temporary breakpoint 1 at 0x4004a0: file /tmp/x1.c, line 14. Starting program: /tmp/x1 warning: no loadable sections found in added symbol-file system-supplied DSO at 0x2aaaaaaab000 Temporary breakpoint 1, main () at /tmp/x1.c:14 14 { (gdb) step 15 *i = 2; (gdb) 18 printf(" *i = %d\n", *i); // prints 2 (gdb) 15 *i = 2; (gdb) 16 *s = 100; (gdb) 18 printf(" *i = %d\n", *i); // prints 2 (gdb) *i = 2 19 printf("ui = %d\n", ui); // prints 100 (gdb) ui = 100 22 } (gdb) 0x0000003fa441d9f4 in __libc_start_main () from /lib64/libc.so.6 (gdb) 

Причина этого, как утверждали другие, заключается в том, что неопределенное поведение позволяет получить доступ к переменной одного типа с помощью указателя на другой тип, даже если рассматриваемая переменная является частью объединения. Поэтому оптимизатор может делать все, что пожелает в этом случае.

Переменная другого типа может быть прочитана только через объединение, которое гарантирует корректное поведение.

Любопытно, что даже с -Wstrict-aliasing=2 , gcc (по 4.8.4) не жалуется на этот код.

Случайно или по дизайну, C89 включает язык, который интерпретируется двумя разными способами (наряду с различными интерпретациями между ними). Речь идет о том, когда необходимо, чтобы компилятор должен был признать, что хранилище, используемое для одного типа, может быть доступно через указатели другого. В примере, приведенном в обосновании C89, aliasing рассматривается между глобальной переменной, которая явно не является частью какого-либо объединения и указателем на другой тип, и ничто в коде не предполагает, что может возникнуть сглаживание.

Одна интерпретация ужасно калечит язык, в то время как другая ограничивает использование определенных оптимизаций «несоответствующими» режимами. Если бы те, кто не имел своей предпочтительной оптимизации, учитывая статус второго classа, написали C89, чтобы однозначно сопоставить их интерпретацию, те части Стандарта были бы широко осуждены, и было бы какое-то четкое признание неразрывного диалекта C, который будет соблюдать некапиталистическую интерпретацию данных правил.

К сожалению, что произошло, так это потому, что правила явно не требуют, чтобы авторы компиляторов применяли искажающую интерпретацию, большинство авторов компиляторов в течение многих лет просто интерпретировали правила таким образом, чтобы сохранить семантику, которая сделала C полезной для системного программирования; у программистов не было никаких оснований жаловаться на то, что в Стандарте не предусмотрено, что компиляторы ведут себя разумно, потому что с их точки зрения всем казалось очевидным, что они должны это делать, несмотря на неряшливость Стандарта. Между тем, однако, некоторые люди настаивают на том, что, поскольку Стандарт всегда позволял компиляторам обрабатывать семантически ослабленное подмножество языка программирования системы Ritchie, нет никаких причин, по которым ожидается, что компилятор, соответствующий стандарту, обработает что-либо еще.

Разумным решением этой проблемы было бы признать, что C используется для достаточно разнообразных целей, поскольку должны быть несколько режимов компиляции – один требуемый режим будет обрабатывать все обращения ко всему, чей адрес был взят так, как если бы они читали и записывали базовое хранилище напрямую , и будет совместим с кодом, который ожидает любой уровень поддержки указателей на основе указателей. Другой режим может быть более строгим, чем C11, за исключением случаев, когда код явно использует директивы для указания того, когда и где хранилище, которое использовалось как один тип, должно быть переинтерпретировано или переработано для использования в качестве другого. Другие режимы позволят некоторые оптимизации, но поддерживают некоторый код, который будет ломаться под более строгими диалектами; компиляторы без конкретной поддержки конкретного диалекта могут заменить один на более определенное поведение псевдонимов.