Превышает ли чтение поведение объекта undefined в C?

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

#include <stdio.h>
#include <stdint.h>

int main() {
    int32_t a = 12;
    double *p = (double*)(&a);
    printf("%lf\n", *p);
    return 0;
}

Да, это неопределенное поведение (по крайней мере, для sizeof(double) > sizeof(int32_t), что обычно так). Что заставляет вас думать, что это может быть не так?

Basile Starynkevitch 16.05.2018 14:37

Было бы строгое нарушение псевдонима, даже если бы размеры типов совпадали.

user694733 16.05.2018 14:38

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

Lasse V. Karlsen 16.05.2018 14:40

Итак, я точно знаю, что ЦП попытается сделать, он будет читать «двойное» количество байтов, интерпретируя это как двойное. Что в этих байтах, разрешено ли чтение этих байтов, можно ли безопасно преобразовать эти байты в двойные и т. д., Все это неопределенный.

Lasse V. Karlsen 16.05.2018 14:41

Строгий псевдоним на самом деле не актуален, если не происходит записи, поскольку нет изменения состояния объекта, которое необходимо синхронизировать.

Leushenko 16.05.2018 14:53

@Leushenko Разыменования несоответствующего указателя, как в коде printf("%lf\n", *p); здесь, достаточно, чтобы вызвать UB. На оборудовании со строгими ограничениями по выравниванию такой код, скорее всего, приведет к срабатыванию SIGSEGV или SIGBUS.

Andrew Henle 16.05.2018 14:56

@Leushenko Актуально, как только к переменной обращаются, читают или пишут. Например, такой код, как int i = 0; short* s = (short*)&i; while(*s != something) { i=something; }, может зависать в вечном цикле, даже если представление short для something идентично представлению int.

Lundin 16.05.2018 15:19
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
7
68
3

Ответы 3

Из пункта 7 Проект комитета C996.5 Expressions:

An object shall have its stored value accessed only by an lvalue expression that has one of the following types:76)
— a type compatible with the effective type of the object,
— a qualified version of a type compatible with the effective type of the object,
— a type that is the signed or unsigned type corresponding to the effective type of the object,
— a type that is the signed or unsigned type corresponding to a qualified version of the effective type of the object,
— an aggregate or union type that includes one of the aforementioned types among its members (including, recursively,amember of a subaggregate or contained union), or
— a character type.

Доступ к объекту типа int осуществляется с помощью выражения lvalue типа double. Типы int и double в любом случае несовместимы, они не являются агрегированными, и double не является символьным типом. Разыменование указателя (выражения lvalue) типа double, который указывает на объект с типом int, является неопределенным поведением. Такие операции называются строгое нарушение псевдонима.

Вы цитируете вики, которая, кажется, цитирует стандарт C++ (?), А не стандарт C. C и C++ в этом случае ведут себя одинаково, но используемый источник не идеален.

Lundin 16.05.2018 14:56

Ты прав. В следующий раз мне следует уделить больше внимания cppreference.

KamilCuk 16.05.2018 15:21

Это неопределенное поведение согласно C11 6.5 («строгое правило сглаживания»):

6 The effective type of an object for an access to its stored value is the declared type of the object, if any.
...

В этом случае эффективный тип - int32_t (который является typedef, соответствующим чему-то вроде int или long).

7 An object shall have its stored value accessed only by an lvalue expression that has one of the following types:
- a type compatible with the effective type of the object,
...

double несовместим с int32_t, поэтому, когда код обращается к данным здесь: *p, он нарушает это правило и вызывает UB.

Подробнее см. Что такое строгое правило псевдонима?.

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

Однако даже игнорируя проблему lvalue-типа, Стандарт не налагает никаких требований относительно того, что произойдет, если приложение попытается прочитать из памяти, которой оно не владеет. Здесь снова выбор поведения может иногда быть проблемой качества реализации. Здесь есть пять основных возможностей:

  1. В некоторых реализациях содержимое хранилища может быть предсказуемо средствами, не описанными в Стандарте, и даст содержимое такого хранилища.

  2. Акт чтения может вести себя так, как будто он дает биты с Неуказанные значения, но не имеют других побочных эффектов.

  3. Попытка чтения может прервать выполнение программы.

  4. На платформах, которые используют ввод-вывод с отображением памяти, чтение за пределами диапазона может выполнить неожиданную операцию с неизвестными последствиями; это возможность применима только на определенных платформах.

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

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

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

Кстати, на некоторых платформах есть еще одна проблема, которая применима, даже если, например, code использовал int[3], а не одно выравнивание int :. На некоторых платформах значения определенных типов могут быть прочитаны или записаны только на / с определенных адресов, а некоторые адреса, подходящие для меньших типов, могут не подходить для больших. На платформах, где int требует 32-битного выравнивания, но double требует 64-битного выравнивания, с учетом int foo[3] компилятор может произвольно разместить foo, чтобы (double*)foo был подходящим адресом для хранения double, или чтобы (double*)(foo+1) был подходящим местом. Программист, знакомый с деталями реализации, может определить, какой адрес будет действительным, и использовать его, но код, слепо предполагающий, что адрес foo будет действительным, может потерпеть неудачу, если double требует 64-битного выравнивания.

В общем, преобразование указателя в другой тип с последующим немедленным выполнением доступа к нему попадает в категорию действий, которые будут поддерживаться качественными компиляторами, которые настроены так, чтобы быть подходящими для системного программирования, но могут не поддерживаться компиляторами. которые действуют тупо. Значит, любой компилятор на оборудовании SPARC или ARM «тупой», а не «качественный»?
Andrew Henle 16.05.2018 18:28

@AndrewHenle: Вы хотите сказать, что я должен был упомянуть о выравнивании? Думаю, это было бы справедливым вопросом. Моя основная мысль заключается в том, что 6.5p7 в том виде, в котором он был написан, не мог разумно предназначаться для описания всех случаев, когда компиляторы должны вести себя предсказуемо, поскольку способ его написания даже не допускает таких случаев, как struct s {int x;} = {0} foo; foo.x = 1; [этот код явно изменяет значение в объект типа struct s, и делает это с использованием lvalue типа int - не одного из типов, перечисленных как подходящие для этой цели], и распознавание производных указателей / lvalue является проблемой QoI.

supercat 16.05.2018 19:12

@AndrewHenle: Что касается того, буду ли я рассматривать clang и gcc как качественные реализации при вызове с -fstrict-aliasing, я бы не стал. Заявленная цель 6.5p7 состояла в том, чтобы позволить компиляторам предполагать, что кажущиеся несвязанными вещи не являются псевдонимами, - не предлагать им игнорировать очевидные отношения между вещами. И gcc, и clang склонны к оптимизации кода, который считывает хранилище как T1 и записывает обратно тот же битовый шаблон, что и T2, даже если в последний раз хранилище было записано как T1, а затем будет прочитано как T2. Сделав это, они будут рассматривать предыдущую запись и последующее чтение как неупорядоченные.

supercat 16.05.2018 19:42
Вы предлагаете, чтобы я упомянул выравнивание? Думаю, это было бы справедливым вопросом. Конечно. Слишком многие программисты, работающие только с x86, даже не думают о выравнивании. Другое оборудование далеко не так снисходительно. Что касается того, буду ли я рассматривать clang и gcc как качественные реализации при вызове с -fstrict-aliasing, я бы не стал. На SPARC я видел (более старые версии), что GCC генерирует двоичные файлы из соответствующего кода, который не работает с SIGBUS, так что да, мне, возможно, придется согласиться с вами, с -fstrict-aliasing или без него.
Andrew Henle 16.05.2018 21:19

@AndrewHenle: Вам нравится то, что я написал о выравнивании?

supercat 16.05.2018 22:28

Другие вопросы по теме

Является ли этот код неопределенным поведением из-за доступа за пределы привязанного элемента в первом цикле for?
C++ безопасно работать с массивом символов, если первое значение массива установлено на нулевой терминатор?
Разрушает, а затем строит в этой внутренней функции-члене поведение, определяемое?
Целочисленное переполнение в промежуточном арифметическом выражении
Является ли mem :: Forgot (mem :: uninitialized ()) определенным поведением?
Приводит ли чтение или запись всего 32-битного слова, даже если у нас есть ссылка только на его часть, к неопределенному поведению?
Хорошо ли определен доступ к частично назначенному массиву за назначенной частью?
Отвергает ли const из * это поведение undefined?
Приведение кучи хранилища из константного в неконстантный вызывает поведение undefined?
В C и C++ это выражение, использующее оператор запятой, например «a = b, ++ a;» неопределенный?