【问题标题】:Strict aliasing and memory locations严格的别名和内存位置
【发布时间】:2014-06-28 06:03:02
【问题描述】:

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

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 所需的字节数,给定地址。然后我在里面写一个浮点数。

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


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

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

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

访问其存储值的对象的有效类型是声明的类型 对象,如果有的话。87) 如果一个值通过 左值的类型不是字符类型,则左值的类型变为 该访问和不修改的后续访问的对象的有效类型 存储的值。如果一个值被复制到一个没有声明类型的对象中 memcpy 或 memmove,或复制为字符类型的数组,则为有效类型 为该访问和后续访问不修改 value 是从中复制值的对象的有效类型(如果有的话)。对于没有声明类型的对象的所有其他访问,对象的有效类型是 只是用于访问的左值的类型。

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

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

这合法吗?如果不是,如果我在第二个示例中使用 void 指针作为左值而不是 int 指针 i 会怎样?如果即使这样也行不通,如果我将分配给第二个示例中的浮点指针的地址作为 memcopied 值获取,并且该地址以前从未用作左值。

【问题讨论】:

  • 你为什么要把应该简单的事情复杂化?使用结构并快乐!
  • 为什么要编写难以阅读和维护的代码?
  • 您是专门询问这是否合法,还是只是在寻找意见?
  • “在这种情况下,f 指向不重叠的不同内存位置;它具有不同的类型。” 它重叠(部分)i[1] 和/或i[2].
  • @Neil:来吧!这样做有很多原因,毕竟 C 是一种低级语言:实现内存分配器或内存区域、垃圾收集器、可变参数类型、解释器的类型系统、虚拟 CPU。 .. 在某些情况下,您可能会使用联合,但并非全部使用。并且知道语言在哪里设置限制总是有用的。

标签: c memory standards strict-aliasing


【解决方案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 的适当值。

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

当我阅读标准时,在 malloc 的内存中没有声明的类型,直到那里写了一些东西。然后声明的类型就是写的任何内容。因此,此类内存的声明类型可以随时更改,用不同类型的值覆盖内存,就像联合一样。

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

【讨论】:

  • 浮点指针运算不会失败吗?您正在尝试使用以字节为单位返回偏移量的 offsetof,并增加每个成员超过一个字节的浮点数,最终不会实际增加 offsetof(struct S, f) * sizeof(float)?
  • @DAhrens:不。代码是p + offset(...)p 是指向字符的指针。到浮点指针的强制转换在赋值的后面隐式完成。
  • 啊,我错过了那部分。感谢您的澄清。
  • @dave:转念一想我不太确定。 char 确实可以为任何其他类型起别名,但char 可以被任何其他类型起别名吗?我不确定......实际上,我认为你不能。此外,使用 char[] 时,您会遇到对齐问题...
  • @dave:一件事是使用char左值来访问int有效类型的对象。另一种是使用int来访问char有效类型的对象。根据引用标准的 6.5.7 允许前者。后者不是。
【解决方案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 节:

Object:执行环境中的数据存储区域, 可以表示值的内容

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 - 毕竟,如果您可以在同一个 malloc() 块中存储不同类型的数组,为什么不你存储了intfloat?您甚至可以将您的代码视为这些数组是单元素数组的特例。

只要您处理对齐问题,代码就完美无缺并且 100% 可移植。

更新(后续,阅读下面的 cmets)

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

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

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

【讨论】:

  • 回顾我的问题,现在已经很清楚了。我想我的下一个问题将涉及该标准是否真的对分配的存储持续时间的内存强制执行严格的别名。如果您阅读我在问题中发布的标准的摘录,您会注意到它暗示您可以更改对象的有效类型。好像您可以指向具有不同类型的相同分配的内存。
  • 因为:对于没有声明类型的对象的所有其他访问,对象的有效类型只是用于访问的左值的类型,并且: 分配的对象没有声明类型。严格的别名不适用于(m)分配的内存。
  • @self。哼。这是一个很好的观点,但我不完全同意你的推理。请参阅我的更新答案。
  • 我刚刚得出了类似的结论。你的解释更清楚了。我想我有我需要的所有信息,我会让它沉没一段时间。
  • @user3489275 如果对齐正确,并且在原始问题中的情况下,是的,它是安全的,因为您并没有真正创建别名 - if 将有效地指向不同的对象 - 没有别名。如果您执行float *f = (float *) i;,则会出现别名。那会是个问题。
【解决方案3】:

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

6.7.2.1 结构和联合说明符

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

[17] 示例声明后:

 结构 s { int n;双 d[]; };

结构 struct s 有一个灵活的数组成员 d。一个典型的 使用方法是:

int m = /* 某个值 */;
struct s *p = malloc(sizeof (struct s) + sizeof (double [m])); 

并假设 对 malloc 的调用成功,p 指向的对象的行为,对于大多数 目的,就好像 p 被声明为:

 结构 { int n;双 d[m]; } > *p;

(在某些情况下,这种等价性会被破坏;特别是成员 d 的偏移量可能不一样)。

使用这样的例子会更公平:

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

在上面引用的示例中,struct s 的大小与int 相同(+ 填充),然后是 double。 (或其他类型,float 在您的情况下)

在结构开始后以double(使用syntactic sugar)访问内存sizeof(int) + PADDING字节看起来很好,所以我相信你的例子是合法的C。

【讨论】:

  • 但是使用灵活的数组成员,您可以通过结构成员和正确的符号 (.) 访问它们。我不太明白你的意思。
  • 您只能通过一种类型访问部分原始内存,因此即使优化级别最高,兼容的编译器也不会给您带来令人讨厌的惊喜。您的示例使用灵活数组执行与struct ss 类似的工作,但不是每次都通过obj.ia[0] 访问int member,而是使用*((char *)&obj + offset_of_first_integer) 访问它,甚至无需实际声明struct ss。比较 2 让我相信您的代码具有明确定义的行为。 (希望我没有遗漏任何东西)
  • 累积我的观点,如果您(m)分配更多字节然后需要 1 种数据类型,您可以使用剩余字节作为不同类型,前提是内存不重叠并且您以 1 访问一块内存仅限数据类型。
  • 将指针转换为任何类型并使用关联的内存作为该类型应该是完全“公平的”,前提是所有使用新转换指针完成的访问都在任何访问之前通过任何其他方式完成。来自一种特定类型的指针很少会转换为指向另一种类型的指针,除非需要重新解释存储的内容。该规则的目的是避免强制对可能的别名进行“过于悲观”的假设,并且假设演员阵容的目标将被类型双关,这几乎不是悲观的。
【解决方案4】:

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

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

具有非字符类型的类型

您的第二段代码是完全安全的。内存区域不重叠,因此内存访问是否跨该边界重新排序无关紧要。事实上,这两段代码的行为是完全不同的。第一个将 int 放入内存区域,然后将 float 放入 same 内存区域,而第二个将 int 放入内存区域,然后将 float 放入内存中给它。即使这些访问被重新排序,那么您的代码也会产生相同的效果。完全合法。

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

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

【讨论】:

  • @self:这不是真的,它是将一个结构指针分配给一个字符指针。这样做是安全的,即使您继续通过 BOTH 指针访问内存,访问也不会重新排序。 (例如,通过结构指针更改字段,然后调用 printf 打印数据包)。
  • 对,但是每个指针(除了 char )都指向不同的内存。在这种情况下,我看不出这与我的第一个例子有什么关系。 (也许您应该更具体地使用外部链接,而是在此处发布文本,以免造成任何混淆)。
  • 我在帖子中所说的全部要点是,如果您确实想要重叠类型,那么您必须使用 char *。所以说“除了 char *”,这就是那个例子所显示的,错过了这个例子和它所连接的句子的要点。您不能在第一个示例中做您想做的事。我认为这很清楚。这是未定义的行为。第二个例子是不同的和安全的,正是因为它没有重叠的指针。
  • @self: 而且,你的第一个例子是 UB,正如我在回答中所说的那样。因此,我将无法找到与您的第一个示例相关的代码。
猜你喜欢
  • 2012-12-19
  • 2021-10-21
  • 2012-11-08
  • 1970-01-01
  • 1970-01-01
  • 2017-10-23
  • 2019-02-09
  • 1970-01-01
  • 2014-07-30
相关资源
最近更新 更多