【问题标题】:Strict aliasing rule and strlen implementation of glibcglibc的严格别名规则和strlen实现
【发布时间】:2023-03-17 23:30:01
【问题描述】:

我已经阅读了一段时间的严格别名规则,但我开始感到非常困惑。首先,我已经阅读了这些问题和一些答案:

据他们说(据我所知),使用指向另一种类型的指针访问 char 缓冲区违反了严格的别名规则。但是,strlen() 的 glibc 实现有这样的代码(删除了 cmets 和 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 实现者所依赖的任何地方,并且 cmets 以某种方式暗示这种转换是在没有任何问题的情况下完成的,很明显不会有任何问题。这让我觉得这确实很明显,我错过了一些愚蠢的东西,但我的搜索失败了。

【问题讨论】:

  • 代码可能只是写得不好。转换指针(unsigned long int) char_ptr 的对齐检查也很可疑。他们经历了所有这些麻烦来尝试一些奇怪的优化,这些优化增加了额外的分支,而且看起来不一定更快,可能更慢。
  • 那么,如果 GNU 人(AFAIK 和 FreeBSD 人也应用了类似的优化)如果没有加快速度,为什么还要经历这一切呢?
  • @curiousguy 很多人,我想。在过去的 5-10 年里,GCC 在各种嵌入式系统编程中变得非常流行。并且因此在此类系统上不断出现错误,其中大多数实际上是由 gcc 基于严格别名的积极优化引起的。
  • 相关的 glibc 邮件列表帖子:sourceware.org/ml/libc-alpha/2016-02/msg00052.html

标签: c glibc strict-aliasing


【解决方案1】:

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

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

【讨论】:

  • 据我所知,glibc 的人打算用 gcc(-std=gnu11 或类似的)编译库。任何地方都没有提到对严格混叠的担忧。 gnu.org/software/libc/manual/html_node/…。可能代码只是写得不好并且包含一个错误。
  • @Lundin 什么错误?
  • @curiousguy 问题中提到的那个。 longword_ptr = (unsigned long int *) char_ptr;
  • @Lundin 那么证明这是一个错误。
  • @Lundin 对不起,他这样做了(这只是浪费大家的时间)。
【解决方案2】:

在编写别名规则时,标准的作者只考虑了对所有实现有用的形式,因此应该强制执行。 C 实现针对多种目的,标准的作者没有尝试指定编译器必须做什么才能适合任何特定目的(例如低级编程),或者就此而言,任何目的。

不应期望像上面这样依赖于低级构造的代码在不声称适合低级编程的编译器上运行。另一方面,任何不支持此类代码的编译器都应被视为不适合低级编程。请注意,编译器可以采用基于类型的别名假设,并且仍然适用于低级编程如果他们做出合理的努力来识别常见的别名模式。一些编译器编写者非常重视既不符合常见低级编码模式也不符合 C 标准的代码视图,但 任何编写低级代码的人都应该简单地认识到那些编译器的 优化器不适合与低级代码一起使用。

【讨论】:

    【解决方案3】:

    标准的措辞实际上比实际的编译器实现更奇怪: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 语言的抽象在这个层次上简单地分解了。

    【讨论】:

    • 我讨厌阅读它,但我没有看到违反 6.5 (7) (C11 draft n1570),最后一个要点,a character type。唯一的 'access' 是字符类型。 (即使那个 char 类型被转换成别的东西)
    • @DavidC.Rankin 严格的别名规则都是关于通过不同类型的指针重新排序访问。如果您有int*,则您引用的异常允许您安全地将其转换为char* 并通过此指针操作其字节。它不允许您随后将 char* 转换为 float*,然后将整数数据作为浮点数据访问:编译器将被允许在通过以下方式存储任何数据之前读取 float* 后面的数据int*strlen()不知道数据是否存储为char,调用者可能已合法地将int* 转换为char*
    • @DavidC.Rankin 数组中一项的有效类型是int。因此,您可以对该地址应用任意数量的指针类型转换,然后将其转换为 int* 最后就可以了。严格别名仅指访问存储值时使用的类型。所以*(int*)(bananas_t*)(void*)(char*)a = 0; 很好,它不违反严格的别名。 *(char*)a = 0; 也没有。违反严格别名的做法是相反的,从 char 到 int:char a[] = {1,2,3,4}; printf("%d", *(int*)a);。请注意,严格的别名规则不会解决错位问题。
    • @DavidC.Rankin 好吧,优化器可以利用未定义的行为,而不是让编译器抱怨 - 当您的程序由于以下原因无法按预期工作时,优化器经常会咬你未定义的行为(= 一个重要的代码部分只是被优化了,因为优化器能够证明它只会在您的程序调用未定义的行为之后执行)。当涉及到未定义的行为时,我从没想过会收到警告。
    • 我没有投票,但你的理由并不充分;严格的别名规则与指针无关,当有积极的优化通过然后是 TBAA 通过时,您对编译器如何工作的假设并不总是成立。
    【解决方案4】:

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

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

    【讨论】:

      猜你喜欢
      • 2015-10-15
      • 2017-02-25
      • 2018-12-14
      • 1970-01-01
      • 1970-01-01
      • 2014-07-13
      • 2015-03-30
      • 1970-01-01
      相关资源
      最近更新 更多