如何推理类似 malloc 的函数的严格别名

Dan*_*und 5 c strict-aliasing

AFAIK,在三种情况下可以使用别名

  1. 仅限定符或符号不同的类型可以相互别名。
  2. 结构或联合类型可以为其中包含的类型指定别名。
  3. 将 T* 转换为 char* 就可以了。(不允许相反)

在阅读John Regehrs 博客文章中的简单示例时,这些是有意义的,但我不确定如何推理较大示例(例如类似 malloc 的内存安排)的别名正确性。

我正在阅读Per Vognsens 重新实现Sean Barrets弹性缓冲区。它使用类似 malloc 的模式,其中缓冲区在其前面具有关联的元数据。

typedef struct BufHdr {
    size_t len;
    size_t cap;
    char buf[];
} BufHdr;
Run Code Online (Sandbox Code Playgroud)

通过从指针减去偏移量来访问元数据b

#define buf__hdr(b) ((BufHdr *)((char *)(b) - offsetof(BufHdr, buf)))
Run Code Online (Sandbox Code Playgroud)

这是原始buf__grow函数的一个稍微简化的版本,它扩展了缓冲区并将 buf 作为void*.

void *buf__grow(const void *buf, size_t new_size) { 
     // ...  
     BufHdr *new_hdr;  // (1)
     if (buf) {
         new_hdr = xrealloc(buf__hdr(buf), new_size);
     } else {
         new_hdr = xmalloc(new_size);
         new_hdr->len = 0;
     }
     new_hdr->cap = new_cap;
     return new_hdr->buf;
}
Run Code Online (Sandbox Code Playgroud)

用法示例(buf__grow隐藏在宏后面,但为了清楚起见,这里将其公开):

int *ip = NULL;
ip = buf__grow(ip, 16);
ip = buf__grow(ip, 32);
Run Code Online (Sandbox Code Playgroud)

在这些调用之后,我们在堆上有 32 + sizeof(BufHdr) 字节的大内存区域。我们已经ip指向该区域,并且在执行过程中的各个点都指向了该区域new_hdrbuf__hdr

问题

这里是否存在严格别名违规?AFAICT,ip某些类型的变量BufHdr不应该被允许指向相同的内存。

或者说,buf__hdr不创建左值意味着它没有为相同的内存设置别名ip?事实上,其中new_hdr包含的不是“实时”的内容意味着它们也不是别名?buf__growip

如果new_hdr在全球范围内,这会改变事情吗?

C 编译器跟踪存储类型还是仅跟踪变量类型?如果有存储,比如分配的内存区域buf__grow没有任何变量指向它,那么该存储的类型是什么?只要没有与该内存关联的变量,我们就可以自由地重新解释该存储吗?

Lun*_*din 2

\n

这里是否存在严格别名违规?AFAICT、ip 和某些 BufHdr 类型的变量不应被允许指向同一内存。

\n
\n\n

需要记住的重要一点是,仅当您对内存位置进行值访问时,才会发生严格的别名冲突,并且编译器认为该内存位置存储的内容属于不同类型。因此,谈论指针的类型并不重要,重要的是谈论它们所指向的任何内容的有效类型。

\n\n

分配的内存块没有声明类型。适用的是 C11 6.5/6:

\n\n
\n

访问其存储值的对象的有效类型是该对象的声明类型(如果有)。87)

\n
\n\n

其中注释 87 澄清分配的对象没有声明类型。这里就是这样,所以我们继续看有效类型的定义:

\n\n
\n

如果通过类型不是字符类型的左值将值存储到没有声明类型的对象中,则左值的类型将成为该访问和后续访问的对象的有效类型不要修改存储的值。

\n
\n\n

这意味着一旦我们访问分配的内存块,存储在那里的任何内容的有效类型就变成我们存储在那里的任何内容的类型。

\n\n

在您的情况下,第一次访问发生在行 和new_hdr->len = 0;new_hdr->cap = new_cap;使这些地址处的数据的有效类型size_t

\n\n

buf仍然未被访问,因此该部分内存还没有有效的类型。您返回new_hdr->buf并设置int*指向那里。

\n\n
\n\n

我认为接下来会发生的事情是buf__hdr(ip)。在该宏中,指针被转换为(char *),然后发生一些指针减法:

\n\n
(b) - offsetof(BufHdr, buf) // undefined behavior\n
Run Code Online (Sandbox Code Playgroud)\n\n

在这里,我们正式得到了未定义的行为,但其原因与严格别名完全不同。b不是指向与之前存储的数组相同的指针b。相关部分是加法算子的规范 6.5.6:

\n\n
\n

对于减法,应满足以下条件之一:
\n \xe2\x80\x94 两个操作数都具有算术类型;
\n \xe2\x80\x94 两个操作数都是指向兼容完整对象类型的限定或非限定版本的指针;或
\n \xe2\x80\x94 左操作数是指向完整对象类型的指针,右操作数具有整数类型。

\n
\n\n

前两个显然不适用。在第三种情况下,我们不指向完整的对象类型,如buf还没有获得有效的类型。据我了解,这意味着我们违反了约束,我在这里并不完全确定。然而,我非常确定违反了以下规定,6.5.6/9:

\n\n
\n

当两个指针相减时,两个指针都应指向同一个数组对象的元素,或者指向数组对象最后一个元素之后的一个;结果是两个数组元素的下标之差。结果的大小是实现定义的,\n 其类型(有符号整数类型)是在<stddef.h>。\n 如果结果不能用该类型的对象表示,则行为未定义

\n
\n\n

所以这绝对是一个错误。

\n\n
\n\n

如果我们忽略该部分,则实际访问(BufHdr *)是好的,因为BufHdr它是一个包含所访问对象的有效类型(2x)的结构(“聚合”)size_tbuf这里第一次访问 的内存,得到有效类型char[],得到有效类型(灵活数组成员)。

\n\n

不存在严格的别名冲突,除非您在调用上述宏后ipint.

\n\n
\n\n
\n

如果 new_hdr 在全局范围内,这会改变事情吗?

\n
\n\n

不,指针类型并不重要,重要的是所指向对象的有效类型。

\n\n
\n

C 编译器跟踪存储类型还是仅跟踪变量类型?

\n
\n\n

如果它希望像 gcc 那样进行优化,则需要跟踪对象的有效类型,假设严格的别名冲突永远不会发生。

\n\n
\n

只要没有与该内存关联的变量,我们就可以自由地重新解释该存储吗?

\n
\n\n

是的,您可以使用任何类型的指针指向它 - 因为它已分配内存,所以在您进行值访问之前它不会获得有效类型。

\n