Почему эта заявка на разыменование предупреждающего указателя типа зависит от компилятора?

38

Я читал различные сообщения о переполнении стека RE: ошибка разыменовывающего указателя типа. Насколько я понимаю, ошибка, по сути, является предупреждением компилятора об опасности доступа к объекту через указатель другого типа (хотя, похоже, для этого сделано исключение char*), что является понятным и разумным предупреждением.

Мой вопрос относится к приведенному ниже коду: почему приведение адреса указателя к void**значению квалифицируется для этого предупреждения (повышается до ошибки через -Werror)?

Более того, этот код скомпилирован для нескольких целевых архитектур, только одна из которых генерирует предупреждение / ошибку - может ли это означать, что это законно является недостатком конкретной версии компилятора?

// main.c
#include <stdlib.h>

typedef struct Foo
{
  int i;
} Foo;

void freeFunc( void** obj )
{
  if ( obj && * obj )
  {
    free( *obj );
    *obj = NULL;
  }
}

int main( int argc, char* argv[] )
{
  Foo* f = calloc( 1, sizeof( Foo ) );
  freeFunc( (void**)(&f) );

  return 0;
}

Если мое понимание, изложенное выше, является правильным, то void**, будучи все еще просто указателем, это должно быть безопасное приведение.

Есть ли обходной путь, не использующий lvalues, который бы успокоил это специфичное для компилятора предупреждение / ошибку? Т.е. я понимаю, что и почему это решит проблему, но я хотел бы избежать этого подхода, потому что я хочу использовать freeFunc() NULL для получения предполагаемого out-arg:

void* tmp = f;
freeFunc( &tmp );
f = NULL;

Компилятор проблемы (один из одного):

user@8d63f499ed92:/build$ /usr/local/crosstool/x86-fc3/bin/i686-fc3-linux-gnu-gcc --version && /usr/local/crosstool/x86-fc3/bin/i686-fc3-linux-gnu-gcc -Wall -O2 -Werror ./main.c
i686-fc3-linux-gnu-gcc (GCC) 3.4.5
Copyright (C) 2004 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

./main.c: In function `main':
./main.c:21: warning: dereferencing type-punned pointer will break strict-aliasing rules

user@8d63f499ed92:/build$

Компилятор без жалоб (один из многих):

user@8d63f499ed92:/build$ /usr/local/crosstool/x86-rh73/bin/i686-rh73-linux-gnu-gcc --version && /usr/local/crosstool/x86-rh73/bin/i686-rh73-linux-gnu-gcc -Wall -O2 -Werror ./main.c
i686-rh73-linux-gnu-gcc (GCC) 3.2.3
Copyright (C) 2002 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

user@8d63f499ed92:/build$

Обновление: я также обнаружил, что предупреждение, похоже, генерируется специально при компиляции -O2(все еще с отмеченным только "проблемным компилятором")

StoneThrow
источник
1
«а void**, оставаясь просто указателем, это должно быть безопасное приведение». Вау там скиппи! Похоже, у вас есть некоторые фундаментальные предположения. Постарайтесь меньше думать с точки зрения байтов и рычагов и больше с точки зрения абстракций, потому что это то, с чем вы на самом деле программируете
гонки на легкость на орбите
7
Тангенциально, используемым вами компиляторам 15 и 17 лет! Я бы не стал полагаться ни на кого.
Тавиан Барнс
4
@TavianBarnes Кроме того, если вы по какой-либо причине должны полагаться на GCC 3, лучше использовать окончательную версию, которая была 3.4.6, я думаю. Почему бы не воспользоваться всеми доступными исправлениями для этой серии, прежде чем она была положена на отдых.
Каз
Какой стандарт кодирования C ++ предписывает все эти пробелы?
Питер Мортенсен

Ответы:

33

Значение типа void**- это указатель на объект типа void*. Объект типа Foo*не является объектом типа void*.

Существует неявное преобразование между значениями типа Foo*и void*. Это преобразование может изменить представление значения. Точно так же вы можете писать, int n = 3; double x = n;и это имеет четко определенное поведение установки xзначения 3.0, но double *p = (double*)&n;имеет неопределенное поведение (и на практике не будет устанавливать p«указатель на 3.0» в любой общей архитектуре).

Архитектуры, в которых разные типы указателей на объекты имеют разные представления, сегодня редки, но они разрешены стандартом C. Существуют (редкие) старые машины с указателями слов, которые являются адресами слова в памяти, и указателями байтов, которые являются адресами слова вместе со смещением байтов в этом слове; Foo*будет указателем слова и void*будет указателем байта на таких архитектурах. Существуют (редкие) машины с толстыми указателями, которые содержат информацию не только об адресе объекта, но также о его типе, размере и списках контроля доступа; указатель на определенный тип может иметь представление, отличное от того, void*которое требует дополнительной информации о типе во время выполнения.

Такие машины редки, но разрешены стандартом C. И некоторые компиляторы C пользуются преимуществом разрешения обрабатывать указатели типа как отдельные для оптимизации кода. Риск создания псевдонимов указателей является основным ограничением способности компилятора оптимизировать код, поэтому компиляторы, как правило, используют такие разрешения.

Компилятор может сказать вам, что вы делаете что-то не так, или тихо делать то, что вы не хотели, или тихо делать то, что вы хотели. Неопределенное поведение допускает любое из них.

Вы можете сделать freefuncмакрос:

#define FREE_SINGLE_REFERENCE(p) (free(p), (p) = NULL)

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

Жиль "ТАК - перестань быть злым"
источник
1
И это хорошо , чтобы знать , что даже если Foo*и void*имеют такое же представление о вашей архитектуры, это еще не определено для типа-каламбур них.
Тавиан Барнс
12

Стандарт A void *специально обрабатывается отчасти потому, что он ссылается на неполный тип. Эта процедура никак не распространяется на , void **как это делает точку полного типа, в частности void *.

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

Вы можете обойти это ограничение, используя вместо функции функциональный макрос:

#define freeFunc(obj) (free(obj), (obj) = NULL)

Который вы можете назвать так:

freeFunc(f);

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

#define freeFunc(obj) ({ typeof (&(obj)) ptr = &(obj); free(*ptr); *ptr = NULL; })
dbush
источник
3
+1 за лучшую реализацию предполагаемого поведения. Единственная проблема, которую я вижу в #defineтом, что она будет оцениваться objдважды. Хотя я не знаю, как избежать второй оценки. Даже выражение оператора (расширение GNU) не справится с задачей, которую вам нужно назначить objпосле того, как вы использовали его значение.
cmaster - восстановить
2
@cmaster: Если вы готовы использовать расширения GNU , такие как выражения заявления, то вы можете использовать , typeofчтобы избежать оценок objдважды #define freeFunc(obj) ({ typeof(&(obj)) ptr = &(obj); free(*ptr); *ptr = NULL; }).
Руах
@ruakh Очень круто :-) Было бы здорово, если бы dbush отредактировал это в ответе, так что он не будет массово удален с комментариями.
cmaster - восстановить
9

Разыменование перенаправленного указателя типа - UB, и вы не можете рассчитывать на то, что произойдет.

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

Случай, который может помочь вам понять, почему в этом случае может быть плохое типизирование, заключается в том, что ваша функция не будет работать на архитектуре, для которой sizeof(Foo*) != sizeof(void*). Это разрешено стандартом, хотя я не знаю ни одного текущего, для которого это правда.

Обходной путь должен был бы использовать макрос вместо функции.

Обратите внимание, что freeпринимает нулевые указатели.

AProgrammer
источник
2
Увлекательно, что это возможно sizeof Foo* != sizeof void*. Я никогда не сталкивался с «дикими» размерами указателей, зависящими от типа, поэтому с годами я пришел к выводу, что аксиоматично, что размеры указателей одинаковы для данной архитектуры.
StoneThrow
1
@Stonethrow стандартный пример - жирные указатели, используемые для адресации байтов в архитектуре с адресацией по словам. Но я думаю, что современные машины, адресуемые по словам, используют альтернативный размер символа == размер слова .
AProgrammer
2
Обратите внимание, что тип должен быть заключен в скобки для sizeof ...
Антти Хаапала
@StoneThrow: Независимо от размеров указателя, анализ псевдонимов на основе типов делает его небезопасным; это помогает компиляторам оптимизировать, предполагая, что хранилище через float*не изменяет int32_tобъект, так что, например, компилятору int32_t*не нужно int32_t *restrict ptrпредполагать, что он не указывает на одну и ту же память. То же самое для магазинов через void**существо, предполагаемое не изменять Foo*объект.
Питер Кордес
4

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

«Правило строгого псевдонима» для доступа к значению через указатель, который был приведен к другому типу указателя, находится в 6.5, параграф 7:

Объект должен иметь свое сохраненное значение, доступное только через выражение lvalue, которое имеет один из следующих типов:

  • тип, совместимый с эффективным типом объекта,

  • квалифицированная версия типа, совместимого с эффективным типом объекта,

  • тип, который является типом со знаком или без знака, соответствующим действующему типу объекта,

  • тип, который является типом со знаком или без знака, соответствующим квалифицированной версии действующего типа объекта,

  • агрегатный или объединенный тип, который включает в себя один из вышеупомянутых типов среди своих членов (включая, рекурсивно, член субагрегированного или автономного объединения), или

  • тип персонажа.

В вашем *obj = NULL;утверждении объект имеет эффективный тип, Foo*но к нему обращается выражение lvalue *objс типом void*.

В пункте 2 пункта 6.7.5.1 мы имеем

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

Так что void*и Foo*не являются совместимыми типами или совместимыми типами с добавленными квалификаторами, и, конечно, не соответствуют ни одному из других параметров правила строгого алиасинга.

Хотя это и не техническая причина, по которой код является недействительным, это также относится к примечанию раздела 6.2.5, пункт 26:

Указатель на voidдолжен иметь те же требования к представлению и выравниванию, что и указатель на символьный тип. Аналогично, указатели на квалифицированные или неквалифицированные версии совместимых типов должны иметь одинаковые требования к представлению и выравниванию. Все указатели на типы конструкций должны иметь те же требования к представлению и выравниванию, что и другие. Все указатели на типы объединения должны иметь те же требования к представлению и выравниванию, что и другие. Указатели на другие типы не обязательно должны иметь одинаковые требования к представлению или выравниванию.

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

aschepler
источник
2

В дополнение к тому, что сказали другие ответы, это классический анти-паттерн в C, и тот, который должен быть сожжен огнем. Появляется в:

  1. Функции освобождения и обнуления, подобные той, в которой вы нашли предупреждение.
  2. Функции распределения, которые избегают стандартного языка C возврата void *(который не страдает от этой проблемы, потому что он включает преобразование значения вместо типа punning ), вместо этого возвращая флаг ошибки и сохраняя результат через указатель на указатель.

Для другого примера (1) в функции ffmpeg / libavcodec был давний печально известный случай av_free. Я полагаю, что в конечном итоге это было исправлено с помощью макроса или другого трюка, но я не уверен.

Для (2) оба cudaMallocи posix_memalignявляются примерами.

Ни в том, ни в другом случае интерфейс по своей природе не требует недопустимого использования, но он настоятельно рекомендует его и допускает правильное использование только с дополнительным временным объектом типа, void *который побеждает назначение функциональности free-and-null-out и делает распределение неудобным.

R .. GitHub СТОП, ПОМОГАЯ ЛЬДУ
источник
У вас есть ссылка, объясняющая больше о том, почему (1) является анти-паттерном? Я не думаю, что я знаком с этой ситуацией / аргументом и хотел бы узнать больше.
StoneThrow
1
@StoneThrow: Это действительно просто - цель состоит в том, чтобы предотвратить злоупотребление путем обнуления объекта, хранящего указатель на освобождаемую память, но единственный способ, которым он может это сделать, - это если вызывающий объект фактически хранит указатель в объекте печатать void *и преобразовывать его каждый раз, когда он хочет разыменовать его. Это очень маловероятно. Если вызывающая сторона хранит какой-то другой тип указателя, единственный способ вызвать функцию без вызова UB - это скопировать указатель на временный объект типа void *и передать его адрес функции освобождения, а затем просто ...
R .. GitHub ОСТАНОВИТЬ ЛЬДА
1
... обнуляет временный объект, а не реальное хранилище, в котором у вызывающей стороны был указатель. Конечно, на самом деле происходит то, что пользователи функции в конечном итоге выполняют (void **)приведение, что приводит к неопределенному поведению.
R .. GitHub ОСТАНОВИТЬ ЛЬДА
2

Хотя C был разработан для машин, которые используют одно и то же представление для всех указателей, авторы Стандарта хотели сделать язык пригодным для использования на машинах, которые используют разные представления для указателей на разные типы объектов. Следовательно, они не требовали, чтобы машины, которые используют разные представления указателей для разных типов указателей, поддерживали тип «указатель на любой вид указателя», хотя многие машины могли сделать это с нулевой стоимостью.

До написания Стандарта реализации для платформ, которые использовали одно и то же представление для всех типов указателей, единодушно позволяли void**бы использовать, по крайней мере, с подходящим приведением, в качестве «указателя на любой указатель». Авторы Стандарта почти наверняка признали, что это будет полезно на платформах, которые его поддерживают, но, поскольку он не может быть поддержан повсеместно, они отказались от его мандата. Вместо этого они ожидали, что качественная реализация обработает такие конструкции, которые Rationale назвал бы «популярным расширением», в тех случаях, когда это имеет смысл.

Supercat
источник