Строгий aliasing и местах памяти

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

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 можно выровнять правильно.

тогда я пишу 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 , на которые я полагался при написании этого примера.

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

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

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

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

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

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

4 ответов


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

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

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;

этот код, ИМО, явно легален, и он эквивалентен Вашему с точки зрения компилятора, для соответствующих значений PaddingBytesFloat() и MAX_PAD.

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

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

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


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

согласно стандарту 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:

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

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

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


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

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

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

[17] пример после объявления:

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

структура структура 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 (+прокладка), а затем следует двойным. (или какой-то другой тип, float в вашем случае)

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


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

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

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

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

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

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