目录
前言
已经熟悉C编程语言的朋友们应该已经很清楚了——C语言程序的构建过程包括了 编译(compile) 和 连接(link) 这两个部分。C语言编译器先将每个源文件( .c 文件)编译成对应的目标文件(Windows系统一般为 .obj 文件;类Unix系统一般为 .o 文件)。随后,再将这些编译完的目标文件连接为一单个可执行文件。
下面我们先列出一些源文件用于后续例子的使用,此外后续的例子若没指明,则均是在Linux+GCC环境下使用:
// afunc.c
#include <stdio.h>
static void InternalFunc(void)
{
puts("This is InternalFunc for FuncA!");
}
void FuncA(void)
{
puts("This is FuncA!");
InternalFunc();
}
void FuncDummyA(void)
{
puts("This is DummyA!");
}
// bfunc.c
#include <stdio.h>
static void InternalFunc(void)
{
puts("This is InternalFunc for FuncB!");
}
void FuncB(void)
{
puts("This is FuncB!");
InternalFunc();
}
void FuncDummyB(void)
{
puts("This is DummyB!");
}
// main_test.c
#include <stdio.h>
extern void FuncA(void);
extern void FuncB(void);
int main(int argc, const char* argv[])
{
FuncA();
FuncB();
}
以上我们编写了三个C语言源文件:afunc.c、bfunc.c、main_test.c。其中,afunc.c与bfunc.c源文件各有自己的 内部连接(internal linkage) 函数——InternalFunc(),还有各自的 外部连接(external linkage) 函数,分别是 FuncA() 与 FuncB()。然后在 main_test.c 源文件中将会调用 afunc.c 里的 FuncA() 以及 bfunc.c 里的 FuncB()。我们可以用下面命令对上述三个文件进行编译并直接生成最终可执行文件——main_test。
gcc main_test.c afunc.c bfunc.c -o main_test -std=gnu17
当我们执行了 main_test 程序之后就会得到以下输出结果:
This is FuncA!
This is InternalFunc for FuncA!
This is FuncB!
This is InternalFunc for FuncB!
而上述 gcc 命令实际上是以下命令的一个聚合,两者生成出来的可执行文件是完全相同的:
#! /bin/sh
# build_test.sh
# gcc main_test.c afunc.c bfunc.c -o main_test -std=gnu17
gcc afunc.c -o afunc.c.o -c -std=gnu17
gcc bfunc.c -o bfunc.c.o -c -std=gnu17
gcc main_test.c -o main_test.c.o -c -std=gnu17
gcc main_test.c.o afunc.c.o bfunc.c.o -o main_test
rm afunc.c.o bfunc.c.o main_test.c.o
我们可以看到上述具体的编译和连接过程:以上 build_test.sh 脚本中,先是用三条 gcc 命令,通过添加 -c 命令选项,先后将 afunc.c、bfunc.c 和 main_test.c 编译为对应的目标文件 afunc.c.o、bfunc.c.o 和 main_test.c.o。而最后一行 gcc 命令则是将这三个目标文件(.o文件)连接在一起,生成最终的可执行文件 main_test。而最后一行 rm 命令则是将生成的三个目标文件进行删除,由于我们已经不再需要了。我们可以通过以下命令来执行此sh脚本:
sh build_test.sh
得到的可执行文件 main_test 对它执行后同样能得到第一种编译后的输出结果。
在Linux环境下,使用GCC等编译器一般是将C语言源文件编译为遵循 ELF 格式的目标文件。因此,我们要查看这些目标文件或是下面介绍的静态库与动态库文件,乃至可执行文件,可使用 readelf 命令来查看包含在其中的各种符号。
readelf -s main_test
执行上述命令之后将会列出包含在此 main_test 可执行文件中的所有符号。这里一般包含了两个表,一个是动态库符号表(Symbol table ‘.dynsym’),还有一个则是普通符号表(Symbol table ‘.symtab’)。我们这里先看普通符号表。
由于系统以及编译环境自带的符号比较多,这里就列出我们关心的一些符号:
| Num | Value | Size | Type | Bind | Vis | Ndx | Name |
|---|---|---|---|---|---|---|---|
| 36 | 0000 | 0 | FILE | LOCAL | DEFAULT | ABS | main_test.c |
| 37 | 0000 | 0 | FILE | LOCAL | DEFAULT | ABS | afunc.c |
| 38 | 116d | 23 | FUNC | LOCAL | DEFAULT | 16 | InternalFunc |
| 39 | 0000 | 0 | FILE | LOCAL | DEFAULT | ABS | bfunc.c |
| 40 | 11b7 | 23 | FUNC | LOCAL | DEFAULT | 16 | InternalFunc |
| 57 | 1184 | 28 | FUNC | GLOBAL | DEFAULT | 16 | FuncA |
| 61 | 11ea | 23 | FUNC | GLOBAL | DEFAULT | 16 | FuncDummyB |
| 67 | 1149 | 36 | FUNC | GLOBAL | DEFAULT | 16 | main |
| 70 | 11ce | 28 | FUNC | GLOBAL | DEFAULT | 16 | FuncB |
| 72 | 11a0 | 23 | FUNC | GLOBAL | DEFAULT | 16 | FuncDummyA |
上述列表中一共有8个字段,下面将对这8个字段分别进行说明:
- Num:表示当前符号的序号,用十进制数表示。在一个ELF文件中符号从序号0开始,然后依次递增。
- Value:表示当前符号位于当前ELF文件中的偏移地址,用十六进制数表示。
- Size:表示当前符号所占存储空间大小(以字节为单位)。
- Type:表示当前符号类型。基本有这几类——NOTYPE 表示一个没有任何类型的纯符号或是一个未定义的符号;SECTION 表示一个存储器段;FILE 表示一个源文件;OBJECT 表示一个对象(比如一个全局变量);FUNC 表示一个函数。
- Bind:表征了当前符号的连接属性,主要有这三种值——LOCAL 表示当前符号具有内部连接(比如一个
static变量或函数);GLOBAL 表示当前符号具有外部连接(比如一个全局变量或函数);WEAK 表示当前符号具有外部连接,但可被其他同名的 GLOBAL 或 WEAK 符号所覆盖(对于GCC而言,相当于一个被__attribute__((weak))所修饰的全局变量或函数),因而对于多个完全相同名称的 WEAK 符号而言不会导致连接失败。 - Vis:表示当前符号的可见性。与GCC的
__attribute__((visibility("vis")))相对应。共有四种值,分别是——default、protected、internal和hidden。 - Ndx:当前符号所处的段序号(section number)。值 ABS 意味着绝对值,从而不会调整到任一段地址的重定向。
- Name:当前符号名。
从上表我们可以知晓,main_test 可执行文件中切切实实地存放了我们在 afunc.c、bfunc.c以及 main_test.c 中所有定义的函数。即使是没有被调用到的 FuncDummyA() 和 FuncDummyB() 也均在其中。
以上描述的是对于一般C语言源文件如何经过编译、连接,最后生成一个可执行文件的详细过程。那么我们有一定经验的程序员知道,我们在企业工作中,往往会把一个较大、较通用的功能进行模块化,以供多个项目使用。当然,最简单粗暴的方式可以直接将实现该功能的所有源文件直接复制黏贴到所需要的项目工程中,但这会带来不少弊病——比如,如果一个项目工程导入的源文件太多会导致编译过程变得十分缓慢,尤其是改动了某个需要被许多源文件所包含的头文件而言,更为如此!所以,为了能提升模块独立化,与其他项目进行解耦,并且提升编译构建、接口抽象性等诸多软件工程上的益处,我们往往会将一个通用的功能模块打包成一个库。
C语言可支持的库有两种,一种是静态连接库,还有一种是动态连接库。下面我们将分别予以介绍。
静态连接库
C语言中的一个静态连接库(

本文详细介绍了C语言中静态连接库和动态连接库的创建与使用方法,分别在Linux和Windows环境下进行了演示。静态连接库将多个目标文件打包,而动态连接库在程序运行时加载。在Linux下使用GCC和ar命令创建静态库,动态库则使用-shared选项。Windows下使用VisualStudio创建静态库和动态库,并使用dumpbin查看库中的符号。此外,讨论了运行时动态加载动态库的API,如dlopen和LoadLibraryA。
最低0.47元/天 解锁文章
2632





