【问题标题】:Strict aliasing and casting union pointers严格的别名和强制转换联合指针
【发布时间】:2019-12-18 13:00:02
【问题描述】:

我环顾了这个网站,试图弄清楚我对不同联合的强制转换是否违反了严格的别名或其他 UB。

我有数据包通过串行线路进入,我存储/获取它们:

union uart_data {
  struct {
    uint8_t start;
    uint8_t addr;
    uin16_t length;
    uint8_t data[];
  };
  uint8_t bytes[BUFFER_SIZE];
};

void store_byte(uint8_t byte) {
  uart_data->start = byte;
  /* and so on with the other named fields. */
}

uint8_t * get_buffer() {
  return uart_data->bytes;
}

我的理解是,至少对于 GCC 和 GNU 扩展来说,这是一种进行类型双关的有效方法。

但是,然后我想将返回值从 get_buffer() 转换为 uart 不需要了解详细信息的更具体类型的数据包。

union spec_pkt {
  struct {
    uint8_t start;
    uint8_t addr;
    uin16_t length;
    uint8_t command;
    uint8_t some_field;
    uint16_t data_length;
    uint8_t data[];
  };
  uint8_t bytes[BUFFER_SIZE];
};

void process(uint8_t *data) {
  union specific_pkt *pkt = (union specific_pkt *)data;
}

我记得在某处读到这是有效的,因为我正在从联合中存在的类型进行强制转换,但我找不到源。

我这样做的理由是我可以拥有一个只需要了解最低级别细节的 uart 驱动程序。我在 MCU 上,所以我只能访问预先分配的数据缓冲区,这样我就不必在缓冲区之间使用memcpy,浪费空间。在我的应用程序代码中,我可以以更好的方式处理数据包:

uint8_t data[BUFFER_SIZE];

data[START_POS];
data[LEN_POS];
data[DATA_POS];

如果这违反了 SA 规则或者是 UB,我希望有一些替代方案来达到同样的效果。

我在支持非对齐访问的目标上使用 GCC,并且 GCC 允许通过联合进行类型双关。

【问题讨论】:

  • 结构中的填充不完全取决于编译器吗?这意味着不同的构建可能会以不同的方式填充bytes
  • 为什么不将您需要的所有可能的数据突变添加为联合中的不同结构?
  • 如果您担心严格的别名,您可以随时使用-fno-strict-aliasing进行编译
  • 因为这是针对嵌入式系统的,所以是的,总是用-fno-strict-aliasing -ffreestanding 编译。 gcc 在设计时并没有真正考虑到嵌入式系统。
  • @JensGustedt:很多混淆源于这样一个事实,即该标准使用相同的术语来表示在某些情况下应该预期行为可预测的行为,但不是全部,基于实现特定的因素,如对于那些在任何情况下通常不应该表现出可预测性的人。出于某种原因,人们似乎认为标准使用术语“实施定义”来表示前者,但事实并非如此;实现定义保留用于所有实现应该总是可预测地处理的操作。

标签: c gcc casting strict-aliasing type-punning


【解决方案1】:

标准完全没有指定结构或联合对象可以通过类型不是结构或联合的非字符左值访问的情况。如果一个人认识到标准的目的是纯粹表明编译器何时必须认识到一个对象正在被一个看似无关的左值访问,但并不意味着适用于编译器将能够看到一种类型的左值或指针用于派生另一种类型,然后用于访问与第一种类型关联的存储,而无需对该存储进行任何干预冲突操作,这种省略是有意义的。例如,给定:

struct sizedPointer { int length,size; int *dat; };
void storeThing(struct sizedPointer *dest, int n)
{
  if (dest->length < dest->size)
  {
    dest->dat[dest->length] = n;
    dest->length++;
  }
}

这样的解释将允许编译器假设dest-&gt;length 不会使用dest-&gt;dat 编写,因为它的值在dest-&gt;dat 形成之后已被观察到,但需要编译器识别给定的:

union blob { uint16_t hh[8]; uint64_t oo[2]; } myBblob;

类似的操作

sscanf(someString, "%4x", &myBlob.hh[1]);

在函数返回后可能与从myBlob 派生的任何左值交互。

不幸的是,gcc 和 clang 将规则解释为仅在不这样做会完全破坏语言的情况下强制识别。因为标准没有强制要求成员类型的左值可以以任何方式使用,并且 gcc 和 clang 已经明确声明不应该依赖它们来做超出标准要求的任何事情,所以对任何有用的东西的支持应该被视为任由 clang 和 gcc 的维护者随心所欲。

【讨论】:

  • 那么结论是什么? OP的代码是否违反规则?还是规则太模糊以至于无法确定?
  • @th33lf:这些规则旨在说明何时必须假定不同类型的左值能够别名,尽管缺乏直接证据表明它们这样做。他们没有说明编译器何时应该识别两个左值访问相同存储的直接证据,因为他们认识到不同的编译器应该根据其设计和预期目的使用稍微不同的规则。例如,不进行优化的编译器不需要将任何访问识别为可能会影响同一存储,一个只执行过程内的...
  • ...优化应该能够只关注当前函数,一个进行过程内优化的函数应该能够识别被优化的函数之间分割的证据,等等。不幸的是,作者的 C89 故意试图避免任何关于编译器应该处理不可移植代码的声明,并且这种避免被解释为一种判断,即编译器不应该努力有效地处理不可移植代码,尽管作者' 在已发布的基本原理中明确声明相反。
  • 在OP的例子中,在函数process()中,只涉及到两个指针,一个是struct,另一个是char。根据标准,编译器不能假设它们没有别名。那么,不考虑别名破坏的优化范围在哪里?另外,可以在线获得基本原理文件吗?事实证明,很难找到 C11!
  • @th33lf:我还没有看到 C11 的基本原理文档,但是 N1570 6.5p6 和 6.5p7 与 C99 中的相应部分相同,其已发布的基本原理可用。该标准通常不承认函数边界,甚至编译单元边界,作为优化障碍,所以给定类似T1 *p1, T2 *p2;... f1(p1); f2(p2); f3(p1); 的东西,编译器可能合理地假设在f3() 中执行的任何加载都将检索到与f1 中相同的值.问题是 gcc 不会努力注意到调用之间的任何操作,这些操作会派生 p1p2 来自...
猜你喜欢
  • 1970-01-01
  • 2023-02-02
  • 2018-09-03
  • 2011-02-23
  • 1970-01-01
  • 2016-11-21
  • 2014-07-13
  • 2017-06-29
相关资源
最近更新 更多