严格别名规则和glibc

时间:2017-06-08 22:41:03

标签: c glibc strict-aliasing

我一直在阅读有关严格别名规则的内容,我开始变得非常困惑。首先,我已经阅读了这些问题和一些答案:

根据它们(据我所知),使用指向另一种类型的指针访问char缓冲区违反了严格别名规则。但是,strlen()的glibc实现有这样的代码(删除了注释和64位实现):

size_t strlen(const char *str)
{
    const char *char_ptr;
    const unsigned long int *longword_ptr;
    unsigned long int longword, magic_bits, himagic, lomagic;

    for (char_ptr = str; ((unsigned long int) char_ptr 
             & (sizeof (longword) - 1)) != 0; ++char_ptr)
       if (*char_ptr == '\0')
           return char_ptr - str;

    longword_ptr = (unsigned long int *) char_ptr;

    himagic = 0x80808080L;
    lomagic = 0x01010101L;

    for (;;)
    { 
        longword = *longword_ptr++;

        if (((longword - lomagic) & himagic) != 0)
        {
            const char *cp = (const char *) (longword_ptr - 1);

            if (cp[0] == 0)
                return cp - str;
            if (cp[1] == 0)
                return cp - str + 1;
            if (cp[2] == 0)
                return cp - str + 2;
            if (cp[3] == 0)
                return cp - str + 3;
        }
    }
}

longword_ptr = (unsigned long int *) char_ptr;行明显将unsigned long int别名为char。我不明白是什么让这成为可能。我看到代码处理对齐问题,所以没有问题,但我认为这与严格的别名规则无关。

第三个相关问题的接受答案是:

  

但是,有一个非常常见的编译器扩展,允许您从char转换到其他类型的正确对齐指针并访问它们,但这是非标准的。

我想到的只有-fno-strict-aliasing选项,是这种情况吗?我无法在glibc实现者所依赖的任何地方找到它,并且这些注释在某种程度上意味着这个演员表没有任何担心,例如很明显没有问题。这让我觉得它确实很明显,我错过了一些愚蠢的东西,但我的搜索失败了。

4 个答案:

答案 0 :(得分:9)

在ISO C中,此代码违反了严格的别名规则。 (并且还违反了无法定义与标准库函数同名的函数的规则)。但是,此代码不受ISO C规则的约束。标准库甚至不必以类C语言实现。该标准仅指定实现实现标准函数的行为。

在这种情况下,我们可以说实现是使用类似C的GNU方言,如果代码是使用编写器的预期编译器和设置编译的,那么它将成功实现标准库函数。

答案 1 :(得分:3)

在编写别名规则时,标准的作者只考虑了所有实现中有用的表格,因此应该强制使用。 C实现针对各种目的,并且标准的作者没有试图指定编译器必须做什么以适合任何特定目的(例如,低级编程),或者就此而言,任何目的。

上述依赖于低级构造的代码不应该在没有声称适合低级编程的编译器上运行。另一方面,任何不支持此类代码的编译器都应被视为不适合低级编程。请注意,编译器可以采用基于类型的别名假设,并且仍适用于低级编程如果他们会合理地努力识别常见的别名模式。一些编译器编写者在代码视图中的投入非常高,既不符合常见的低级编码模式,也不符合C标准,但是 任何编写低级代码的人都应该只知道那些编译器' 优化器不适合与低级代码一起使用。

答案 2 :(得分:0)

标准的措辞实际上比实际的编译器实现更奇怪:C标准讨论了声明的对象类型,但编译器只能看到指向这些对象的指针。因此,当编译器看到从char*unsigned long*的强制转换时,它必须假设char*实际上是对声明类型为unsigned long的对象进行别名,使演员阵容正确。

提醒一句:我假设strlen()被编译成一个库,后来只链接到应用程序的其余部分。因此,优化器在编译时没有看到函数的使用,迫使它假设转换为unsigned long*确实是合法的。如果您使用

致电strlen()
short myString[] = {0x666f, 0x6f00, 0};
size_t length = strlen((char*)myString);    //implementation now invokes undefined behavior!

strlen()内的强制转换是未定义的行为,如果在编译strlen()时看到你的使用,你的编译器将被允许剥离strlen()的整个主体。允许strlen()在此调用中按预期运行的唯一事实是,strlen()作为库单独编译,从优化器中隐藏未定义的行为,因此优化器必须假设转换在编译strlen()时合法。

因此,假设优化器无法调用“未定义的行为”,从char*转换为其他任何内容的原因是危险的,不是别名,而是对齐。在某些硬件上,如果您尝试访问未对齐的指针,则会发生奇怪的事情。硬件可能会从错误的地址加载数据,引发中断,或者只是非常缓慢地处理请求的内存负载。这就是为什么C标准通常声明这种演员未定义的行为。

然而,您会看到有问题的代码实际上明确地处理了对齐问题(包含(unsigned long int) char_ptr & (sizeof (longword) - 1)子条件的第一个循环)。之后,char*已正确对齐,可重新解释为unsigned long*

当然,所有这些都不是真正符合C标准,但它符合编译器的C实现,这个代码是用来编译的。如果gcc人修改了他们的编译器以对这段代码采取行动,那么glibc人就会抱怨它足够大,以便将gcc更改回来处理这种正确投射。

在一天结束时,标准C库实现必须违反严格的别名规则才能正常工作并提高效率。 strlen()只需要违反这些规则才能提高效率,malloc() / free()函数对必须能够采用声明类型为Foo的内存区域,并且将其转换为声明类型为Bar的内存区域。并且malloc()实现中没有malloc()调用,它将首先为对象提供声明的类型。 C语言的抽象简单地在这个级别上被打破。

答案 3 :(得分:-4)

潜在的假设可能是函数是单独编译的,不能用于内联或其他交叉函数优化。这意味着没有编译时信息在函数内部或外部流动。

该函数不会尝试通过指针修改任何内容,因此没有冲突。