将 uint8_t* 转换为 uint32_t* 或 uint64_t* 是否在 C99 中定义明确,只要我们确定不创建别名?

Is casting uint8_t* to uint32_t*, or uint64_t* well-defined in C99 as long as we are sure we do not create aliases?

考虑一个 C99 程序,该程序从通过链接器文件链接到程序二进制文件的只读二进制 blob 中读取。程序知道 blob 在内存中的起始位置,但在编译期间不知道其布局。 blob 由无符号的 32 位和 64 位整数组成。我们注意确保它们的字节序对应于所用平台上的(数据)字节序。我们还注意将 blob 放入内存中,使其 4B 对齐。

要求:

  1. (performance) 我们希望根据各个平台的可能性,用最少的指令读取 32 位和 64 位整数(例如,在适用的情况下使用单一加载指令)

    • 我们不想逐字节读取值,然后使用移位和添加来重构 4B/8B 整数。
  2. (可移植性) 该程序必须在 ARM、x86_64 和 MIPS 架构上 运行。还有一些架构有 32 位系统总线,其他有 64 位总线。

    • 我们不希望使用内联汇编代码为每个体系结构维护特定于架构的改编。
    • 我们不想对使用的工具链做出假设,例如我们不想使用 -fno-strict-aliasing 和类似的东西。

看起来,这可以通过类型双关来完成。我们知道内存中的哪个位置是我们想要读取的值,我们可以将指针从原始 (unsigned char*) 转换为 uint32_t*uint64_t*.

之一

但是 C99 严格的别名规则让我很困惑。

不会有别名,我们可以肯定 - 我们不会将同一内存位置双关到两种不同的类型,而不是 unsigned char。二进制 blob 的布局不允许这样做。

问题:

正在将 const uint8_t* 转换为 const uint32_t*,或 const uint64_t* 在 C99 中定义明确,只要我们确定我们不会为相同的指针设置别名const uint32_t*const uint64_t*?

Is casting a const uint8_t* to const uint32_t*, or const uint64_t* well-defined in C99, as long as we are sure we do not alias the same pointers to both const uint32_t* and const uint64_t*?

一般来说,不需要,因为 const uint32_t*const uint64_t* 的对齐需求可能会超过 const uint8_t*

在 OP 的情况下,可能没问题。代码的描述不如真正的代码确定。

严格的别名规则是有效的(双关语(第二个双关语也是)) 6.5p6 and 6.5p7.

如果您通读声明的字符缓冲区,例如:

char buf[4096];
//...
read(fd, buf, sizeof(buf);
//...

*(uint32_t*)(buf+position)那么你肯定违反了

6.5p7

An object shall have its stored value accessed only by an lvalue expression that has one of the following types:

  • a type compatible with the effective type of the object,

如果你对缓冲区进行 mmap 或 malloc(使内存动态类型化),那么它会更复杂,但无论如何,standard-compliant 读取这样一个 uint32_t--through memcpy--在任何一种情况下都有效,并且通常不会带来性能损失,因为优化编译器会识别 memcpy 调用并对其进行特殊处理。

示例:

#include <stdint.h>
#include <string.h>

uint32_t get32_noalias(void const *P) 
{
     return *(uint32_t*)(P);
}


static inline uint32_t get32_inl(void const *P) 
{ 
    uint32_t const*p32 = P; 
    //^optional (might not affect codegen)
    //to assert that P is well-aligned for uint32_t
    uint32_t x; memcpy(&x,p32,sizeof(x)); 
    return x; 
}

//should generate same code as get32_noalias
//but without violating 6.5p7 when P points to a char[] buffer
uint32_t get32(void const *P) 
{ 
    return get32_inl(P);
}

https://gcc.godbolt.org/z/sGf4rf

在 x86-64 上生成的程序集:

get32_noalias:                          # @get32_noalias
        movl    (%rdi), %eax
        retq

get32:                                  # @get32
        movl    (%rdi), %eax
        retq

虽然 *(uint32_t*)p 在实践中可能不会在您的情况下爆炸(如果您只进行只读访问或与 char-based 交织在一起的只读访问,就像 read 所做的那样系统调用,那么它“实际上”不应该崩溃),我看不出有什么理由避免使用 fully-standard 兼容的基于 memcpy 的解决方案。