【问题标题】:Is memcpy(&a + 1, &b + 1, 0) defined in C11?memcpy(a + 1, b + 1, 0) 是在 C11 中定义的吗?
【发布时间】:2014-10-12 23:25:51
【问题描述】:

这个问题跟在previous question 之后,是关于memcpy(0, 0, 0) 的定义性,它已被最终确定为未定义的行为。

如链接问题所示,答案取决于 C11 条款 7.1.4:1 的内容

除非在随后的详细描述中另有明确说明,否则以下每个语句均适用:如果函数的参数具有无效值(例如函数域之外的值,或地址空间之外的指针程序或空指针,[…]) […] 行为未定义。 […]

标准函数memcpy() 需要指向voidconst void 的指针,如下所示:

void *memcpy(void * restrict s1, const void * restrict s2, size_t n);

这个问题完全值得问,因为标准中有两个“有效”指针的概念:有些指针可以通过指针算法有效地获得,并且可以有效地与<、@987654330 进行比较@ 指向同一对象内的其他指针。并且有些指针对取消引用有效。前一个类在后面的 sn-p 中包含诸如 &a + 1&b + 1 之类的“过去”指针,而后一个类不包含这些为有效的指针。

char a;
const char b = '7';
memcpy(&a + 1, &b + 1, 0);

是否应该将上述 sn-p 视为已定义的行为,因为memcpy() 的参数无论如何都被键入为指向void 的指针,因此它们各自的有效性问题不能与取消引用它们有关。还是应该将&a + 1&b + 1 视为“在程序的地址空间之外”?

这对我很重要,因为我正在将标准 C 函数的效果形式化。我已经将memcpy() 的一个前提条件写为requires \valid(s1+(0 .. n-1));,直到有人指出我注意到GCC 4.9 已经开始aggressively 优化此类库函数调用,超出了上面公式中表达的内容(indeed) .当n0 时,此特定规范语言中的公式\valid(s1+(0 .. n-1)) 等价于true,并且不捕获GCC 4.9 依赖于优化的未定义行为。

【问题讨论】:

  • 您的示例(我相信有人会使用适当的“之前询问”链接对此进行标记)调用未定义的行为,因为您正在访问未由您的程序分配或定义的内存。
  • 我相信它不是 UB,因为表达式&a + 1&b + 1 有效(C11 sec 6.5.6)只要因为它们没有被取消引用。但是,如果 memcpy(0, 0, 0) 被认为是 UB,那么这也是 UB。
  • @JohnH:你可能是对的(我倾向于像你一样相信),但你能引用 C11 标准来捍卫你的主张吗? Pascal Cuoq 非常了解 C ....
  • @JohnH 好吧,memcpy 的规范说它复制了n 字符,在我的示例中n0,所以我不确定“访问”是正确的词在这里。
  • @Leushenko:至少在某些不引用基础对象的上下文中,“一个结束”指针是完全有效的。示例:int i = 42; int *p = &i + 1; int *q = p;。在 q 的初始化程序中对 p 的引用是有效且无害的,只要 pq 都没有被取消引用。

标签: c language-lawyer c11 gcc4.9


【解决方案1】:

C11 说:

(C11, 7.24.2.1p2) "memcpy函数将n个字符从s2指向的对象复制到s1指向的对象中。"

&a + 1 本身是一个指向整数加法的有效指针,但 &a + 1 不是指向对象的指针,因此调用会调用未定义的行为。

【讨论】:

  • 另见 C11 7.24.1p2: "... n 在调用该函数时可以将值设为零。除非在描述中另有明确说明如 7.1.4 中所述,此子条款中的特定函数,此类调用上的指针参数仍应具有有效值。” (强调补充)。如果您愿意,请随时将此添加到您的答案中。
  • 我不相信。这里的指针不是无效值,因此导致memcpy(0, 0, 0) 的推理不适用。引用的文本并没有说 s2 必须指向一个对象; “s2 指向”是一个谓词,用于描述从何处获取字符,但如果没有获取字符,则不会读取不包含值的内存。
  • @chux:嗯。现在我实际上阅读了 7.1.4,我不太确定。 “如果一个函数有一个无效的值(比如 ...)”; “例如”意味着它并不详尽。 “如果函数参数被描述为数组,则实际传递给函数的指针应具有一个值,使得所有地址计算和对对象的访问(如果指针确实指向此类数组的第一个元素,这将是有效的) ) 实际上是有效的。” -- 但是使用n == 0,不会访问任何元素。但是我们仍然可以回退到“指向对象”的措辞(在这个答案中引用)来推断 UB。
  • @chux:另一方面,鉴于memcpy(&a + 1, &b + 1, 0),我想不出有什么好的理由让一个实现行为不端(这里的“行为不端”我的意思是把它当作不是-op)。
  • @MattMcNabb 我之前想为一个不相关的 C 标准问题(惊喜,惊喜)提交 DR,但不清楚你是否可以在不成为某种国家组织的付费会员的情况下做到这一点.我想我会继续以 SO 问题的形式提交我的 DR,并希望有适当权限的人能够接受它们。
【解决方案2】:

虽然根据标准的“正确”答案似乎不同意,但我发现在 int a[6]; 之后只是虚伪的。诠释 b[6];全部

memcpy(a+0, b+0, 6);
memcpy(a+1, b+1, 5);
memcpy(a+2, b+2, 4);
memcpy(a+3, b+3, 3);
memcpy(a+4, b+4, 2);
memcpy(a+5, b+5, 1);

应该是有效的(并复制一个以数组末尾结尾的区域)而

memcpy(a+6, b+6, 0);

根据计数有效,但根据地址无效。这是复制区域的同一端!

就个人而言,我倾向于定义 memcpy(0,0,0) 也是有效的(理由是只要求有效的指针但没有对象)但至少这是一个奇异的情况,而“数组的结尾” case 是复制数组末尾区域的常规模式的实际例外。

【讨论】:

  • a+6 可能导致 UB 的原因是函数需要指向内存的“有效”指针,并且可能会在检查 n==0 之前对它们进行指针数学运算。当添加 +1 时,应该允许指针有一个有效的结果。众所周知,&a[0]&a[5] 是指向内存的有效指针,它们中的每一个都可以具有 +1,从而导致用于指针数学目的的有效指针。 a+6 产生一个有效的指针数学指针,但 a+6+1memcpy() 可能会这样做)似乎把事情推得太远了。尽管memcpy()需要 进行该计算听起来很愚蠢,但它必须被允许。为您的想法 +1。
  • 明智地处理极限情况很重要。
  • @chux: a+6+1 不好,但 a+1+6 也不好,这并不会使这个答案的第二行无效。 memcpy 不应计算超出指定范围的指针。
  • @Ben Voigt 同意 100% memcpy(s1=a+6,..., n=0) 不应该需要计算 s1+1。但是memcpy() 期望接收s1 作为指向对象的有效指针(C11dr §7.24.2.1)并且不是每个指向对象的有效指针 + 1 也对指针数学有效? IMO memcpy(any_value, any_value, 0) 根据规范无效,但规范应更改以允许它。
  • @chux:在某些系统上,实现void memcpy(void const *src, void *dest, size_t length) { char const*s = (char*)src; char *d = (char*)dest; char const *e=s+length; while(s!=e) {*d++ = *s++;} 可能是合理的。这样的实现会导致带有memcpy(0,0,0) 的UB,但在给出“过去”指针时会很好。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-11-16
  • 2019-08-07
  • 1970-01-01
  • 2011-10-21
  • 1970-01-01
相关资源
最近更新 更多