Какое лечение может пройти указатель и по-прежнему действовать?
Какой из следующих способов лечения и попытки восстановления C-указателя гарантированно будет действительным?
1) Отбрасывать указатель void и назад
int f(int *a) {
void *b = a;
a = b;
return *a;
}
2) Приведение в нужный размер и обратно
int f(int *a) {
uintptr_t b = a;
a = (int *)b;
return *a;
}
3) Несколько тривиальных целых операций
int f(int *a) {
uintptr_t b = a;
b += 99;
b -= 99;
a = (int *)b;
return *a;
}
4) Целочисленные операции, нетривиальные, чтобы затушевать провенанс, но которые тем не менее оставят неизменным значение
int f(int *a) {
uintptr_t b = a;
char s[32];
// assume %lu is suitable
sprintf(s, "%lu", b);
b = strtoul(s);
a = (int *)b;
return *a;
}
5) Больше косвенных целых операций, которые оставят неизменным значение
int f(int *a) {
uintptr_t b = a;
for (uintptr_t i = 0;; i++)
if (i == b) {
a = (int *)i;
return *a;
}
}
Очевидно, что случай 1 действителен, и случай 2 тоже должен быть. С другой стороны, я наткнулся на сообщение Криса Лэттнера, которого я, к сожалению, сейчас не могу найти, - что-то похожее на случай 5 недействительно, что стандарт лицензирует компилятор, чтобы просто скомпилировать его в бесконечный цикл. Тем не менее каждый случай выглядит как неочевидное расширение предыдущего.
Где линия, заключенная между действительным случаем и недопустимым?
Добавлен на основе обсуждения в комментариях: пока я до сих пор не могу найти сообщение, которое вдохновило случай 5, я не помню, какой тип указателя был задействован; в частности, это мог быть указатель на функцию, и именно поэтому этот случай продемонстрировал неверный код, тогда как мой случай 5 является допустимым кодом.
Второе дополнение: хорошо, вот еще один источник, который говорит, что есть проблема, и у меня есть ссылка. https://www.cl.cam.ac.uk/~pes20/cerberus/notes30.pdf - обсуждение провенанса указателя - говорит и подтверждает доказательства, что нет, если компилятор теряет следы, где появился указатель from, it undefined.
Ответы
Ответ 1
В соответствии с Стандарт проекта C11:
Пример 1
Допустим, в соответствии с §6.5.16.1, даже без явного приведения.
Пример 2
Типы intptr_t
и uintptr_t
являются необязательными. Назначение указателя на целое число требует явного приведения (§6.5.16.1), хотя gcc и clang будут предупреждать вас, если у вас его нет. С этими оговорками конвертация в оба конца действительна в §7.20.1.4. ETA: Джон Беллингер показывает, что поведение указывается только при промежуточном нажатии на void*
в обоих направлениях. Тем не менее, как gcc, так и clang позволяют прямое преобразование как документированное расширение.
Пример 3
Безопасно, но только потому, что вы используете неподписанную арифметику, которая не может переполняться и, следовательно, гарантированно возвращает одно и то же представление объекта. intptr_t
может переполняться! Если вы хотите безопасно выполнять арифметику указателей, вы можете преобразовать любой указатель в char*
, а затем добавить или вычесть смещения в пределах той же структуры или массива. Помните, sizeof(char)
всегда 1
. ETA:. Стандарт гарантирует, что два указателя сравниваются одинаково, но ваша ссылка на Chisnall et al. дает примеры, когда компиляторы все же предполагают, что два указателя не являются псевдонимами друг друга.
Пример 4
Всегда, всегда, всегда проверяйте переполнение буфера всякий раз, когда вы читаете, и особенно всякий раз, когда вы пишете в буфер! Если вы можете математически доказать, что переполнение не может произойти при статическом анализе? Затем выпишите предположения, которые оправдывают это, явным образом, и assert()
или static_assert()
, которые они изменили. Используйте snprintf()
, а не устаревшее, небезопасное sprintf()
! Если вы ничего не помните из этого ответа, помните об этом!
Чтобы быть абсолютно педантичным, переносным способом сделать это было бы использование спецификаторов формата в <inttypes.h>
и определить длину буфера в терминах максимального значения любого представления указателя. В реальном мире вы будете печатать указатели с форматом %p
.
Ответ на вопрос, который вы намеревались спросить, да, хотя: все, что имеет значение, - это то, что вы возвращаете одно и то же представление объекта. Heres менее надуманный пример:
#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
int i = 1;
const uintptr_t u = (uintptr_t)(void*)&i;
uintptr_t v;
memcpy( &v, &u, sizeof(v) );
int* const p = (int*)(void*)v;
assert(p == &i);
*p = 2;
printf( "%d = %d.\n", i, *p );
return EXIT_SUCCESS;
}
Все, что имеет значение, это биты в представлении объекта. Этот код также следует строгим правилам псевдонимов в п. 6.5. Он компилирует и отлично работает на компиляторах, которые давали Chisnall и другие проблемы.
Пример 5
Это работает так же, как указано выше.
Одна чрезвычайно педантичная сноска, которая никогда не будет иметь отношения к вашему кодированию: какое-то устаревшее эзотерическое аппаратное обеспечение имеет одно дополнение или знаковое представление целых чисел со знаком, и на них может быть отличное значение отрицательного нуля, может или не ловить ловушку. На некоторых процессорах это может быть допустимое представление указателя или нулевого указателя, отличное от положительного нуля. И на некоторых процессорах положительный и отрицательный ноль могут сравниться с равными.
Ответ 2
1) Отбрасывать указатель void и назад
Это дает действительный указатель, равный оригиналу. В пункте 6.3.2.3/1 стандарта четко указано следующее:
Указатель на void может быть преобразован в указатель или из указателя на любой тип объекта. Указатель на любой тип объекта может быть преобразован в указатель на void и обратно; результат сравнивается с исходным указателем.
2) Приведение в нужный размер и обратно
3) Несколько тривиальных целых операций
4) Целочисленные операции, нетривиальные, чтобы затушевать провенанс, но которые тем не менее оставят неизменным значение
5) Больше косвенных целых операций, которые оставят неизменным значение
[...] Очевидно, что случай 1 действителен, и случай 2 тоже должен быть. С другой стороны, я наткнулся на сообщение Криса Лэттнера, которого я, к сожалению, сейчас не могу найти, - говоря, что случай 5 недействителен, что стандарт лицензирует компилятор, чтобы просто скомпилировать его в бесконечный цикл.
C требует применения при преобразовании в любом направлении между указателями и целыми числами, и вы опустили некоторые из них в вашем примере кода. В этом смысле ваши примеры (2) - (5) являются несоответствующими, но для остальной части этого ответа я буду притворяться, что необходимые броски есть.
Тем не менее, будучи очень педантичным, все эти примеры имеют поведение, определяемое реализацией, поэтому они не являются строго соответствующими. С другой стороны, поведение, определяемое реализацией, по-прежнему определяется поведением; означает ли это, что ваш код "действителен" или нет, зависит от того, что вы подразумеваете под этим термином. В любом случае, какой код компилятор может испустить для любого из примеров, это отдельный вопрос.
Это соответствующие положения стандарта из раздела 6.3.2.3 (выделено мной):
Целое число может быть преобразовано в любой тип указателя. За исключением, как указано ранее, результат определяется, может быть не правильно выровнен, может не указывать на объект ссылочного типа и может быть ловушечным представлением.
Любой тип указателя может быть преобразован в целочисленный тип. Если не указано выше, результат определяется. Если результат не может быть представлен в целочисленном типе, поведение undefined. Результат не обязательно должен находиться в диапазоне значений любого целочисленного типа.
Определение uintptr_t
также относится к вашему конкретному примеру кода. Стандарт описывает его таким образом (C2011, 7.20.1.4/1, добавлено выделение):
беззнаковый целочисленный тип с тем свойством, что любой действительный указатель для void может быть преобразован в этот тип, а затем преобразован обратно в указатель в void, и результат сравним равный исходному указателю.
Вы конвертируете между int *
и uintptr_t
. int *
не void *
, поэтому 7.20.1.4/1 не применяется к этим преобразованиям, а поведение определено в соответствии с разделом 6.3.2.3.
Предположим, однако, что вы конвертируете назад и вперед через промежуточный void *
:
uintptr_t b = (uintptr_t)(void *)a;
a = (int *)(void *)b;
В реализации, которая предоставляет uintptr_t
(что необязательно), это сделало бы ваши примеры (2 - 5) абсолютно строго соответствующими. В этом случае результат преобразований целочисленного указателя зависит только от значения объекта uintptr_t
, а не от того, как это значение было получено.
Что касается претензий, которые вы относите к Крису Лэттнеру, они в основном неверны. Если вы представили их точно, то, возможно, они отражают путаницу между реализацией, определенной поведением, и поведение undefined. Если код показал поведение undefined, то в заявке может содержаться некоторая вода, но это не так.
Независимо от того, как его значение было получено, b
имеет определенное значение типа uintptr_t
, и цикл должен в конечном итоге увеличивать i
до этого значения, после чего будет выполняться блок if
. В принципе, поведение, связанное с реализацией преобразования из uintptr_t
непосредственно в int *
, может быть чем-то сумасшедшим, например, пропустить следующий оператор (тем самым вызывая бесконечный цикл), но такое поведение совершенно неправдоподобно. Каждая реализация, которую вы когда-либо встречали, либо сбой в этом случае, либо сохранение некоторого значения в переменной a
, а затем, если она не завершилась сбой, она выполнит оператор return
.