gcc编译流程

Posted chendeqiang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了gcc编译流程相关的知识,希望对你有一定的参考价值。

开放源码:就是程式码,写给人类看的程式语言,但机器并不认识,所以无法执行;
编译器:将程式码转译成为机器看的懂得语言,就类似翻译者的角色;
可执行程序:经过编译器变成二进制程式后,机器看的懂所以可以执行的文件。

make 与 configure
当执行 make 时,make 会在当时的目录下搜寻 Makefile (or makefile) 这个文字档,而 Makefile 里面则记录了源代码如何编译的详细资讯! make 会自动的判别源代码是否经过变动了,而自动更新执行档,是软件工程师相当好用的一个辅助工具呢!

咦!make 是一支程式,会去找 Makefile ,那 Makefile 怎么写? 通常软件开发商都会写一支侦测程式来侦测使用者的作业环境, 以及该作业环境是否有软件开发商所需要的其他功能,该侦测程式侦测完毕后,就会主动的建立这个 Makefile 的规则档案啦!通常这支侦测程式的档名为 configure 或者是 config 。

gcc

新建.c文件:
hello.c

#include <stdio.h>
int main(void)
{
        printf("Hello World
");
}

编译:

gcc hello.c
./a.out
Hello World

在预设的状态下,如果我们直接以 gcc 编译源代码,并且没有加上任何参数,则执行档的档名会被自动设定为 a.out 这个档案名称! 所以你就能够直接执行 ./a.out 这个执行档啦!
那个 hello.c 就是源代码,而 gcc 就是编译器,至于 a.out 就是编译成功的可执行 binary program 啰!
咦!那如果我想要产生目标档 (object file) 来进行其他的动作,而且执行档的档名也不要用预设的 a.out ,那该如何是好?其实你可以将上面的第 2 个步骤改成这样:

[root@study ~]# gcc -c hello.c
[root@study ~]# ll hello*
-rw-r--r--. 1 root root   71 Sep  4 11:32 hello.c
-rw-r--r--. 1 root root 1496 Sep  4 11:34 hello.o  <==就是被产生的目标档

[root@study ~]# gcc -o hello hello.o
[root@study ~]# ll hello*
-rwxr-xr-x. 1 root root 8503 Sep  4 11:35 hello  <==这就是可执行档! -o 的结果
-rw-r--r--. 1 root root   71 Sep  4 11:32 hello.c
-rw-r--r--. 1 root root 1496 Sep  4 11:34 hello.o

[root@study ~]# ./hello
Hello World

这个步骤主要是利用 hello.o 这个目标档制作出一个名为 hello 的执行档,详细的 gcc 语法我们会在后续章节中继续介绍!透过这个动作后,我们可以得到 hello 及 hello.o 两个档案, 真正可以执行的是 hello 这个 binary program 喔! 或许你会觉得,咦!只要一个动作作出 a.out 就好了,干嘛还要先制作目标档再做成执行档呢? 呵呵!透过下个范例,你就可以知道为什么啦!

如果我们在一个主程式里面又呼叫了另一个副程式呢?这是很常见的一个程式写法, 因为可以简化整个程式的易读性!在底下的例子当中,我们以 thanks.c 这个主程式去呼叫 thanks_2.c 这个副程式,写法很简单:
thanks.c

#include <stdio.h>
int main(void)
{
        printf("Hello World
");
        thanks_2();
}

上面的 thanks_2(); 那一行就是呼叫副程式啦!
thanks_2.c

#include <stdio.h>
void thanks_2(void)
{
        printf("Thank you!
");
}

开始将源代码编译成为可执行的 binary file :

gcc -c thanks.c thanks_2.c
gcc -o thanks thanks.o thanks_2.o
./thanks
Hello World
Thank you!

知道为什么要制作出目标档了吗?由于我们的源代码档案有时并非仅只有一个档案,所以我们无法直接进行编译。 这个时候就需要先产生目标档,然后再以连结制作成为 binary 可执行档。另外,如果有一天,你更新了 thanks_2.c 这个档案的内容,则你只要重新编译 thanks_2.c 来产生新的 thanks_2.o ,然后再以连结制作出新的 binary 可执行档即可!而不必重新编译其他没有更动过的源代码档案。 这对于软件开发者来说,是一个很重要的功能,因为有时候要将偌大的源代码全部编译完成,会花很长的一段时间呢!

引入函数库
新版的 GCC 会主动帮你将所需要的函式库抓进来编译,所以不会出现怪异的错误讯息! 事实上,数学函式库使用的是 libm.so 这个函式库,你最好在编译的时候将这个函式库纳进去比较好~另外要注意, 这个函式库放置的地方是系统预设会去找的 /lib, /lib64 ,所以你无须使用底下的 -L 去加入搜寻的目录! 而 libm.so 在编译的写法上,使用的是 -lm (lib 简写为 l 喔!) 喔!因此就变成:

gcc sin.c -lm -L/lib -L/lib64 

-l :是‘加入某个函式库(library)’的意思,
m :则是 libm.so 这个函式库,其中, lib 与副档名(.a 或 .so)不需要写

所以 -lm 表示使用 libm.so (或 libm.a) 这个函式库的意思~至于那个 -L 后面接的路径呢?这表示: ‘我要的函式库 libm.so 请到 /lib 或 /lib64 里面搜寻!’

除了连结的函式库之外,你或许已经发现一个奇怪的地方,那就是在我们的 sin.c 当中第一行‘ #include <stdio.h>’,这行说的是要将一些定义资料由 stdio.h 这个档案读入,这包括 printf 的相关设定。这个档案其实是放置在 /usr/include/stdio.h 的!那么万一这个档案并非放置在这里呢?那么我们就可以使用底下的方式来定义出要读取的 include 档案放置的目录:

gcc sin.c -lm -I/usr/include

gcc简易用法

# 仅将源代码编译成为目标档,并不制作连结等功能:
[root@study ~]# gcc -c hello.c
# 会自动的产生 hello.o 这个档案,但是并不会产生 binary 执行档。

# 在编译的时候,依据作业环境给予最佳化执行速度
[root@study ~]# gcc -O hello.c -c
# 会自动的产生 hello.o 这个档案,并且进行最佳化喔!

# 在进行 binary file 制作时,将连结的函式库与相关的路径填入
[root@study ~]# gcc sin.c -lm -L/lib -I/usr/include
# 这个指令较常下达在最终连结成 binary file 的时候,
# -lm 指的是 libm.so 或 libm.a 这个函式库档案;
# -L 后面接的路径是刚刚上面那个函式库的搜寻目录;
# -I 后面接的是源代码内的 include 档案之所在目录。

# 将编译的结果输出成某个特定档名
[root@study ~]# gcc -o hello hello.c
# -o 后面接的是要输出的 binary file 档名

# 在编译的时候,输出较多的讯息说明
[root@study ~]# gcc -o hello hello.c -Wall
# 加入 -Wall 之后,程式的编译会变的较为严谨一点,所以警告讯息也会显示出来!

gcc参数

-o:指定生成可执行文件的名称。使用方法为:g++ -o afile file.cpp file.h ... (可执行文件不可与待编译或链接文件同名,否则会生成相应可执行文件且覆盖原编译或链接文件),如果不使用-o选项,则会生成默认可执行文件a.out。
-c:只编译不链接,只生成目标文件。
-g:添加gdb调试选项。

make

make 的功能是可以简化编译过程里面所下达的指令,同时还具有很多很方便的功能!
为什么要用 make
先来想像一个案例,假设我的执行档里面包含了四个源代码档案,分别是 main.c haha.c sin_value.c cos_value.c 这四个档案,这四个档案的目的是:

  • main.c :主要的目的是让使用者输入角度资料与呼叫其他三支副程式;
  • haha.c :输出一堆有的没有的讯息而已;
  • sin_value.c :计算使用者输入的角度(360) sin 数值;
  • cos_value.c :计算使用者输入的角度(360) cos 数值。

由于这四个档案里面包含了相关性,并且还用到数学函式在里面,所以如果你想要让这个程式可以跑, 那么就需要这样编译:

# 1. 先进行目标档的编译,最终会有四个 *.o 的档名出现:
[root@study ~]# gcc -c main.c
[root@study ~]# gcc -c haha.c
[root@study ~]# gcc -c sin_value.c
[root@study ~]# gcc -c cos_value.c

# 2. 再进行连结成为执行档,并加入 libm 的数学函式,以产生 main 执行档:
[root@study ~]# gcc -o main main.o haha.o sin_value.o cos_value.o -lm

# 3. 本程式的执行结果,必须输入姓名、360 度角的角度值来计算:
[root@study ~]# ./main 
Please input your name: VBird  <==这里先输入名字
Please enter the degree angle (ex> 90): 30   <==输入以 360 度角为主的角度
Hi, Dear VBird, nice to meet you.    <==这三行为输出的结果喔!
The Sin is:  0.50
The Cos is:  0.87

编译的过程需要进行好多动作啊!而且如果要重新编译,则上述的流程得要重新来一遍,光是找出这些指令就够烦人的了! 如果可以的话,能不能一个步骤就给他完成上面所有的动作呢?那就利用 make 这个工具吧! 先试看看在这个目录下建立一个名为 makefile 的档案,内容如下:

# 1. 先编辑 makefile 这个规则档,内容只要作出 main 这个执行档
[root@study ~]# vim makefile
main: main.o haha.o sin_value.o cos_value.o
	gcc -o main main.o haha.o sin_value.o cos_value.o -lm
# 注意:第二行的 gcc 之前是 <tab> 按键产生的空格喔!

# 2. 尝试使用 makefile 制订的规则进行编译的行为:
[root@study ~]# rm -f main *.o   <==先将之前的目标档去除
[root@study ~]# make
cc    -c -o main.o main.c
cc    -c -o haha.o haha.c
cc    -c -o sin_value.o sin_value.c
cc    -c -o cos_value.o cos_value.c
gcc -o main main.o haha.o sin_value.o cos_value.o -lm
# 此时 make 会去读取 makefile 的内容,并根据内容直接去给他编译相关的档案啰!

# 3. 在不删除任何档案的情况下,重新执行一次编译的动作:
[root@study ~]# make
make: `main‘ is up to date.
# 看到了吧!是否很方便呢!只会进行更新 (update) 的动作而已。

或许你会说:‘如果我建立一个 shell script 来将上面的所有动作都集结在一起,不是具有同样的效果吗?’呵呵! 效果当然不一样,以上面的测试为例,我们仅写出 main 需要的目标档,结果 make 会主动的去判断每个目标档相关的源代码档案,并直接予以编译,最后再直接进行连结的动作! 真的是很方便啊!此外,如果我们更动过某些源代码档案,则 make 也可以主动的判断哪一个源代码与相关的目标档档案有更新过, 并仅更新该档案,如此一来,将可大大的节省很多编译的时间呢!要知道,某些程式在进行编译的行为时,会消耗很多的 CPU 资源呢!所以说, make 有这些好处:

  • 简化编译时所需要下达的指令;
  • 若在编译完成之后,修改了某个源代码档案,则 make 仅会针对被修改了的档案进行编译,其他的 object file 不会被更动;
  • 最后可以依照相依性来更新 (update) 执行档。

既然 make 有这么多的优点,那么我们当然就得好好的了解一下 make 这个令人关心的家伙啦!而 make 里面最需要注意的大概就是那个规则档案,也就是 makefile 这个档案的语法啦!所以底下我们就针对 makefile 的语法来加以介绍啰。

{%y%}
脚本

有时配合脚本先执行一系列命令,例如:

sh make.sh
{%endy%}

{%y%}
使用多线程编译

make -j2

j2即双线程编译,一般和内核数保持一致是最快的。
{%endy%}

makefile

基本的 makefile 规则是这样的:

目标(target): 目标档1 目标档2
<tab>   gcc -o 欲建立的执行档 目标档1 目标档2

那个标的 (target) 就是我们想要建立的资讯,而目标档就是具有相关性的 object files ,那建立执行档的语法就是以 按键开头的那一行!特别给他留意喔,‘命令列必须要以 tab 按键作为开头’才行!他的规则基本上是这样的:

* 在 makefile 当中的 # 代表注解;
*  需要在命令行 (例如 gcc 这个编译器指令) 的第一个字元;
* 标的 (target) 与相依档案(就是目标档)之间需以‘:’隔开。

同样的,我们以刚刚上一个小节的范例进一步说明,如果我想要有两个以上的执行动作时, 例如下达一个指令就直接清除掉所有的目标档与执行档,该如何制作呢?

# 1. 先编辑 makefile 来建立新的规则,此规则的标的名称为 clean :
[root@study ~]# vi makefile
main: main.o haha.o sin_value.o cos_value.o
	gcc -o main main.o haha.o sin_value.o cos_value.o -lm
clean:
	rm -f main main.o haha.o sin_value.o cos_value.o

# 2. 以新的标的 (clean) 测试看看执行 make 的结果:
[root@study ~]# make clean  <==就是这里!透过 make 以 clean 为标的
rm -rf main main.o haha.o sin_value.o cos_value.o

如此一来,我们的 makefile 里面就具有至少两个标的,分别是 main 与 clean ,如果我们想要建立 main 的话,输入‘make main’,如果想要清除有的没的,输入‘make clean’即可啊!而如果想要先清除目标档再编译 main 这个程式的话,就可以这样输入:‘make clean main’,如下所示:

[root@study ~]# make clean main
rm -rf main main.o haha.o sin_value.o cos_value.o
cc    -c -o main.o main.c
cc    -c -o haha.o haha.c
cc    -c -o sin_value.o sin_value.c
cc    -c -o cos_value.o cos_value.c
gcc -o main main.o haha.o sin_value.o cos_value.o -lm

这样就很清楚了吧!但是,你是否会觉得,咦! makefile 里面怎么重复的资料这么多啊!没错!所以我们可以再借由 shell script 那时学到的‘变数’来更简化 makefile 喔:

[root@study ~]# vi makefile
LIBS = -lm
OBJS = main.o haha.o sin_value.o cos_value.o
main: ${OBJS}
        gcc -o main ${OBJS} ${LIBS}
clean:
        rm -f main ${OBJS}

与 bash shell script 的语法有点不太相同,变数的基本语法为:

  • 变数与变数内容以‘=’隔开,同时两边可以具有空格;
  • 变数左边不可以有 ,例如上面范例的第一行 LIBS 左边不可以是
  • 变数与变数内容在‘=’两边不能具有‘:’;
  • 在习惯上,变数最好是以‘大写字母’为主;
  • 运用变数时,以 ${变数} 或 $(变数) 使用;
  • 在该 shell 的环境变数是可以被套用的,例如提到的 CFLAGS 这个变数!
  • 在指令列模式也可以给予变数。

由于 gcc 在进行编译的行为时,会主动的去读取 CFLAGS 这个环境变数,所以,你可以直接在 shell 定义出这个环境变数,也可以在 makefile 档案里面去定义,更可以在指令列当中给予这个咚咚呢!例如:

[root@study ~]# CFLAGS="-Wall" make clean main
# 这个动作在上 make 进行编译时,会去取用 CFLAGS 的变数内容!

也可以这样:

[root@study ~]# vi makefile
LIBS = -lm
OBJS = main.o haha.o sin_value.o cos_value.o
CFLAGS = -Wall
main: ${OBJS}
	gcc -o main ${OBJS} ${LIBS}
clean:
	rm -f main ${OBJS}

咦!我可以利用指令列进行环境变数的输入,也可以在档案内直接指定环境变数,那万一这个 CFLAGS 的内容在指令列与 makefile 里面并不相同时,以那个方式输入的为主?呵呵!问了个好问题啊! 环境变数取用的规则是这样的:

  • make 指令列后面加上的环境变数为优先;
  • makefile 里面指定的环境变数第二;
  • shell 原本具有的环境变数第三。

此外,还有一些特殊的变数需要了解的喔:

  • $@:代表目前的标的(target)

所以我也可以将 makefile 改成:

[root@study ~]# vi makefile
LIBS = -lm
OBJS = main.o haha.o sin_value.o cos_value.o
CFLAGS = -Wall
main: ${OBJS}
	gcc -o $@ ${OBJS} ${LIBS}   <==那个 $@ 就是 main !
clean:
	rm -f main ${OBJS}

这样是否稍微了解了 makefile (也可能是 Makefile) 的基本语法?这对于你未来自行修改源代码的编译规则时,是很有帮助的喔!_

源码安装的基本步骤

Tarball是个压缩/解压工具。
我们提过以 Tarball 方式释出的软件是需要重新编译可执行的 binary program 的。而 Tarball 是以 tar 这个指令来打包与压缩的档案,所以啦,当然就需要先将 Tarball 解压缩,然后到源代码所在的目录下进行 makefile 的建立,再以 make 来进行编译与安装的动作啊!所以整个安装的基础动作大多是这样的:

  • 取得原始档:将 tarball 档案在 /usr/local/src 目录下解压缩;
  • 取得步骤流程:进入新建立的目录底下,去查阅 INSTALL 与 README 等相关档案内容 (很重要的步骤!);
  • 相依属性软件安装:根据 INSTALL/README 的内容察看并安装好一些相依的软件 (非必要);
  • 建立 makefile:以自动侦测程式 (configure 或 config) 侦测作业环境,并建立 Makefile 这个档案;
  • 编译:以 make 这个程式并使用该目录下的 Makefile 做为他的参数设定档,来进行 make (编译或其他) 的动作;
  • 安装:以 make 这个程式,并以 Makefile 这个参数设定档,依据 install 这个标的 (target) 的指定来安装到正确的路径!

注意到上面的第二个步骤,通常在每个软件在释出的时候,都会附上 INSTALL 或者是 README 这种档名的说明档,这些说明档请‘确实详细的’ 阅读过一遍,通常这些档案会记录这个软件的安装要求、软件的工作项目、 与软件的安装参数设定及技巧等,只要仔细的读完这些档案,基本上,要安装好 tarball 的档案,都不会有什么大问题啰。

至于 makefile 在制作出来之后,里头会有相当多的标的 (target),最常见的就是 install 与 clean 啰!通常‘make clean’代表着将目标档 (object file) 清除掉,‘make’则是将源代码进行编译而已。 注意喔!编译完成的可执行档与相关的设定档还在源代码所在的目录当中喔!因此,最后要进行‘make install’来将编译完成的所有咚咚都给他安装到正确的路径去,这样就可以使用该软件啦!

OK!我们底下约略提一下大部分的 tarball 软件之安装的指令下达方式:

  1. ./configure
    这个步骤就是在建立 Makefile 这个档案啰!通常程式开发者会写一支 scripts 来检查你的 Linux 系统、相关的软件属性等等,这个步骤相当的重要, 因为未来你的安装资讯都是这一步骤内完成的!另外,这个步骤的相关资讯应该要参考一下该目录下的 README 或 INSTALL 相关的档案!

  2. make clean
    make 会读取 Makefile 中关于 clean 的工作。这个步骤不一定会有,但是希望执行一下,因为他可以去除目标档案!因为谁也不确定源代码里面到底有没有包含上次编译过的目标档案 (*.o) 存在,所以当然还是清除一下比较妥当的。 至少等一下新编译出来的执行档我们可以确定是使用自己的机器所编译完成的嘛!

  3. make
    make 会依据 Makefile 当中的预设工作进行编译的行为!编译的工作主要是进行 gcc 来将源代码编译成为可以被执行的 object files ,但是这些 object files 通常还需要一些函式库之类的 link 后,才能产生一个完整的执行档!使用 make 就是要将源代码编译成为可以被执行的可执行档,而这个可执行档会放置在目前所在的目录之下, 尚未被安装到预定安装的目录中;

  4. make install
    通常这就是最后的安装步骤了,make 会依据 Makefile 这个档案里面关于 install 的项目,将上一个步骤所编译完成的资料给他安装到预定的目录中,就完成安装啦!

请注意,上面的步骤是一步一步来进行的,而其中只要一个步骤无法成功,那么后续的步骤就完全没有办法进行的! 因此,要确定每一的步骤都是成功的才可以!举个例子来说,万一今天你在 ./configure 就不成功了,那么就表示 Makefile 无法被建立起来,要知道,后面的步骤都是根据 Makefile 来进行的,既然无法建立 Makefile,后续的步骤当然无法成功啰!

另外,如果在 make 无法成功的话,那就表示原始档案无法被编译成可执行档,那么 make install 主要是将编译完成的档案给他放置到档案系统中的,既然都没有可用的执行档了,怎么进行安装? 所以啰,要每一个步骤都正确无误才能往下继续做!此外,如果安装成功, 并且是安装在独立的一个目录中,例如 /usr/local/packages 这个目录中好了,那么你就必需手动的将这个软件的 man page 给他写入 /etc/man_db.conf 里面去。

编译参数

gcc和g++时候使用的参数。
编译<pthrend.h>时需要附加 -pthread-lpthread
编译<stdlib.h>时需要附加 -lstdc++
编译带有c++11特性的代码时,需要附加-std=c++11,如,编译带有thread.h库的时候:

g++ main.cpp -std=c++11 -pthread

支持 auto 时需要添加--std=c++11

参考链接:
https://linux.vbird.org/linux_basic/centos7/0520source_code_and_tarball.php




























以上是关于gcc编译流程的主要内容,如果未能解决你的问题,请参考以下文章

gcc编译流程

GCC:编译成程序集并明确与代码的对应关系?

GCC编译器

C程序存储结构

OpenJDK系列:从JVM谈C/C++编译流程

OpenJDK系列:从JVM谈C/C++编译流程