假设你有下面的这样一个叫name.c
的C源文件:
#include <stdio.h>
#include <stdlib.h>
void print_name(const char * name)
{
printf("My name is %s\n", name);
}
当你编译时,使用cc name.c
生成name.o
. 这个.o
包含了name.c
中定义的所有函数和变量的编译后的代码和数据,以及关联名称与实际代码的索引。 如果你看一下这个索引,比如使用nm
工具(在Linux和许多其他Unix上都可用),你会注意到两个entry:
00000000 T print_name
U printf
上面两行意思是:.o
中存储了两个符号(函数或变量的名称,但不是类、结构或任何类型的名称)。 第一个标记为T
的符号实际上包含了它在name.o
中的定义,另一个标记为U
的符号仅仅是一个引用。 可以在此处找到print_name
的代码,但printf
的代码找不到。 当你的实际程序运行时,它将需要找到所有为引用的符号,并在其他object文件中查找它们的定义,以便链接称为一个完整的程序或完整的库。 因此,object文件是在源文件中找到的定义,转换成了二进制形式,并且可用于组成完整程序。
您本可以逐个链接.o
文件的,但实际上你并没有这么做:因为一般.o
文件非常多,并且它们是一种实现细节,你实际上更倾向于将相关 object 组织到名称极易分辨的多个包中。这些包称为库,有两种形式:静态和动态。
静态库(在Unix中)几乎总是以.a
为后缀(例子包括libc.a
,它是C核心库,libm.a
是C数学库),依此类推。 继续讨论上面的例子,你将使用ar rc libname.a name.o
构建静态库。 如果你用nm
命令查看libname.a
,你会看到:
name.o:
00000000 T print_name
U printf
正如你所看到的那样,libname.a
基本上是一个大的表,包含了一条可查找到里面的所有名字的索引。正如 object 文件一样,它包含了每个.o
中定义的符号和它们引用的符号。 如果你要链接另一个.o
(例如date.o
到print_date
),你会看到另一个像上面那样的 entry。
你如果将一个静态库链接成可执行文件,则会将整个库嵌入到可执行文件中, 这就像链接所有单个.o
文件一样。 你可以想象这可以使你的程序非常大,特别是如果你使用了很多库(事实上绝大多数现代应用程序都是如此)。
动态或共享库以.so
为后缀, 和它的静态模拟一样,是一个大的object文件表,指向所有编译过的代码。 你将使用cc -shared libname.so name.o
构建它, 如果用nm
查看,你会发现动态库与静态库有所不同。 在我的系统上,它包含有大约24个符号,而其中只有两个是print_name
和printf
:
00001498 a _DYNAMIC
00001574 a _GLOBAL_OFFSET_TABLE_
w _Jv_RegisterClasses
00001488 d __CTOR_END__
00001484 d __CTOR_LIST__
00001490 d __DTOR_END__
0000148c d __DTOR_LIST__
00000480 r __FRAME_END__
00001494 d __JCR_END__
00001494 d __JCR_LIST__
00001590 A __bss_start
w __cxa_finalize@@GLIBC_2.1.3
00000420 t __do_global_ctors_aux
00000360 t __do_global_dtors_aux
00001588 d __dso_handle
w __gmon_start__
000003f7 t __i686.get_pc_thunk.bx
00001590 A _edata
00001594 A _end
00000454 T _fini
000002f8 T _init
00001590 b completed.5843
000003c0 t frame_dummy
0000158c d p.5841
000003fc T print_name
U printf@@GLIBC_2.0
共享库与静态库有一个非常重要的区别:共享库不会将自身嵌入到最终的可执行文件中, 相反,可执行文件包含了对已解析的共享库的一个引用,是在运行时,而不是在链接时。
使用共享库有许多优点:
- 可执行文件要小得多,因为它只含有你通过object文件显式链接的代码。 外部库是引用,它们的代码不会进入二进制文件中。
- 多个可执行文件可以共享(名称就是这么来的)一个库。
- 你可以, 如果你对二进制兼容比较小心的话,可以在程序运行之间更新库中的代码,程序将在不需要更改的情况下选择新库。
还有下面的这些缺点:
将程序链接在一起需要时间。 对于共享库,有时这些时间会延迟到每次运行可执行文件时。With shared libraries some of this time is deferred to every time the executable runs.
这个过程比较复杂。 共享库中的所有其他符号都是在运行时使库链接所需的基础结构的一部分。
- 你可能会遇到库的不同版本之间存在细微不兼容的风险。 在Windows上,这称为“DLL地狱”。
(如果你想一想,这些就是程序使用或不使用引用和指针,而不是直接将类的对象嵌入到其他对象中的原因。这个类比非常直接。)
Ok,已经讲了很多细节,跳过的也很多,比如链接过程实际上是如何工作的, 我希望你不需要澄清就看得懂。