一、链接器的基本知识
在说库之前,先简单介绍一下链接器的原理。
编译的时候,每个.c(这里简单一点,只考虑C程序,C++原理类似)源程序会被编译成.o文件。在C源代码中声明的符号,如函数,全局变量等 等,编译器在当前的C源代码中能够找到定义的会被直接编译,而不能找到定义,只有声明的会作为外部符号,存放在目标文件的导入表中。(有人问,如果是又没 声明又没定义的符号呢?废话,当然通不过编译了-_-)嗯…………编译器没有办法确定这个未知符号的定义,但它相信这个定义是肯定存在的,自己既然无能为 力,就只能交给链接器去办啦。而对于C源程序中定义的全局函数、变量等等,编译器会把它放到目标文件的导出表中,因为它相信,这些符号也许其他模块需要 的。
轮到链接器上场的时候,简单的讲,它会把这一堆的目标文件“组合”起来组装成一个可执行文件(Linux是ELF、COFF或者a.out格式的 文件,Windows是PE或者LE文件)。组装的过程是什么?简单的讲,它从这一堆的目标文件中抽出机器码部分,把这些机器码组合起来,然后生成输出文 件的文件头等等结构,最后拼装成最终的可执行文件。这个过程是比较复杂的,我们暂时不深究。但是,我们对这个过程中链接器处理符号的方式比较感兴趣:它是 怎么处理上面提到的那些导入和导出的符号的?答案是,链接器从目标文件中取出符号表并进行合并操作,具体就是,如果目标文件甲中声明了导出符号abcd, 而目标文件乙中声明了同名的导入符号abcd,两个符号会被合并,导入符号会从最终文件的导入表中去除。处理机器码时,链接器会把目标文件乙中所有对外部 符号abcd的引用替换成目标文件甲中定义的符号abcd的地址。当然,链接器不可能处理完所有的符号,如果它还有导入符号无法处理,但是它相信操作系统 的装载器能够处理的话,它会把这些符号保留在最终可执行文件的导入表中,交由操作系统的装载器(可以认为包含一个链接器)去处理。当然如果它发现操作系统 仍然无法处理这个导入符号的话,就是大家熟悉的错误 啦:ld error xxxx: yyyy.o: Cannot resolve external symbol zzzzzzz。对于导出符号,可 以原样放在导出表中以供其他模块使用,也可以去除。
最终的可执行文件产生了,是不是链接任务完成了呢?没有!生成的可执行文件中仍然会有导入和导出表,需要在执行的时候链接,不过这个任务杀操作系统的加载器完成的。
总结一下,链接主要分以下两种:
编译时链接:GCC的ld链接器完成的任务
运行时链接:操作系统的加载器完成的任务
以上就是链接器的基本原理,它对于理解静态和动态库的原理很重要。
FAQ:
为什么我的程序编译能通过,链接的时候报错:xxxx :Cannot resolve/find symbol/function/variable yyyy?
初学者提问频率比较高的问题之一,如果你看完上面一段的话就不难自己回答这个问题了。
TIP:
查看nm命令和objdump命令的man手册…………
二、静态库
问题:如果我有一些可复用的代码,用什么方式“复用”?(这里的“复用代码”是模块级的,而非代码片断)
当然,如果每写一个新程序就把那些可复用的代码源程序原样拷贝过来,随新程序一起编译链接的话,当然可以达到目的,不过这样每次构建都得把那些复 用代码一起编译链接,如果复用代码很大的话,费时又费力,要是能够把这些代码预先编译好,一定能够节省不少时间和精力。可是,如果分别把源代码编译成目标 文件,会产生大量目标文件,不便使用和管理。有什么好办法?静态库就是解决方案。
创建静态库用ar命令,库文件习惯以lib打头,扩展名.a
TIP:
使用man ar查看ar的说明
[案例分析] 一个简单的程序
程序由a.c, b.asm, main.c组成。a.c中实现了加法函数add(),b.asm是用汇编语言实现的减法运算(用汇编语言的目的是为了说明链接原理和编程语言无关)substract()函数。main.c使用了以上两个函数
a.c
Code
int add(int a, int b) { return a + b; }
b.asm
Code
section .text global substract ;声明导出符号substract substract: push ebp mov ebp, esp push ebx mov eax, dword [ebp + 8] ;a mov ebx, dword [ebp + 12] ;b sub eax, ebx ;result pop ebx leave ret
main.c
Code
#include <stdio.h> int add(int a, int b); int subtract(int a, int b); int main() { int a, b, c, tmp; printf("Input 3 numbers:"); scanf("%d%d%d", &a, &b, &c); tmp = add(a, b); tmp = substract(tmp, c); printf("result:%d/n", tmp); return 0; }
假设a.c b.asm是复用的代码
编译:
gcc -c -g -o a.o a.c
yasm -g dwarf2 -f elf -o b.o b.asm
gcc -c -g -o main.o main.c
链接:
gcc -g -o main a.o b.o main.o
使用nm查看符号:
> nm a.o
00000000 T add
nm b.o
00000000 T substract
nm main.o
U add
00000000 T main
U printf
U scanf
U substract
可以看到标着U(Undefined)的符号是导入符号,而T(Text,定义在.text段中的符号)是导出符号。
现在我们用ar命令创建一个库:
ar rc libdemo.a a.o b.o
怎么使用它?
gcc -g -o main main.o -L./ -ldemo
-L指明附加库的路径(这里是当前目录),-l 指明附加库名,这里链接demo库,文件名是libdemo.a,就是我们刚才用ar命令创建的。
这是什么原理?答案是,ar命令只是简单的把目标文件打包,链接时,链接器从打包的库中取出各个目标文件和程序产生的目标文件链接。
链接的原理请看文章第一部分
TIP:
使用ar t libdemo.a查看库中包含的目标文件
> ar t libdemo.a
a.o
b.o
二、共享库
Linux支持共享库已经有很久远的历史了,它十分类似于Windows操作系统中的dll文件,但是提供了更加完备的机制来防止出现一些Windows系统中老大难的问题。
问题:为什么要动态链接的共享库?静态库不是很好了么?
举C库为例,如果C库做成静态的当然没有问题,但是如果系统中有100个进程的映像文件链接的时候都使用了C库,内存中就会有100份相同代码的拷贝。这是一种极大的浪费!如果让这100个程序共享同一个C库,自然就可以节约不少内存空间了。
如何编写和使用共享库?
[案例分析]
程序同静态库部分的,2个C文件加一个asm汇编文件。
现在按照共享库的方式编译
gcc -c -fpic -g -o a.o a.c
gcc -c -fpic -g -o main.o main.c
yasm -f elf -g dwarf2 -o b.o b.asm
-fpic生成与加载地址无关的代码(可重定位)
链接库:
gcc -shared -g -fpic -o libdemo.so a.o b.o
关键是一个-shared参数,让gcc产生共享库
TIPS:
man gcc看看这个参数的详细解释
链接程序:
gcc -g -o main main.o -L./ -ldemo
现在运行,嗯,怎么不能运行?
> ./main
./main: error while loading shared libraries: libdemo.so: cannot open shared object file: No such file or directory
什么?找不到?不就在当前目录下嘛,哦,想起来了,Linux的惯例,不会到自动到当前目录下查找文件的,我们必须手动设置一下。
设置什么?LD_LIBRARY_PATH环境变量, 这个环境变量告诉装载器到哪里去找共享库.
> export LD_LIBRARY_PATH=./
> ./main
这下可以了。
我们看一下可执行文件中extern节的内容
; Segment type: Externs
extern:804A028 ; extern
extern:804A028 extrn __libc_start_main@@GLIBC_2_0:near
extern:804A02C extrn scanf@@GLIBC_2_0:near
extern:804A030 extrn printf@@GLIBC_2_0:near
extern:804A034 extrn add:near ; CODE XREF: _add j
extern:804A034 ; DATA XREF: .got.plt:off_804A010 o
extern:804A038 extrn __libc_start_main:near ; CODE XREF: ___libc_start_main j
extern:804A038 ; DATA XREF: .got.plt:off_804A004 o
extern:804A03C ; int scanf(const char *,...)
extern:804A03C extrn scanf:near ; CODE XREF: _scanf j
extern:804A03C ; DATA XREF: .got.plt:off_804A008 o
extern:804A040 ; int printf(const char *,...)
extern:804A040 extrn printf:near ; CODE XREF: _printf j
extern:804A040 ; DATA XREF: .got.plt:off_804A00C o
extern:804A044 extrn __gmon_start__ ; weak ; DATA XREF: .got:08049FF0 o
extern:804A044 ; .got.plt:off_804A000 o
extern:804A048 extrn _Jv_RegisterClasses ; weak
extern:804A04C extrn substract ; DATA XREF: .got.plt:off_804A014 o
可以看到熟悉的add 和substract ^_^
这种方式是运行时静态链接,装载器在装入可执行程序的时候会一并把库也装入并且做好链接
怎么?没有下文啦?共享库不会这么简单吧,当然不会。共享库最大的魅力还没有介绍呢──动态载入
前面提到链接的方式有两种,实际上,还有一种链接方式也是很常用的,那就是运行时动态链接。
补上前面两种,链接方式有:
1.编译时静态链接
2.运行时静态链接
3.运行时动态链接
下面介绍第3种链接方式
[案例分析]
程序需要之前给出的libdemo.so库
dymlnk.c
Code
#include <dlfcn.h> #include <stdlib.h> #include <stdio.h> int main(int argc, char *argv[]) { char *errmsg; // error message void *lib; // shared library handle int (*add)(int a, int b); // add() function int (*substract)(int a, int b); // substract() function // OPen the library lib = dlopen("./libdemo.so", RTLD_LAZY); if (!lib) { fprintf(stderr, dlerror()); exit(-1); } // get export symble address add = dlsym(lib, "add"); errmsg = dlerror(); if (errms substract = dlsym(lib, "substract"); errmsg = dlerror(); if (errmsg != NULL) { fprintf(stderr, errmsg); exit(-1); } // the same as the old one int a, b, c, tmp; printf("Input 3 numbers:"); scanf("%d%d%d", &a, &b, &c); tmp = add(a, b); tmp = substract(tmp, c); printf("result:%d/n", tmp); // close lib dlclose(lib); return 0; }
程序编译:
gcc -o dymlnk dymlnk.c -ldl
需要链接库dl
这个程序有点复杂,简单解释一下
程序中首先调用dlopen打开一个库,这里就是我们刚才写的库, 这个函数会返回一个共享库的"handle"(怎么象Windows了?句柄?)
然后调用dlsym获取add和substract函数的指针
计算的代码和之前的main.c一样
最后关闭共享库
当然这个程序运行的时候不需要LD_LIBRARY_PATH变量,因为是全路径指明共享库路径的.
TIP:
man dlopen可以查到以上函数的详细说明