执行memcpy(0,0,0)是否保证安全?

95

我对C标准不是很熟悉,请见谅。

我想知道是否有保证,按照标准,memcpy(0,0,0)是安全的。

我找到的唯一限制是如果内存区域重叠,则行为是未定义的...

但是我们可以认为这里的内存区域重叠吗?


8
数学上两个空集的交集是空集。 - Benoit
21
我喜欢这个问题,因为它是一个奇怪的边缘情况,并且我认为memcpy(0,0,0)是我见过的C代码中最奇怪的部分。 - templatetypedef
3
@eq 你真的想知道,还是在暗示没有必要使用它?你有没有考虑实际调用可能是 memcpy(outp, inp, len) 这种情况呢?并且这可能发生在动态分配 outpinp 并且最初为 0 的代码中。例如,在 plen0 时,p = realloc(p, len+n) 可以这样工作。我自己使用过这样的 memcpy 调用 -- 尽管技术上属于未定义行为,但我从未遇到过不是空操作的实现,也永远不会期望遇到。 - Jim Balter
9
@templatetypedef 的意思是memcpy(0, 0, 0)很可能是用于表示动态调用,而不是静态调用...也就是说,这些参数值不需要是字面量。 - Jim Balter
1
与C++不同,C不将null视为指向数组的指针。(这非常奇怪。) - curiousguy
显示剩余5条评论
3个回答

82

我有一份C标准(ISO/IEC 9899:1999)的草稿版本,它对于memcpy函数的调用有一些有趣的说法。就像(§7.21.1/2)中提到的那样:

当参数声明为size_t n时,在函数中n指定数组长度,但在函数调用时,n的值可以为零。除非在本分语言描述中特别说明,否则在这种情况下,对于在该调用中作为指针参数的变量仍然应该有有效值,如7.1.4所述。在这种情况下,查找字符的函数将不会找到字符,比较两个字符序列的函数将返回零,复制字符的函数将复制零个字符。

这里指示的引用指向这里:

如果一个函数的参数具有无效值(例如函数域外的值、程序地址空间外的指针、或者空指针,或者当相应的参数未被const修饰时,指向不可修改的存储器的指针),或者类型(在提升后)不是由带有可变参数的函数所期望的,行为是未定义的

因此,根据C规范,调用

memcpy(0, 0, 0)

使用空指针会导致未定义的行为,因为空指针被认为是“无效值”。

话虽如此,如果你这样做,我会非常惊讶地发现任何实际的memcpy实现会出错,因为我能想到的大多数直观实现方式在复制零字节时根本不会执行任何操作。


4
我可以确定草案标准中引用的部分与最终文件中完全相同。对于这样的调用,不应该出现任何问题,但您仍然依赖于未定义的行为。因此,“是否保证”是“不保证”。 - DevSolar
10
在生产中使用的任何实现都不会对这样的调用产生其他结果,而如果有不同的实现是被允许和合理的...例如,一个带有错误检查并因其非符合性而拒绝调用的C解释器或增强编译器。当然,如果标准允许该调用,就像realloc(0,0)一样,则这将是不合理的。使用情况相似,我已经使用过它们两个(请参见我的评论)。标准将此定义为未定义行为是毫无意义且遗憾的。 - Jim Balter
7
如果你这样做,我会非常惊讶如果任何实际的memcpy实现会破坏。 我曾经使用过一个,事实上如果您传递长度为0的有效指针,则会复制65536个字节。(它的循环递减长度然后测试)。 - M.M
19
那个实现有问题。 - Jim Balter
3
“将“实际的”加上“正确的”,也许会更好。我想我们都有那些不太令人愉快的旧式C语言库的回忆,不确定有多少人希望被提起这些回忆。” - tmyklebu
显示剩余15条评论

28

仅供娱乐

gcc-4.9版本的发布说明指出,其优化器使用了这些规则,例如可以删除以下条件:

int copy (int* dest, int* src, size_t nbytes) {
    memmove (dest, src, nbytes);
    if (src != NULL)
        return *src;
    return 0;
}

当调用copy(0,0,0)时会产生意外的结果(请参见https://gcc.gnu.org/gcc-4.9/porting_to.html),这可能导致一些问题。

对于gcc-4.9的行为,我有些犹豫;该行为可能符合标准,但能够调用memmove(0,0,0)有时是标准的有用扩展。


2
有趣。我理解你的矛盾心理,但这是C语言优化的核心:编译器假设开发人员遵循某些规则,因此推断出一些优化是有效的(如果遵循了这些规则,则确实如此)。 - Matthieu M.
2
@tmyklebu:假设有char *p = 0; int i=something;,即使i为零,表达式(p+i)的求值也会产生未定义行为。 - supercat
1
@tmyklebu:允许在无效指针上进行计算而不被捕获,从中获得的好处微乎其微;这种情况只有在特定于系统的情况下才有任何实际希望工作,并且可以通过将其转换为intptr_t,对生成的值执行适当的计算并进行转换来以实现定义(而不是未定义)的方式。我认为,在C语言中指定p-q的三种情况将会有很大的优势... - supercat
1
具体来说,如果p和q是有效的,并且p不包含在对象q的前s个字节中,则p-q可以是0..s-1范围之外的任何值。如果p和q是同一先前有效对象的一部分,则p-q将具有与对象有效时相同的有效性。如果对象被移动,则给定p = realloc(q);,p-q将保持任意非零值。 - supercat
8
请问有人能告诉我,为什么我没有获得Stack Overflow的“发起了一场火并引发口水战”徽章呢? :-) - user1998586
显示剩余21条评论

1

你也可以考虑在 Git 2.14.x (Q3 2017) 中看到的 memmove 的使用方法。

请查看提交 168e635(2017年7月16日),以及提交 1773664, 提交 f331ab9, 提交 5783980(2017年7月15日)由René Scharfe (rscharfe)提交。
(由Junio C Hamano -- gitster --提交 32f9025中合并,2017年8月11日)

它使用一个helper macro MOVE_ARRAY,该宏基于我们指定的元素数量计算大小,并在该数字为零时支持NULL指针。
带有NULL的原始memmove(3)调用可能会导致编译器(过度地)优化后续的NULL检查。 MOVE_ARRAY 添加了一个安全方便的帮助程序,用于移动可能重叠的数组条目范围。
它推断元素大小,自动且安全地乘以字节大小,通过比较元素大小进行基本类型安全检查,并且与memmove(3)不同,它支持NULL指针,当要移动0个元素时。
#define MOVE_ARRAY(dst, src, n) move_array((dst), (src), (n), sizeof(*(dst)) + \
    BUILD_ASSERT_OR_ZERO(sizeof(*(dst)) == sizeof(*(src))))
static inline void move_array(void *dst, const void *src, size_t n, size_t size)
{
    if (n)
        memmove(dst, src, st_mult(size, n));
}

示例

- memmove(dst, src, (n) * sizeof(*dst));
+ MOVE_ARRAY(dst, src, n);

它使用BUILD_ASSERT_OR_ZERO,作为表达式(其中@cond是必须为真的编译时条件)来断言构建时依赖关系。
如果条件不为真或编译器无法评估,则编译将失败。

#define BUILD_ASSERT_OR_ZERO(cond) \
(sizeof(char [1 - 2*!(cond)]) - 1)

例子:

#define foo_to_char(foo)                \
     ((char *)(foo)                     \
      + BUILD_ASSERT_OR_ZERO(offsetof(struct foo, string) == 0))

5
优化器存在“聪明”和“愚蠢”是反义词的想法,使得测试n成为必要,但在保证memmove(any,any,0)将成为无操作的实现上,更高效的代码通常是可能的。除非编译器可以用memmoveAtLeastOneByte()替换对memmove()的调用,否则为了防范聪明/愚蠢编译器的“优化”,解决方法通常会导致额外的比较,编译器无法消除。 - supercat
注意:在C11中,BUILD_ASSERT_OR_ZERO宏不再必要,因为我们有static_assert()函数。 - undefined
1
@user16217248 很好的观点,谢谢你的反馈。我已经将你的评论包含在答案中,以增加可见性。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接