2015-09-15 3 views
14

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Редактировать: Под «доступом» подразумевается загрузка и хранение. Что-то вроде

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

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

Edit 2: Пожалуйста цитировать источники (т.е., С ++ стандарт, раздел и пункт) в ответах.

+0

Что означает доступ? Псевдонимы или просто набрасывание типов указателей туда и обратно? – Columbo

+0

Алиасинг - в частности, меня интересуют нагрузки и магазины, которые были неправильно настроены 'uint32_t's. – nemequ

+0

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

ответ

7

Глядя на 3.11/1:

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

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

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

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

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

+0

Мне нравится этот ответ, но я думаю, что он неполный, не отвечая на вопрос о том, что представляет собой выделение объекта типа. Если никто не ответит на эту часть, я приму этот ответ и создаю еще один вопрос для этой проблемы. – nemequ

+1

@nemequ, это будет отдельный вопрос. Сначала прочитайте [этот вопрос] (http://stackoverflow.com/questions/30114397/constructing-a-ivivi-copyable-object-with-memcpy). –

3

EDIT Это отвечает на исходный вопрос OP, который был «имеет доступ к неправильному указателю безопасности». С тех пор ОП редактировал свой вопрос: «разыскивает неверный указатель безопасности», гораздо более практичный и менее интересный вопрос.


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

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

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

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

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

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

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

+2

Если система должна была использовать одно слово для хранения 'int *', но два слова для хранения 'char *' или 'void *' [например. некоторые системы используют слова-адреса, но включают в себя инструкцию для доступа к полуслову при определенном смещении байта от заданного адреса слова], будет ли система допущена к ловушке, если была сделана попытка применить к 'int *' a 'char * 'с нечетным смещением, или система должна была иметь листинг, чтобы получить указатель, который может быть назначен (хотя и не обязательно разыменованный) без захвата? – supercat

+0

@supercat хороший вопрос; Я должен был бы проверить, что стандарт говорит о ловушных представлениях самих указателей. Может ли копирование указателя с неуказанным значением вызвать ловушку? – Yakk

+0

Данный 'int foo; int * x, * y; ', выражение' x = (int *) (((char *) & foo) +1); 'может законно заставить' x' удерживать ловушечное представление, и если это так, y = x; 'будет иметь неопределенное поведение. Что несколько менее понятно, так это то, является ли сохранение определенного или неопределенного значения * непосредственно * из выражающего его выражения определенного поведения. Я не вижу никакой пользы от запрещения компиляторам от улавливания при попытках * генерировать * недопустимые значения, но это может сделать различие между «сгенерировать неуказанное значение» или «генерировать значение, определенное реализацией» [это может быть ловушка ...] ».. – supercat

0

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

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

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

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


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

int some_ints[3] ={0}; 

то есть Int указатель, смещенный:

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

Мы предполагаем, что копируя этот откалибрована указатель Безразлично На данный момент это неопределенное поведение.

Значение стандарта 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*, приводит к неопределенному значению (указателя).

Смежные вопросы