Тэги указателя в C не определены в соответствии со стандартом?
Некоторые динамически типизированные языки используют тегирование указателя как быстрый способ определить или сузить тип времени выполнения представляемого значения. Классический способ сделать это - преобразовать указатели в целое число подходящего размера и добавить значение тега к младшим значащим битам, которые предполагаются равными нулю для выровненных объектов. Когда к объекту требуется доступ, биты тега маскируются, целое число преобразуется в указатель, а указатель разыменовывается как обычно.
Это само по себе все в порядке, за исключением того, что все зависит от одного колоссального предположения: выровненный указатель преобразуется в целое число, гарантированно имеющее нулевые биты в нужных местах.
Можно ли гарантировать это согласно букве стандарта?
Хотя в стандартном разделе 6.3.2.3 (ссылки на черновик C11) говорится, что результат преобразования указателя в целое число определяется реализацией, меня интересует, действительно ли арифметические правила указателя в 6.5.2.1 и 6.5.6 эффективно ограничить результат преобразования указатель-> целое число в соответствии с теми же предсказуемыми арифметическими правилами, которые уже приняты многими программами. (6.3.2.3 примечание 67, по-видимому, предполагает, что в любом случае это является намеченным духом стандарта, а не то, что это много значит.)
Я конкретно имею в виду случай, когда можно выделить большой массив, чтобы он выступал в качестве кучи для динамического языка, и поэтому указатели, о которых мы говорим, относятся к элементам этого массива. Я предполагаю, что начало самого C-распределенного массива может быть помещено в выровненную позицию некоторыми вторичными средствами (во что бы то ни стало обсудите это тоже, хотя). Скажем, у нас есть массив восьмибайтовых "минусов"; Можем ли мы гарантировать, что указатель на любую ячейку преобразуется в целое число с младшими тремя битами, свободными для тега?
Например:
typedef Cell ...; // such that sizeof(Cell) == 8
Cell heap[1024]; // such that ((uintptr_t)&heap[0]) & 7 == 0
((char *)&heap[11]) - ((char *)&heap[10]); // == 8
(Cell *)(((char *)&heap[10]) + 8); // == &heap[11]
&(&heap[10])[0]; // == &heap[10]
0[heap]; // == heap[0]
// So...
&((char *)0)[(uintptr_t)&heap[10]]; // == &heap[10] ?
&((char *)0)[(uintptr_t)&heap[10] + 8]; // == &heap[11] ?
// ...implies?
(Cell *)((uintptr_t)&heap[10] + 8); // == &heap[11] ?
(Если я правильно понимаю, если реализация обеспечивает uintptr_t
тогда неопределенное поведение, на которое намекает пункт 6.3.2.3, не имеет значения, верно?)
Если все это выполнено, то я предполагаю, что это означает, что вы можете положиться на младшие биты любого преобразованного указателя на элемент выровненного Cell
массив, который будет свободен для маркировки. Они это делают?
(Насколько мне известно, этот вопрос является гипотетическим, так как в любом случае нормальное предположение справедливо для общих платформ, и если вы нашли такую, где ее нет, вам, вероятно, не хотелось бы обращаться к руководству по стандарту C, а не к документы по платформам, но это не относится к делу.)
2 ответа
Это само по себе все в порядке, за исключением того, что все зависит от одного колоссального предположения: выровненный указатель преобразуется в целое число, гарантированно имеющее нулевые биты в нужных местах.
Можно ли гарантировать это согласно букве стандарта?
Реализация может гарантировать это. Результат преобразования указателя в целое число определяется реализацией, и реализация может определять его по своему усмотрению, если оно соответствует требованиям стандарта.
Стандарт абсолютно не гарантирует этого в целом.
Конкретный пример: я работал над системой Cray T90, в которой компилятор C работал под UNIX-подобной операционной системой. В аппаратном обеспечении адрес представляет собой 64-разрядное слово, содержащее адрес 64-разрядного слова; не было аппаратных байтовых адресов. Байтовые указатели (void*
, char*
) были реализованы в программном обеспечении путем сохранения 3-битного смещения в неиспользуемых в противном случае старших 3-х битах 64-битного указателя слова.
Все преобразования указатель-указатель, указатель-целое и целое-указатель просто копировали представление.
Это означает, что указатель на 8-байтовый выровненный объект при преобразовании в целое число может иметь любую битовую комбинацию в своих младших 3 битах.
Ничто в стандарте не запрещает это.
Итог: схема, подобная той, которую вы описываете, которая играет в игры с представлениями указателей, может работать, если вы сделаете определенные предположения о том, как текущая система представляет указатели - до тех пор, пока эти предположения окажутся действительными для текущей системы.
Но никакие такие допущения не могут быть надежными на 100%, потому что стандарт ничего не говорит о том, как представлены указатели (за исключением того, что они имеют фиксированный размер для каждого типа указателя, и что представление можно рассматривать как массив unsigned char
).
(Стандарт даже не гарантирует, что все указатели имеют одинаковый размер.)
Вы правы в отношении соответствующих частей стандарта. Для справки:
Целое число может быть преобразовано в любой тип указателя. За исключением случаев, указанных ранее, результат определяется реализацией, может быть неправильно выровнен, может не указывать на объект ссылочного типа и может быть представлением прерывания.
Любой тип указателя может быть преобразован в целочисленный тип. За исключением указанного ранее, результат определяется реализацией. Если результат не может быть представлен в целочисленном типе, поведение не определено. Результат не обязательно должен находиться в диапазоне значений любого целочисленного типа.
Поскольку преобразования определяются реализацией (кроме случаев, когда целочисленный тип слишком мал, и в этом случае он не определен), стандарт ничего не скажет об этом поведении. Если ваша реализация дает гарантии, которые вы хотите, то все готово. В противном случае тоже плохо.
Я думаю, ответ на ваш явный вопрос:
Можно ли гарантировать это согласно букве стандарта?
Это "да", так как стандарт указывает на это поведение и говорит, что реализация должна его определить. Возможно, "нет" - такой же хороший ответ по той же причине.