关于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. 数据预处理之数据规约(Data Reduction)

    数据归约策略 数据仓库中往往具有海量的数据,在其上进行数据分析与挖掘需要很长的时间 数据归约 用于从源数据中得到数据集的归约表示,它小的很多,但可以产生相同的(几乎相同的)效果 数据归约策略 维归约  ...

  2. MySQL的GTID复制与传统复制的相互切换

    MySQL的GTID复制与传统复制的相互转换 1. GTID复制转换成传统复制 1.1 环境准备 1.2 停止slave 1.3 查看当前主从状态 1.4 change master 1.5 启动主从 ...

  3. Voyager的安装及配置文件

    使用代理服务器安装laravel http_proxy=http://localhost:1080 composer create-project --prefer-dist laravel/lara ...

  4. linux+ARM学习路线

    学习步骤如下: 1.Linux 基础 安装Linux操作系统 Linux文件系统 Linux常用命令 Linux启动过程详解 熟悉Linux服务能够独立安装Linux操作系统 能够熟练使用Linux系 ...

  5. Java线程和多线程(四)——主线程中的异常

    作为Java的开发者,在运行程序的时候会碰到主线程抛异常的情况.如果开发者使用Java的IDE比如Eclipse或者Intellij IDEA的话,可能是不需要直接面对这个问提的,因为IDE会处理运行 ...

  6. luogu2293 [JSOI2008]Blue Mary开公司

    ref好像叫什么李超线段树?--这篇不是太通用-- #include <iostream> #include <cstdio> #include <cmath> u ...

  7. SDOJ 3742 黑白图

    [描述] 一个 n 个点 m 条边构成的无向带权图.由一些黑点与白点构成 树现在每个白点都要与他距离最近的黑点通过最短路连接(如果有很多个,可以选 取其中任意一个),我们想要使得花费的代价最小.请问这 ...

  8. js时间格式化工具,时间戳格式化,字符串转时间戳

    在开发中经常会用到时间格式化,有时候在网上搜索一大堆但不是自己想要的,自己总结一下,写一个时间格式化工具方便以后直接使用,欢迎大家来吐槽…… 1 2 3 4 5 6 7 8 9 10 11 12 13 ...

  9. Octave 里的 fminunc

    ptions = optimset('GradObj', 'on', 'MaxIter', '100'); initialTheta = zeros(2,1); [optTheta, function ...

  10. JDBC初探(一)

    下载好JDBC之后,首先做的应该是查看它的文档 打开connector-j.html import java.sql.Connection; import java.sql.DriverManager ...