Linux【8】-软件管理-1-2-使用传统程式语言进行编译的简单范例(gcc)
一、单一程式:印出Hello World
我们以Linux上面最常见的C语言来撰写第一支程式!第一支程式最常作的就是…..在萤幕上面印出『Hello World!』的字样~
注:请先确认你的Linux 系统里面已经安装了gcc 了喔!如果尚未安装 gcc 的话,请先参考下一节的RPM 安装法,先安装好gcc 之后,再回来阅读本章。如果你已经有网路了,那么直接使用『 yum groupinstall “Development Tools” 』 预先安装好所需的所有软体即可。rpm 与yum 均会在下一章介绍。
编辑程式码,亦即原始码
[root@study ~]# vim hello.c <==用C语言写的程式副档名建议用.c
#include <stdio.h>
int main(void)
{
printf("Hello World\n");
}
上面是用C 语言的语法写成的一个程式档案。第一行的那个『 # 』并不是注解喔!
开始编译与测试执行
[root@study ~]# gcc hello.c
[root@study ~]# ll hello.c a.out
-rwxr-xr-x . 1 root root 8503 Sep 4 11:33 a.out <==此时会产生这个档名
-rw-r--r--. 1 root root 71 Sep 4 11:32 hello.c
[root@study ~]# ./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
二、主、副程式连结:副程式的编译
如果我们在一个主程式里面又呼叫了另一个副程式呢?这是很常见的一个程式写法, 因为可以简化整个程式的易读性!在底下的例子当中,我们以 thanks.c 这个主程式去呼叫thanks_2.c 这个副程式,写法很简单:
撰写所需要的主、副程式
# 1.编辑主程式:
[root@study ~]# vim thanks.c
#include <stdio.h>
int main(void)
{
printf("Hello World\n");
thanks_2();
}
#上面的thanks_2();那一行就是呼叫副程式啦!
[root@study ~]# vim thanks_2.c
#include <stdio.h>
void thanks_2(void)
{
printf("Thank you!\n");
}
进行程式的编译与连结(Link)
# 2.开始将原始码编译成为可执行的binary file :
[root@study ~]# gcc -c thanks.c thanks_2.c
[root@study ~]# ll thanks*
-rw-r--r--. 1 root root 75 Sep 4 11:43 thanks_2.c
-rw-r--r--. 1 root root 1496 Sep 4 11:43 thanks_2.o <==编译产生的!
-rw-r--r--. 1 root root 91 Sep 4 11:42 thanks.c
-rw-r--r--. 1 root root 1560 Sep 4 11:43 thanks.o <==编译产生的!
[root@study ~]# gcc -o thanks thanks.o thanks_2.o
[root@study ~]# ll thanks*
-rwxr-xr-x. 1 root root 8572 Sep 4 11:44 thanks <==最终结果会产生这玩意儿
# 3.执行一下这个档案:
[root@study ~]# ./thanks
Hello World
Thank you!
知道为什么要制作出目标档了吗?由于我们的原始码档案有时并非仅只有一个档案,所以我们无法直接进行编译。这个时候就需要先产生目标档,然后再以连结制作成为binary可执行档。另外,如果有一天,你更新了thanks_2.c这个档案的内容,则你只要重新编译thanks_2.c来产生新的thanks_2.o ,然后再以连结制作出新的binary可执行档即可!而不必重新编译其他没有更动过的原始码档案。这对于软体开发者来说,是一个很重要的功能,因为有时候要将偌大的原始码全部编译完成,会花很长的一段时间呢!
此外,如果你想要让程式在执行的时候具有比较好的效能,或者是其他的除错功能时, 可以在编译的过程里面加入适当的参数,例如底下的例子:
[root@study ~]# gcc -O -c thanks.c thanks_2.c <== -O为产生最佳化的参数
[root@study ~]# gcc -Wall -c thanks.c thanks_2.c
thanks.c: In function 'main':
thanks.c:5:9: warning: implicit declaration of function 'thanks_2' [-Wimplicit-function-declaration]
thanks_2();
^
thanks.c:6:1: warning: control reaches end of non-void function [-Wreturn-type]
}
^
# -Wall 为产生更详细的编译过程资讯。上面的讯息为警告讯息(warning) 所以不用理会也没有关系!
至于更多的gcc 额外参数功能,就得要man gcc 啰~呵呵!可多的跟天书一样~
三、 呼叫外部函式库:加入连结的函式库
刚刚我们都仅只是在萤幕上面印出一些字眼而已,如果说要计算数学公式呢?例如我们想要计算出三角函数里面的 sin (90度角)。要注意的是,大多数的程式语言都是使用径度而不是一般我们在计算的『角度』, 180 度角约等于3.14 径度!嗯!那我们就来写一下这个程式吧!
[root@study ~]# vim sin.c
#include <stdio.h>
#include <math.h>
int main(void)
{
float value;
value = sin ( 3.14 / 2 );
printf("%f\n",value);
}
那要如何编译这支程式呢?我们先直接编译看看:
[root@study ~]# gcc sin.c
#新的GCC会主动将函数抓进来给你用,所以只要加上include <math.h>就好了!
新版的GCC 会主动帮你将所需要的函式库抓进来编译,所以不会出现怪异的错误讯息!事实上,数学函式库使用的是libm.so 这个函式库,你最好在编译的时候将这个函式库纳进去比较好~另外要注意, 这个函式库放置的地方是系统预设会去找的/lib, /lib64 ,所以你无须使用底下的-L 去加入搜寻的目录!而libm.so 在编译的写法上,使用的是-lm (lib 简写为l 喔!) 喔!因此就变成:
编译时加入额外函式库连结的方式:
[root@study ~]# gcc sin.c -lm -L/lib -L/lib64 <==重点在-lm
[root@study ~]# ./a.out <==尝试执行新档案!
1.000000
特别注意,使用gcc 编译时所加入的那个-lm 是有意义的,他可以拆开成两部份来看:
-l :是『加入某个函式库(library)』的意思,
m :则是libm.so 这个函式库,其中, lib 与副档名(.a 或.so)不需要写
所以-lm表示使用libm.so (或libm.a)这个函式库的意思~至于那个-L后面接的路径呢?这表示: 『我要的函式库libm.so请到/lib或/lib64里面搜寻!』
上面的说明很清楚了吧!不过,要注意的是,由于Linux 预设是将函式库放置在/lib 与/lib64 当中,所以你没有写-L/lib 与-L/lib64 也没有关系的!不过,万一哪天你使用的函式库并非放置在这两个目录下,那么 -L/path 就很重要了!否则会找不到函式库喔!
除了连结的函式库之外,你或许已经发现一个奇怪的地方,那就是在我们的sin.c当中第一行
『#include <stdio.h>』
这行说的是要将一些定义资料由stdio.h这个档案读入,这包括printf的相关设定。这个档案其实是放置在/usr/include/stdio.h的!那么万一这个档案并非放置在这里呢?那么我们就可以使用底下的方式来定义出要读取的include档案放置的目录:
[root@study ~]# gcc sin.c -lm -I/usr/include
-I/path 后面接的路径( Path )就是设定要去搜寻相关的 include 档案的目录啦!不过,同样的,预设值是放置在/usr/include 底下,除非你的 include 档案放置在其他路径,否则也可以略过这个项目!
四、gcc 的简易用法(编译、参数与链结)
前面说过, gcc为Linux上面最标准的编译器,这个gcc是由GNU计画所维护的,有兴趣的朋友请自行前往参考。既然gcc对于Linux上的Open source是这么样的重要,所以底下我们就列举几个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之后,程式的编译会变的较为严谨一点,所以警告讯息也会显示出来!
比较重要的大概就是这一些。另外,我们通常称-Wall或者-O这些非必要的参数为旗标(FLAGS),因为我们使用的是C程式语言,所以有时候也会简称这些旗标为CFLAGS ,这些变数偶尔会被使用的喔
四、讨论
4.1 我们用gcc编译程序时,可能会用到“-I”(大写i),“-L”(大写l),“-l”(小写l)
例子1:
gcc -o example1 example1.c -I /usr/local/include/freetype2 -lfreetype -lm
上面这句话在编译example1.c 时,-I /usr/local/include/freetype2 表示将/usr/local/include/freetype2作为第一个寻找头文件的目录。
-lfreetype ,-l (小写的l)参数就是用来指定程序要链接的库,-l参数紧接着就是库名。指定程序链接的库名是freetype。那么库名跟真正的库文件名有什么关系呢?就拿数学库来说,他的库名是m,他的库文件名是libm.so,很容易看出,把库文件名的头lib和尾.so去掉就是库名了。
-lm 表示程序指定的链接库名是m (math数学库)
例2:
gcc -o hello hello.c -I /home/hello/include -L /home/hello/lib -lworld
上面这句表示在编译hello.c时:
-
-I /home/hello/include 表示将/home/hello/include目录作为第一个寻找头文件的目录,寻找的顺序是:/home/hello/include–>/usr/include–>/usr/local/include
-
-L /home/hello/lib 表示将/home/hello/lib目录作为第一个寻找库文件的目录,寻找的顺序是:/home/hello/lib–>/lib–>/usr/lib–>/usr/local/lib
-
-lworld表示在上面的lib的路径中寻找libworld.so动态库文件(如果gcc编译选项中加入了“-static”表示寻找libworld.a静态库文件),程序链接的库名是world
-include用来包含头文件,但一般情况下包含头文件都在源码里用#include xxxxxx实现,-include参数很少用。-I参数是用来指定头文件目录,/usr/include目录一般是不用指定的,gcc知道去那里找,但是如果头文件不在/usr/include里我们就要用-I参数指定了,比如头文件放在/myinclude目录里,那编译命令行就要加上-I/myinclude参数了,如果不加你会得到一个"xxxx.h: No such file or directory"的错误。-I参数可以用相对路径,比如头文件在当前目录,可以用-I.来指定
4.2 Linux添加头文件路径—INCLUDE_PATH
对所有用户有效在/etc/profile增加以下内容。 如果只对当前用户有效在Home目录下的.bashrc或.bash_profile里增加下面的内容: (注意:等号前面不要加空格,否则可能出现 command not found)
#在PATH中找到可执行文件程序的路径。
export PATH =$PATH:$HOME/bin
#gcc找到头文件的路径
C_INCLUDE_PATH=/usr/include/libxml2:/MyLib
export C_INCLUDE_PATH
#g++找到头文件的路径
CPLUS_INCLUDE_PATH=$CPLUS_INCLUDE_PATH:/usr/include/libxml2:/MyLib
export CPLUS_INCLUDE_PATH
#找到动态链接库的路径
LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/MyLib
export LD_LIBRARY_PATH
#找到静态库的路径
LIBRARY_PATH=$LIBRARY_PATH:/MyLib
export LIBRARY_PATH
CPATH环境变量同时支持C语言和C++语言,并且优先级高于以上2个环境变量。
通过命令行添加,使用命令行添加,home优先于tmp:
gcc -I/home -I/tmp main.c
g++ -I/home -I/tmp main.cpp
1、查看gcc的include路径命令:
`gcc -print-prog-name=cc1` -v
`g++ -print-prog-name=cc1` -v
2、查看g++的include路径命令:
`gcc -print-prog-name=cc1plus` -v
`g++ -print-prog-name=cc1plus` -v
4.3 include头文件位置
本文介绍在linux中头文件的搜索路径,也就是说你通过include指定的头文件,linux下的gcc编译器它是怎么找到它的呢。在此之前,先了解一个基本概念。
头文件是一种文本文件,使用文本编辑器将代码编写好之后,以扩展名.h保存就行了。头文件中一般放一些重复使用的代码,例如函数声明、变量声明、常数定义、宏的定义等等。当使用#include语句将头文件引用时,相当于将头文件中所有内容,复制到#include处。#include有两种写法形式,分别是:
#include"stdio.h"
#include<math.h>
但是这两种形式是有区别的:使用尖括号表示在包含文件目录中去查找(包含目录是由用户在设置环境时 设置的),而不在源文件目录去查找;使用双引号则表示首先在当前的源文件目录中查找,若未找到才到包含目录中去查找。用户编程时可根据自己文件所在的目录来选择某一种命令形式。
- 一个include命令只能指定一个被包含文件,若有多个文件要包含,则需用多个include命令。
- 6I9文件包含允许嵌套,即在一个被包含的文件中又可以包含另一个文件。
#include文件可能会带来一个问题就是重复应用,如a.h引用的一个函数是某种实现,而b.h引用的这个函数却是另外一种实现,这样在编译的时候将会出现错误。所以,为了避免因为重复引用而导致的编译错误,头文件常具有:
#ifndef LABEL
#define LABEL
//代码部分
#endif
的格式。其中LABEL为一个唯一的标号,命名规则跟变量的命名规则一样。常根据它所在的头文件名来命名,例如,如果头文件的文件名叫做hardware.h,那么可以这样使用:
#ifndef __HARDWARE_H__
#define __HARDWARE_H__
//代码部分
#endif
这样写的意思就是,如果没有定义__HARDWARE_H__,则定义__HARDWARE_H__,并编译下面的代码部分,直到遇到#endif。这样当重复引用时,由于__HARDWARE_H__已经被定义,则下面的代码部分就不会被编译了,这样就避免了重复定义。
一句话,头文件事实上只是把一些常用的命令集成在里面,你要用到哪方面的命令就载入哪个头文件就可以了。
gcc寻找头文件的路径(按照1->2->3的顺序)
1.在gcc编译源文件的时候,通过参数-I指定头文件的搜索路径,如果指定路径有多个路径时,则按照指定路径的顺序搜索头文件。命令形式如:“gcc -I /path/where/theheadfile/in sourcefile.c“,这里源文件的路径可以是绝对路径,也可以是相对路径。eg:
设当前路径为/root/test,include_test.c如果要包含头文件“include/include_test.h“,有两种方法:
-
include_test.c中#include “include/include_test.h”或者#include “/root/test/include/include_test.h”,然后gcc include_test.c即可
-
include_test.c中#include <include_test.h>或者#include <include_test.h>,然后gcc –I include include_test.c也可
2.通过查找gcc的环境变量C_INCLUDE_PATH/CPLUS_INCLUDE_PATH/OBJC_INCLUDE_PATH来搜索头文件位置。
3.再找内定目录搜索,分别是
/usr/include
/usr/local/include
/usr/lib/gcc-lib/i386-linux/2.95.2/include
最后一行是gcc程序的库文件地址,各个用户的系统上可能不一样。
gcc在默认情况下,都会指定到/usr/include文件夹寻找头文件。
gcc还有一个参数:-nostdinc,它使编译器不再系统缺省的头文件目录里面找头文件,一般和-I联合使用,明确限定头文件的位置。在编译驱动模块时,由于非凡的需求必须强制GCC不搜索系统默认路径,也就是不搜索/usr/include要用参数-nostdinc,还要自己用-I参数来指定内核头文件路径,这个时候必须在Makefile中指定。
4.当#include使用相对路径的时候,gcc最终会根据上面这些路径,来最终构建出头文件的位置。如#include <sys/types.h>就是包含文件/usr/include/sys/types.h
五、报错
5.1 /usr/bin/ld: cannot find -lxxx
lxxx的全称应该是 libxxx.so
放在/lib和/usr/lib和/usr/local/lib里的库直接用-l参数就能链接了,但如果库文件没放在这三个目录里,而是放在其他目录里,这时我们只用-l参数的话,链接还是会出错,出错信息大概是:“/usr/bin/ld: cannot find -lxxx”,也就是链接程序ld在那3个目录里找不到libxxx.so,这时另外一个参数-L就派上用场了,比如常用的X11的库,它放在/usr/X11R6/lib目录下,我们编译时就要用-L /usr/X11R6/lib -lX11参数,-L参数跟着的是库文件所在的目录名。再比如我们把libtest.so放在/aaa/bbb/ccc目录下,那链接参数就是-L/aaa/bbb/ccc -ltest
5.2 编译gcc时,如果遇到下面这个错误:
fatal error: gnu/stubs-32.h: No such file or directory
这是因为在x86_64上,默认会编译出32位和64位两个版本。这样编译32位时,需要机器上有32位的libc头文件和库文件,但一些机器上可能没有,比如没有/lib目录,只有/lib64目录,这表示不支持32位的libc。为解决这个问题,可以禁止编译32位版本,在configure时带上参数–disable-multilib,或者安装32位版本的glibc。
参考资料
个人公众号,比较懒,很少更新,可以在上面提问题,如果回复不及时,可发邮件给我: tiehan@sina.cn