Linux开发技巧──库文件的使用

一、链接器的基本知识

在说库之前,先简单介绍一下链接器的原理。

编译的时候,每个.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

view plain print ?
  1. int add(int a, int b) {  
  2.     return a + b;  
  3. }  

int add(int a, int b) { return a + b; }


b.asm

Code

view plain print ?
  1. section .text  
  2.   
  3. global substract     ;声明导出符号substract  
  4. substract:  
  5.      push     ebp  
  6.      mov ebp, esp  
  7.      push     ebx  
  8.      mov eax, dword [ebp + 8]     ;a  
  9.      mov ebx, dword [ebp + 12]    ;b  
  10.      sub eax, ebx         ;result  
  11.     pop ebx  
  12.      leave  
  13.      ret  

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

view plain print ?
  1. #include <stdio.h>  
  2.   
  3. int add(int a, int b);  
  4. int subtract(int a, int b);  
  5.   
  6. int main() {  
  7.     int a, b, c, tmp;  
  8.       
  9.      printf("Input 3 numbers:");  
  10.      scanf("%d%d%d", &a, &b, &c);  
  11.      tmp = add(a, b);  
  12.      tmp = substract(tmp, c);  
  13.      printf("result:%d/n", tmp);  
  14.       
  15.     return 0;  
  16. }  

#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

view plain print ?
  1. #include <dlfcn.h>  
  2. #include <stdlib.h>  
  3. #include <stdio.h>  
  4.   
  5.   
  6. int main(int argc, char *argv[]) {  
  7.       
  8.     char *errmsg;   // error message  
  9.     void *lib;  // shared library handle  
  10.     int (*add)(int a, int b);   // add() function  
  11.     int (*substract)(int a, int b); // substract() function  
  12.   
  13.     // OPen the library  
  14.      lib = dlopen("./libdemo.so", RTLD_LAZY);  
  15.     if (!lib) {  
  16.          fprintf(stderr, dlerror());  
  17.          exit(-1);  
  18.      }  
  19.       
  20.     // get export symble address  
  21.      add = dlsym(lib, "add");  
  22.      errmsg = dlerror();  
  23.     if (errms  
  24.       
  25.      substract = dlsym(lib, "substract");  
  26.      errmsg = dlerror();  
  27.     if (errmsg != NULL)   {  
  28.          fprintf(stderr, errmsg);  
  29.          exit(-1);  
  30.      }  
  31.   
  32.     // the same as the old one  
  33.     int a, b, c, tmp;  
  34.       
  35.      printf("Input 3 numbers:");  
  36.      scanf("%d%d%d", &a, &b, &c);  
  37.      tmp = add(a, b);  
  38.      tmp = substract(tmp, c);  
  39.      printf("result:%d/n", tmp);  
  40.   
  41.     // close lib  
  42.      dlclose(lib);  
  43.       
  44.     return 0;  
  45. }  

#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可以查到以上函数的详细说明

你可能感兴趣的:(linux,windows,function,gcc,library,Numbers)