版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
一、库
本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。由于Windows和linux本质不同,因此二者库的二进制是不兼容的。库有两种:静态库(.a、.lib)和共享库也称动态库(.so、.dll)。回顾下,我们之前讲gcc编译过程可分为四个阶段:预处理->>编译->>汇编->>链接。而所谓的静态、动态就指的是链接阶段。
注意,可执行代码的二进制形式,即ELF格式。以后有时间会详细介绍下它。
参看:C语言再学习-- readelf、objdump、nm使用详解
二、静态库
1、静态库介绍
静态库指将所有相关的目标文件打包成为一个单独的文件,即静态库文件,其缺省扩展名是 .a 。
链接静态库就是将库中被调用的代码复制到调用模块中。静态库占用空间大,库中代码一旦修改必须重新链接。使用静态库的代码在运行时无需依赖库,且执行效率高。
静态库命名规范,必须是"lib[your_library_name].a":lib为前缀,中间是静态库名,扩展名为.a。
例如:libadd.a
以下源码文件将用于下面的讲解:
-
//add.c 加法运算函数
-
#include "add.h"
-
int add_int (int ia, int ib)
-
{
-
return ia + ib;
-
}
-
//add.h 头文件
-
#ifndef ADD_H//防止头文件被多次包含
-
#define ADD_H
-
#include <stdio.h>
-
//声明一个函数
-
int add_int(int ia,int ib);
-
#endif
-
//main.c 主函数
-
#include "add.h"
-
int main()
-
{
-
printf(
"计算两个整数的和是:%d\n",add_int(
20,
40));
-
return
0;
-
}
2、静态库的创建和使用
(1)只编译不链接,生成目标文件
gcc -c add.c main.c 生成目标文件 add.o main.o
(2)使用 ar -r 命令创建静态库文件 (创建)
ar -r lib库名 .a 目标文件
ar -r libadd.a add.o 创建静态库文件 libadd.a
(3)链接测试程序和库文件 (使用)
主要有三种方法:
1)直接连接
2)通过编译器选项进行间接链接 (重点)
gcc main.o libadd.a -o add 生成可执行文件 add
gcc/cc main.o -l 库名 -L 库文件所在的路径
3)配置环境变量 LIBRARY_PSTH 进行连接
gcc main.o -l add -L . -o add 生成可执行文件 add
export LIBRARY_PATH=$LIBRARY_PATH:.
gcc/cc main.o -l 库名
gcc/cc main.o -l 库名
-
未配置环境变量之前编译错误:
-
gcc main.o -l add
-
/usr/bin/ld: cannot find -ladd
-
collect2: ld 返回
1
-
配置环境变量
export LIBRARY_PATH=$LIBRARY_PATH:.
-
gcc main.o -l add -o add
-
生成可执行文件 add
3、讲解
(1)首先gcc编译过程,之前有讲参看:C语言再学习 -- GCC编译过程 在此就不重复了。不过需要知道为什么只编译不链接,生成目标文件。前面已经讲到了,gcc编译过程可分为四个阶段:预处理->>编译->>汇编->>链接。而所谓的静态、动态就指的是链接阶段。所以它需要在汇编阶段生成目标文件,然后链接静态库/动态库生成可执行文件。
(2)ar命令
感兴趣的可以 man ar
-
The GNU ar program creates, modifies,
and extracts from archives.
An archive is a single file holding a collection of other files
-
in a structure that makes it possible to retrieve the original individual files (called members of the archive).
-
The original files' contents, mode (permissions), timestamp, owner, and group are preserved in the archive, and can be restored on
-
extraction.
-
ar [选项] <静态库文件> <目标文件列表>
-r 将目标文件插入到静态库中,已存在则更新
-q 将目标文件追加到静态库尾
-d 从静态库中删除目标文件
-t 列表显示静态库中的目标文件
-x 将静态库展开为目标文件
(3)链接
gcc编译器,连接程序选项说明:
-L dir:将dir所指出的目录加到“函数库搜索列表”中,dir 为库文件所在的路径
-llib: 链接lib库,lib 为库名
-I name: 连接时,加载名字为name的函数库。该库位于系统预设的目录或者由-L选项确定的目录下。实际的库名是libname(后缀为.a或.so)
-L dir:将dir所指出的目录加到“函数库搜索列表”中,dir 为库文件所在的路径
-llib: 链接lib库,lib 为库名
-I name: 连接时,加载名字为name的函数库。该库位于系统预设的目录或者由-L选项确定的目录下。实际的库名是libname(后缀为.a或.so)
(4)配置环境变量
上篇文章已经专门讲了,参看:Unix再学习 -- 环境变量 需要注意的是,静态库属于编译链接阶段,所以如此配置 export LIBRARY_PATH=$LIBRARY_PATH:. 而它的意思是,静态库文件在当前目录下查找,配置的环境变量对当前用户临时有效。
三、共享库
1、共享库介绍
共享库和静态库最大的不同就是,链接共享库并不需要将库中被调用的代码复制到调用模块中,相反被嵌入到调用模块中的仅仅是被调用代码在共享库中的相对地址。如果共享库中的代码同时为多个进程所用,共享库的实例在整个内存空间中仅需一份,这正是共享的意义所在。共享库占用空间小,即使修改了库中的代码,只要接口保持不变,无需重新链接。使用共享库的代码在运行时需要依赖库,执行效率略低。而共享库的缺省扩展名是: .so
共享库命名规范,必须是"lib[your_library_name].so":lib为前缀,中间是共享库名,扩展名为 .so
例如:libadd.so
2、共享库的创建和使用
(1)只编译不链接,生成目标文件
gcc -c -fpic add.c main.c 生成目标文件 add.o main.o
(2)创建共享库文件 (创建)
gcc/cc -shared xxx.o -o lib库名.so
gcc -shared add.o -o libadd.so 生成共享库文件 libadd.so
(3)链接测试程序和库文件 (使用)
主要有三种方法:
1)直接链接
2)通过编译器选项进行间接链接 (重点)
gcc main.o libadd.so -o add 生成可执行文件 add
gcc/cc main.o -l 库名 -L 库文件所在的路径
3)配置环境变量 LIBRARY_PSTH 和 LD_LIBRARY_PSTH 进行连接、运行
gcc main.o -l add -L . -o add 生成可执行文件 add
export LIBRARY_PATH=$LIBRARY_PATH:.
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
gcc/cc main.o -l 库名
gcc/cc main.o -l 库名
-
未配置环境变量 LIBRARY_PATH 之前编译错误:
-
gcc main.o -l add
-
/usr/bin/ld: cannot find -ladd
-
collect2: ld 返回
1
-
-
未配置环境变量 LD_LIBRARY_PATH 之前运行错误:
-
./add: error
while loading shared libraries: libadd.so: cannot open shared object file: No such file
or directory
-
配置环境变量
export LIBRARY_PATH=$LIBRARY_PATH:.
-
gcc main.o -l add -o add
-
生成可执行文件 add
-
-
配置环境变量
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
-
运行 ./add 成功
3、讲解
(1)gcc编译过程,同静态库一样。不过gcc编译里面使用了选项 -fpic 需要讲一下。
PIC (Position Independent Code,告诉编译器产生与位置无关代码)
调用代码通过相对地址标识被调用代码的位置,模块中的指令与该模块被加载到内存中的位置无关。
(通俗点就是在可执行程序装载它们的时候,它们可以放在可执行程序的内存里的任何地方。)
-fPIC:大模式,生成代码比较大,运行速度比较慢,所有平台都支持。
-fpic :小模式,生成代码比较小,运行速度比较快,仅部分平台支持。
我们可以比较下未使用 -fpic 和使用后生成的目标文件大小。
可以看出,使用 fpic 生成的目标文件,多了.group 和 .text
详细可参看:GCC参数的官方介绍
-
-fpic
-
Generate position-
independent code (PIC) suitable for use in a shared library, if supported for the target machine. Such code accesses all constant addresses through a global offset table (GOT). The dynamic loader resolves the GOT entries when the program starts (the dynamic loader is not part of GCC; it is part of the operating system). If the GOT size for the linked executable exceeds a machine-specific maximum size, you get an error message from the linker indicating that -fpic does not work; in that
case, recompile with -fPIC instead. (These maximums are
8k on the SPARC,
28k on AArch64
and
32k on the m68k
and RS/
6000. The x86 has no such limit.)
-
-
Position-independent code requires special support,
and therefore works only on certain machines. For the x86, GCC supports PIC
for System V but
not
for the Sun
386i. Code generated
for the IBM RS/
6000 is always position-independent.
-
-
When
this flag is
set, the macros __pic__
and __PIC__ are defined to
1.
-
-
-fPIC
-
If supported
for the target machine, emit position-independent code, suitable
for dynamic linking
and avoiding any limit on the size of the global offset table. This option makes a difference on AArch64, m68k, PowerPC
and SPARC.
-
-
Position-independent code requires special support,
and therefore works only on certain machines.
-
-
When
this flag is
set, the macros __pic__
and __PIC__ are defined to
2.
(2)gcc编译 -shared选项
产生共享库文件
-
-shared
-
Produce a shared object which can then be linked with other objects to form an executable. Not all systems support
this option.
(3)配置环境变量通过 未配置环境变量 LIBRARY_PATH 之前编译错误、未配置环境变量 LD_LIBRARY_PATH 之前运行错误 可以明显验证,它们的作用了。
LIBRARY_PATH:Linux gcc编译链接时的共享库搜索路径。
LIBRARY_PATH:执行二进制文件时的共享库搜索路径。
LIBRARY_PATH:执行二进制文件时的共享库搜索路径。
在可执行程序的链接阶段,并不将所调用函数的二进制代码复制到可执行程序中,而只是将该函数在共享库中的地址嵌入到调用模块中,因此运行时需要依赖共享库。
(4)gcc缺省链接共享库,可通过 -static 选项强制链接静态库。
参看:GCC -static常见问题
(1)加载共享库 dlopen
-
-
static
-
On systems that support dynamic linking,
this prevents linking with the shared libraries. On other systems,
this option has no effect.
在GCC中,会优先使用shard library. 为了确保使用的是静态库,则使用此选项。例如:
-
链接时有-
static选项,却链接了共享库,则会报错
-
# gcc -static main.o libadd.so -o add
-
/usr/bin/ld: attempted
static link of dynamic object `libadd.so'
-
collect2: ld 返回
1
四、静态库与共享库比较
(1)静态库在程序编译时会被连接到目标代码中,程序运行时将不再需要该静态库。当程序与静态库连接时,库中目标文
件所含的所有将被程序使用的函数的机器码被copy到最终的可执行文件中。这就会导致最终生成的
可执行代码量相对
变多
,相当于编译器将代码补充完整了,这样
运行起来相对就快些
。不过会有个缺点:
占用磁盘和内存空间。
静态库
会
被添加到和它连接的每个程序中,而且这些程序运行时,都会被加载到内存中,无形中又多消耗了更多的内存空间。
(2)动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入,因此在程序运行时还需要动态库存
在
。与共享库连接的可执行文件只包含它需要的函数的引用表,而不是所有的函数代码,只有在程序执行时,那些需
要的函数代码才被拷贝到内存中。这样就使
可执行文件比较小,节省磁盘空间
,更进一步,操作系统使用虚拟内存,
使得一份共享库驻留在内存中被多个程序使用,也同时节约了内存。不过由于运行时要去链接库会花费一定的时间,
执行速度相对会慢一些
。
(3)总的来说静态库是牺牲了空间效率,换取了时间效率,共享库是牺牲了时间效率换取了空间
效率,没有好与坏的区别,只看具体需要了。
(4)另外,一个程序编好后,有时需要做一些修改和优化,如果我们要修改的刚好是库函数的话,在接口不变的前
提下,使用共享库的程序只需要将共享库重新编译就可以了,而使用静态库的程序则需要将静态库重新编译好后,将
程序再重新编译一遍。
例如,将 add.c改为乘法运算:
-
//乘法运算
-
#include "add.h"
-
-
int add_int(int ia,int ib)
-
{
-
return ia*ib;
-
}
共享库操作是:
-
生成目标文件add.o: gcc -c -fpic add.c
-
生成共享库文件libadd.so: gcc -shared add.o -o libadd.so
-
不需要再编译生成可执行文件
-
直接执行./add
-
输出结果:
-
计算两个整数的和是:
800
静态库操作是:
-
生成目标文件add.o: gcc -c add.c
-
生成静态库文件libadd.a: ar -r libadd.a add.o
-
重新编译生成可执行文件add: gcc main.o libadd.a -o add
-
执行./add
-
输出结果:
-
计算两个整数的和是:
800
五、动态库的显式调用
参看:C++静态库与动态库
#include <dlfcn.h>,提供了下面几个接口:(1)加载共享库 dlopen
void * dlopen( const char * pathname, int mode )
函数功能:
以指定模式打开指定的动态连接库文件,并返回一个句柄给调用进程。
打开模式:
RTLD_LAZY 暂缓决定,等有需要时再解出符号
RTLD_NOW 立即决定,返回前解除所有未决定的符号。
RTLD_NOW 立即决定,返回前解除所有未决定的符号。
返回值:
打开错误返回NULL,成功,返回库引用
编译时候要加入 -ldl (指定dl库)
打开错误返回NULL,成功,返回库引用
编译时候要加入 -ldl (指定dl库)
感兴趣的可以 man dlopen
-
One of the following two values must be included in flag:
-
-
RTLD_LAZY
-
Perform lazy binding. Only resolve symbols as the code that references them is executed. If the symbol is never referenced,
-
then it is never resolved. (Lazy binding is only performed
for function references; references to variables are always imme‐
-
diately bound when the library is loaded.)
-
-
RTLD_NOW
-
If
this value is specified,
or the environment variable LD_BIND_NOW is
set to a nonempty
string,
all undefined symbols in the
-
library are resolved before dlopen() returns. If this cannot be done, an error is returned.
(2)获取函数地址 dlsym
void* dlsym(void* handle,const char* symbol)
函数功能:
dlsym根据动态链接库操作句柄(handle)与符号(symbol),返回符号对应的地址。使用这个函数不但可以获取函数地址,也可以获取变量地址。
handle是由dlopen打开动态链接库后返回的指针,symbol就是要求获取的函数或全局变量的名称。
(3)卸载共享库 dlclose
(3)卸载共享库 dlclose
int dlclose (void *handle)
函数功能:
dlclose用于关闭指定句柄的动态链接库,只有当此动态链接库的使用计数为0时,才会真正被系统卸载。
(4)获取错误信息 dlerror
(4)获取错误信息 dlerror
const char *dlerror(void)
函数功能:
当动态链接库操作函数执行失败时,dlerror可以返回出错信息,返回值为NULL时表示操作函数执行成功。
举个栗子:
生成共享库文件:gcc -shared add.o -o libadd.so
将上面 main.c 改为如下,使用动态度显示调用函数,调用 add_int 函数:
-
#include <stdio.h>
-
#include <stdlib.h>
-
#include <dlfcn.h>
-
//动态链接库路径
-
#define LIB_ADD_PATH "./libadd.so"
-
//函数指针
-
typedef int (*ADD_FUNC) (int, int);
-
-
int main (void)
-
{
-
void *handle;
-
char *error;
-
ADD_FUNC add_func =
NULL;
-
//打开动态链接库
-
handle = dlopen (LIB_ADD_PATH, RTLD_LAZY);
-
if (!handle)
-
{
-
fprintf (
stderr,
"%s\n", dlerror ());
-
exit (EXIT_FAILURE);
-
}
-
//清除之前存在的错误
-
dlerror ();
-
//获取一个函数
-
add_func = (ADD_FUNC)dlsym (handle,
"add_int");
-
if (!add_func)
-
{
-
fprintf (
stderr,
"%s\n", dlerror ());
-
exit (EXIT_FAILURE);
-
}
-
printf (
"计算两个整数的和是:%d\n", add_func (
20,
40));
-
-
if(dlclose (handle))
-
{
-
fprintf (
stderr,
"%s\n", dlerror ());
-
exit (EXIT_FAILURE);
-
}
-
return
0;
-
}
编译选项如下:
gcc -rdynamic -o add main.c -ldl
-
执行 ./add
-
输出结果:
-
计算两个整数的和是:
60
六、库相关命令
(1)nm命令
其中,nm 命令可以打印库中所涉及到的所有符号,既可以用以静态库也可以用以共享库。
-
//用于共享库
-
# nm libadd.so
-
00001f28 a _DYNAMIC
-
00001ff4 a _GLOBAL_OFFSET_TABLE_
-
w _Jv_RegisterClasses
-
00001f18 d __CTOR_END__
-
00001f14 d __CTOR_LIST__
-
00001f20 d __DTOR_END__
-
00001f1c d __DTOR_LIST__
-
000004ec r __FRAME_END__
-
00001f24 d __JCR_END__
-
00001f24 d __JCR_LIST__
-
0000200c A __bss_start
-
w __cxa_finalize@@GLIBC_2
.1
.3
-
00000420 t __do_global_ctors_aux
-
00000350 t __do_global_dtors_aux
-
00002008 d __dso_handle
-
w __gmon_start__
-
00000407 t __i686.get_pc_thunk.bx
-
0000200c A _edata
-
00002014 A _end
-
00000458 T _fini
-
000002f0 T _init
-
0000040c T add_int
-
0000200c b completed
.6159
-
00002010 b dtor_idx
.6161
-
000003d0 t frame_dummy
-
//用于静态库
-
nm libadd.a
-
-
add.o:
-
00000000 T add_int
nm列出的符号有很多,常见的有三种:1) 一种是在库中被调用,但并没有在库中定义(表明需要其他库支持),用U表示;
2) 一种是库中定义的函数,用T表示,这是最常见的;
3) 一种是所谓的弱态”符号,它们虽然在库中被定义,但是可能被其他库中的同名符号覆盖,用W表示。
2、ldd命令
ldd命令,可以查看一个可执行程序依赖的共享库。
-
//查看静态库,错误
-
#ldd libadd.a
-
不是动态可执行文件
-
-
//查看共享库
-
# ldd libadd.so
-
linux-gate.so
.1 => (
0xb7772000)
-
libc.so
.6 => /lib/i386-linux-gnu/libc.so
.6 (
0xb75b6000)
-
/lib/ld-linux.so
.2 (
0xb7773000)
3、strip 命令
去除目标文件、可执行文件、静态库和共享库中的符号表、调试信息等。
# strip add
参看:linux下strip的用法strip经常用来去除目标文件中的一些符号表、调试符号表信息,以减小程序的大小,在rpmbuild包的最后就用到。
用法:strip <选项> 输入文件
从文件中删除符号和节
选项为:
-
-I --input-target=<bfdname> Assume input file is in format <bfdname>
-
-O --output-target=<bfdname> Create an output file in format <bfdname>
-
-F --target=<bfdname> Set both input
and output format to <bfdname>
-
-p --preserve-dates Copy modified/access timestamps to the output
-
-R --remove-section=<name> Remove section <name> from the output
-
-s --strip-all Remove all symbol
and relocation information
-
-g -S -d --strip-debug Remove all debugging symbols & sections
-
--strip-unneeded Remove all symbols
not needed by relocations
-
--only-keep-debug Strip everything but the debug information
-
-N --strip-symbol=<name> Do
not copy symbol <name>
-
-K --keep-symbol=<name> Do
not strip symbol <name>
-
--keep-file-symbols Do
not strip file symbol(s)
-
-w --wildcard Permit wildcard in symbol comparison
-
-x --discard-all Remove all non-global symbols
-
-X --discard-locals Remove any compiler-generated symbols
-
-v --verbose List all object files modified
-
-V --version Display
this program's version number
-
-h --help Display
this output
-
--info List object formats & architectures supported
-
-o <file> Place stripped output into <file>
-
-
strip: 支持的目标: elf32-i386 a.out-i386-linux efi-app-ia32 elf32-little elf32-big elf64-alpha ecoff-littlealpha elf64-little elf64-big elf32-littlearm elf32-bigarm elf32-hppa-linux elf32-hppa elf64-ia64-little elf64-ia64-big efi-app-ia64 elf32-m68k a.out-m68k-linux elf32-powerpc aixcoff-rs6000 elf32-powerpcle ppcboot elf64-powerpc elf64-powerpcle aixcoff64-rs6000 elf32-s390 elf64-s390 elf32-sparc a.out-sparc-linux elf64-sparc a.out-sunos-big elf64-x86
-64 pe-i386 pei-i386 srec symbolsrec tekhex binary ihex trad-core
目标文件分为:可重定位文件、可执行文件、共享文件
strip的默认选项会去除.symbol节的内容以及.debug节的内容,因此尽量只对可执行文件执行strip而不要对静态库或动态库等目标文件strip。
strip的默认选项会去除.symbol节的内容以及.debug节的内容,因此尽量只对可执行文件执行strip而不要对静态库或动态库等目标文件strip。
测试:
-
生成目标文件 add.o main.o: gcc -c add.c main.c
-
生成静态库文件 libadd.a: ar -r libadd.a add.o
-
生成可执行文件 add_a: gcc main.o libadd.a -o add_a
-
-
-
生成目标文件 add.o main.o: gcc -c -fpic add.c main.c
-
生成共享库文件 libadd.so: gcc -shared add.o -o libadd.so
-
生成可执行文件add_so: gcc main.o libadd.so -o add_so
-
-
做备份,使用strip指令
-
strip add_a add_so libadd.a libadd.so main.o add.o
-
# ls -l
-
总用量
84
-
-rwxr-xr-x
1 root root
5516 Mar
20
10:
38 add_a
-
-rwxr-xr-x
1 root root
7206 Mar
20
10:
33 add_a_bak
-
-rw-r--r--
1 root root
80 Mar
17
14:
23 add.c
-
-rw-r--r--
1 root root
147 Mar
17
14:
23 add.h
-
-rw-r--r--
1 root root
596 Mar
20
10:
38 add.o
-
-rw-r--r--
1 root root
860 Mar
20
10:
38 add.o_bak
-
-rwxr-xr-x
1 root root
5520 Mar
20
10:
38 add_so
-
-rwxr-xr-x
1 root root
7188 Mar
20
10:
34 add_so_bak
-
-rw-r--r--
1 root root
728 Mar
20
10:
38 libadd.a
-
-rw-r--r--
1 root root
1004 Mar
20
10:
34 libadd.a_bak
-
-rwxr-xr-x
1 root root
5356 Mar
20
10:
38 libadd.so
-
-rwxr-xr-x
1 root root
6654 Mar
20
10:
34 libadd.so_bak
-
-rw-r--r--
1 root root
123 Mar
20
10:
20 main.c
-
-rw-r--r--
1 root root
876 Mar
20
10:
38 main.o
-
-rw-r--r--
1 root root
1416 Mar
20
10:
34 main.o_bak
选项简释:
The -fPIC flag directs the compiler to generate position independent code section).
The -shared flag directs the linker to create a shared object file.
可见无论是静态库 (libadd.a) 还是动态库 (libadd.so) 还是可执行文件(add_a、add_so),去掉一些符号信息后都减小了很多,但如果这时再链接这两个库的话是编不过的,因此,如果不是指定特殊的 strip 选项的话,还是尽量不要对库文件 strip,只对链接后的可执行文件 strip 就可以了 (如果也不调试) 。
简而言之,strip 和 -fpic 正好相反 ,一个是去除符号信息,一个为添加符号信息。
4、ldconfig 命令
用专门的配置文件管理共享库的搜索路径。
事先将共享库的路径信息写入 /etc/ld.so.conf 配置文件中。执行 ldconfig 命令,将 /etc/ld.so.conf 配置文件转换为 /etc/ld.so.cache 缓冲文件,并将后者加载到系统内存中,借以提高共享库的搜索和加载速度。
每次系统启动时都会自动执行 ldconfig 命令。如果修改了共享库配置文件 /etc/ld.so.conf,则需要手动执行 ldconfig 命令,更新缓冲文件并重新加载到系统内存。
5、ls -l 命令
ls -l命令,查看库大小。
-
# ls -l libadd.*
-
-rw-r--r--
1 root root
1004 Mar
17
15:
24 libadd.a
-
-rwxr-xr-x
1 root root
6654 Mar
17
15:
24 libadd.so
查看属性,也可以得到:
七、Windows下静态库和动态库操作
参看:程序员的自我修养--链接、装载与库(高清带完整书签版).pdf 在此不作讲解。
八、感言
花了两天半总结完,静态库和动态库。一开始真没觉得能讲这么多东西,越发感到对之前培训时浅尝辄止的惭愧。
踏实一点吧,深入学习,才是关键!