将数组强制转换为char *是否暗示对字符串长度的限制?

时间:2018-06-21 07:44:39

标签: c undefined-behavior

此代码应打印什么?

#include <stdio.h>
#include <string.h>

struct S
{
    int x[1];
};

union U
{
    struct S arr[64];
    char s[256];
};

int main()
{
    union U u;
    strcpy(u.s, "abcdefghijklmnopqrstuvwxyz");
    size_t len = strlen((char*)&u.arr[1].x);
    puts(len > 10 ? "YES" : "NO");
    return 0;
}

Clang始终打印“是”。尽管不会发出警告,但GCC 8.1会在优化后显示“否”。它是否利用了一些未定义的行为?

2 个答案:

答案 0 :(得分:2)

是的,gcc 8.1正在使用未定义的行为。调用int时,对大小为1 strlen的数组的访问权限不受限制。

strlen((char*)&u.arr[1].x);

&u.arr[1].x的类型为int (*)[1]。然后,您已将其转换为char*。除非用作sizeof的操作数,否则数组的地址与第一个元素的地址具有相同的值。因此,在强制类型转换之前,它将具有&u.arr[1].x[0]类型的int[1]值。假设sizeof(int) == 4,您会看到读取超过4个字节会导致超出限制的访问。

大小为1的数组之后是否有有效的内存都没关系。如果使用小尺寸的基本指针派生指针并从中读取指针,则行为是不确定的。

您可以通过将数组大小更改为1、2和3并从gcc检查生成的程序集来确认的确切原因。

对于1和2,它将生成puts("NO")。但是对于3,它会生成预期的代码。 这是因为您要与10进行比较。使用int[2]时,长度决不能大于10(不调用UB)。但是3的最大字节是12。

您可以在此处看到生成的程序集-

array of size 3array of size 2

对于2D阵列的类似讨论,您可能还想看看我的this老问题。

答案 1 :(得分:1)

适用于系统编程的实现将允许使用指向内部对象的指针来派生指向包含对象的指针。但是,C标准并未试图要求所有符合标准的实现都适合任何目的(作者承认,从原理上讲,有可能构建质量低至根本没有用的符合标准的实现),更不用说它们都适合系统编程了。另一方面,它确实描述了一种相当简单的方法,旨在用于系统编程的实现可以提供必要的语义。

尤其是,尽管标准没有规定从T*V*的直接转换将表现为从T*U*的转换,然后再进行转换从U*V*,如果存在某种类型的U*支持往返T*V*的往返转换,那么这种行为在书面。许多其行为本来不会由标准定义的动作将在一个实现中定义,该实现可确保指针强制转换具有传递性。

除其他事项外,该标准还指定了经过适当转换的指向聚集(数组,结构或联合)的指针将产生指向其第一个元素/成员的指针,反之亦然。因此,将&u.x [0]转换为int(*)[1],然后将其转换为struct S*,然后转换为union U*,最后转换为char*,将产生一个char*,可用于索引整个结构。尽管Standard可能允许符合标准的实现以仅允许访问其地址已转换的特定“内部”对象的方式处理对char*的转换,但它几乎不暗示实现应该这样做,也不意味着限制不会使实现不适合系统编程。

PS-我当然可以看到范围限制限定符的好处,该限定符表明将不使用指向特定对象的指针来派生该对象外部的任何地址。给出类似的东西:

struct foo {int x,y,z; };
...
int test(struct foo restrict *it)
{
  it->y++;
  doSomething(&it->x);
  it->y--;
  return it->y;
}
在参数上存在这样的限定词 doSomething()将允许编译器优化 是否对doSomething()的代码一无所知。但是请注意,这种限定符要最有用,就要求与restrict一样,通常会清洗指针的操作不会擦除其影响。因此,在可能的范围内,将不合格的类型转换作为洗涤指针 比将类型转换作为产生范围限制的指针更为有意义,除非进行了明确的洗涤。