【问题标题】:Strict aliasing seems inconsistant严格的别名似乎不一致
【发布时间】:2013-07-18 11:28:06
【问题描述】:

有几个来自严格别名的错误,所以我想我会尝试修复所有这些错误。仔细研究了它是什么,有时 GCC 似乎不会发出警告,而且有些事情是不可能实现的。至少根据我的理解,下面的每一个都被打破了。那么我的理解是错误的,是否有正确的方法来做所有这些事情,或者某些代码只需要在技术上打破规则并被系统测试很好地覆盖?

错误来自一些混合了 char 和 unsigned char 缓冲区的代码,例如如下:

size_t Process(char *buf, char *end)
{
    char *p = buf;
    ProcessSome((unsigned char**)&p, (unsigned char*)end);
    //GCC decided p could not be changed by ProcessSome and so always returned 0
    return (size_t)(p - buf);
}

将其更改为以下似乎可以解决问题,尽管它仍然涉及强制转换,所以我不确定为什么它现在有效并且没有警告:

size_t Process(char *buf, char *end)
{
    unsigned char *buf2 = (unsigned char *)buf;
    unsigned char *p = buf2;
    unsigned char *end2 = (unsigned char*)end;
    ProcessSome(&p, end2);
    return (size_t)(p - buf2);
}

还有很多其他地方似乎在没有警告的情况下工作

//contains a unsigned char* of data. Possibly from the network, disk, etc.
//the buffer contents itself is 8 byte aligned.
const Buffer *buffer = foo();
const uint16_t *utf16Text = (const uint16_t*)buffer->GetData();//const unsigned char*
//... read utf16Text. Does not even seem to ever be a warning


//also seems to work fine
size_t len = CalculateWorstCaseLength(...);
Buffer *buffer = new Buffer(len * 2);
uint16_t *utf16 = (uint16_t*)buffer->GetData();//unsigned char*
len = DoSomeProcessing(utf16, len, ...);
buffer->Truncate(len * 2);
send(buffer);

还有一些...

struct Hash128
{
    unsigned char data[16];
};
...
size_t operator ()(const Hash128 &hash)
{
    return *(size_t*)hash.data;//warning
}

非字符大小写。这没有警告,即使它很糟糕,我该如何避免它(两种方法似乎都有效)?

int *x = fromsomewhere();//aligned to 16 bytes, array of 4
__m128i xmm = _mm_load_si128((__m128*i)x);
__m128i xmm2 = *(__m128i*)x;

查看其他 API 似乎也有各种情况,据我了解,这些情况违反了规则(没有遇到 Linux/GCC 特定的情况,但肯定会在某个地方出现)。

  1. CoCreateInstance 有一个需要显式指针转换的 void** 输出参数。 Direct3D 也有类似的。

  2. LARGE_INTEGER 是一个联合体,可能会对不同的成员进行读/写(例如,某些代码可能使用高/低,然后其他一些可能会读取 int64)。

  3. 我记得 CPython 实现非常高兴地将 PyObject* 投射到一堆其他的东西上,这些东西在开始时恰好具有相同的内存布局。

  4. 我见过的很多哈希实现会将输入缓冲区转换为 uint32_t*,然后可能使用 uint8_t 来处理最后的 1-3 个字节。

  5. 我见过的几乎所有内存分配器实现都使用 char* 或 unsigned char*,然后必须将其强制转换为所需的类型(可能通过返回的 void*,但在分配内部至少它是字符)

【问题讨论】:

  • 您的第一个示例是否真的表现出总是返回零的行为,或者它只是类似于这样做的代码?我无法重现这种行为。
  • 差不多就是这样,只是函数名称不同。这是针对 x64 的经过修改的企业红帽上的 gcc 4.4.5。然而,所有内容都被内联了,因此它可能非常具体地说明 GCC 决定如何优化整体。

标签: c++ gcc strict-aliasing


【解决方案1】:

首先,指向charunsigned char 的指针差不多 免除关于字符串别名的规则;你被允许 将任何类型的指针转​​换为char*unsigned char*,并将指向的对象视为char 的数组 或unsigned char。现在,关于您的代码:

size_t Process(char *buf, char *end)
{
    char *p = buf;
    ProcessSome((unsigned char**)&p, (unsigned char*)end);
    //GCC decided p could not be changed by ProcessSome and so always returned 0
    return (size_t)(p - buf);
}

这里的问题是你试图看char* 好像 这是一个unsigned char*。这是保证的。给定 演员表清晰可见,g ++有点迟钝 关于不关闭严格的混叠分析 自动,但从技术上讲,它已包含在标准中。

size_t Process(char *buf, char *end)
{
    unsigned char *buf2 = (unsigned char *)buf;
    unsigned char *p = buf2;
    unsigned char *end2 = (unsigned char*)end;
    ProcessSome(&p, end2);
    return (size_t)(p - buf2);
}

另一方面,所有转换都涉及char*unsigned char*,两者都可以别名任何东西,所以 需要编译器才能完成这项工作。

关于其余的,你没有说返回类型是什么 buffer->GetData() 是,所以很难说。但如果是 char*unsigned char*void*,代码完全合法 (除了在第二次使用时缺少演员表 buffer->GetData())。只要所有演员都涉及 一个char*、一个unsigned char* 或一个void*(忽略const 限定符),那么编译器需要假设有 是一个可能的别名:当原始指针有一个 这些类型,它可以通过从 指向目标类型的指针,并且语言保证 您可以将任何指针转换为其中一种类型,然后返回 原始类型,并恢复相同的值。 (当然,如果 char* 最初不是 uint16_t,您最终可能会得到 对齐问题,但编译器通常无法知道这一点。)

关于最后一个例子,你没有指明类型 hash.data,所以不好说;如果是char*void*unsigned char*,语言保证你的代码 (从技术上讲,假设 char 指针是由 转换size_t*;在实践中,只要 指针充分对齐并且指向的字节没有 为size_t 形成一个陷印值。

一般来说:“类型双关语”唯一真正有保证的方式是 通过memcpy。否则,指针强制转换,例如你 做,只要是到void*或从void*得到保证, char*unsigned char*,至少就别名而言 担心的。 (其中之一可能导致对齐 问题,或者在取消引用时访问陷阱值。)

请注意,您可能会从其他 标准。 Posix 需要类似的东西:

void (*pf)();
*((void**)&pf) = ...

例如工作。 (通常,强制转换和取消引用 立即可以工作,即使使用 g++,if 你什么也不做 else 在可能与别名相关的函数中。)

我知道的所有编译器都允许使用union 类型双关语,有时。 (至少有一些,包括 g++,在其他情况下会因union 的合法使用而失败。 正确处理 union 对于编译器编写者来说很棘手 如果union 不可见。)

【讨论】:

  • 我总是对能够跟踪别名规则的人感到敬畏。 +1 :)
  • 缓冲区总是包含一个无符号字符数组。 GetData 只返回 (const) unsigned char*。哈希只是一个带有无符号字符数据的结构[16]。更新了代码示例。 CPython 的事情呢,我记得套接字地址也与 sockaddr 做类似的事情。
  • @VaughnCato unsigned char**char** 类型是不相关的类型。规则与int*double* 的规则大致相同。
  • 这是否意味着unsigned char **p2 = (unsigned char **)&p; char **p3 = (char **)p2; ++*p3; 是UB? (其中pchar *,如第一个示例所示)
  • 我认为这与对齐要求有关。例如,如 N3242、5.2.10p7 中所述。当然,char *unsigned char *(作为对象)具有相同的对齐要求。
【解决方案2】:

char/unsigned char 指针不受严格的别名规则的约束。

联合技巧在技术上是一个别名错误,但主流编译器仍然明确允许它。

因此,您的一些示例是有效的(根据语言,有些是 UB,但由编译器明确定义)。

但是是的,有很多代码违反了别名规则。另请注意,MSVC 不会基于严格别名进行优化,因此特别是为 Windows 编写的代码可能容易违反严格的别名规则。

【讨论】:

  • 所以如果 char/unsigned char 总是被豁免的(我认为这只是 T* 到 char* 的转换,而不是其他方式),那么我的 128->size_t 哈希转换和原始流程(GCC 在发布版本中实际上中断了)?还有哪些真的是UB?
  • 哎呀,好问题。我无法背诵别名规则。正如你所知道的,它们非常复杂。 :) 对不起。如果我记得,我稍后会尝试查找它
  • 值得指出的是,GCC 的严格别名警告 不一致。正确检测所有混叠违规在计算上是不可行的。因此,GCC 会就它能够检测到的情况向您发出警告,但没有收到任何别名警告并不意味着您的代码中不存在别名违规。
  • @WillNewbery To/from char*unsigned char*void* 保证保值:您可以将指针转换为 char*,然后将其转换回原来的值类型,并且保证您获得相同的值。如果编译器看不到原始转换,而只能看到char*,它必须假设char* 可能来自这样的转换。
  • @WillNewbery 在您的第一个示例中,您不是转换为char*,而是从char** 转换为unsigned char**。没有保证。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-10-15
  • 1970-01-01
  • 2020-05-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多