调用 free() 包装器:取消引用类型双关指针将破坏严格别名规则
Posted
技术标签:
【中文标题】调用 free() 包装器:取消引用类型双关指针将破坏严格别名规则【英文标题】:Calling a free() wrapper: dereferencing type-punned pointer will break strict-aliasing rules 【发布时间】:2016-07-25 13:53:01 【问题描述】:我已尝试阅读此处有关 SO 的其他具有类似标题的问题,但它们都有点过于复杂,我无法将解决方案(甚至解释)应用于我自己的问题,这似乎具有更简单的性质。
在我的例子中,我有一个围绕 free()
的包装器,它在释放它之后将指针设置为 NULL
:
void myfree(void **ptr)
free(*ptr);
*ptr = NULL;
在我正在做的项目中,它是这样调用的:
myfree((void **)&a);
如果我将优化级别提高到 -O3
并添加 -Wall
(否则不会)。
以下列方式调用 myfree()
不会使编译器发出该警告:
myfree((void *)&a);
所以我想知道我们是否应该将调用 myfree()
的方式改为这样。
我相信我通过调用myfree()
的第一种方式调用了未定义的行为,但我无法理解为什么。此外,在我有权访问的所有编译器(clang
和 gcc
)上,在所有系统(OpenBSD、Mac OS X 和 Linux)上,这是唯一真正给我警告的编译器和系统(我知道发出警告是一个不错的选择)。
在调用 myfree()
之前、内部和之后打印指针的值,两种调用方式都会给我相同的结果(但如果它是未定义的行为,那可能没有任何意义):
#include <stdio.h>
#include <stdlib.h>
void myfree(void **ptr)
printf("(in myfree) ptr = %p\n", *ptr);
free(*ptr);
*ptr = NULL;
int main(void)
int *a, *b;
a = malloc(100 * sizeof *a);
b = malloc(100 * sizeof *b);
printf("(before myfree) a = %p\n", (void *)a);
printf("(before myfree) b = %p\n", (void *)b);
myfree((void **)&a); /* line 21 */
myfree((void *)&b);
printf("(after myfree) a = %p\n", (void *)a);
printf("(after myfree) b = %p\n", (void *)b);
return EXIT_SUCCESS;
编译并运行它:
$ cc -O3 -Wall free-test.c
free-test.c: In function 'main':
free-test.c:21: warning: dereferencing type-punned pointer will break strict-aliasing rules
$ ./a.out
(before myfree) a = 0x15f8fcf1d600
(before myfree) b = 0x15f876b27200
(in myfree) ptr = 0x15f8fcf1d600
(in myfree) ptr = 0x15f876b27200
(after myfree) a = 0x0
(after myfree) b = 0x0
我想了解第一次调用myfree()
有什么问题,我想知道第二次调用是否正确。谢谢。
【问题讨论】:
我不建议在释放后总是将指针设置为 NULL,因为你不会看到双重释放,(这不应该发生) @mou 嗯,这是一个有效的观点。反驳的论点是不将其设置为NULL
可能会隐藏对已释放内存的访问。
是的,但是 valgrind & stuff 应该告诉我们,因为我们正在谈论堆内存
@mou 调用free(NULL)
是无害的,取消引用NULL
则不然。因此,我相信原因是(这是在一个已经运行了多年的协作项目中)最好被告知引用释放的内存而不是捕获双重free()
调用。我一直在考虑使用 Boehm GC 进行内存泄漏检测和/或 Valgrind,但由于我是该项目的新手,所以还没有开始这样做。
【参考方案1】:
由于a
是int*
而不是void*
,因此&a
不能转换为指向void*
的指针。 (假设 void*
比指向整数的指针更宽,这是 C 标准所允许的。)因此,您的选择——myfree((void**)a)
和 myfree((void*)a)
——都不正确。 (转换为 void*
不是严格的别名问题。但它仍然会导致未定义的行为。)
更好的解决方案(恕我直言)是强制用户插入可见的分配:
void* myfree(void* p)
free(p);
return 0;
a = myfree(a);
使用clang和gcc,你可以使用一个属性来表示必须使用my_free
的返回值,这样如果你忘记赋值,编译器就会警告你。或者你可以使用宏:
#define myfree(a) (a = myfree(a))
【讨论】:
您的第一句话是否意味着 both 调用不正确(由于ptr
属于 void **
类型)?
@kusalananda:是的。我应该将其添加到答案中。
我们试图尽可能地保持 C99,因此使用编译器特定的属性将是一个禁忌。需要考虑一下并与合作者讨论。
@kusalanada:属性的常用方法是在不支持它的实现上#define away __attribute__
。 (它不会改变程序的行为;它只是一个警告。)我一直在避免这样说,但我个人不喜欢你试图实现的习惯用法,尤其是当被释放的指针本身位于即将释放的内存块中时被释放。如果要调试 double-free 或 use-after-free,请使用 valgrind。但是很多人不同意我的观点。
我非常感谢 cmets。这是仍在开发中的旧代码(一个生物信息学应用程序),我正在清理它的一部分。不是我试图实现某些东西,而是我试图让它正确。【参考方案2】:
这里有一个建议:
-
不违反严格的别名规则。
让通话更自然。
void* myfree(void *ptr)
free(ptr);
return NULL;
#define MYFREE(ptr) ptr = myfree(ptr);
您可以将宏简单地用作:
int* a = malloc(sizeof(int)*10);
...
MYFREE(a);
【讨论】:
【参考方案3】:基本上有几种方法可以让函数以与指针的目标类型无关的方式使用和修改指针:
将指针作为 void* 传递给函数并将其作为 void* 返回,在调用站点在两个方向上应用适当的转换。这种方法的缺点是绑定了函数的返回值,无法将其用于其他目的,并且还排除了在锁内执行指针更新的可能性。
将一个指针传递给接受两个 void* 的函数,将其中一个转换为适当类型的指针,另一个转换为该类型的双间接指针,并且可能还有第二个函数可以读取作为 void* 传入的指针,并使用这些函数来读取和写入有问题的指针。这应该是 100% 可移植的,但可能效率很低。
在别处使用指针变量和 void* 类型的字段,并在实际使用时将它们转换为真正的指针类型,从而允许使用 void** 类型的指针来修改指针变量。
使用memcpy
读取或修改未知类型的指针,给定识别它们的双间接指针。
记录代码是用 1990 年代流行的 C 方言编写的,它将“void**”视为指向任何类型的双间接指针,并使用支持该方言的编译器和/或设置. C 标准允许实现对指向不同类型事物的指针使用不同的表示,并且因为这些实现不支持通用的双间接指针类型,并且因为可以很容易地允许 void**
以这种方式使用的实现已经在编写标准之前这样做了,因此标准没有必要描述该行为。
拥有通用双间接指针类型的能力在 90% 以上可以(并且确实)支持它的实现中非常有用,标准的作者当然知道这一点,但作者是对描述明智的编译器编写者无论如何都会支持的行为的兴趣远不如对强制执行总体上有益的行为即使在无法廉价支持的平台上感兴趣(例如,即使在平台上也强制执行)其无符号数学指令包装 mod 65535,编译器必须生成任何代码来进行计算包装 mod 65536)。我不确定为什么现代编译器作者无法认识到这一点。
也许,如果程序员开始公开为正常的 C 方言编写代码,标准的维护者可能会认识到这些方言是有价值的。 [请注意,从别名的角度来看,将void**
视为通用双间接指针将比强制程序员使用上述任何替代方案 2-4 的性能成本要低得多;编译器作者声称将void**
视为通用双间接指针会降低性能,因此应持怀疑态度]。
【讨论】:
以上是关于调用 free() 包装器:取消引用类型双关指针将破坏严格别名规则的主要内容,如果未能解决你的问题,请参考以下文章
GCC 7,aligned_storage 和“取消引用类型双关指针将破坏严格别名规则”