gcc编译过程示例及建立静态库

1.基本概念

①头文件: 用C语言及其他语言进行程序设计时, 你需要用头文件来提供对常量的定义和对系统函数及库函数调用的声明. 对C语言来说, 这些头文件几乎总是位于/usr/include及其子目录中. 那些依赖于特定Linux版本的头文件通常可在目录/usr/include/sys和/usr/include/linux中找到.
在调用C语言编译器时, 你可以使用-I来包含保存子目录或非标准位置的头文件. 例如:

$ gcc -I/usr/openwin/include fred.c
# 它指示编译器不仅在标准位置, 也在/usr/openwin/include目录中查找程序fred.c中包含的头文件.

②库文件: 库是一组预先编译好的函数的集合, 这些函数都是按照可重用的原则编写的. 它们通常由一组相互关联的函数组成心执行某项常见的任务, 比如屏幕处理函数(curse和ncurses库)和数据库访问例程(dbm库). 标准系统库文件一般存放在/lib和/usr/lib目录中. C语言编译器(或更确切地说是链接程序)需要知道要搜索哪些库文件, 因为在默认情况下, 它只搜索标准C语言库.
仅把库文件放在标准库目录里, 就希望编译器能够找到它是不够的, 库文件必须遵循特定的命名规范并且需要在命令行中明确指定. 库文件的名字总是以lib开头, 随后的部分指明这是什么库(例如, c代表C语言库, m代表数学库). 文件名的最后以.开始, 然后给出文件的类型:

.a代表传统的静态函数库;
.so代表共享函数库.

函数库通常以这两种格式存在. 你可以给出完整的库文件路径名或用-l标志来告诉编译器要搜索的库文件. 例如:

$ gcc -o fred fred.c /usr/lib/libm.a

这条命令告诉编译器编译文件fred.c, 将编译产生的程序文件命名为fred, 并且除了搜索标的C语言函数库以外,  还搜索数学库以解决函数引用问题. 下面的命令也能产生类似的结果:

$ gcc -o fred fred.c -lm
#  -lm(在l和m之间没有空格)是简写方式(简写在UNIX环境里很有用), 它代表的是标准库目录(本例中是/usr/lib)中名为libm.a的函数库. -lm标志的另一个好处是如果有共享库, 编译器会优先选择共享库.

虽然库文件和头文件一样, 通常都保存在标准位置, 但你也可以通过使用-L标志为编译器增加库的搜索路径. 例如:

$ gcc -o x11fred -L/usr/openwin/lib x11fred.c -lX11
# 这条命令用/usr/openwin/lib目录中的libX11库版本来编译和链接程序x11fred.

2.编译示例:

下面给出一个C语言示例, 其中$后为命令, #后为注释, 对理解整个过程非常重要. 以下是测试的源代码文件结构及内容:

fred.c, bill.c –./source—-|
lib.h  ———-./include—|———c
program.c——————|
makefile——————–|

目录c中内容如下(给出makefile作参考使用):
include   makefile  program.c   source

#include <stdio.h>
#include <stdlib.h>
#include "lib.h"

int main(){
    bill("Hello World");
    printf("%d\n",LIB);
    exit(0);
}
/*
This is lib.h. It declares the functions fred and bill for users
*/

int LIB=100;
void bill(char *);
void fred(int);
#include <stdio.h>
void bill(char* arg){
    printf("bill: we passed %s\n", arg);
}
#include <stdio.h>

void fred(int arg){
    printf("fred: we passed %d\n", arg);
}
CC = gcc
CFLAGS = -g -Wall -ansi

OBJS = program.o bill.o
EXECNAME = program

$(EXECNAME) : $(OBJS)
    $(CC) $(OBJS) -o $(EXECNAME)
program.o : program.c
    $(CC) $(CFLAGS) -I./include -c program.c
bill.o : ./source/bill.c
    $(CC) $(CFLAGS) -c ./source/bill.c

.PHONY : clean
clean :
-rm -f $(OBJS);
-rm -f $(EXECNAME)
-rm -f *~

以下是具体的编译过程(也可以直接一个make, 过程是一样的):

$ gcc  -g -Wall -ansi -c ./source/bill.c ./source/fred.c
# 编译源文件bill.c和fred.c, 生成目标文件bill.o和fred.o. 此时目录c内容如下:
# bill.o  include  fred.o  makefile  program.c  source
# 至此, 主程序文件program.c中调用的函数bill已经存在于文件bill.o中了.

$ gcc  -g -Wall -ansi -I./include -c program.c
# 编译program.c文件, 生成program.o目标文件. 此时目录c内容如下:
# bill.o  include  program.o fred.o  makefile  program.c  source
# 如果没有-I选项会出错(自己试验一下), 因为gcc在标准头文件目录找不到它引用的函数bill的声明;
# 需要注意的是, 这一步主函数只需要其引用函数bill的声明, 而不需要目标文件bill.o, 所以这一步可以放在上一步之前而没有任何问题;
# 如果program.c内包含对函数bill的声明, 也就不需要-I选项了.

$ gcc program.o bill.o -o program
# 链接目标文件program.o和bill.o生成可执行文件program. 此时目录c内容如下:
# bill.o  include  program.o fred.o  makefile  program program.c  source

$ ./program
# 执行.

至此, 编译过程也就结束了, 如果想从更专业的术语角度理解一般的编译过程, 请参考gcc的使用手册或网上高手的一些文章.

3. 创建静态库

在理解gcc编译过程之后, 我们可以改进一下上述过程: 将函数bill及fred放入某个静态库中, 以供其它函数重复调用. 也就是说我们希望上述过程的链接命令用下面的命令代替:

$ gcc -o program program.o libfoo.a

其中libfoo.a就是我们接下来要建立的静态库. 在前一过程中, 我们已经产生了文件bill.o及fred.o, 现在在这两个文件所在目录执行如下命令:

$ ar crv libfoo.a bill.o fred.o
$ ranlib libfoo.a
# 第二命令的意义是为了兼顾某些系统, 但如果在使用GNU软件时, 这一步骤不是必需的(做了也无妨).

这样库文件就创建好了(在目录c下), 两个目标文件已添加进去, 库文件libfoo.h就可以使用了. 接着, 我们就来看一下怎么使用它. 假设文件夹c下已经存在目标文件program.o(与库libfoo.a在同一目录下), 目录c如下所示:
include  libfoo.a  makefile  program.c  program.o  source
现在就可以在编译器的文件列表中添加库文件心创建程序:

$ gcc -o program program.o libfoo.a
$ ./program

$ gcc -o program program.o -L. -lfoo
# -L.选项(注意L后有个点)告诉编译器在当前目录(.)中查找库.
# -lfoo选项告诉编译器使用名为libfoo.a的函数库(或者名为libfoo.so的共享库, 如果存在的话).

4. 总结

①可以使用命令nm查看哪些函数被包含在目标文件, 函数库或者可执行文件中, 如

$ nm libfoo.a
bill.o:
00000000 T bill
U printf

fred.o:
00000000 T fred
U printf
# 静态库libfoo.a中的bill.o包含两个函数bill及printf.

②虽然程序中的头文件包含函数库中所有函数的声明, 但当程序被创建时, 它只包含函数库中它实际调用的函数. 例如, 程序program中, 不会包含库libfoo.a中的函数fred, 因为没有调用.
③要弄清楚一件事情很不容易, 但如果能够明白gcc的编译过程, 清楚从开始到产生可执行文件这中间每一步的前提和后果, 必将对以后的科研学习大有好处!

(注: 部分内容摘自Linux程序设计4th, 做了些许修改)