C语言的GCC编译过程和静态库的建立
C 语言的编译过程是学习 C 工程的基础. 本文中, 我们以一个简单的 C 语言程序为例来介绍开源免费的 GCC 的编译过程, 同时给出了相应的 Makefile 源文件.
基本概念
头文件
用 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.
编译示例
文件目录
下面给出一个 C 语言示例, 以下是测试的源代码结构:
c/
├── include
│ └── mylib.h
├── makefile
├── program.c
└── source
├── bill.c
└── fred.c
这个测试文件的组织可以看成是更加复杂的工程的一个缩影, 一般情况下一个工程中会包含许多源文件和头文件, 如何组织它们也是我们需要注意的问题. 在实际应用中, 我们开发的工程在解决某个问题时, 通常不会同时用到工程中的所有函数或文件, 所以, 我们可以将需要的文件用软链接的方式放到 main 函数所在的文件夹中. 具体地, 以上述工程为例, 在命令行 (Terminal) 中输入如下语句建立链接文件bill.c@, fred.c@, mylib.h@
:
ln -s ./source/bill.c ./
ln -s ./source/fred.c ./
ln -s ./include/mylib.h ./
此时, 目录 c 中内容如下 (给出 makefile 作参考使用):
c/
├── bill.c -> ./source/bill.c
├── fred.c -> ./source/fred.c
├── include
│ └── mylib.h
├── makefile
├── mylib.h -> ./include/mylib.h
├── program.c
└── source
├── bill.c
└── fred.c
其中, ->
前后分别表示该链接文件的名字和指向文件. 如果编译链接文件其实就是在编译它所指向的原文件, 但是删除链接文件并不会删除原文件.
源文件
文件 program.c:
#include<stdlib.h>
#include<stdio.h>
#include"lib.h"
int main(){
int i=10;
bill("Hello World");
fred(i);
printf("Main: Age=%d\n", Age);
exit(0);
}
文件 mylib.h:
/*
This is lib.h. It declares the functions fred and bill for users
*/
#ifndef H_MYLIB_H
#define H_MYLIB_H
static int Age=100;
void bill(char *);
void fred(int);
#endif /* H_LIB_H */
文件 bill.c:
#include<stdio.h>
void bill(char* arg){
printf("bill: we passed %s\n", arg);
}
文件 fred.c:
#include<stdio.h>
#include"mylib.h"
void fred(int arg){
printf("fred: Age=%d\n", Age);
printf("fred: we passed %d\n", arg);
}
文件 makefile:
# This is a makefile for testing c program
CC = gcc
# option for release
# CFLAGS = -O -Wall -ansi
# Options for development
CFLAGS = -g -Wall -ansi
OBJS = program.o bill.o fred.o
# 目标文件
LIB = -L /usr/local/lib/
# path for c lib
LIBB = -lgsl -lgslcblas -lm
# standard c lib
EXECNAME = program
# exe file name, also name of main .c file
$(EXECNAME) : $(OBJS)
$(CC) $(OBJS) -o $(EXECNAME)
program.o : program.c
$(CC) $(CFLAGS) -c program.c
bill.o : $(SOURCE)bill.c
$(CC) $(CFLAGS) -c bill.c
fred.o : $(SOURCE)fred.c
$(CC) $(CFLAGS) -c fred.c
.PHONY : clean exe
exe :
./$(EXECNAME)
clean :
-rm -f $(OBJS);
-rm -f $(EXECNAME);
-rm -f *~
编译过程
以下是具体的编译过程 (也可以直接一个 make, 过程是一样的):
1. 编译源文件 bill.c 和 fred.c, 生成目标文件 bill.o 和 fred.o.
$ gcc -g -Wall -ansi -c bill.c
$ gcc -g -Wall -ansi -c fred.c
此时目录 c 内容如下:bill.c bill.o fred.c fred.o include makefile mylib.h program.c source
2. 编译 program.c 文件, 生成目标文件 program.o:
$ gcc -g -Wall -ansi -c program.c
注意. 如果没有利用链接文件的话, 需要指定头文件的目录, 例如:$ gcc -g -Wall -ansi -I./include -c program.c
此时目录 c 内容如下:bill.c bill.o fred.c fred.o include makefile mylib.h program.c program.o source
3. 所有目标文件都已生成, 现在链接目标文件 program.o, fred.o 和 bill.o 生成可执行文件program.
$ gcc program.o bill.o fred.o -o program
此时目录c内容如下:bill.c bill.o fred.c fred.o include makefile mylib.h program program.c program.o source
4. 程序执行. 此时没有后缀名的 program 就是我们编译生成的可执行程序, 利用下面的语句执行该程序:
$ ./program
至此, 编译和执行的过程也就结束了. 如果想从更专业的术语角度理解一般的编译过程, 请参考 gcc 的作用手册或网上的教程, 如, 跟我一起写Makefile.
使用 makefile
上一节给出的编译过程可以利用 makefile 来实现. 在利用 ln 创建链接文件之后, 我们可以在命令行输入如下语句$ make
这个 make 命令会检查当前目录中是否存在 makefile 或 Makefile 文件, 如果存在的话就执行它. 以我们的测试工程为例, 执行结果如下:
gcc -g -Wall -ansi -c program.c
gcc -g -Wall -ansi -c bill.c
gcc -g -Wall -ansi -c fred.c
gcc program.o bill.o fred.o -o program
上述命令正是上一节中我们给出的一步步的编译过程! 所以如果我们写好 makefile 文件, 只需要一个 make 就可以完成所有的编译任务. 因为我们在 makefile 定义了伪目标 exe (执行程序) 和 clean (删除中间文件), 所以我们可以利用如下语句执行程序$ make exe
并且在执行完后删除不必要的中间文件$ make clean
clean 之后的目录文件如下:bill.c fred.c include makefile mylib.h program.c source
即恢复了工程编译前的初始状态.
创建静态库
在理解 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的共享库, 如果存在的话).
总结
可以使用命令 nm 查看哪些函数被包含在目标文件, 函数库或者可执行文件中, 如
$ nm libfoo.a
# bill.o:
# 00000000 T bill
# U printf
# fred.o:
# 00000000 T fred
# U printf
静态库 libfoo.a 中的 bill.o 包含两个函数 bill 及 printf.
- 虽然程序中的头文件包含函数库中所有函数的声明, 但当程序被创建时, 它只包含函数库中它实际调用的函数.
- 要弄清楚一件事情很不容易, 但如果能够明白gcc的编译过程, 清楚从开始到产生可执行文件这中间每一步的前提和后果, 必将对以后的科研学习大有好处!
参考
- Linux程序设计 4th.
- 跟我一起写Makefile.
- GNU make 官方文档.