• 关于gcc内置函数和c隐式函数声明的认识以及一些推测


      最近在看APUE,不愧是经典,看一点就收获一点。但是感觉有些东西还是没说清楚,需要自己动手验证一下,结果发现需要用gcc,就了解一下。

      有时候,你在代码里面引用了一个函数但是没有包含相关的头文件,这个时候gcc报的错误比较诡异,一般是这样:【math.c:6:25: 警告:隐式声明与内建函数‘sin’不兼容 [默认启用]】。这个错误网上大量博客都在说需要包含XXX.h文件,但是没有人解释这个错误信息为什么这样表达。什么是隐式声明,什么是内建函数,我就纠结了。

      

      隐式声明函数的概念网上有相关的资料,有兴趣的同学可以自行查阅,这里简要的提一下。如果你调用了一个函数a,但是gcc找不到函数a的定义,那就默认帮你定义一个函数a,大概如下。

      int a(XXX){return XXX}

      显然这个不是件好事,因为,有时候gcc这样做会发现问题,提示这个错误,如果你用了这样的语句int i = a(XX);这样的话gcc是不会报错的,具体的行为我也没有深入研究。C语言后来的标准都慢慢放弃了隐式声明函数,C++里面会直接报错。

      内建函数,讲这个的资料就比较少。最后是在gcc的官方文档里面看到了相关的介绍,我也没有时间去细究只是看了几段话,再结合一些帖子里面的只言片语,大概得出如下推测。。

      顾名思义,内建函数就是一个系统或者工具提供的默认就能用的函数。这里面可以有两种理解,可以是gcc支持的c语言默认让你用这些函数,这些是gcc-c的内建函数;还有一种理解就是gcc指定的函数,gcc允许你使用这些函数。官方文档里面说gcc的内建函数大多是为了对代码进行优化,所以我更倾向于后一种理解。我觉得gcc的内建函数可以认为是gcc提供的一些类似预处理功能,以C函数的形式提供给编程人员使用,就是说看着是c函数,其实最后跟c语言没关系。比如下面的例子里面会用到,如果代码里面直接有sin(1)这样的调用,那gcc会直接算出sin(1)的值,然后在生成代码的时候直接使用这个值,而不会使用call sin命令调用sin函数。这就是所谓的优化(还有其他类型的优化,这个只是其中一种情况)。

      官方文档里面说gcc的内建函数主要分两类,一类以_builtin_为前缀,一类没有前缀。后者往往与某一个标准库的函数相对应,如sin,printf,exit。当编译器认为可以对相关的代码进行优化的时候(比如上面提到的直接得出某个结果,比如忽略没有意义的计算等等),会直接进行优化,而这些函数就相当于gcc的内置函数了。

      上面对内置函数进行了也说明,不知道我表达清楚没有,下面讲几个具体的例子。

      一、不连接libm的情况下使用sin函数

      file:math.c。

    #include <stdio.h>
    #include <math.h>
      
    int main(){
        //int i = 1;
        //printf("sin(1)=%f.
    ", sin(i));
        printf("sin(1)=%f.
    ", sin(1));
        return 0;
    }
    

      这个代码可以直接gcc math.c -o math.out。然后./math.out直接执行。

      输出结果:sin(1)=0.841471.

      习惯了window编程的同学可能觉得没什么,但是在linux编程中是有问题的。gcc中,include <math.h>这条语句只是将math.h(标准库头文件)文件包含进math.c(我们的例子文件)中来,但是math.h中只有sin函数的声明,并没有sin函数的定义。正常而言,使用了math.h中声明的函数,就需要在编译(准确说是连接)的时候指定实现了math.h中函数声明的库,这里math.h对应标准库libm.a和libm.so。前者为静态库,后者为动态库。你可以这样理解,所有的.h文件是不需要编译的(如果被include,直接就相当于插入到了代码中),所有的.c文件都需要编译。.h文件中只是定义一个函数的形式,而不管这个函数具体做什么,比如sin函数需要一个double型的参数,执行完后返回一个double型的值。对汇编和编译原理有所了解的同学都应该懂,这样就可以暂时的编译一个调用了sin函数的.c文件,而不管sin函数具体怎定义了,直到生成汇编源代码。最后编译成汇编源代码大概就是

      push XXX //参数压栈

      call sin

      mov XXX XXX 或者pop XXX //获取返回值。

      有个函数声明,编译器就知道参数压栈怎么压,同时也知道返回的时候怎么获取返回值。

      但是代码最后还是要执行的,也就是说生成了汇编源代码还不行,还要把汇编源代码汇编成机器代码。这个时候,没有sin函数具体的代码,编译器没办法继续将汇编源代码汇编成机器代码,只能停留在这里。编译一份代码的最后一步就是连接。连接会将所有指定的.c文件编译的结果连接在一起。如上所述,libm.a和libm.so实现了sin,要想上面的代码能够运行,需要将libm.a(这里面只用到静态链接库)和math.c(示例代码)的编译结果连接起来。

      说了半天编译器的事,如果你听不明白上面的内容,那估计就不用往下看了,先补充一下相关的知识再说。

      总而言之,在gcc中如果代码使用了math.h中声明的函数,不但要在代码里include <math.h>,还需要编译的时候指定连接libm.a。理解了这点,就知道为什么上面的例子使用"gcc math.c -o math.out"很奇怪了。言归正传,为什么这个例子不需要连接libm.so。

      一开始,我以为是gcc编译器比较智能,能自动识别sin是math.h中的函数,然后自动连接libm.a。或者gcc默认就连接libm.a,但是网上并没找到这样的资料。直到看到一个帖子也是问类似的问题,有一个回答的人大意如下:gcc会对代码进行优化,但是优化也是基于gcc能够确定这个优化是没问题的。比如把sin(1)替换为sin(1)的真实值,这个就可以,因为代码里面使用sin(1)的目的99.9999999%是要计算sin(1)的值,而这个值是确定的,那gcc就在编译的时候算好,运行的时候就不用再算了。为了验证这点,可以使用gcc -S math.c -o math.s命令查看gcc将math.c编译成的汇编源代码(-S指定编译行为停止在生成汇编源代码阶段)。

     1     .file    "math.c"
     2     .section    .rodata
     3 .LC1:
     4     .string    "sin(1)=%f.
    "
     5     .text
     6     .globl    main
     7     .type    main, @function
     8 main:
     9 .LFB0:
    10     .cfi_startproc
    11     pushq    %rbp
    12     .cfi_def_cfa_offset 16
    13     .cfi_offset 6, -16
    14     movq    %rsp, %rbp
    15     .cfi_def_cfa_register 6
    16     subq    $16, %rsp
    17     movabsq    $4605754516372524270, %rax
    18     movq    %rax, -8(%rbp)
    19     movsd    -8(%rbp), %xmm0
    20     movl    $.LC1, %edi
    21     movl    $1, %eax
    22     call    printf
    23     movl    $0, %eax
    24     leave
    25     .cfi_def_cfa 7, 8
    26     ret
    27     .cfi_endproc
    28 .LFE0:
    29     .size    main, .-main
    30     .ident    "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-4)"
    31     .section    .note.GNU-stack,"",@progbits

      注意main函数的内容,里面只有一个call printf,并没有call sin。同时注意到第17行有一个莫名其妙的数字$4605754516372524270。个人认为这个就是sin(1)的值经过莫中变化后的8进制代码。至于经过了什么变化我也说不清楚,这个值好像也不是sin(1)浮点结果的8进制,可能经过了一些运算,或者sin(1)的结果只是这个8进制值的一部分,这个有心的同学可以研究研究。不管怎么样,汇编代码里面没有call sin。说明sin(1)已经被优化了。

      同样是sin(1),在什么情况下gcc没办法优化呢?很简单,int i = 1; sin(i),这样gcc就没法优化了。虽然也是计算sin(1),但是gcc在编译代码的时候只知道求sin(i),但是他不知道i值是多少。为什么不知道?这个是编译优化的内容,有兴趣的同学可以了解一下。简单来说就是,有些变量的值在某些状态下是可以推导的,但是目前的技术能推导的情况不多,而且需要大量的编译处理才能推导,gcc对sin(i)这种情况大概是选择直接不推导。

    1 #include <stdio.h>
    2 #include <math.h>
    3   
    4 int main(){
    5     int i = 1;
    6     printf("sin(1)=%f.
    ", sin(i));
    7     printf("sin(1)=%f.
    ", sin(1));
    8     return 0;
    9 }

      注意之前math.c的代码,将其中的注释去掉,就是现在math.c的代码。这个时候"gcc math.c -o math.out"就会报错:

        /tmp/ccYkhbgg.o:在函数‘main’中:
        math.c:(.text+0x15):对‘sin’未定义的引用
        collect2: 错误:ld 返回 1

      再看看汇编代码,注意这个时候到汇编的代码还是可以生成的,只是将汇编源程序会变成机器代码的时候,才发现call sin的sin函数没定义。

     1     .file    "math.c"
     2     .section    .rodata
     3 .LC0:
     4     .string    "sin(1)=%f.
    "
     5     .text
     6     .globl    main
     7     .type    main, @function
     8 main:
     9 .LFB0:
    10     .cfi_startproc
    11     pushq    %rbp
    12     .cfi_def_cfa_offset 16
    13     .cfi_offset 6, -16
    14     movq    %rsp, %rbp
    15     .cfi_def_cfa_register 6
    16     subq    $32, %rsp
    17     movl    $1, -4(%rbp)
    18     cvtsi2sd    -4(%rbp), %xmm0
    19     call    sin
    20     movsd    %xmm0, -24(%rbp)
    21     movq    -24(%rbp), %rax
    22     movq    %rax, -24(%rbp)
    23     movsd    -24(%rbp), %xmm0
    24     movl    $.LC0, %edi
    25     movl    $1, %eax
    26     call    printf
    27     movabsq    $4605754516372524270, %rax
    28     movq    %rax, -24(%rbp)
    29     movsd    -24(%rbp), %xmm0
    30     movl    $.LC0, %edi
    31     movl    $1, %eax
    32     call    printf
    33     movl    $0, %eax
    34     leave
    35     .cfi_def_cfa 7, 8
    36     ret
    37     .cfi_endproc
    38 .LFE0:
    39     .size    main, .-main
    40     .ident    "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-4)"
    41     .section    .note.GNU-stack,"",@progbits

      这个时候有两个call printf,第一个call printf之前有一个call sin。第二个call printf前面还是没有call sin。

      gcc官方文档里面有一段话,大意是:对于内置函数,如果能对代码进行优化,gcc会优化代码,如果不能优化,往往就是直接调用同名的标准库函数。我的理解就是sin(1)能优化就给你优化了,sin(i)优化不了,就还是调用math.h中声明的sin函数。

      GCC includes built-in versions of many of the functions in the standard C library. These functions come in two forms: one whose names start with the __builtin_ prefix, and the other without. Both forms have the same type (including prototype), the same address (when their address is taken), and the same meaning as the C library functions even if you specify the -fno-builtin option see C Dialect Options). Many of these functions are only optimized in certain cases; if they are not optimized in a particular case, a call to the library function is emitted.

      修改的后代码编译时制定libm.a就可以,具体命令如下 gcc math.c -lm -o math.out。 -lxxx参数就是到相关目录中找libxxx.so和libxxx.a。这样就可以连接到libm.a了。

      gcc内建函数是可选的,我们可以在编译的时候指定不使用某些内建函数,gcc -fno-builtin-xxx。还是一开始的例子,使用命令:gcc -fno-builtin-sin math.c -o math.out。这次就会报错,因为我们指定不使用内建函数sin,那就会使用math.h中声明的sin函数,同时编译的时候并没有指定连接libm.a,这样就会报错:

        /tmp/ccKy8vEG.o:在函数‘main’中:
        math.c:(.text+0x11):对‘sin’未定义的引用
        collect2: 错误:ld 返回 1

      

      最初的问题,【math.c:6:25: 警告:隐式声明与内建函数‘sin’不兼容 [默认启用]】是什么意思?这个其实我自己也不清楚,我只是大概弄清楚了什么叫做隐式声明函数和内建函数。在论坛上有人这样回复:内建函数也是有原型的,当隐式声明和对应的内建函数的声明不一致的时候,可能会出问题,所以gcc就警告一下。

      最后一个默认启用是什么意思我就不清楚了,推测是使用内置函数。

      最后补充一个例子

    1 #include <stdio.h>
    2 //#include <math.h>
    3 
    4 int main(){
    5     int i = 1;
    6     printf("sin(1)=%f.
    ", sin(i));
    7     //printf("sin(1)=%f.
    ", sin(1));
    8     return 0;
    9 }

      编译的时候使用 gcc -lm math.c -o math.out。会有【math.c:6:25: 警告:隐式声明与内建函数‘sin’不兼容 [默认启用]】警告,但是却还是能生成可执行文件,并且执行结果正确。这个例子中,我们没有包含math.h,所以sin肯定是一个隐式声明函数,会和内建函数不兼容,gcc发出警告,但是由于gcc无法优化sin(i),所以转而调用标准库的sin(这个调用应该是内置的,因为我们没有包含math.h,应该gcc自动调用math.c中sin函数)。同时连接的时候制定了-lm,连接成功。所以生成的可执行文件正常计算sin(1)。如果默认启用是使用隐式声明函数,那结果应该会有问题。

      好了,这些就是我对gcc内建函数的一些了解以及一些猜测,如有说的不好的地方,同学们见谅,如有说的不对的地方,欢迎指正。

  • 相关阅读:
    WCF开发实战系列二:使用IIS发布WCF服务
    电脑远程登录控制Android手机Webkey For Android使用教程
    WCF的https安全(ssl)访问实例
    IIS中“使用 XSL 样式表无法查看 XML 输入”问题的解决
    服务器禁止被ping的设置方法(图文)
    Windows Server 2008 R2 MSDN
    IIS7配置https
    C# 检查网络是否连通 判断远程文件是否存在 C#获取程序路径的方法中需要注意的地方
    c#,winform,treeview,选中节点,选中相应的全部子节点,取消节点,取消父节点,小技巧
    sql大全(一)
  • 原文地址:https://www.cnblogs.com/hujichen/p/5612826.html
Copyright © 2020-2023  润新知