概述
在Linux系统中,应用程序表现为两种文件,一种是可执行文件, 另一种是脚本文件。
可执行文件
可执行文件是计算机可以直接执行的程序,与windows系统的.exe
程序相似,它是由源代码经过一定的手段翻译成计算机能够读懂的二进制编码,由计算机直接去执行,这个翻译的过程就称之为编译。
脚本文件
脚本文件是一系列指令的组合,由另一个解释器来执行,相当于windows系统的.bat
文件。
与windows系统不同之处在于,windows系统通常由后缀来决定该文件是什么文件,而Linux系统则与后缀无关,而是由文件的权限来决定的。可以有后缀,也可以没有后缀。
关于文件的权限相关,会在后续的文章中展开讨论,此处就不多做说明了。
Linux应用程序目录结构
一级目录 | 二级目录 | 三级目录 | 说明 |
---|---|---|---|
/bin | 二进制文件目录,用于存放启动时用到的程序 | ||
/usr | bin | 用户二进制目录,用于存放用户使用的标准程序 | |
/usr | local | bin | 本地二进制目录,用于存放软件安装的程序 |
/usr | sbin | root用户登录后的PATH管理目录 | |
/opt | 第三方应用程序安装目录 |
编译器初探
将源代码转换成计算机能够识别的二进制编码的过程称之为编译,编译使用的工具即为编译器。
在POSIX兼容的系统中,C语言编译器被称为c89,简称为cc。在Unix系统中,普遍使用的都是cc编译器。而我们这里讨论的gcc编译器是随着Linux发行版一起提供的,全称是GNU C编译器。
普通程序的编译
关于编译器的使用,不妨通过一个简单的例子来说明。
我们简单的写出一个C程序如下,该程序即编程者的入门程序hello.c
:
#include <stdio.h>
int main(void)
{
printf("hello world\n");
return 0;
}
该程序的编译命令如下:
$ gcc -o hello hello.c
执行以上命令后,会在该目录下生成一个名为hello
的可执行文件,使用以下命令即可执行该程序:
$ ./hello
hello world
下面来对该编译命令做一下说明:
-o
紧跟着的是指定编译后可执行文件的名称,上述命令中,-o hello
即指定hello
为可执行文件。如果不指定-o
, 则默认生成一个叫做a.out
的可执行文件。即:上述编译命令直接写成gcc hello.c
也是没有问题的,不过可执行文件变成了a.out
,执行该程序就是:
$ ./a.out
hello world
hello.c
是需要编译的源文件,该文件是开发者写好的代码,可以有多个,也可以只有一个。- 在执行应用程序中,
./
代表的是当前目录,如果不加./
,系统会默认到PATH
环境变量中去寻找,如果找不到则会报错,如果恰巧找到了一个同名的可执行程序,程序会执行,但得到的结果并不是我们所需要的,因为它执行的并不是我们编译好的可执行程序。
链接头文件
使用C语言进行程序设计时,需要链接头文件进行系统及库函数的调用,这时候就需要链接头文件。Linux系统常用的系统头文件都存放在/usr/include
目录下,该目录能够被gcc编译器自行检索到并主动链接到程序里。
但对于有些用户自定义的头文件,编译器并不能搜索到其目录,这时候就需要在编译的时候去指定需要链接的头文件的路径。链接头文件的命令是在编译的时加上大写的-I
,后面紧跟头文件的路径。如下所示:
$ gcc -o fred -I/usr/openwin/include fred.c
注意:-I
和头文件路径之间不要有空格。
库文件
库是一组预先编译好的函数组合,其特点是可重用性好,通常由一组相互关联的函数组成,用以执行某项任务。
系统的标准库函数存放在/lib
或者/usr/lib
目录下,与头文件必须以.h
作为后缀一样,库函数同样也需要遵循一些规范。
库函数必须以lib
作为开头,以.a
或者.so
作为结尾。其中,.a
代表静态函数库,.so
代表动态函数库。比较典型的比如:libc.a
、libm.a
即代表标准C函数库和标准数学函数库。
静态编译
由于历史原因,编译器仅能识别标准c函数库,部分系统库函数,即使已经放在/usr/lib
目录下,编译器仍然不能够识别,这时候就需要在编译的时候告诉编译器使用的是哪个库函数,编译时可以通过给出完整的静态库绝对路径的方式,或使用-l
标志来告诉编译器你所使用的静态库函数。如:
$ gcc -o fred fred.c /usr/lib/libm.a
$ gcc -o fred fred.c -lm
以上两个命令达到的效果是一样的,可以通过ls /usr/lib
命令来查看系统的库函数。
除此之外,用户也可以自己定义库函数,链接非标准位置的自定义库函数可以通过大写的-L
标志来实现,命令如下:
$ gcc -o xllfred -L/usr/openwin/lib xllfred.c -lxll
创建静态库
使用ar
命令(archive)可以很容易地创建属于自己的静态库。ar
命令一般对.o
的目标文件进行操作,目标文件可以由gcc -c
命令得到。
下面就以一个具体的例子来说明一下。
首先,我们有如下两个源程序文件:
$ ls
main.c print.c test.h
$ pg print.c
#include <stdio.h>
int print()
{
printf("Hello world\n");
return 0;
}
$ pg main.c
#include "test.h"
int main()
{
print();
return 0;
}
$ pg test.h
int print();
先通过gcc -c
命令将其编译成.o
文件:
$ gcc -c *.c
$ ls
main.c main.o print.c print.o test.h
我们可以看到两个.o
的目标文件已经成功生成。
这时候,如果我们使用以下命令,是可以直接编译成功的:
$ gcc -o test *.o
$ ls
main.c main.o print.c print.o test.h test
$ ./test
Hello world
但是这里由于我们是要创建静态库,所以可以使用ar
命令来创建一个归档文件:
$ ar crv libtest.a print.o
a - test2.o
$ ls
libtest.a main.c main.o print.c print.o test test.h
可以看到,静态库libtest.a已经成功创建,这时,还需要使用ranlib
命令来生成一个内容表,这一步在Unix系统中必不可少,但在Linux中,当使用的是GUN开发工具时,这一步可以省略。以上步骤完成后,即可以使用下面的命令来编译程序:
$ ranlib libtest.a
$ gcc -o testa main.o -L./ -ltest
$ ls
libtest.a main.c main.o print.c print.o test testa test.h
$ ./testa
Hello world
通过以上案例,可以发现得到的效果其实是一样的。
当然,也可以使用以下命令,得到相同的效果(这里因为没有链接头文件,会报一个错,但是结果没有影响):
$ gcc -o testb main.c -L./ -ltest
$ ls
libtest.a main.c main.o print.c print.o test testa testb test.h
$ ./testb
Hello world
动态编译
静态库有一个局限性,当同时运行多个程序,并且这些程序都去调用同一个静态库函数时,就会出现多个函数库副本,会占用大量的虚拟内存和磁盘空间,这时候,动态库(又叫共享库)可以解决这个问题。
当程序使用动态库时,其链接方式是这样的:程序本身不包含代码,而是引用运行时可访问的共享代码。即,只有在必要的时候,才会加载到内存中。
即,可以简单的理解如下:静态库在编译时就已经把内存给分配好了,每一次调用,都会分配一份内存。而动态库是在运行时才会去访问共享代码分配的内存,永远只会存在一份,因此不会出现内存浪费。
动态库的格式是以.so
结尾,比如典型的有/lib/libm.so
。
装载和解析动态库的工具是ld.so
,如果需要搜索标准位置以外的动态库,则需要在/etc/ld.so.conf
中进行配置,然后执行ldconfig
来处理它。
可以使用ldd
命令来查看程序所需要的动态库文件,如:
$ ldd test
linux-vdso.so.1 => (0x00007ffc0e4a8000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd26176b000)
/lib64/ld-linux-x86-64.so.2 (0x000056170c352000)
静态库与动态库的一个明显的区别在于:使用静态库编译完成后,静态库被删除,不会影响可执行文件的执行,但是如果删除了动态库,可执行文件执行就会报错。
创建动态库
仍然是看上面这个程序:
$ ls
libtest.a main.c main.o print.c print.o test testa testb test.h
动态编译的命令是针对共享代码进行操作的,命令如下:
$ gcc -shared -fPIC -o libtest.so print.c
$ ls
libtest.a libtest.so main.c main.o print.c print.o test testa testb test.h
这样libtest.so
就已经生成了,编译时链接如下:
$ gcc -o testc main.c ./libtest.so
$ ls
libtest.a libtest.so main.c main.o print.c print.o test testa testb testc test.h
$ ./testc
Hello world
程序的编译过程
C程序的编译共有四个步骤组成,分别为:
- 预处理
- 编译
- 汇编
- 链接
为了可以直观的说明这个过程,我们不妨使用一个案例演示一下:
假设有两个文件,头文件hello.h
和源文件 hello.c
:
$ ls
hello.c hello.h
$ pg hello.h
# include<stdio.h>
$ pg hello.c
#include "hello.h"
int main()
{
printf("Hello world\n");
return 0;
}
预处理
预处理主要完成的工作是去注释、头文件包含和宏替换,该步骤并不会检查语法。预处理命令为:
$ gcc -E -I./ hello.c -o hello.i
$ ls
hello.c hello.h hello.i
预处理完成后,会由.c
文件生成一个.i
文件。
编译
编译步骤完成的功能是将预处理之后的程序转换为汇编语言代码。编译命令如下:
$ gcc -S hello.i -o hello.s
$ ls
hello.c hello.h hello.i hello.s
这一步会将.i
文件生成为.s
格式的汇编语言代码。在该步骤中会检查语法,如果语法有错误,会在这一步报出来。
汇编
汇编就是将汇编语言程序处理成二进制目标文件。其命令如下:
$ gcc -c hello.s -o hello.o
$ ls
hello.c hello.h hello.i hello.o hello.s
或者使用以下命令:
$ as hello.s -o hello.o
$ ls
hello.c hello.h hello.i hello.o hello.s
这两个命令实际是等价的。
链接
链接是最后一步,即将多个目标文件,或者静态库文件(.a)以及动态库文件(.so)链接成最后的可执行程序的过程。其命令如下:
$ gcc -o hello hello.o
$ ls
hello hello.c hello.h hello.i hello.o hello.s
$ ./hello
Hello world
如果使用了动态库,可能使用到ld
链接命令。
通过以上分析发现,看似简单的一条编译命令,其内部完成的步骤是相当复杂的,能够理解编译器编译的原理,对编程是有相当大的帮助的。当然实际开发过程中,只需要使用以下编译命令一步到位,它完成了以上所有四步命令的所有过程:
$ gcc -o hello hello.c
$ ./hello
hello world
结语
关于gcc
编译命令还有很多,这里就不多做介绍了,如果有需要,可以通过man gcc
或者info gcc
来获取帮助。