严格的别名和内存位置

时间:2014-05-10 14:56:40

标签: c memory standards strict-aliasing

严格别名会阻止我们使用不兼容的类型访问相同的内存位置。

int* i = malloc( sizeof( int ) ) ;  //assuming sizeof( int ) >= sizeof( float )
*i = 123 ;
float* f = ( float* )i ;
*f = 3.14f ;
根据C标准,这将是非法的,因为编译器“知道”int左值无法访问float

如果我使用该指针指向正确的内存,如下所示:

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

首先,我为intfloat分配内存,最后一部分是内存,允许float存储在对齐的地址上。 float需要在4的倍数上对齐。MAX_PAD通常是16个字节中的8个,具体取决于系统。无论如何,MAX_PAD足够大,因此float可以正确对齐。

然后我将int写入i,到目前为止一直很好。

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

我使用指针i,用int的大小递增它,并将其与函数PaddingBytesFloat()正确对齐,函数float返回对齐{{1}所需的字节数给出一个地址。然后我写了一个浮点数。

在这种情况下,f指向不重叠的不同内存位置;它有不同的类型。


以下是标准(ISO / IEC 9899:201x)6.5中的一些部分,我在编写此示例时依赖于此。

别名是指多个左值指向同一个内存位置。标准要求这些左值具有与对象的有效类型兼容的类型。

什么是有效类型,引自标准:

访问其存储值的对象的有效类型是声明的类型 object,如果有的话.87)如果一个值存储到一个没有声明类型的对象中 lvalue的类型不是字符类型,那么左值的类型就变成了 该访问的对象的有效类型以及不修改的后续访问 储存的价值。如果将值复制到没有使用声明类型的对象中 memcpy或memmove,或者被复制为字符类型数组,然后是有效类型 用于该访问的修改对象以及不修改该访问的后续访问 value是从中复制值的对象的有效类型(如果有)。对于没有声明类型的对象的所有其他访问,对象的有效类型是 只是用于访问的左值的类型。

87)已分配的对象没有声明的类型。

我正在尝试连接各个部分并弄清楚是否允许这样做。在我的解释中,分配对象的有效类型可以根据该内存上使用的左值的类型进行更改,因为这部分: For 对没有声明类型的对象的所有其他访问,对象的有效类型只是用于访问的左值的类型。

这合法吗?如果没有,如果我在第二个例子中使用void指针作为lvalue而不是int指针i怎么办?如果即使这样也行不通,如果我将第二个例子中指定给浮点指针的地址作为memcopied值,并且该地址以前从未用作左值,那该怎么办呢。

4 个答案:

答案 0 :(得分:10)

我认为是的,这是合法的。

为了说明我的观点,让我们看看这段代码:

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的值。

请注意,我的代码不使用struct S类型的任何l值,它仅用于简化填充的计算。

当我阅读标准时,在malloc的内存中没有声明类型,直到写入内容。然后声明的类型是写入的内容。因此,可以随时更改此类内存的声明类型,使用不同类型的值覆盖内存,非常类似于union。

TL; DR:我的结论是,对于动态内存,只要您使用与上次写入该内存相同的类型(或兼容的类型)读取内存,就可以安全地使用严格别名。

答案 1 :(得分:9)

是的,这是合法的。要了解原因,您甚至不需要考虑严格的别名规则,因为在这种情况下它并不适用。

根据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块,但它们相距足够远,所以这不是问题。

你可以看看这个相关的问题:What alignment issues limit the use of a block of memory created by malloc?并阅读Eric Postpischil的好答案:https://stackoverflow.com/a/21141161/2793118 - 毕竟,如果你可以将不同类型的数组存储在同一个{ {1}}阻止,为什么不存储malloc()int?您甚至可以将代码视为这些数组是单元素数组的特殊情况。

只要您处理对齐问题,代码就可以完全正常并且100%便携。

更新(跟进,阅读以下评论)

我相信您对float&n;对象的标准不强制严格别名的推理是错误的。确实可以改变动态分配对象的有效类型,如标准所传达的(这是使用不同类型的左值表达式来存储新值),但请注意,一旦你做了那就是,确保没有其他类型的其他左值表达式访问对象值是你的工作。这由第6.5节的规则7强制执行,您在问题中引用它:

  

对象的存储值只能由左值访问   具有以下类型之一的表达式:    - 与对象的有效类型兼容的类型;

因此,当您更改对象的有效类型时,您隐含地向编译器承诺,您不能使用具有不兼容类型的旧指针访问此对象(与新的有效类型相比) 。对于严格别名规则而言,这应该足够了。

答案 2 :(得分:2)

我发现了一个很好的类比。您可能还会发现它很有用。引自ISO/IEC 9899:TC2 Committee Draft — May 6, 2005 WG14/N1124

6.7.2.1结构和联合说明符

  

[16]作为特例,结构的最后一个元素有多个   命名成员可能具有不完整的数组类型;这叫做a   灵活的阵列成员。在大多数情况下,灵活的阵列成员   被忽略了。特别是,结构的大小就像是   柔性阵列成员被省略,除了它可能有更多   拖尾填充比遗漏意味着。但是,当一个。 (要么    - >)运算符有一个左操作数,它是一个带有灵活数组成员的结构(指向),右操作数命名该成员,   它的行为好像该成员被最长的数组所取代   (具有相同的元素类型)不会使结构更大   而不是被访问的对象;阵列的偏移量应保持不变   柔性阵列成员的那个,即使这与此不同   替换数组。如果这个数组没有元素,那么   表现得好像它有一个元素,但行为是未定义的,如果有的话   尝试访问该元素或生成指针   过去了。

     

[17]示例声明后:

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

结构struct具有灵活的数组成员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

在struct start之后访问内存sizeof(int) + PADDING字节为double(使用syntactic sugar)看起来很好,因此我相信你的例子是合法的C.

答案 3 :(得分:1)

严格的别名规则允许更积极的编译器优化,特别是能够重新排序对不同类型的访问,而不必担心它们是否指向相同的位置。例如,在您的第一个示例中,编译器将写入重新排序为if是完全合法的,因此您的代码是未定义行为(UB)的示例。

此规则有一个例外,您可以从标准中获得相关引用

  

具有非字符类型的类型

你的第二段代码是完全安全的。存储区域没有重叠,因此如果跨越该边界重新排序存储器访问并不重要。实际上,两段代码的行为完全不同。第一个将一个int放在一个内存区域,然后浮动到相同的内存区域,而第二个将一个int放入一个内存区域,一个浮点放入一个内存区域它。即使重新排序这些访问,您的代码也会产生相同的效果。完全合法。

我觉得我在这里错过了真正的问题。

如果你确实想要在你的第一个程序中使用行为,那么摆弄低级内存的最安全的方法是(a)联合或(b)char *。在很多C代码中使用char *然后转换为正确的类型,例如:在这个pcap tutorial中(向下滚动到"对于那些坚持指针是指针的新C程序员没用,我打你。"