gcc内置函数和自定义函数在同一程序中调用

iaqfqrcu  于 2022-11-12  发布在  其他
关注(0)|答案(1)|浏览(181)

我试图理解什么时候使用gcc的内置函数。在下面的代码中,当我在没有使用-fno-builtin的情况下编译时,gcc的sqrt()和我的自定义sqrt()都被调用了。有人能解释一下这是怎么回事吗?
另外,我知道gcc的内置函数列表位于https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html,并且意识到解决这些问题的推荐方法是重命名冲突的函数。当自定义函数与内置函数同名时,或者当使用内置函数而不是自定义函数时,是否会显示gcc输出选项/警告?

#include <stdio.h>

double sqrt(double);

int main(void)
{
    double n;

    n = 2.0;
    printf("%f\n", sqrt(n));

    printf("%f\n", sqrt(2.0));

    return 0;
}

double sqrt(double x)
{
    printf("my_sqrt ");
    return x;
}

使用gcc -o my_sqrt my_sqrt.c编译后运行,输出为:

my_sqrt 2.000000
1.414214

使用gcc -fno-builtin -o my_sqrt my_sqrt.c编译后运行,输出为:

my_sqrt 2.000000
my_sqrt 2.000000
44u64gxh

44u64gxh1#

在运行时调用两个不同的sqrt函数并不是这种情况。对sqrt(2.0)的调用发生在编译时,这是法律的的,因为2.0是一个常量,而sqrt是一个标准的库函数,所以编译器知道它的语义。而且编译器可以假设你没有违反规则。我们稍后会讨论这意味着什么。
在运行时,不能保证sqrt函数会被sqrt(n)调用,但有可能会被调用。GCC使用sqrt函数,除非您将n声明为const double; Clang在编译时继续进行计算,因为它可以计算出n包含的内容。对于在编译时无法知道值的表达式,它们都将使用内置的sqrt函数(除非您指定-fno-builtin)。但这并不意味着它们将发布代码来调用函数;如果机器具有可靠的SQRT操作码,则编译器可以选择仅发出该操作码,而不是发出函数调用。
C标准在这里给了编译器很大的自由度,因为它只要求程序的 * 可观察行为 * 与标准中语义指定的结果一致,而且它只要求程序没有表现出未定义的行为。因此,编译器基本上可以在编译时自由地进行任何它想要的计算。假设一个没有未定义行为的程序会产生相同的结果。[注1]。
此外,“相同结果”的定义对于浮点计算来说也有点宽松,因为标准语义并不阻止计算以比数据类型理论上所能表示的更高的精度进行,这看起来似乎是无害的,但是在某些情况下,具有额外精度的计算在舍入之后可以产生不同的结果。因此,如果在编译期间,编译器可以计算出比它为同一表达式的运行时计算生成的代码更准确的中间结果,那么就标准而言,这是很好的。(而且大多数时候,你也可以,但也有例外。)
回到主要问题,编译器知道您已经重新定义了sqrt函数,但仍然可以在编译时计算中使用内置的sqrt函数,这似乎仍然令人惊讶。原因很简单(尽管经常被忽略):你的程序是无效的。2它表现出未定义的行为,当你的程序有未定义的行为时,所有的赌注都是无效的。
标准的§7.1.3中规定了未定义的行为,它涉及到 Reserved Identifiers。它提供了一个保留标识符的列表,这些标识符实际上是保留的,不管你正在使用的编译器是否警告过你。该列表包括以下内容,我将全文引用:
以下任何子条款(包括将来的库方向)和errno中的所有具有外部链接的标识符始终保留用作具有外部链接的标识符。
点的“以下子条款”包含了标准库函数的列表,所有这些函数都有外部链接。为了明确这一点,该标准继续如下:
如果程序在保留标识符的上下文中声明或定义了标识符(7.1.4所允许的除外),则该行为未定义。[注2]
你已经将sqrt声明为一个外部可见的函数,无论你是否包含math.h,这都是不允许的。所以你处于未定义的行为领域,编译器完全有资格在编译时计算时不用担心你对sqrt函数的定义。[注3]
(You我可以试着将你的sqrt实现声明为static,以避免对外部可见名称的限制。这将适用于GCC的最新版本;它允许static声明覆盖标准库定义。Clang在编译时计算方面更激进,它仍然使用sqrt的标准定义。使用MSVC(在www.example.com上)进行的快速测试godbolt.org似乎表明它完全禁止重新定义标准库函数。)
那么,如果您真的想为自己定义的sqrt编写sqrt(x),该怎么办呢?标准确实为您提供了一个出路:因为sqrt不是为宏名保留的,你可以将它定义为一个宏,用你的实现的名称来代替它[注4],至少如果你不包含#include <math.h>的话。如果你包含了头,那么这可能是不一致的,因为在这种情况下,标识符也是为宏名保留的[注5]。

备注

1.这种自由并没有扩展到 integer 常量表达式,结果是编译器不能在需要整型常量表达式的上下文中将strlen("Hello")转换为常量值5。因此,这是不法律的的:

switch (i) {
    case strlen("Hello"):
        puts("world");
        break;
    default: break;
}

但是,这可能不会调用strlen六次(尽管您也不应该指望这种优化):

/* Please don't do this. Calling strlen on every loop iteration
 * blows up linear-time loops into quadratic time monsters, which is
 * an open invitation for someone to do a denial-of-service attackç
 * against you by supplying a very long string.
 */
for (int i = 0; i < strlen("Hello"); ++i) {
    putchar("world"[i]);
}

1.在当前的C标准中,这一声明是在§7.1.3的第2段中,但在C23草案中,它被移到§6.4.2.1的第8段(标识符的词法规则)。对保留标识符的限制也有一些其他的变化(以及大量新的保留标识符),但在这个特定的情况下,这并没有任何区别。
1.在许多未定义行为的示例中,这样做的目的仅仅是让编译器避免做额外的健全性检查。相反,它可以假设你没有违反规则,然后做它本来会做的任何事情。
1.请不要使用名称_sqrt,即使它可能会工作。名称以下划线开头都是保留的,同样的§7.1.3。如果名称以两个下划线开头,或者下划线后面跟着一个大写字母,它保留用于所有用途。其他以下划线开头的标识符保留用于文件范围(既作为函数名,也作为struct标记)。所以不要这样做。如果你想用下划线来表示名称是代码内部的,把它放在标识符的末尾,而不是开头。
1.标准头也可将标准库函数的名称定义为类似函数的宏,可能是为了替换编译器已知的不同保留名称,这可能使用专用机器操作码来导致内联代码的生成。无论如何,标准要求函数存在,它允许你对宏进行#undef操作,以保证实际的函数被使用,但是它不明确地允许重定义名称。

相关问题