Преобразуйте целочисленный адрес в двойной указатель и прочтите его, но размер целого числа меньше, чем тип double, и операция чтения будет читать, превышая размер объекта. Я считаю, что это неопределенное поведение, но я не нашел описания в стандарте C, поэтому я публикую этот вопрос, чтобы найти ответ, подтверждающий мою точку зрения.
#include <stdio.h>
#include <stdint.h>
int main() {
int32_t a = 12;
double *p = (double*)(&a);
printf("%lf\n", *p);
return 0;
}
Было бы строгое нарушение псевдонима, даже если бы размеры типов совпадали.
Концепция «undefined» не означает, что обязательно означает «мы не знаем, что произойдет». Это означает «у нас нет указан, что происходит должен». По замыслу вы не можете полагаться на конкретное поведение, потому что не только текущий наблюдаемый эффект может зависеть от конкретных обстоятельств, но также нет гарантии, что различные компиляторы, версии, среды и т. д. Могут повлиять на это.
Итак, я точно знаю, что ЦП попытается сделать, он будет читать «двойное» количество байтов, интерпретируя это как двойное. Что в этих байтах, разрешено ли чтение этих байтов, можно ли безопасно преобразовать эти байты в двойные и т. д., Все это неопределенный.
Строгий псевдоним на самом деле не актуален, если не происходит записи, поскольку нет изменения состояния объекта, которое необходимо синхронизировать.
@Leushenko Разыменования несоответствующего указателя, как в коде printf("%lf\n", *p); здесь, достаточно, чтобы вызвать UB. На оборудовании со строгими ограничениями по выравниванию такой код, скорее всего, приведет к срабатыванию SIGSEGV или SIGBUS.
@Leushenko Актуально, как только к переменной обращаются, читают или пишут. Например, такой код, как int i = 0; short* s = (short*)&i; while(*s != something) { i=something; }, может зависать в вечном цикле, даже если представление short для something идентично представлению int.





Из пункта 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++ в этом случае ведут себя одинаково, но используемый источник не идеален.
Ты прав. В следующий раз мне следует уделить больше внимания cppreference.
Это неопределенное поведение согласно 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-типа, Стандарт не налагает никаких требований относительно того, что произойдет, если приложение попытается прочитать из памяти, которой оно не владеет. Здесь снова выбор поведения может иногда быть проблемой качества реализации. Здесь есть пять основных возможностей:
В некоторых реализациях содержимое хранилища может быть предсказуемо средствами, не описанными в Стандарте, и даст содержимое такого хранилища.
Акт чтения может вести себя так, как будто он дает биты с Неуказанные значения, но не имеют других побочных эффектов.
Попытка чтения может прервать выполнение программы.
На платформах, которые используют ввод-вывод с отображением памяти, чтение за пределами диапазона может выполнить неожиданную операцию с неизвестными последствиями; это возможность применима только на определенных платформах.
Реализации, которые пытаются быть "умными" разными способами, могут пытаться делать выводы, основанные на том, что чтение не может произойти, таким образом приводя к побочным эффектам, выходящим за рамки законов времени и причинности.
Если вы знаете, что ваш код будет работать на платформе, где чтение имеет никаких побочных эффектов, реализация не будет пытаться быть «умной», а ваш код подготовлен для любого набора битов, который может дать чтение, тогда под этими обстоятельства, такое чтение может иметь полезное поведение, но вы ограничиваете ситуации, в которых может быть использован ваш код.
Обратите внимание, что хотя реализации, которые определяют __STDC_ANALYZABLE__, требуются
чтобы большинство действий подчинялось законам времени и причинности даже в тех случаях, когда
Стандарт не налагает никаких других требований, чтения за пределами допустимого диапазона
классифицируется как критическое неопределенное поведение, и поэтому его следует рассматривать
опасно для любой реализации, в которой прямо не указано иное.
Кстати, на некоторых платформах есть еще одна проблема, которая применима, даже если, например, code использовал int[3], а не одно выравнивание int :. На некоторых платформах значения определенных типов могут быть прочитаны или записаны только на / с определенных адресов, а некоторые адреса, подходящие для меньших типов, могут не подходить для больших. На платформах, где int требует 32-битного выравнивания, но double требует 64-битного выравнивания, с учетом int foo[3] компилятор может произвольно разместить foo, чтобы (double*)foo был подходящим адресом для хранения double, или чтобы (double*)(foo+1) был подходящим местом. Программист, знакомый с деталями реализации, может определить, какой адрес будет действительным, и использовать его, но код, слепо предполагающий, что адрес foo будет действительным, может потерпеть неудачу, если double требует 64-битного выравнивания.
@AndrewHenle: Вы хотите сказать, что я должен был упомянуть о выравнивании? Думаю, это было бы справедливым вопросом. Моя основная мысль заключается в том, что 6.5p7 в том виде, в котором он был написан, не мог разумно предназначаться для описания всех случаев, когда компиляторы должны вести себя предсказуемо, поскольку способ его написания даже не допускает таких случаев, как struct s {int x;} = {0} foo; foo.x = 1; [этот код явно изменяет значение в объект типа struct s, и делает это с использованием lvalue типа int - не одного из типов, перечисленных как подходящие для этой цели], и распознавание производных указателей / lvalue является проблемой QoI.
@AndrewHenle: Что касается того, буду ли я рассматривать clang и gcc как качественные реализации при вызове с -fstrict-aliasing, я бы не стал. Заявленная цель 6.5p7 состояла в том, чтобы позволить компиляторам предполагать, что кажущиеся несвязанными вещи не являются псевдонимами, - не предлагать им игнорировать очевидные отношения между вещами. И gcc, и clang склонны к оптимизации кода, который считывает хранилище как T1 и записывает обратно тот же битовый шаблон, что и T2, даже если в последний раз хранилище было записано как T1, а затем будет прочитано как T2. Сделав это, они будут рассматривать предыдущую запись и последующее чтение как неупорядоченные.
-fstrict-aliasing, я бы не стал. На SPARC я видел (более старые версии), что GCC генерирует двоичные файлы из соответствующего кода, который не работает с SIGBUS, так что да, мне, возможно, придется согласиться с вами, с -fstrict-aliasing или без него.
@AndrewHenle: Вам нравится то, что я написал о выравнивании?
Да, это неопределенное поведение (по крайней мере, для
sizeof(double) > sizeof(int32_t), что обычно так). Что заставляет вас думать, что это может быть не так?