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

Строгие псевдонимы и места памяти

Строгое сглаживание не позволяет нам получить доступ к одному и тому же месту памяти с помощью несовместимого типа.

int* i = malloc( sizeof( int ) ) ;  //assuming sizeof( int ) >= sizeof( float )
*i = 123 ;
float* f = ( float* )i ;
*f = 3.14f ;

это было бы незаконным в соответствии со стандартом C, потому что компилятор "знает", что int не может получить доступ к float lvalue.

Что делать, если я использую этот указатель для указания правильности памяти, например:

int* i = malloc( sizeof( int ) + sizeof( float ) + MAX_PAD ) ;
*i = 456 ;

Сначала я выделяю память для int, float, а последняя часть - это память, которая позволит сохранять float на выровненном адресе. float требует выравнивания по кратным 4. MAX_PAD обычно составляет 8 из 16 байтов в зависимости от системы. В любом случае MAX_PAD достаточно велик, поэтому float можно правильно выровнять.

Затем я пишу a int в i, насколько это хорошо.

float* f = ( float* )( ( char* )i + sizeof( int ) + PaddingBytesFloat( (char*)i ) ) ;
*f= 2.71f ;

Я использую указатель i, увеличиваю его с размером int и правильно выравниваю его с помощью функции PaddingBytesFloat(), которая возвращает количество байтов, необходимых для выравнивания float, заданного адреса. Затем я пишу в него поплавок.

В этом случае f указывает на другую ячейку памяти, которая не перекрывается; он имеет другой тип.


Вот некоторые части из стандарта (ISO/IEC 9899: 201x) 6.5, на которые я ссылался при написании этого примера.

Алиасинг - это когда более одного значения lLame указывает на то же место памяти. Стандарт требует, чтобы эти lvalues ​​имели совместимый тип с эффективным типом объекта.

Что такое эффективный тип, цитата из стандарта:

Эффективным типом объекта для доступа к его сохраненному значению является объявленный тип объект, если таковой имеется .87) Если значение хранится в объекте, не имеющем объявленного типа, через lvalue, имеющий тип, который не является типом символа, тогда тип lvalue становится эффективный тип объекта для этого доступа и для последующих доступов, которые не изменяются сохраненное значение. Если значение копируется в объект, не имеющий объявленного типа, используя memcpy или memmove, или копируется как массив типа символа, тогда эффективный тип измененного объекта для этого доступа и для последующих доступов, которые не изменяют value - это эффективный тип объекта, из которого копируется значение, если оно есть. Для всех других доступов к объекту, не имеющему объявленного типа, эффективный тип объекта просто тип lvalue, используемый для доступа.

87) Выделенные объекты не имеют объявленного типа.

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

Является ли это законным? Если нет, что, если я использовал указатель void как lvalue вместо int pointer i в моем втором примере? Если даже это не сработает, что делать, если я получил адрес, который присваивается указателю float во втором примере, как memcopied значение, и этот адрес никогда не использовался как lvalue раньше.

4b9b3361

Ответ 1

Я думаю, что да, это законно.

Чтобы проиллюстрировать мою мысль, см. этот код:

struct S
{
    int i;
    float f;
};
char *p = malloc(sizeof(struct S));

int *i = p + offsetof(struct S, i);  //this offset is 0 by definition
*i = 456;
float *f = p + offsetof(struct S, f);
*f= 2.71f;

Этот код, IMO, явно легальный, и он эквивалентен вашему с точки зрения компилятора, для соответствующих значений PaddingBytesFloat() и MAX_PAD.

Обратите внимание, что в моем коде не используется l-значение типа struct S, оно используется только для облегчения вычисления paddings.

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

TL; DR: Мое заключение заключается в том, что с динамической памятью вы в безопасности, в отношении строгого сглаживания, если вы читаете память с использованием того же типа (или совместимого), который используется для последней записи в эту память.

Ответ 2

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

В соответствии со стандартом C99, когда вы это делаете:

int* i = malloc( sizeof( int ) + sizeof( float ) + MAX_PAD ) ;
*i = 456 ;

malloc вернет указатель на блок памяти, достаточно большой, чтобы удерживать объект размером sizeof(int)+sizeof(float)+MAX_PAD. Однако обратите внимание, что вы используете только небольшой кусок этого размера; в частности, вы используете только первые sizeof(int) байты. Следовательно, вы оставляете некоторое свободное пространство, которое можно использовать для хранения других объектов, если вы храните их в непересекающемся смещении (то есть после первых sizeof(int) байтов). Это тесно связано с определением того, что именно является объектом. Из раздела C99 3.14:

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

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

Этот код:

float* f = ( float* )( ( char* )i + sizeof( int ) + PaddingBytesFloat( (char*)i ) ) ;
*f= 2.71f ;

Эффективно прикрепляет другой объект к подблоку выделенной памяти. Пока результирующая ячейка памяти для f не будет перекрываться с адресом i, и остается достаточно места для хранения float, вы всегда будете в безопасности. Строгое правило псевдонимов здесь даже не применяется, поскольку указатели указывают на объекты, которые не перекрываются - места в памяти различаются.

Я думаю, что ключевым моментом здесь является понимание того, что вы эффективно манипулируете двумя разными объектами с двумя разными указателями. Так получилось, что оба указателя указывают на один и тот же блок malloc() 'd, но они достаточно далеки друг от друга, поэтому это не проблема.

Вы можете посмотреть на этот связанный вопрос: Какие проблемы выравнивания ограничивают использование блока памяти, созданного malloc? и читают Eric Postpischil отличный ответ: fooobar.com/questions/412695/... - в конце концов, если вы можете хранить массивы разных типов в одном блоке malloc(), почему бы вам не сохранить int и a float? Вы даже можете посмотреть на свой код как на специальный случай, когда эти массивы являются одноэлементными массивами.

Пока вы заботитесь о проблемах с выравниванием, код отлично подходит и переносится на 100%.

ОБНОВЛЕНИЕ (продолжение, комментарии ниже):

Я считаю, что ваши рассуждения о стандарте, не применяющем строгий псевдоним на объектах malloc() ', неверны. Верно, что эффективный тип динамически выделенного объекта может быть изменен, как передается стандартом (речь идет о использовании выражения lvalue с другим типом для хранения нового значения там), но обратите внимание, что, как только вы это сделаете что ваша работа должна гарантировать, что никакое другое выражение lvalue с другим типом не получит доступ к значению объекта. Это применяется в соответствии с правилом 7 раздела 6.5, и вы процитировали его в своем вопросе:

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

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

Ответ 3

Я нашел хорошую аналогию. Вы также можете найти это полезным. Цитирование из ISO/IEC 9899:TC2 Committee Draft — May 6, 2005 WG14/N1124

6.7.2.1 Спецификаторы структуры и объединения

[16] В качестве частного случая последний элемент структуры с более чем одним именованный элемент может иметь неполный тип массива; это называется гибкий элемент массива. В большинстве ситуаций гибкий элемент массива игнорируется. В частности, размер структуры выглядит так, как если бы гибкий элемент массива был исключен, за исключением того, что он может иметь больше это означает, что это будет означать пропуски, отличные от пропусков. Однако, когда a. (или → ) имеет левый операнд, который является (указателем на) структуру с гибким членом массива, а правый операнд - этим членом, он ведет себя так, как если бы этот элемент был заменен самым длинным массивом (с тем же типом элемента), который не сделает структуру более крупной чем доступ к объекту; смещение массива останется что элемент гибкого массива, даже если это будет отличаться от этого матрицы замены. Если бы этот массив не имел элементов, это ведет себя так, как если бы у него был один элемент, но поведение undefined, если оно делается попытка получить доступ к этому элементу или создать указатель один мимо него.

[17] ПРИМЕР После объявления:

 struct s { int n; double d[]; };

структура struct s имеет гибкий элемент массива d. Типичный способ использования:

int m = /* some value */;
struct s *p = malloc(sizeof (struct s) + sizeof (double [m])); 

и полагая, что вызов malloc преуспевает, объект, на который указывает p ведет себя, для большинства цели, как если бы p был объявлен как:

 struct { int n; double d[m]; } > *p;

(есть обстоятельства, в которых эта эквивалентность нарушена, в частности, смещения элемента d могут быть не одинаковыми).

Было бы более справедливо использовать пример, например:

struct ss {
  double da;
  int ia[];
}; // sizeof(double) >= sizeof(int)

В примере выше цитаты размер struct s совпадает с int (+ padding), после чего следует double. (или какой-либо другой тип, float в вашем случае)

Доступ к памяти sizeof(int) + PADDING байтов после начала структуры как double (используя синтаксический сахар) выглядит отлично, как в этом примере, поэтому я верьте, что ваш пример является законным.

Ответ 4

Существуют строгие правила псевдонимов, позволяющие более агрессивно оптимизировать компиляторы, в частности, имея возможность переупорядочивать обращения к различным типам, не беспокоясь о том, указывают ли они на одно и то же местоположение. Так, например, в вашем первом примере для компилятора совершенно законно переупорядочивать записи до i и f, и, следовательно, ваш код является примером поведения undefined (UB).

Существует исключение из этого правила, и у вас есть соответствующая цитата из стандартов

имеющий тип, который не является типом символа

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

У меня такое чувство, что я, таким образом, упустил настоящий вопрос.

Самый безопасный способ возиться с низкоуровневой памятью, если вы действительно хотите, чтобы поведение в вашей первой программе было (a) соединением или (b) a char *. Используя char *, а затем приведение к правильному типу используется во множестве кода C, например: в этом pcap tutorial (прокрутите вниз до "для всех тех новых программистов, которые настаивают на том, что указатели бесполезны, я поражаю вас".