您的位置:首页 - 教程 - C语言 - 正文
关于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.\n", sin(i));
    printf("sin(1)=%f.\n", 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指定编译行为停止在生成汇编源代码阶段)。

     .file    "math.c"
     .section    .rodata
 .LC1:
     .string    "sin(1)=%f.\n"
     .text
     .globl    main
     .type    main, @function
 main:
 .LFB0:
     .cfi_startproc
     pushq    %rbp
     .cfi_def_cfa_offset 16
     .cfi_offset 6, -16
     movq    %rsp, %rbp
     .cfi_def_cfa_register 6
     subq    $16, %rsp
     movabsq    $4605754516372524270, %rax
     movq    %rax, -8(%rbp)
     movsd    -8(%rbp), %xmm0
     movl    $.LC1, %edi
     movl    $1, %eax
     call    printf
     movl    $0, %eax
     leave
     .cfi_def_cfa 7, 8
     ret
     .cfi_endproc
 .LFE0:
     .size    main, .-main
     .ident    "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-4)"
     .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)这种情况大概是选择直接不推导。

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

  注意之前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函数没定义。

     .file    "math.c"
     .section    .rodata
 .LC0:
     .string    "sin(1)=%f.\n"
     .text
     .globl    main
     .type    main, @function
 main:
 .LFB0:
     .cfi_startproc
     pushq    %rbp
     .cfi_def_cfa_offset 16
     .cfi_offset 6, -16
     movq    %rsp, %rbp
     .cfi_def_cfa_register 6
     subq    $32, %rsp
     movl    $1, -4(%rbp)
     cvtsi2sd    -4(%rbp), %xmm0
     call    sin
     movsd    %xmm0, -24(%rbp)
     movq    -24(%rbp), %rax
     movq    %rax, -24(%rbp)
     movsd    -24(%rbp), %xmm0
     movl    $.LC0, %edi
     movl    $1, %eax
     call    printf
     movabsq    $4605754516372524270, %rax
     movq    %rax, -24(%rbp)
     movsd    -24(%rbp), %xmm0
     movl    $.LC0, %edi
     movl    $1, %eax
     call    printf
     movl    $0, %eax
     leave
     .cfi_def_cfa 7, 8
     ret
     .cfi_endproc
 .LFE0:
     .size    main, .-main
     .ident    "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-4)"
     .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就警告一下。

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

  最后补充一个例子

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

  编译的时候使用 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内建函数的一些了解以及一些猜测,如有说的不好的地方,同学们见谅,如有说的不对的地方,欢迎指正。


评论: