Подтвердить что ты не робот

Какое лечение может пройти указатель и по-прежнему действовать?

Какой из следующих способов лечения и попытки восстановления 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.

4b9b3361

Ответ 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.