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

Этот код печатает разные значения после компиляции с помощью -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;
}

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

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

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

Ответ 1

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

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

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

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

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

Однако все вещи, похоже, меняются, когда обращения "скрыты" за указателями на функцию.

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

Их вывод состоит в том, что пример 1 "неразрешен", а пример 2 - UB. Однако этот отличный пост в блоге указывает, что логика, используемая DR236 при достижении этих выводов, непоследовательна. (Спасибо Tor Klingberg за то, что вы нашли это).

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

Обе программы вызывают поведение undefined, вызывая функцию 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 после этого должно приводить к поведению undefined, поэтому компилятору разрешено генерировать выходные данные 5 или 6 или что-то еще.

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

Ответ 2

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

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

Пока main() использует union, check() не делает.

Ответ 3

Я скомпилировал ваш код с -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. объединение отсутствует в стеке, но оно хранится в регистре и поэтому оно печатает при печати, или я или 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 <[email protected]>

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

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

Ответ 4

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

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

Ответ 5

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

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