Linux下的静态链接库和动态链接库编程
1、链接库概述
Linux下得库有动态与静态两种,动态通常用.so为后缀,静态用.a为后缀。面对比一下两者:
静态链接库:当要使用时,连接器会找出程序所需的函数,然后将它们拷贝到执行文件,由于这种拷贝是完整的,所以一旦连接成功,静态程序库也就不再需要了。
动态库而言:某个程序在运行中要调用某个动态链接库函数的时候,操作系统首先会查看所有正在运行的程序,看在内存里是否已有此库函数的拷贝了。如果有,则让其共享那一个拷贝;只有没有才链接载入。在程序运行的时候,被调用的动态链接库函数被安置在内存的某个地方,所有调用它的程序将指向这个代码段。因此,这些代码必须使用相对地址,而不是绝对地址。在编译的时候,我们需要告诉编译器,这些对象文件是用来做动态链接库的,所以要用地址无关代码(Position Independent Code (PIC))。
动态链接库的加载方式有两种:隐式加载和显示加载。
注意:linux下进行连接的缺省操作是首先连接动态库,也就是说,如果同时存在静态和动态库,不特别指定的话,将与动态库相连接(见本文第四部分)。
2、静态链接库
下面就通过实际的例子来向大家演示一下,该怎样编译和使用静态和动态链接库:
2.1 编辑测试文件
二个文件:add.c、 sub.c、add.h 、sub.h 和 main.c
/*add.h */
#ifndef _ADD_H_
#define _ADD_H_
int add(int a, int b);
#endif
-------------------------------------------------------------------------------------------------
/*add.c*/
#include "add.h"
int add(int a, int b)
{
return a+b;
}
-------------------------------------------------------------------------------------------------
/*sub.h*/
#ifndef _SUB_H_
#define _SUB_H_
int sub(int a, int b);
#endif
-------------------------------------------------------------------------------------------------
/*sub.c*/
#include "add.h"
int sub(int a, int b)
{
return a-b;
}
-------------------------------------------------------------------------------------------------
/*main.c*/
#include <stdio.h>
#include "add.h"
#include "sub.h"
int main(void)
{
printf("1 + 2 =%d\n", add(1, 2));
printf("1 - 2 =%d\n", sub(1, 2));
return 0;
}
-------------------------------------------------------------------------------------------------
2.2 将.c 编译生成 .o文件
gcc -c add.c
gcc -c sub.c
生成的文件:sub.o ,add.o
无论是静态库文件还是动态库文件,都是由 .o 文件创建的。
2.3 由 .o 文件创建.a静态库
ar crlibmymath.a sub.o add.o
ar:静态函数库创建的命令 -c :create的意思 -r :replace的意思,表示当前插入的模块名已经在库中存在,则替换同名的模块。如果若干模块中有一个模块在库中不存在,ar显示一个错误信息,并不替换其他同名的模块。默认的情况下,新的成员增加在库德结尾处。 |
库文件的命名规范是以lib开头(前缀),紧接着是静态库名,以 .a 为后缀名。
2.4 在程序中使用静态库
gcc -o main main.c -L. –lmymath
-L 指定函数库查找的位置,注意L后面还有'.',表示在当前目录下查找 -l则指定函数库名,其中的lib和.a(.so)省略。 注意:-L是指定查找位置,-l指定需要操作的库名。 |
静态库制作完了,如何使用它内部的函数呢?只需要在使用到这些公用函数的源程序中包含这些公用函数的原型声明,然后在用gcc命令生成目标文件时指明静态库名(是mymath 而不是libmymath.a ),gcc将会从静态库中将公用函数连接到目标文件中。注意,gcc会在静态库名前加上前缀lib,然后追加扩展名.a得到的静态库文件名来查找静态库文件。在程序main.c中,我们包含了静态库的头文件add.h和sub.h,然后在主程序main中直接调用公用函数add()和sub()即可。
2.5 生成目标程序main,然后运行。
./main
1 + 2 = 3
1 - 2 = -1
3、动态库(隐式链接)
3.1 由 .o创建.so动态库
动态库文件名命名规范和静态库文件名命名规范类似,也是在动态库名增加前缀lib,但其文件扩展名为.so。例如:我们将创建的动态库名为mymath,则动态库文件名就是libmamath.so。用gcc来创建动态库。在系统提示符下键入以下命令得到动态库文件libmamath.so。
gcc -fPIC-o add.o -c add.c
gcc -fPIC-o sub.o -c sub.c
gcc -shared-o libmamath.so add.o sub.o
或者:
gcc –c –o add.oadd.c
gcc –c –o sub.osub.c
gcc -shared -fPCI-o libmyhello.so add.o sub.o
这里:
-fpic:产生代码位置无关代码 -shared :生成共享库 |
3.2 隐式方式使用动态库
在程序中隐式使用动态库和使用静态库完全一样,也是在使用到这些公用函数的源程序中包含这些公用函数的原型声明,然后在用gcc命令生成目标文件时指明动态库名进行编译。我们先运行gcc命令生成目标文件,再运行它看看结果。
gcc -o main main.c -L. -lmymath
./main
./main: error while loading shared libraries:libmymath.so: cannot open shared object file: No such file or directory
出错了!!!
快看看错误提示,原来是找不到动态库文件libmyhello.so。程序在运行时,会在/usr/lib和/lib等目录中查找需要的动态库文件。若找到,则载入动态库,否则将提示类似上述错误而终止程序运行。
动态库的搜索路径搜索的先后顺序是:
1.编译目标代码时指定的动态库搜索路径;
2.环境变量LD_LIBRARY_PATH指定的动态库搜索路径;
3.配置文件/etc/ld.so.conf中指定的动态库搜索路径;//只需在在该文件中追加一行库所在的完整路径如"/root/test/conf/lib"即可,然后ldconfig是修改生效。
4.默认的动态库搜索路径/lib;
5.默认的动态库搜索路径/usr/lib。
为此解决方法:
1. 我们将文件libmyhello.so复制到目录/usr/lib中:
mv libmyhello.so/usr/lib/
2. 将libmyhello.so拷贝到可执行文件main的同一目录下。
再次运行:./main
1 + 2 = 3
1 - 2 = -1
成功了!这也进一步说明了动态库在程序运行时是需要的。
3.3 动态库的初始化和解析
Windows下的动态库加载,卸载都会有初始化函数以及卸载函数来完成库的初始化以及资源回收,linux当然也可以实现,这些初始化函数主要包含两个部分:动态库的构造和析构函数机制、动态库的全局变量初始化工作。
(1)动态库的构造和析构函数机制
在Linux中,提供了一个机制:在加载和卸载动态库时,可以编写一些函数,处理一些相应的事物,我们称这些函数为动态库的构造和析构函数,其代码格式如下:
void __attribute__ ((constructor)) my_init(void); // my_init为自定义的构造函数名
void __attribute__ ((destructor)) my_fini(void); //my_fini为自定义的析构函数名
在编译共享库时,不能使用"-nonstartfiles"或"-nostdlib"选项,否则构建与析构函数将不能正常执行(除非你采取一定措施)。
注意,构造函数的参数必须为空,返回值也必须为空。
举个例子,动态库文件a.c的代码如下:
void __attribute__((constructor)) my_init(void)
{
printf("init library\n");
}
编译成动态库:
gcc -fPIC -shared a.c -o liba.so
主程序main.c如下:
#include<stdlib.h>
#include<stdio.h>
int main()
{
pause();
return 0;
}
编译:
gcc -L./ -la main.c -o main.bin
运行main.bin程序:
也就是说,在运行main时,加载完liba.so后,自动运行liba.so的初始化函数。
(2)全局变量初始化
①先看如下例子:
//文件名:b1.c
#include<stdlib.h>
#include<stdio.h>
int reti()
{
printf("reti\n");
return 10;
}
int g1=reti(); // g1是个全局变量。
使用GCC对其进行编译:
gcc -fPIC -shared b1.c -o libb.so
编译错误!使用G++对其进行编译:
g++ -fPIC -shared b1.c -o libb.so
编译成功!可见GCC和G++对于这种全局变量初始化的方法,支持力度是不一样的。
//主程序
//文件名:main.c
#include <stdlib.h>
#include <stdio.h>
int main()
{
pause();
return 0;
}
编译执行文件:
gcc -L./ -lb main.c -o main.bin
运行main.bin:
这说明,进程在加载libb.so后,为了初始化全局变量g1,其会运行reti来初始化g1。
②再来看一个C++的例子:
//文件名:b2.cpp
class Myclass
{
public:
Myclass();
int i;
};
Myclass::Myclass()
{
printf("constructMyclass\n");
};
Myclass g1;
编译动态库:
g++ -fPIC -shared b2.cpp-o libb.so
在动态库libb.so中,声明了一个类型为Myclass的全局变量g1。
//主程序
//文件名:main.cpp
#include <stdlib.h>
#include <stdio.h>
#include<unistd.h>
int main()
{
pause();
return 0;
}
编译执行文件:
g++ -L./ -lb main.cpp -o main.bin
运行main.bin:
这说明,进程在加载liba.so后,为了初始化全局变量g1,程序在进入main函数前将会运行Myclass的构造函数。
4、动态链接库(显式链接)
4.1 重要的dlfcn.h头文件
LINUX下使用动态链接库,源程序需要包含dlfcn.h头文件,此文件定义了调用动态链接库的函数的原型。下面详细说明一下这些函数。
函数dlerror:
原型为: const char *dlerror(void);
当动态链接库操作函数执行失败时,dlerror可以返回出错信息,返回值为NULL时表示操作函数执行成功。
函数dlopen:打开指定的动态链接库文件
原型为: void *dlopen (const char *filename, int flag);
dlopen用于打开指定名字(filename)的动态链接库,并返回操作句柄。
filename: 如果名字不以/开头,则非绝对路径名,将按下列先后顺序查找该文件:
(1) 用户环境变量中的LD_LIBRARY值;
(2) 动态链接缓冲文件/etc/ld.so.cache
(3) 目录/lib,/usr/lib
flag表示在什么时候解决未定义的符号(调用)。取值有两个:
1) RTLD_LAZY : 表明在动态链接库的函数代码执行时解决。
2) RTLD_NOW : 表明在dlopen返回前就解决所有未定义的符号,一旦未解决,dlopen将返回错误。
dlopen调用失败时,将返回NULL值,否则返回的是操作句柄。
函数dlsym : 取函数执行地址
原型为: void *dlsym(void *handle, char *symbol);
dlsym根据动态链接库操作句柄(handle)与符号(symbol),返回符号对应的函数的执行代码地址。由此地址,可以带参数执行相应的函数。
如程序代码: void (*add)(int x,int y); /* 说明一下要调用的动态函数add */
add=dlsym("xxx.so","add"); /* 打开xxx.so共享库,取add函数地址 */
add(89,369); /* 带两个参数89和369调用add函数 */
函数dlclose : 关闭动态链接库
原型为: int dlclose (void *handle);
dlclose用于关闭指定句柄的动态链接库,只有当此动态链接库的使用计数为0时,才会真正被系统卸载。
4.2 显加载示动态链接库的实例
在下面这个实例中将通过动态加载libmymath.so链接库,来调用add()和sub()两个函数。
/*main.c*/
#include <stdio.h>
#include <dlfcn.h>
int main(void)
{
void*dp=dlopen("libmymath.so",RTLD_LAZY);
if(NULL==dp)
{
printf("打开动态链接库时失败!");
return1;
}
//定义函数指针
int(*fn_add)(int,int)=NULL;
int(*fn_sub)(int,int)=NULL;
fn_add=dlsym(dp,"add");
fn_sub=dlsym(dp,"sub");
if(NULL==fn_add|| NULL==fn_sub)
{
printf("在动态链接库中寻找函数失败!");
return1;
}
printf("1+ 2 = %d\n", fn_add(1, 2));
printf("1- 2 = %d\n", fn_sub(1, 2));
dlclose(dp);
return0;
}
将libmymath.so和main.c放在同一个目录下,执行如下命令:
gcc -rdynamic -s -o main.bin main.c -ldl
-rdynamic选项以指定输出文件为动态链接的方式 -s指定删除目标文件中的符号表, -ldl则指示装配程序ld需要装载dl函数库。 |
最后运行main.bin的结果同上。
4.3 Windows下和Linux下显示加载动态链接库的比较
Windows下动态链接库以“.dll”为后缀,而Linux下得动态链接库是以”.so”为后缀的。
函数功能 |
Windows下 |
Linux下 |
打开加载动态链接库 |
LoadLibrary |
dlopen |
获取动态链接库中的函数地址 |
GetProcAddress |
dlsym |
关闭动态链接库 |
FreeLibrary |
dlclose |
在使用时应包含的头文件 |
Winbase.h(include Windows.h) |
dlfcn.h |
5、特殊情况
我们回过头看看,发现使用静态库和隐式方式使用动态库时编译成目标程序使用的gcc命令完全一样,那当静态库和动态库同名时,gcc命令会使用哪个库文件呢?抱着对问题必究到底的心情,来试试看。先删除除.c和.h外的所有文件,恢复成我们刚刚编辑完举例程序状态。
gcc -c add.c
gcc -c sub.c
ar crlibmymath.a sub.o add.o
gcc -shared -fPCI -olibmyhello.so sub.o add.o
现在目录有两个同名的库文件(动态库文件和静态库文件同名):
libmymath.a 、 libmymath.so
编译运行程序:
gcc -o main main.c -L. -lmymath
./main
./main: error while loading shared libraries:libmymath.so: cannot open shared object file: No such file or directory
从程序./main运行的结果中很容易知道,当Linux静态库和Linux动态库同名时, gcc命令将优先使用动态库。如果强制使用静态库则需要加-static选项支持,即:
gcc-static -o main main.c -L. -lmymath
链接静态库的可执行程序明显比链接动态库的可执行文件大。
6、查看库中的符号
1、使用nm命令可以打印出库中涉及到的所有符号。库既可以是静态库也可以是动态的。
常见的三种符号:
①在库中被调用,但没有在库中定义(表明需要其他库支持),用U表示
②在库中定义的函数,用T表示
③“弱态”符号,他们虽然在库中被定义但是可能被其他库中同名的符号覆盖,用W表示。
2、用ldd命令可以查看一个可执行程序依赖的共享库。
7、Linux下so导出指定函数
Linux下编译so导出源文件里面的指定函数:
1、在文件里面最前面加上:#defineDLL_PUBLIC __attribute__((visibility("default")))
2、在文件里面需要导出的函数前加上:extern "C" DLL_PUBLIC
3、Linux下动态库(so)编译时默认不导出,在Makefile中需要添加:-fvisibility=hidden