【问题标题】:dereferencing type-punned pointer will break strict-aliasing rules取消引用类型双关指针将破坏严格别名规则
【发布时间】:2011-11-29 14:49:33
【问题描述】:

我有一个包含结构的无符号字符指针。现在我想做以下事情

unsigned char buffer[24];

//code to fill the buffer with the relevant information.

int len = ntohs((record_t*)buffer->len);

其中 record_t 结构包含一个名为 len 的字段。我无法这样做并且收到错误。

error: request for member ‘len’ in something not a structure or union.

然后我尝试了:

int len = ntohs(((record_t*)buffer)->len);

以使运算符优先级正确。这给了我warning: dereferencing type-punned pointer will break strict-aliasing rules。

然后我声明了

record_t *rec = null;

rec = (record_t*)

我在这里做错了什么?

【问题讨论】:

  • 您的编译器选项是什么?另外,专注于手头的问题,不要告诉我们你之前遇到的语法错误。
  • 在 C 中,它是 NULL,而不是 null。

标签: c pointers gcc


【解决方案1】:

根据 C 和 C++ 标准,通过指向另一个类型的指针访问给定类型的变量是未定义的行为。示例:

int a;
float * p = (float*)&a;  // #1
float b = *p;            // #2

这里 #2 会导致未定义的行为。 #1 处的分配称为“类型双关语”。术语“别名”指的是几个不同的指针变量可能指向相同的数据——在这种情况下,p 为数据a 取别名。合法别名是优化的一个问题(这是 Fortran 在某些情况下性能优越的主要原因之一),但我们这里有一个完全的非法别名。

你的情况也不例外;您正在通过指向不同类型的指针(即不是 char * 的指针)访问 buffer 的数据。这是绝对不允许的。

结果是:你一开始就不应该在buffer 有数据。

但是如何解决呢?确保你有一个有效的指针!类型双关语有一个例外,即通过指向 char 的指针访问数据,这是允许的 。所以我们可以这样写:

record_t data;
record_t * p = &data;          // good pointer
char * buffer = (char*)&data;  // this is allowed!

return p->len;                 // access through correct pointer!

关键的区别在于我们将真实数据存储在正确类型的变量中,并且只有在分配了该变量之后,我们才会将该变量视为字符数组(这是允许的)。这里的寓意是字符数组总是排在第二位,真正的数据类型排在第一位。

【讨论】:

  • +1 不仅告诉 OP 出了什么问题,而且还展示了正确的方法来做到这一点!
  • 不,不,您在夸大其词并没有抓住重点。通过另一种类型访问对象可能是未定义的行为,如果这会导致例如陷阱表示或错位,但不一定如此。混叠的点完全不同。这是因为标准允许编译器假设不同类型的指针不会相互别名,因此他可能会执行更多的 agressif 优化,如果实际上这些指针指向同一个对象,则可能会出错。
  • @JensGustedt:我不认为对齐在规则中起作用。你可以来回投射,这没关系(如T * p = &x; S * q = (S*)p; T * r = (T*)q; T y = *r;),但除此之外它只是未定义的行为(可能表现如预期)。 别名是一个广泛的概念(例如,考虑“向量加法器”add(float * a, float * b, float * c),知道a 或b 别名c 很有用),但“严格别名规则”说不同类型的指针永远不能相互别名。
【解决方案2】:

您收到该警告是因为您通过让两个不同类型的指针指向同一位置来破坏严格别名。

解决这个问题的一种方法是使用联合:

union{
    unsigned char buffer[24];
    record_t record_part;
};

//code to fill the buffer with the relavent information.

int len = ntohs(record_part.len);

编辑:

严格来说,这并不比您的原始代码更安全,但它不违反严格别名。

【讨论】:

  • 乱序访问联合是未定义的行为。你不是在“绕过”任何东西(而是直接通往坟墓的路线)。
  • @Kerrek SB:您能解释一下“乱序”是什么意思吗?是的,我知道联合方法也是未定义的,但原始代码也是如此。
  • 只允许读取上次写入的同一个联合成员。不过,您的解决方案几乎肯定需要阅读其他成员。没有办法绕过无效的类型双关语;你真的必须从正确类型的变量开始(见我的回答)。
  • @Mystical,应该是:int len = ntohs(record_part.len);
  • @AndreyT - C99 TC3 允许使用联合执行类型双关语是合法的。 (我不能引用它,但请查看stackoverflow.com/questions/7411233/… 以了解一些同意我的人。)
【解决方案3】:

你可以试试这个:

unsigned char buffer[sizeof(record_t)];
record_t rec;
int len;

// code to fill in buffer goes here...

memcpy(&rec, buffer, sizeof(rec));
len = ntohs(rec.len);

【讨论】:

  • 这实际上是正确的方法(尽管不需要强制转换为void *)。将一种类型的数据重新解释为另一种类型的数据的正确方法是使用 memcpy 在对象之间复制该数据(而不是直接基于指针的重新解释或基于联合的重新解释)。
  • 我同意这是正确的(我给了它+1),但最好不要使用unsigned char[] 开头并将数据直接读入正确类型的变量中。
【解决方案4】:

您可能设置了一个警告级别,其中包括严格的别名警告(它曾经不是默认的,但在某一时刻 gcc 翻转了默认)。尝试-Wno-strict-aliasing 或-fno-strict-aliasing -- 那么 gcc 不应该生成警告

一个相当好的解释(基于粗略的一瞥)是What is the strict aliasing rule?

【讨论】:

  • 警告与否,OP 的代码可能是未定义的行为,不应忽略。
  • 我正在考虑是否要 -1 这个。就如何禁用危险 UB 的警告并添加选项以让带有 UB 的代码“按预期”工作提供 OP 建议似乎没有建设性。特别是因为代码还有其他 UB,因为 alignment 问题没有被捕获并且不会被选项解决。它会在 x86 上愉快地工作,然后在其他拱门上崩溃......
  • @R.有许多习语明确使用了这种行为(尤其是在网络代码中),并且很长一段时间以来,gcc 在默认情况下专门禁用了警告,以便网络代码不会产生大量警告:) 鉴于我们正在谈论网络代码(假设基于ntohs 调用),至少提及这一事实是有道理的。
  • 正如我所指出的,由于 gcc 无法解决的原因,此类代码几乎肯定是错误的:未对齐。读入一个 char 缓冲区然后试图将该缓冲区解释为另一种类型是根本错误的,它表明代码的作者不明白如何将一个指向正确类型的实际对象的 void 指针传递给read/ recv(或者在缓冲较大的情况下,如何使用memcpy...)
  • @FooBah:你确定你看到的代码实际上并没有做不同的事情吗?例如,有许多看起来相似的习语实际上是完全合法的。例如,访问不同结构的公共初始元素就可以了。如果该数组是从所需类型的现有变量中转换的,则写入字符数组也很好......这些东西的细节非常重要。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-12-30
  • 2016-12-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-09-22
相关资源
最近更新 更多