Является ли неопределенным поведением вызов функции с указателями на разные элементы объединения в качестве аргументов?

Этот код выводит разные значения после компиляции с -O1 и -O2 (как gcc, так и clang):

#include <stdio.h>

static void check (int *h, long *k)
{
  *h = 5;
  *k = 6;
  printf("%d\n", *h);
}

union MyU
{
    long l;
    int i;
};

int main (void)
{
  union MyU u;
  check(&u.i, &u.l);
  return 0;
}

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

Он записывает в один элемент объединения, а затем читает из другого, но в соответствии с Отчет о дефекте № 283, который разрешен. Является ли это UB, когда доступ к элементам объединения осуществляется через указатели, а не напрямую?

Этот вопрос похож на Доступ к членам союза C через указатели, но я думаю, что на один так и не ответили полностью.


person Tor Klingberg    schedule 06.04.2014    source источник
comment
Не то чтобы я эксперт, но, читая ссылку, которую вы указали в последнем абзаце, кажется, что вы нарушаете правило, указанное в принятом ответе: значение члена союза, отличного от последнего, сохраненного в (6.2. 6.1). В частности, вы записываете 6 в u.l, а затем читаете из *h, что указывает на u.i, что делает то, что утверждает этот пост, неуказанным поведением.   -  person Turix    schedule 06.04.2014
comment
@Tor: Так и было, и ваш пример подробно объясняется принятым ответом.   -  person Deduplicator    schedule 06.04.2014
comment
возможный дубликат Доступ к членам объединения C через указатели   -  person Deduplicator    schedule 06.04.2014
comment
6.2.6.1.7 стандарта ISO гласит: Когда значение хранится в члене объекта типа union, байты представления объекта, которые не соответствуют этому члену, но соответствуют другим членам, принимают неуказанные значения.   -  person mfro    schedule 06.04.2014
comment
В C++ запись в один член объединения, а затем чтение из другого не определена, но в C99 она просто не определена (или определена реализацией). Естественно, результат каламбура типа должен быть определен реализацией, поскольку он зависит от битовых форматов, но он не должен различаться между уровнями оптимизации.   -  person Tor Klingberg    schedule 06.04.2014
comment
Более высокий уровень оптимизации, вероятно, заставит компилятор сохранить значение *h в регистре или переупорядочить присвоение *h ближе к printf() (поскольку это значение все равно нужно там снова). Вы не можете сказать наверняка (поскольку вы полагаетесь на неопределенное поведение), пока вы не проверяете сгенерированную сборку.   -  person mfro    schedule 06.04.2014
comment
Я нашел эту запись в блоге и другой отчет о дефекте #236. Я думаю, что ответ заключается в том, что у союза есть активный член (член, в который последний раз записывались), и активный член может быть изменен только через объединение. Попытка изменить активный элемент путем записи указателя на член объединения является поведением undefined.   -  person Tor Klingberg    schedule 06.04.2014
comment
@TorKlingberg: здесь речь идет об оптимизации -fstrict-aliasing, которая включается с помощью -O2, -O3 и -Os. Вы можете отключить его с помощью -fno-strict-aliasing для проверки.   -  person rici    schedule 06.04.2014
comment
@Deduplicator, я думаю, что предлагаемый дубликат отличается. В правилах псевдонимов указано, что можно использовать char для псевдонимов других типов, однако в этом посте int и long используются псевдонимы, поэтому unspecified больше не является правильным ответом IMO; нарушение правил алиасинга важнее этого.   -  person M.M    schedule 07.04.2014


Ответы (5)


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

Если мы просто обсуждаем псевдонимы членов объединения, то это было бы проще. В следующем коде:

u.i = 5;
u.l = 6;
printf("%d\n", u.i);

поведение не определено, поскольку эффективным типом u является long; то есть хранилище u содержит значение, которое было сохранено как long. Но доступ к этим байтам через lvalue типа int нарушает правила псевдонимов 6.5p7. Текст о неактивных членах союза, имеющих неуказанные значения, не применяется (IMO); правила псевдонимов превосходят это, и этот текст вступает в игру, когда правила псевдонимов не нарушаются, например, при доступе через lvalue символьного типа.

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

Однако все меняется, когда доступ «спрятан» за указателями на функцию.

DR236 решает эту проблему с помощью двух примеров. Оба примера имеют check(), как в этом посте. Пример 1 malloc занимает немного памяти и передает h и k, указывающие на начало этого блока. В примере 2 есть союз, аналогичный этому посту.

Их вывод состоит в том, что пример 1 является «неразрешенным», а пример 2 — UB. Однако в этом превосходном сообщении в блоге указывается, что логика, используемая DR236, в достижении этих выводов непоследовательно. (Спасибо Тору Клингбергу за это).

В последней строке DR236 также говорится:

Обе программы вызывают неопределенное поведение, вызывая функцию f с указателями qi и qd, которые имеют разные типы, но обозначают одну и ту же область памяти. Переводчик имеет полное право изменить доступ к *qi и *qd по обычным правилам псевдонимов.

(очевидно, в противоречии с более ранним утверждением, что пример 1 не был решен).

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

Было высказано предположение, что правила псевдонимов позволяют компилятору сделать вывод, что int * и long * не могут получить доступ к одной и той же памяти. Однако примеры 1 и 2 этому категорически противоречат.

Если бы указатели имели один и тот же тип, то я думаю, мы согласны с тем, что компилятор не может изменить порядок доступа, потому что они оба могут указывать на один и тот же объект. Компилятор должен предположить, что указатели не являются restrict, если они специально не объявлены как таковые.

Тем не менее, я не вижу разницы между этим случаем и случаями примеров 1 и 2.

DR236 также говорит:

Общее понимание состоит в том, что объявление объединения должно быть видно в единице перевода.

что снова противоречит утверждению о том, что пример 2 является UB, потому что в примере 2 весь код находится в одной и той же единице перевода.

Мой вывод: мне кажется, что формулировка C99 указывает на то, что компилятору не следует разрешать переупорядочивать *h = 5; и *k = 6; в случае, если они перекрывают хранилище. Несмотря на то, что DR236 противоречит формулировке C99 и ничего не проясняет. Но чтение *h после этого должно привести к неопределенному поведению, поэтому компилятору разрешено генерировать вывод 5 или 6 или что-то еще.

По моему мнению, если вы измените check() на *k = 6; *h=5;, тогда должно быть четко определено, чтобы напечатать 5. Было бы интересно посмотреть, делает ли компилятор что-то еще в этом случае, а также обоснование компилятора, если он это делает.

person M.M    schedule 07.04.2014
comment
Но доступ к этим байтам через lvalue типа int нарушает правила псевдонимов 6.5p7. Доступ к значению осуществляется через объект объединения, что является одним из случаев, явно разрешенных 6.5p7. 6.2.6.1p7 абсолютно применим, фактически нет другой ситуации, в которой он мог бы применяться. - person tab; 07.04.2014
comment
u имеет тип агрегата или объединения, как в 6.5p7, а u.l — нет. Как подробно обсуждалось в связанном сообщении в блоге, не очень ясно, что пытается сказать этот маркер, но цель может состоять в том, чтобы убедиться, что он четко определен для написания union MyU v; v = u;. - person M.M; 07.04.2014
comment
Это ооочень сложно, но я думаю, что ваш ответ не совсем правильный (я автор того сообщения в блоге, на которое вы ссылаетесь). Во-первых, сноска 82 C99 (95 в C11) специально разрешает ваш пример, который, как вы утверждаете, имеет неопределенное поведение. Будучи сноской, это не является нормативным; (искаженное) объяснение, которое поддерживает это, состоит в том, что оператор доступа к члену объединения работает с объектом объединения (и извлекает из него значение члена), а не обращается к объекту-члену напрямую (6.5.2.3p3). Это другой случай, чем попытка доступа к элементам через указатели, и в этом случае в игру вступает 6.5p7. - person davmac; 27.04.2015
comment
(Сноска 82, на которую я ссылаюсь, была добавлена ​​в Техническое исправление № 3 и не является частью оригинальной, неисправленной спецификации C99. Кроме того, я думаю, вам следует игнорировать DR236 — он несовместим ни с самим собой, ни со стандартом, и хотя он призывает внести поправку в 6.5p6/p7, такая поправка никогда не вносилась. Я искренне верю, что члены комитета, участвовавшие в том обсуждении, просто не поняли проблему должным образом. Во всяком случае, компиляторы, которые я тестировал, похоже, следуют моей интерпретации здесь). - person davmac; 27.04.2015
comment
@davmac Я согласен, что DR236 кажется запутанным и его следует игнорировать. Однако я не уверен, что мы ближе к решению. Я думаю, что намерение правил псевдонимов заключается в том, что код OP является UB, если h и k указывают на перекрывающееся хранилище. Однако я не вижу никакого текста в C11, подтверждающего это для случая объединения, если только мы (несколько произвольно) не решим, что сноска 95 C11 применяется только к доступу к объединению непосредственно именованным членом, а не указателем на этот член или иным образом. (в этом случае строгий псевдоним снова превалирует над псевдонимом объединения) - person M.M; 27.04.2015
comment
Что ж, 6.5.2.3p3 определяет оператор доступа к членам — постфиксное выражение, за которым следует расширение . оператор и идентификатор обозначают член структуры или объект объединения. - поэтому u.i обращается к содержащему объекту объединения, но не обращается к объекту-члену; скорее он интерпретирует содержимое объединения в соответствии с типом члена (и генерирует lvalue, которое можно использовать для установки активного члена). Это контрастирует с доступом через указатель. Эта интерпретация объясняет, почему сноска 95 применима только к доступу к объединению напрямую через именованный элемент. - person davmac; 27.04.2015
comment
@davmac: Я бы хотел, чтобы Стандарт был написан больше как протокольный документ и указывал, какие предположения разрешено делать компилятору и что ему разрешено делать на основе этих предположений, а не как набор иногда- неоднозначные требования к программам. Разрешение компилятору делать предположения о том, что будет, а что не будет псевдонимом, может сделать возможными некоторые полезные оптимизации, но если некоторые люди, читающие стандарт, придут к выводу, что конкретная программа строго соответствует, в то время как другие придут к выводу, что та же программа вызывает неопределенное поведение, это означало бы... - person supercat; 06.07.2015
comment
...что Стандарт неисправен. Протокольный документ мог бы избежать таких проблем, если бы имел отдельные граничные условия путем разделения. Компиляторы должны чисто обрабатывать программы, соответствующие X, а строго соответствующий код должен соответствовать Y, так что код, который соблюдает любую правдоподобную интерпретацию Y, будет соблюдать все правдоподобные интерпретации. X, и код, который не поддастся ни одной правдоподобной интерпретации X, не подведет и все правдоподобные интерпретации Y. - person supercat; 06.07.2015
comment
@supercat Действительно. Хотя я считаю, что в целом согласованное понимание стандарта поставщиками компиляторов нормально, это понимание зависит от случайных отклонений от фактической формулировки стандарта или дополнений к ней. Существует много непоследовательного использования терминов, чего вы не должны видеть в формальном документе, определяющем стандарт. У меня есть полный список (IMO) некоторых из наиболее серьезных проблем здесь: davmac.wordpress.com/ c99-ошибка - person davmac; 07.07.2015
comment
@davmac: Я думаю, что более общая проблема заключается в том, что некоторые люди, кажется, не знают, что популярность C проистекает из того факта, что, хотя Стандарт не признает отдельных ролей для компиляторов и базовых платформ, обычная историческая практика заключалась в том, что компиляторы воздерживались от участия в чрезмерно дурацкое поведение, когда платформа этого не делает. Кроме того, многие формы Undefined Behavior предлагают поведенческие гарантии на многих платформах, которые довольно слабы, но, тем не менее, очень полезны. Например, некоторые платформы могут гарантировать, что u>>N with произвольно даст `u››(N & 31) или... - person supercat; 07.07.2015
comment
...N > 31 ? 0 : u >> n. Возможно, не так приятно, как гарантировать ту или иную форму, но, тем не менее, часто полезно. Многие программы имеют два требования: (1) при правильном входе выдавать правильный вывод; (2) Не запускайте ядерные ракеты, даже если введены неверные данные. ИМХО, хороший языковой стандарт должен облегчать написание программ, которые как можно быстрее удовлетворят №1, сохраняя при этом №2. Однако для этого требуется, чтобы язык предоставлял способы, с помощью которых программисты могут предоставить компилятору широкую свободу, не давая ему полной свободы — то, чего нет в C, но что можно было бы добавить... - person supercat; 07.07.2015
comment
... способами, которые не нарушили бы смысла существующего кода - даже [особенно] кода, который в настоящее время опирается на фактические или де-факто слабые поведенческие гарантии. - person supercat; 07.07.2015
comment
@davmac: я просто просматривал этот связанный документ; одна из самых больших проблем заключается в том, что правила C89 были написаны таким образом, что не имели смысла применительно к хранилищу в куче, а компиляторам 1989 года не нужны были правила псевдонимов для хранилища в куче, но авторы C99 хотели сделать вид, что они разъясняли правила, а не добавляли новые. Чистый эффект — это совокупность правил, вредоносное воздействие которых выходит далеко за рамки любой возможной выгоды, поскольку не существует правдоподобного обходного пути, кроме массового чрезмерного использования memcpy. Мне кажется ироничным, что двадцать лет назад меня раздражало... - person supercat; 03.05.2016
comment
... тот факт, что C, казалось, очень сильно полагался на memcpy, но надеялся, что ситуация улучшится. Однако потребность в memcpy со временем только возрастает. - person supercat; 03.05.2016
comment
@supercat эти правила пересматриваются для C2X, см. N2012 - person M.M; 03.05.2016
comment
@M.M: Там есть кое-что хорошее. Я думаю, что должно быть больше средств, с помощью которых реализации могут обещать ограниченное поведение для вещей, которые теперь являются UB, особенно потому, что такие обещания могут позволить писать код более сжато и эффективно, чем если бы он должен был быть написан как if (safe_case) some_code; else safer_code;, но сгенерированный машинный код for some_code удовлетворял бы требованиям для safer_code, даже если исходный код этого не делал. Я также думаю, что концепция происхождения указателя хороша, но должны быть какие-то аварийные выходы. - person supercat; 04.05.2016
comment
@MM: ИМХО, к чему C действительно должен стремиться, так это иметь определение условного соответствия, где, если программа X условно соответствует, любой компилятор Y может на досуге либо отказаться от компиляции, либо запустить ее таким образом, чтобы удовлетворить требования, но Y должен сделать одно или другое. Невозможно определить диалект C, который поддерживался бы всеми реализациями, но при этом корректно выполнял бы весь корпус кода C, но должна быть возможность определить такой диалект, который позволил бы полностью отбрасывать программы всеми компиляторами, которые могут это сделать. т поддерживать их. - person supercat; 04.05.2016
comment
@MM: один недостаток, который я вижу в предлагаемом документе, заключается в том, что он не может распознать возможность того, что на платформах, где компилятор не знает всего о среде выполнения (почти ничего, кроме виртуальной машины), программа может получать указатели извне. world, через ввод-вывод или другие средства, которые идентифицируют хранилище, о котором компилятор ничего не может знать. - person supercat; 05.05.2016

Соответствующая цитата из стандарта - это соответствующие правила псевдонимов, которые нарушаются. Нарушение нормативного shall всегда приводит к Неопределенному поведению, поэтому все идет так:

6.5 Выражения §7
Доступ к хранимому значению объекта должен осуществляться только выражением lvalue, имеющим один из следующих типов:88)
— тип, совместимый с действующим типом объекта object,
— уточненная версия типа, совместимая с эффективным типом объекта,
— тип, который является подписанным или беззнаковым типом, соответствующим эффективному типу объекта,
— тип, который — тип со знаком или без знака, соответствующий уточненной версии эффективного типа объекта,
— тип агрегата или объединения, который включает в себя один из вышеупомянутых типов среди своих членов (включая, рекурсивно, член подагрегата или содержащийся в нем тип). union), или
— тип символа.

В то время как main() использует union, check() нет.

person Deduplicator    schedule 07.04.2014
comment
Что вы думаете об альтернативной версии check с *k = 6; *h = 5; printf("%d\n", *h); ? - person M.M; 07.04.2014
comment
@MattMcNabb: Насколько я вижу, все еще UB. Хотя я не думаю, что что-то катастрофическое произойдет в обоих случаях. - person Deduplicator; 07.04.2014
comment
Однако в этой альтернативной версии нет нарушения псевдонимов (если записи не переупорядочены) - *h читается только сразу после записи *h. Кроме того, OP занимался благотворительностью, используя int и long; если бы один тип был double или типом указателя, тогда все могло бы стать волосатым. - person M.M; 07.04.2014
comment
@MattMcNabb: Хм. Не могу найти ничего, что, я уверен, сделало бы альтернативную версию UB, так что все может быть в порядке. Но я бы не стал делать ставку ни на это, ни на какой-либо конкретный компилятор, правильно следующий этому пониманию. Просто слишком опасно. - person Deduplicator; 07.04.2014
comment
@MattMcNabb Я действительно считаю, что в альтернативной версии есть нарушение псевдонимов (поскольку член, присутствующий в объединении после хранилища через *k, имеет объявленный тип long, запись через *h, которая ссылается на тот же объект через несовместимый тип, представляет собой нарушение псевдонимов. См. мой комментарий к вашему ответу для получения дополнительной информации. Однако вы вряд ли когда-либо увидите, что это вызывает проблему на практике). - person davmac; 27.04.2015
comment
(также не ясно, что начальное хранилище через *k будет законным с аналогичными рассуждениями). - person davmac; 27.04.2015
comment
@davmac Теперь я с тобой согласен - person M.M; 27.04.2015

Я скомпилировал ваш код с параметрами -O1 и -O2 и запустил сеанс gdb, вот результат:

(gdb) r
Starting program: /home/sheri/test 
Breakpoint 1, main () at test.c:17
17  {
(gdb) s
19          check(&u.i, &u.l);
(gdb) p u
$1 = <optimized out>
(gdb) p u.i
$2 = <optimized out>
(gdb) p u.l
$3 = <optimized out>`

Я не эксперт по gdb, но вот что следует отметить. 1. союза нет в стеке, но он хранится в регистре, и поэтому он печатает, когда вы его печатаете, или это i или l

Я разобрал исполняемый файл и посмотрел на main, и вот что я нашел: 0000000000400440 :

400440: 48 83 ec 08             sub    $0x8,%rsp
400444: ba 06 00 00 00          mov    $0x6,%edx
400449: be 3c 06 40 00          mov    $0x40063c,%esi
40044e: bf 01 00 00 00          mov    $0x1,%edi
400453: 31 c0                   xor    %eax,%eax
400455: e8 d6 ff ff ff          callq  400430 <__printf_chk@plt>

Таким образом, в строке 2 компилятор поместил 0x6 в регистр %edx напрямую и не создал проверку функции в первую очередь, так как уже понял, что значение, передаваемое в printf, всегда будет равно 6.

Может быть, вам стоит попробовать то же самое и посмотреть, какой результат вы получили на своей машине.

person silentnights    schedule 06.04.2014
comment
UB может быть ожидаемым поведением, а может быть чем-то совершенно другим. Тот факт, что он работает с ‹версией› компилятора ‹поставщика› на ‹процессоре› под управлением ‹операционной системы›, не означает, что он будет вести себя одинаково со всеми их конфигурациями. - person dave; 07.04.2014
comment
Однако машинный код @dave может показать, что что-то не работает разумным образом, что указывает либо на то, что он не определен, либо на ошибку компилятора. - person Kaz; 07.04.2014
comment
@Kaz Моя точка зрения заключалась в том, что демонстрация того, что это работает, не означает, что это не неопределенное поведение. И это проблема с вашим ответом. Вы можете показать, что это не работает разумным образом, но не можете показать, что это работает разумно. - person dave; 26.04.2014

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

Стандарт C99 хотел позволить компиляторам быть более агрессивными с псевдонимами на основе типов, несмотря на тот факт, что его квалификатор «restrict» устраняет большую часть необходимости в этом, но не мог делать вид, что приведенный выше код недопустим, поэтому он добавляет требование, согласно которому, если компилятор видит, что два указателя могут быть членами одного и того же объединения, он должен допускать такую ​​возможность. В отсутствие оптимизации всей программы это позволило бы сделать большинство программ C89 совместимыми с C99, обеспечив видимость подходящих определений типов объединения во всех функциях, которые могут видеть оба типа указателей. Чтобы ваш код был действителен в соответствии с C99, вам нужно переместить объявление типа объединения над функцией, которая получает два указателя. Это по-прежнему не заставит код работать для gcc, потому что авторы gcc не хотят, чтобы такие детали, как правильное стандартное поведение, мешали созданию «эффективного» кода.

person supercat    schedule 03.05.2016

Взятие адресов абсолютно нормально.

Что не так: Чтение объекта с использованием типа, отличного от того, который использовался для его записи. Таким образом, после записи в int* чтение long* является неопределенным поведением и наоборот. Запись в int*, затем запись в long* и т. д. определяется поведением (теперь объединение имеет член long с определенным значением).

person gnasher729    schedule 03.05.2016