Несвязанный доступ через reinterpret_cast

Я нахожусь в середине обсуждения, пытаясь выяснить, допустим ли недопустимый доступ в С++ через reinterpret_cast. Я думаю, что нет, но мне трудно найти правильную часть (-ы) стандарта, которые подтверждают или опровергают это. Я смотрел на С++ 11, но я был бы в порядке с другой версией, если это более понятно.

Несвязанный доступ undefined в C11. Соответствующая часть стандарта C11 (п. 6.3.2.3, пункт 7):

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

Поскольку поведение неприсоединенного доступа undefined, некоторые компиляторы (по крайней мере, GCC) принимают это за то, что это нормально, чтобы генерировать инструкции, которые требуют согласованных данных. В большинстве случаев код по-прежнему работает для неуравновешенных данных, потому что большинство команд x86 и ARM в наши дни работают с неуравновешенными данными, но некоторые из них этого не делают. В частности, некоторые векторные инструкции этого не делают, а это значит, что по мере того, как компилятор лучше справляется с созданием оптимизированных инструкций, код, который работал со старыми версиями компилятора, может не работать с более новыми версиями. И, конечно же, некоторые архитектуры (как MIPS) не работают с неуравновешенными данными.

С++ 11, конечно, сложнее. § 5.2.10, пункт 7 гласит:

Указатель объекта может быть явно преобразован в указатель объекта другого типа. Когда prvalue v типа "указатель на T1" преобразуется в тип "указатель на cv T2", результат static_cast<cv T2*>(static_cast<cv void*>(v)), если оба T1 и T2 являются стандартными типами макета ( 3.9), а требования к выравниванию T2 не более строгие, чем требования для T1, или если любой тип void. Преобразование prvalue типа "указатель на T1" в тип "указатель на T2" (где T1 и T2 являются типами объектов и где требования к выравниванию T2 не более строгие, чем требования T1) и обратно к исходному типу дает исходное значение указателя. Результат любого другого такого преобразования указателя не указан.

Обратите внимание, что последнее слово "неуказано", а не "undefined". В § 1.3.25 определяется "неуказанное поведение" как:

для хорошо сформированной программы и правильных данных, которая зависит от реализации

[Примечание. Реализация не требуется для документирования поведения. Диапазон возможных видов поведения обычно определяется в этом международном стандарте. - конечная нота]

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

Человек, с которым я обсуждаю это, однако, имеет другую интерпретацию. Они ссылаются на § 1.9, параграф 5:

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

Так как не существует поведения undefined, они утверждают, что компилятор С++ не имеет права предполагать, что неприсоединенный доступ не возникает.

Итак, являются ли невыполненные обращения через reinterpret_cast безопасными в С++? Где говорится в спецификации (любая версия)?

Изменить. Под "доступом" я подразумеваю загрузку и хранение. Что-то вроде

void unaligned_cp(void* a, void* b) {
  *reinterpret_cast<volatile uint32_t*>(a) =
    *reinterpret_cast<volatile uint32_t*>(b);
}

Как выделяется память, фактически вне моей области (это для библиотеки, которую можно вызывать с данными из любого места), но malloc и массив в стеке являются вероятными кандидатами. Я не хочу устанавливать какие-либо ограничения на выделение памяти.

Изменить 2. Просьба привести источники (например, стандарт, раздел и параграф С++) в ответах.

Ответы

Ответ 1

Взгляд на 3.11/1:

Типы объектов имеют требования к выравниванию (3.9.1, 3.9.2), которые устанавливают ограничения на адреса, на которых может быть выделен объект этого типа.

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

Возьмите *reinterpret_cast<uint32_t*>(a), например. Если это выражение не вызывает UB, то (согласно правилу строгого псевдонимов) в этом месте после этого утверждения должен быть объект типа uint32_t (или int32_t). Был ли объект уже там, или эта запись создала его, не имеет значения.

В соответствии с приведенной выше стандартной цитатой объекты с требованием выравнивания могут существовать только в правильно выровненном состоянии.

Поэтому любая попытка создать или записать объект, который неправильно выровнен, вызывает UB.

Ответ 2

РЕДАКТИРОВАТЬ. Это отвечает на исходный вопрос OP, который был "имеет доступ к неправильному указателю безопасности". OP с тех пор отредактировал свой вопрос: "разыменовывает неверный указатель безопасности", гораздо более практичный и менее интересный вопрос.


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

Если требования к выравниванию не выполняются, чем это путешествие в оба конца - указатель-на-указатель-на-к указателю-на-A выводит указатель с неопределенным значением.

Как есть недопустимые значения указателя, разыменование указателя с неопределенным значением может привести к поведению undefined. Это ничем не отличается от *(int*)0xDEADBEEF в некотором смысле.

Простое сохранение этого указателя не является, однако, undefined.

Ни одна из приведенных выше цитат на С++ не говорит о фактическом использовании указателя-на-A в качестве указателя-на-B. Использование указателя на "неправильный тип" во всех случаях, кроме очень ограниченного числа обстоятельств, - это поведение undefined, период.

Примером этого является создание std::aligned_storage_t<sizeof(T), alignof(T)>. Вы можете создать свой T в этом месте, и он будет жить их счастливо, даже если он "на самом деле" является aligned_storage_t<sizeof(T), alignof(T)>. (Тем не менее, вы можете использовать указатель, возвращенный из места размещения new для полного соответствия стандарту, я не уверен. См. Строгий псевдоним.)

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

Ответ 3

Все ваши кавычки относятся к значению указателя, а не к разыменованию.

5.2.10, в пункте 7 указано, что если int имеет более строчное выравнивание, чем char, то круговое путешествие от char* до int* до char* генерирует неуказанное значение для полученного char*.

С другой стороны, если вы конвертируете int* в char* в int*, вам гарантированно вернут тот же самый указатель, с которого вы начали.

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


Предположим, что у вас есть несколько int и alignof(int) > 1:

int some_ints[3] ={0};

тогда у вас есть указатель int, который смещен:

int* some_ptr = (int*)(((char*)&some_ints[0])+1);

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

Значение some_ptr не указано стандартом. Мы будем щедры и предположим, что на самом деле это указывает на некоторый фрагмент байтов внутри some_bytes.

Теперь мы имеем int*, указывающий на то, что int не может быть выделено (3.11/1). В (3.8) использование указателя на int ограничено несколькими способами. Обычное использование ограничено указателем на T, срок жизни которого был назначен правильно (/3). Некоторое ограниченное использование разрешено указателем на T, который был правильно назначен, но его время жизни не началось (/5 и /6).

Нет способа создать объект int, который не подчиняется ограничениям выравнивания int в стандарте.

Итак, теоретический int*, который утверждает, что указывает на несогласованный int, не указывает на int. Никаких ограничений на поведение указанного указателя при разыменовании; обычные правила разыменования обеспечивают поведение действительного указателя на объект (включая int) и то, как он ведет себя.


И теперь наши другие предположения. Никаких ограничений на значение some_ptr здесь не производится по стандарту: int* some_ptr = (int*)(((char*)&some_ints[0])+1);.

Это не указатель на int, так как (int*)nullptr не является указателем на int. Круглый отключение его к char* приводит к указанию с неопределенным значением (оно может быть 0xbaadf00d или nullptr) явно в стандарте.

Стандарт определяет, что вы должны делать. Есть (почти? Я предполагаю, что его оценка в логическом контексте должна возвращать bool). Никакие требования, предъявляемые стандартом к стандарту some_ptr, не заменяют его обратно на char*, приводит к неопределенному значению (указателя).