关于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。

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));
    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.\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 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.\n", sin(i));
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内建函数的一些了解以及一些猜测,如有说的不好的地方,同学们见谅,如有说的不对的地方,欢迎指正。

gcc 内置函数的更多相关文章

  1. GCC内置函数

    在C语言写的程序中,有时候没有包含头文件,直接调用一些函数,如printf,也不会报错,因为GCC内置和一些函数.如果包含了头文件,则去第三方库中链接这个函数,不再使用GCC内置的函数.每个编译器的内 ...

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

    最近在看APUE,不愧是经典,看一点就收获一点.但是感觉有些东西还是没说清楚,需要自己动手验证一下,结果发现需要用gcc,就了解一下. 有时候,你在代码里面引用了一个函数但是没有包含相关的头文件,这个 ...

  3. python 之 内置函数大全

    一.罗列全部的内置函数 戳:https://docs.python.org/2/library/functions.html 二.range.xrange(迭代器) 无论是range()还是xrang ...

  4. Entity Framework 6 Recipes 2nd Edition(11-12)译 -> 定义内置函数

    11-12. 定义内置函数 问题 想要定义一个在eSQL 和LINQ 查询里使用的内置函数. 解决方案 我们要在数据库中使用IsNull 函数,但是EF没有为eSQL 或LINQ发布这个函数. 假设我 ...

  5. Oracle内置函数:时间函数,转换函数,字符串函数,数值函数,替换函数

    dual单行单列的隐藏表,看不见 但是可以用,经常用来调内置函数.不用新建表 时间函数 sysdate 系统当前时间 add_months 作用:对日期的月份进行加减 写法:add_months(日期 ...

  6. python内置函数

    python内置函数 官方文档:点击 在这里我只列举一些常见的内置函数用法 1.abs()[求数字的绝对值] >>> abs(-13) 13 2.all() 判断所有集合元素都为真的 ...

  7. DAY5 python内置函数+验证码实例

    内置函数 用验证码作为实例 字符串和字节的转换 字符串到字节 字节到字符串

  8. python之常用内置函数

    python内置函数,可以通过python的帮助文档 Build-in Functions,在终端交互下可以通过命令查看 >>> dir("__builtins__&quo ...

  9. freemarker内置函数和用法

    原文链接:http://www.iteye.com/topic/908500 在我们应用Freemarker 过程中,经常会操作例如字符串,数字,集合等,却不清楚Freemrker 有没有类似于Jav ...

随机推荐

  1. C语言程序运行

    vs2013编辑器 c程序的运行   一.启动Microsoft Visual C++  2013版.新建项目 . 1.  文件——> 新建——> 项目.       2. 确定之后 弹出 ...

  2. Python爬虫系列-分析Ajax请求并抓取今日头条街拍图片

    1.抓取索引页内容 利用requests请求目标站点,得到索引网页HTML代码,返回结果. 2.抓取详情页内容 解析返回结果,得到详情页的链接,并进一步抓取详情页的信息. 3.下载图片与保存数据库 将 ...

  3. Yii2 HelloWord

    一.安装Yii2 可以直接官网或github下载: https://github.com/yiisoft/yii2 下载完后进入项目的web/目录,入口文件为该目录下的index.php,这里直接启用 ...

  4. 请问batch_normalization做了normalization后为什么要变回来?

    请问batch_normalization做了normalization后为什么要变回来? 请问batch_normalization做了normalization后为什么要变回来? - 莫驚蟄的回答 ...

  5. LeetCode(168) Excel Sheet Column Title

    题目 Given a positive integer, return its corresponding column title as appear in an Excel sheet. For ...

  6. debian安装中文字体

    debian刚安装完成之后,因为没有中文字体,会出现方框. 安装中文字体: $ su # apt-get install fonts-arphic-bkai00mp fonts-arphic-bsmi ...

  7. 使用supervisor方便调试程序

    调试过程中,有时需要修改代码,并时刻看到运行效果.如果每次终止程序又重启,会很麻烦. 可以使用supervisor,它可以监听代码文件,一旦发生改动会自动重启程序. 安装supervisor命令: n ...

  8. git 强制回到以前版本

    git reset dfd3e36a641340a0b86f811df869c4375fabeff2 --hard

  9. syntax error, error in :'e id=1?', expect QUES, actual QUES pos 66, line 1, column 66, token QUES错误

    在查询数据库的时候报了下面的异常: syntax error, error in :'e id=1?', expect QUES, actual QUES pos 66, line 1, column ...

  10. 修改Typora的快捷键【markdown软件】

    修改Typora的快捷键 魔芋:Typora是一款不错的编写markdowm的软件,推荐使用. 魔芋:修改这个文件conf.user.json   "keyBinding": { ...