Подтвердить что ты не робот

Does & ((название структуры *) NULL → b) вызывает поведение undefined в C11?

Пример кода:

struct name
{
    int a, b;
};

int main()
{
    &(((struct name *)NULL)->b);
}

Означает ли это поведение undefined? Мы могли бы обсудить, является ли это "разыменованием нулевым", однако C11 не определяет термин "разыменование".

6.5.3.2/4 ясно говорит, что использование * по нулевому указателю вызывает поведение undefined; однако он не говорит то же самое для ->, а также не определяет a -> b как (*a).b; он имеет отдельные определения для каждого оператора.

В семантике -> в 6.5.2.3/4 говорится:

Постфиксное выражение, за которым следует оператор → , и идентификатор обозначает член структуры или объекта объединения. Значение - это имя именованного элемента объекта которое первое выражение указывает, и является значением l.

Однако NULL не указывает на объект, поэтому второе предложение кажется underspecified.

Также может быть 6.5.3.2/1:

Ограничения:

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

Однако я чувствую, что полужирный текст неисправен и должен читать значение lvalue, которое потенциально обозначает объект, согласно 6.3.2.1/1 (определение lvalue) - C99 испортил определение lvalue, поэтому C11 пришлось переписать его и, возможно, этот раздел был пропущен.

6.3.2.1/1 говорит:

Значение lvalue является выражением (с типом объекта, отличным от void), которое потенциально обозначает объект; если lvalue не обозначает объект при его оценке, поведение undefined

однако оператор & выполняет свой операнд. (Он не получает доступ к сохраненному значению, но отличается).

Эта длинная цепочка рассуждений, по-видимому, предполагает, что код вызывает UB, но он довольно незначительный, и мне непонятно, что предполагали авторы стандарта. Если на самом деле они намеревались что-либо, а не оставляли его для обсуждения:)

4b9b3361

Ответ 1

С точки зрения адвоката выражение &(((struct name *)NULL)->b); должно привести к UB, поскольку вы не смогли найти путь, в котором не было бы UB. ИМХО, основная причина заключается в том, что в момент вы применяете оператор -> к выражению, которое не указывает на объект.

С точки зрения компилятора, предполагая, что программист-компилятор не был слишком сложным, ясно, что выражение возвращает то же значение, что и offsetof(name, b), и я уверен, что если он скомпилирован без ошибок, любой существующий компилятор даст этот результат.

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

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

Ответ 2

Да, это использование -> имеет поведение undefined в прямом смысле английского термина undefined.

Поведение определяется только в том случае, если первое выражение указывает на объект и не определено (= undefined) в противном случае. В общем, вы не должны искать больше в терминах undefined, это означает только то: стандарт не дает значения для вашего кода. (Иногда он явно указывает на такие ситуации, которые он не определяет, но это не меняет общий смысл этого термина.)

Это вялость, которая внедряется, чтобы помочь разработчикам компилятора справляться с вещами. Они могут определять поведение даже для кода, который вы представляете. В частности, для реализации компилятора отлично использовать такой код или аналогичный для макроса offsetof. Приведение этого кода в нарушение ограничений блокирует этот путь для реализаций компилятора.

Ответ 3

Начнем с оператора косвенности *:

6.5.3.2 p4: Унарный оператор * обозначает косвенное направление. Если операнд указывает на функцию, результат обозначение функции; если он указывает на объект, результатом является значение l, обозначающее объект. Если операнд имеет тип "указатель на тип", результат имеет тип "тип". Если неверное значение было присвоено указателю, поведение унарного оператора * равно undefined. 102)

* E, где E - нулевой указатель, - это поведение undefined.

Есть сноска, которая гласит:

102) Таким образом, &*E эквивалентно E (даже если E - нулевой указатель) и & (E1 [E2]) - ((E1) + (E2)). это всегда верно, что если E является обозначением функции или значением l, которое является допустимым операндом унарного & оператор, * & E - обозначение функции или lvalue, равное E. Если * P - значение l, а T - имя тип указателя объекта * (T) P - это значение l, которое имеет тип, совместимый с тем, к которому относится T.

Это означает, что & * E, где E является NULL, определено, но вопрос в том, верно ли то же самое для & (* E).m, где E - нулевой указатель, а его тип - это структура который имеет член m?

Стандарт C не определяет это поведение.

Если бы это было определено, возникли новые проблемы, один из которых приведен ниже. C Стандарт верен, чтобы сохранить его undefined и предоставляет макрос offset, который обрабатывает проблему внутренне.

6.3.2.3 Указатели

  1. Целочисленное константное выражение со значением 0 или такое выражение, отлитое для типа void *, называется константой нулевого указателя. 66) Если константа нулевого указателя преобразуется в тип указателя, полученный указатель, называемый нулевым указателем, гарантированно сравнивает неравные к указателю на любой объект или функцию.

Это означает, что целочисленное константное выражение со значением 0 преобразуется в константу нулевого указателя.

Но значение константы нулевого указателя не определяется как 0. Значение определяется реализацией.

7.19 Общие определения

  1. Макросы НОЛЬ который расширяется до константы нулевого указателя, определяемой реализацией

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

Другая проблема заключается в том, как вы оцениваете & (* E).m? Применяются ли скобки и сначала оцениваются *. Хранение undefined решает эту проблему.

Ответ 4

Сначала установите, что нам нужен указатель на объект:

6.5.2.3 Структура и элементы объединения

4 Постфиксное выражение, за которым следует оператор ->, и идентификатор обозначает член структуры или объекта объединения. Значение - это имя именованного элемента объекта которое первое выражение указывает, и является lvalue.96) Если первое выражение является указателем на квалифицированный тип, результат имеет соответствующую квалификацию типа назначенного член.

К сожалению, никакой нулевой указатель никогда не указывает на объект.

6.3.2.3 Указатели

3 Целочисленное константное выражение со значением 0 или такое выражение, отлитое для типа void *, называется константой нулевого указателя .66) Если константа нулевого указателя преобразуется в тип указателя, полученный указатель, называемый нулевым указателем, гарантированно сравнивает неравные к указателю на любой объект или функцию.

Результат: Undefined Поведение.

Как примечание, некоторые другие вещи, чтобы пережевать:

6.3.2.3 Указатели

4 Преобразование нулевого указателя в другой тип указателя дает нулевой указатель этого типа. Любые два нулевых указателя сравнивают одинаковые. 5 Целое число может быть преобразовано в любой тип указателя. За исключением случаев, указанных ранее, результат определяется реализацией, может быть неправильно выровнен, может не указывать на объект ссылочного типа и может быть ловушечным представлением .67)
6 Любой тип указателя может быть преобразован в целочисленный тип. За исключением случаев, указанных ранее, результат определяется реализацией. Если результат не может быть представлен в целочисленном типе, поведение undefined. Результат не должен быть в диапазоне значений любого целого числа тип.

67) Функции отображения для преобразования указателя на целое число или целое число в указатель должны соответствовать структуре адресации среды выполнения.

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

Ответ 5

Нет. Оставим это отдельно:

&(((struct name *)NULL)->b);

совпадает с:

struct name * ptr = NULL;
&(ptr->b);

Первая строка, очевидно, действительна и определена корректно.

Во второй строке мы вычисляем адрес поля относительно адреса 0x0, который также является совершенно законным. Например, у Amiga был указатель на ядро ​​в адресе 0x4. Поэтому вы можете использовать такой метод, чтобы вызвать функции ядра.

Фактически, тот же подход используется для макроса C offsetof (wikipedia):

#define offsetof(st, m) ((size_t)(&((st *)0)->m))

Таким образом, путаница здесь вращается вокруг того, что NULL-указатели страшны. Но из компилятора и стандартной точки зрения выражение является законным в C (С++ - это другой зверь, поскольку вы можете перегрузить оператор &).

Ответ 6

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

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

Сама суть того, что означало Undefined Behavior, в то время.

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