gcc内置函数
  最近在看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。
1 2 3 4 5 6 7 8 9#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));    return0;
}
  这个代码可以直接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.\n"
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.\n", sin(i));
7    printf("sin(1)=%f.\n", 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.\n"
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 ). 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.\n", sin(i));
printf函数原型在什么头文件里7    //printf("sin(1)=%f.\n", 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内建函数的⼀些了解以及⼀些猜测,如有说的不好的地⽅,同学们见谅,如有说的不对的地⽅,欢迎指正。