c多模块
多模块软件编译和链接
编译和运行过程
- 编译和运行过程图示
源文件(.c) === c编译器 ===> 目标文件(.o) === 连接器(linker) ===> 可执行文件
/usr/lib: +
所有标准函数库文件 函数库
/usr/include +
所有标准头文件。 其他目标文件
+
针对不同系统的启动代码
编译环境
- 将c源程序转换为特定平台的可执行程序
- 预处理阶段 -> 编译阶段 -> 汇编阶段 -> 链接阶段
运行环境
- 运行可执行程序的环境
编译流程
预处理阶段
gcc -E -o example.i example.c
- 预处理指令的处理,如,将声明的头文件中的内容加到生成的
.i
文件中
编译阶段
gcc -S -o example.s example.i
- 将
.i
文件内容编译称汇编语言后生成.s
文件
汇编阶段
gcc -o example.o -c example.s
- 将
.s
文件内容汇编成机器语言后生成.o
文件
链接阶段
gcc -o example example.o
- 将
.o
文件链接生成可执行文件
直接编译生成可执行程序时,中间过程产生的文件缓存在内存中
多模块编译
多模块软件
- 多数软件被分割为多个源文件,每个文件称为一个模块
多模块软件的建立
首先将一个大型程序根据其功能合理地划分为若干个小的源程序,每个小的源程序均以程序文件(扩展名为
.c
)的形式保存在磁盘上。- 如:一个较大的程序分解成file1.c,file2.c,file3.c...等多个源程序,各自独立地保存在磁盘上
- 这些若干个源程序可以进行单独编译,分别形成
.o
目标文件 - 最后将这个
.o
目标文件链接生成一个大的可执行程序
多模块软件的设计思路
- 将整个大问题化成各个小模块,再利用模块之间的联系来完成整个流程
优点
- 较小的程序文件更易于管理和维护,也就是说编辑,编译,测试和调试较小的程序文件所花的的时间更少
- 只需编译经过修改的源文件,而不是编译整个软件系统
缺点
- 必须知道构成整个系统的所有文件,这些文件的相互依赖性(即哪些文件需要重新编译),以及生成最终可执行系统后哪些文件被修改过
- 跟踪所有文件修改的时间戳
- 必须键入很长的命令行来编译
静态库和共享库
函数库
- 函数库时由系统建立的具有一定功能的函数的集合。库中存放函数的名称和对应的目标代码,以及链接过程中所需的重定位信息
- linux中标准的c函数库放置在
/usr/lib
下,以文件形式存放 - 用户可以根据自己的需要建立自己的用户函数库
- 函数库分为静态库(
.a
)和共享库(或称动态链接库或动态库.so
)
库函数
- 存放在库函数种的函数。库函数具有明确的宫恩,入口调用参数和返回值
- 库函数的源代码一般是不可见的,但在对应的头文件中你可以看到它对外的接口(函数原型)。
静态库的概念
- 静态库就是一些
.o
目标文件的集合,以.a
结尾 - 静态库在程序链接时使用,连接器会将程序中使用到的函数的代码从库文件中拷贝到可执行文件中。一旦链接完成,在执行程序的时候就不需要静态库了。
- 由于每个使用静态库的应用程序都需要拷贝所用函数的代码,所以生成的可执行文件会比较大。
- 静态库就是一些
静态库的创建
ar rcs lib库文件名.a 目标文件1 目标文件2 目标文件3 ...
- r表示将
.o
的目标文件加入到静态库中 - c表示创建静态库
- s表示生成索引
- 创建的库文件名字前面建议加上前缀
lib
,即lib库文件名.a
- r表示将
静态库的使用
gcc -o 可执行文件 调用者的目标文件 -Ldir -l库文件名
gcc -o 可执行文件 -ldir 调用者的c源文件 -Ldir -l库文件名
-Ldir
选项表示将制定的库文件所在目录加入到库搜索路径中,默认的库搜索路径在/usr/lib
目录下-lname
选项(前缀lib不要加上),表示库搜索路径下的libname.a
或lib name.so
文件,这也是为什么库文件都以lib
开头的原因之一,如果你的库文件不是以lib开头(如hello.a
),那就不能用-l
选项,而是直接写上hello.a
,将原来的-lhello
换成hello.a
即可。-l
时链接器选项,必须要放到被编译和链接的文件后面- 删除静态库文件后不会影响到可执行文件的运行
- 示例
-bash-4.1$ gcc -c -o obj/01_lib.o src/01_lib.c -I include
-bash-4.1$ ar rcs lib/lib01.a obj/01_lib.o
-bash-4.1$ gcc -o bin/01 src/01.c -I include -L lib -l 01
共享库
共享库的概念
- 共享库即动态链接库,在linux中以
.so(share object)
为后缀,windows中以.dll
为后缀 - 共享库在程序链接时并不像静态库那样会拷贝使用函数的代码,而只是做些标记。在程序开始启动(实际上在加载程序时)运行的时候,加载所需函数
- 可执行程序在运行时仍需要共享库的支持
- 共享库链接出来的文件比静态库小得多
- c语言中的所有标准的共享库放置在
/usr/lib
目录中
- 共享库即动态链接库,在linux中以
共享库的创建
gcc -shared -fPCI -o lib库文件名.so 目标文件1 目标文件2 ...
-shared
表示是使用共享库-fpci
或-fPCI
表明创建产生独立目标代码,具体取决于平台
共享库的使用
gcc -o 可执行文件 调用者的目标文件 -Ldir -l库文件名
gcc -o 可执行文件 调用者的c源文件 -Ldir -l库文件名
运行可执行文件的出错解决方案(库文件找不到)
cp lib hello.so /usr/lib
(需root用户操作)export LD_LIBRARY_PATH=库文件目录
- 删除库文件会导致程序失效
make和makefile
make引入的原因
- 一个软件工程中的源文件不计其数,其按类型,功能,模块分别放在若干个目录中,哪些文件需要首先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于更复杂的功能操作,这就需要一个工具来全面地,系统地处理
make和Makefile
- linux/unix系统有一个非常强大的使用程序,称为make,可以用它来管理多模块程序的编译和链接,直至生成可执行程序
- make使用程序读取一个说明文件,称为
Makefile
。Makefile文件中描述了整个软件工程的编译规则及各文件之间的依赖关系
- Makefile就像一个shell脚本,其中也可以执行操作系统的命令。它带来的好处就是自动化编译,一旦写好,只需要一个make命令,整个软件工程完全自动编译,极大的提高了软件开发的效率
- make时一个命令行工具,时一个解释Makefile中指令的命令工具,一般来说,大多数的IDE都有这个命令,比如:Delphi的make,Visual C++的nmake,linux下GNU的make
- make使冲洗编译的次数最小化
- Makefile实质上是一种脚本语言
make使用语法
make [选项] [目标] [宏定义]
常用选项 | 特性 |
---|---|
-d | 显示调试信息 |
-f <文件> | 指定从哪个文件中读取依赖关系信息。默认文件是Makefile 或makefile ,- 表示从标准输入读取 |
-h | 显示所有选项的简要说明 |
-n | 不运行任何Makefile命令,只显示它们 |
-s | 安静的方式运行,不显示任何信息 |
Makefile的编写原则
- 当make命令不带选项运行时,它从Makefile中读取制定规则
- 当制定规则在不同于Makefile(或makefile)的其他文件中,就要运行带有-f的make命令,比如make.fray.Makefile
Makefile的编写规则一
目标列表:关联性列表
<TAB>命令列表
- 目标列表:是用一个或多个空格分开的目标文件的清单
- 关联性列表(也称先决条件):同样是一个或多个空格分开的目标文件,是目标列表依赖的多个目标文件的清单
- 命令列表:用于创建目标文件的将要执行的命令清单,这些命令被换行符分开。命令列表中的每个命令必须以<tab>字符开始
Makefile的编写规则二
目标列表:关联性列表:命令列表
- 命令列表是一系列被分号隔开的命令。一个很长的命令行要续行时可以用反斜线符号。
- 示例
cd /home/sarwar/courses/bin;rm file1 file2\
lab2 lab3\
prog1.c prog2.c prog3.c
等同于
cd /home/sarwar/courses/bin;rm file1 file2 lab2 lab3 prog1.c prog2.c prog3.c
Makefile中可以使用shell中的一些通配符
- 注释(#),连接符()
- 关联列表和命令列表中可使用shell通配符?,*等
- Makefile示例1
# xxxxxxxxx
#xxxxx
power:power.c
gcc -o power power.c
依赖树
- power 依赖于 power.c
- Makefile示例2
power:power.o compute.o
gcc -o power power.o compute.o
power.o:power.c
gcc -o power.o power.c
compute.o:compute.c
gcc -o compute.o compute.c
依赖树
- power依赖于power.o和compute.o
- power.o依赖于power.c
- compute.o依赖于compute.c
- 示例
# 编写makefile文件
vim 01_makefile
bin/01:obj/01.o obj/01_lib.o
gcc -o bin/01 obj/01.o obj/01_lib.o -I include
obj/01.o:src/01.c
gcc -c -o obj/01.o src/01.c -I include
obj/01_lib.o:src/01_lib.c
gcc -c -o obj/01_lib.o src/01_lib.c -I include
# 使用make编译
-bash-4.1$ make -f 01_makefile -B
gcc -c -o obj/01.o src/01.c -I include
gcc -c -o obj/01_lib.o src/01_lib.c -I include
gcc -o bin/01 obj/01.o obj/01_lib.o -I include
make命令不加-B
的话,不会重现生成在上次编译之后没有修改的文件,加上则强制重新生成
makefile的变量的使用
简单变量
定义
变量名:=[文本]
- 这类变量的实质就是一组字符串
添加
变量名+=[文本]
等价于变量名:=[文本] [文本]
引用变量
- $(变量名)
$单字符变量
如:G:=gcc
- $G -o power power.c
- 示例
builder:=gcc
H:=-I
H+= include
TARGET:=bin/01
$(TARGET):obj/01.o obj/01_lib.o
$(builder) -o $(TARGET) obj/01.o obj/01_lib.o
obj/01.o:src/01.c
$(builder) -c -o obj/01.o src/01.c $H
obj/01_lib.o:src/01_lib.c
$(builder) -c -o obj/01_lib.o src/01_lib.c $H
内置变量
- make中有几个内置变量,使用这些变量工作会变的更简单,常用的内建(内部)变量和它们的意义
变量名 | 意义 |
---|---|
$@ | 当前目标的名称 |
$? | 比当前目标更新的以修改依赖性列表 |
$< | 依赖性列表中的第一个文件 |
$^ | 用空格分开的所有依赖性列表 |
builder:=gcc
H:=-I
H+= include
TARGET:=bin/01
$(TARGET):obj/01.o obj/01_lib.o
$(builder) -o $(TARGET) $^
obj/01.o:src/01.c
$(builder) -c -o $@ $< $H
obj/01_lib.o:src/01_lib.c
$(builder) -c -o $@ $^ $H
虚目标
- 不存在的文件,而且也无需创建它们
- 允许你强制执行某些事件,而这些事件在正常规则中时不会发生的
虚目标不是真正的文件,make命令可以使用针对它们的任意规则
- 虚目标总是使与之有关的命令被执行
常见的虚目标列表
目标 | 意义 |
---|---|
all | 生成工程中所有可以执行者,通常是makefile的第一个生成目标 |
test | 允许程序的自动测试套件 |
clean | 删除make all 生成的所有文件 |
install | 在系统目录中安装工程项目生成的可执行文件和文档 |
uninstall | 删除make install 安装的文件 |
示例
# makefile文件内容
vim 04_makefile
builder:=gcc
H:=-I
H+= include
TARGET:=bin/01
DEPEND:=obj/01.o obj/01_lib.o
$(TARGET):$(DEPEND)
$(builder) -o $(TARGET) $^
obj/01.o:src/01.c
$(builder) -c -o $@ $< $H
obj/01_lib.o:src/01_lib.c
$(builder) -c -o $@ $^ $H
clean:
rm -rf $(TARGET) $(DEPEND)
# 执行make命令
make -f 04_makefile clean
- 当存在一个与虚目标相同的文件时,命令会执行不正常(如下),碰到这种情况,可以用特殊目标处理
-bash-4.1$ make -f 04_makefile clean
make: “clean”是最新的。
特殊目标
- make中有一些预定义的目标,这些预定义目标被make以一种特殊的方式进行处理,这些目标称为特殊目标
特殊目标 | 目的 |
---|---|
.DEFAULTS | 如果make找不到生成目标的任何makefile入口或后缀规则,它就执行与目标相关的命令 |
.IGNORE | 如果某一行makefile包含该目标,make忽略错误代码并继续建立,如果一个命令不正常存在,make自然会停止。带有-i 选项的make命令可以执行相同的任务 |
.PHONY | 允许您制定一个不是文件的目标,所以您能指示make调用一系列makefile中的命令,即使在您的当前目录中有一个具有目标名称的文件 |
.SILENT | make执行这些命令但不显示这些命令,带有-s 选项的make可以执行相同的任务。如前面讨论的,该命令前防治一个@ 符号就可以执行一个特别命令 |
.SUFFIXES | 为目标制定的前提(后缀)可以与后缀规则相关联。如果与目标没有相关联的前提,已存在的后缀列表就会被删除 |
示例
# 编辑makefile文件
vim 05_makefile
builder:=gcc
H:=-I
H+= include
TARGET:=bin/01
DEPEND:=obj/01.o obj/01_lib.o
$(TARGET):$(DEPEND)
$(builder) -o $(TARGET) $^
obj/01.o:src/01.c
$(builder) -c -o $@ $< $H
obj/01_lib.o:src/01_lib.c
$(builder) -c -o $@ $^ $H
.PHONY:clean
clean:
rm -rf $(TARGET) $(DEPEND)
# 执行,存在相同名字的文件也可以正常执行了
-bash-4.1$ make -f 05_makefile clean
rm -rf bin/01 obj/01.o obj/01_lib.o
默认模式规则
- make中有许多预定义的制定规则(也称为后缀规则),它可以让make自动执行许多任务。
- make中已经内建许多其他语言的规则和unix实用程序(包括SCCS,PCS,ar,lex和yacc)的规划。
- 为了建立一个目标,make会遍历一遍依赖关系,这是为了决定从何处开始建立(或重建)。如果没有找到目标文件,make就按照优先顺序查找源文件。
- 为了生成目标文件,它首先查找带
.c
,.f
或.s
后缀的文件,如果一个都没找到,他会继续寻找带.c
后缀的文件并继续搜索,直到找到一个源文件。如果没有找到一个源文件,make就会报告一个异常。 - 默认的规则(GNU make)
%.o:%.c:
$(CC) $(CFLAGS) -c $<
%.o:%.s
$(AS) $(ASFLAGS) -o $@ $<
- 默认的后缀规则
SUFFIXES:.o.c.s
.c.o:
$(CC) $(CFLAGS) -c $<
.s.o:
$(AS) $(ASFLAGS) -o $@ $<
- 示例
builder:=gcc
H:=-I
H+= include
TARGET:=bin/01
DEPEND:=obj/01.o obj/01_lib.o
$(TARGET):$(DEPEND)
$(builder) -o $(TARGET) $^
# 这两条条就相当于下面的四条
obj/%.o:src/%.c
$(builder) -c -o $@ $< $H
#obj/01.o:src/01.c
# $(builder) -c -o $@ $< $H
#obj/01_lib.o:src/01_lib.c
# $(builder) -c -o $@ $^ $H
.PHONY:clean
clean:
rm -rf $(TARGET) $(DEPEND)