1.4 calc.h calc.c show.h show.c math.h
一、前言
1.0 .o .a .so 目标文件、静态库文件、动态库文件均为二进制文件
1.1 为何要把一个程序分成多个源文件,并由每个.c源文件编译生成独立的.o目标文件?
化整为零、易于维护、便于协作。
1.2 为何要把多个.o目标文件合并成一个.a或.so库文件?
集零为整、方便易用、易于复用。
1.3 可以把库文件看成一种代码仓库,它提供给user一些可以直接拿来用的变量、函数或类。
1.4 calc.h calc.c show.h show.c math.h
//calc.h 计算模块头文件
#ifndef __CALC_H__
#define __CALC_H__
//加法计算
int add(int,int);
//减法计算
int sub(int,int);
#endif //__CALC_H__
//show.h 显示模块头文件
#ifndef __SHOW_H__
#define __SHOW_H__
//显示等式 3 + 5 = 8 4 - 2 = 1
void show(int,char,int,int);
#endif //__SHOW_H__
//math .h 接口文件
#ifndef __MATH_H__
#define __MATH_H__
#include"calc.h"
#include"show.h"
#endif //__MATH_H__
//calc.c 计算模块实现
#include"calc.h"
//加法函数
int add(int a,int b){
return a + b;
}
//减法计算
int sub(int a,int b){
return a - b;
}
//show.c 显示模块实现
#include<stdio.h>
#include"show.h"
//显示等式
void show(int l,char op,int r,int res){
printf("%d %c %d = %d\n",l,op,r,res);
}
二、静态库的制作和使用
2.0 静态库
静态库的本质就是将多个.o目标文件打包成一个文件。
链接静态库的过程就是将库中被调用的代码复制到调用模块中。
静态库名:libxxxx.a
以构建数学库为例,libmath.a静态库的构建顺序如下:
2.1 编辑库的实现代码和接口声明:
计算模块:calc.h calc.c
显示模块:show.h show.c
接口文件:math.h
2.2 编译成目标文件
gcc -c calc.c
gcc -c show.c
2.3 打包成静态库
ar -r libmath.a calc.o show.o
ar [选项] <静态库文件> <目标文件列表>
-r 将目标插入到静态库库中,已存在则更新
-q 将目标文件追加到静态库尾
-d 从静态库中删除目标文件
-t 列表显示静态库中的目标文件
-x 将静态库展开为目标文件
2.4 库的使用代码main.c
2.5 编译和链接静态库(得到可执行文件main):
//编译并链接相应库,链接方式有3种
$gcc main.c libmath.a -o main //库的使用文件 和 库文件 同路径
$gcc main.c -lmath -L../mylibss -o main //-l接库小名,没有lib和.a -L接路径
$export LIBRARY_PATH=$LIBRARY_PATH:../mylibss
$gcc main.c -lmath -o main //路径添加到环境变量里了,无需-L路径
2.6 执行程序
执行程序前,可删除静态库文件。
$./main
3 + 5 = 8
3 - 5 = -2
$
三、动态库的制作和使用
3.0 动态库
动态库和静态库不同,链接动态库不需要将被调用的函数代码(.a)复制到包含调用代码(main.c)的可执行文件(main)中,相反,链接器会在调用语句处嵌入一段指令,在该程序执行到这段指令时,会加载该动态库并寻找被调用函数的入口地址并执行之。
如果动态库中的代码同时为多个进程所用,动态库在内存中的实例仅存一份,为所有使用该库的进程所共享(必须-fpic),因此动态库也称共享库。
动态库名:libxxxx.so (shared object)
3.1 编辑库的实现代码和接口声明:
计算模块:calc.h calc.c
显示模块:show.h show.c
接口文件:math.h
3.2 编译成目标文件
gcc -c -fpic calc.c
gcc -c -fpic show.c
3.3 打包成动态库
gcc -shared calc.o show.o -o libmath.so
2+3可同时执行:
gcc -shared -fpic calc.c show.c -o libmath.so
3.3* PIC(动态库必备)
Position Independent Code, 位置无关码
调用代码通过相对地址标识调用代码的位置,
模块中的指令与该模块被加载到内存中的位置无关。
-fPIC 大模式:生成代码大,运行速度慢,支持所有平台
-fpic 小模式:生成代码小,运行速度快,支持部分平台
3.4 库的使用代码main.c:
3.5 编译和链接动态库(得到可执行文件main):
//编译并链接相应库,链接方式有3种
$gcc main.c libmath.so -o main //库的使用文件 和 库文件 同路径
$gcc main.c -lmath -L../mylibss -o main //-l接库小名,没有lib和.a -L接路径
$export LIBRARY_PATH=$LIBRARY_PATH:../mylibss
$gcc main.c -lmath -o main //路径添加到环境变量里了,无需-L路径
3.6 执行程序(LD_LIBRARY_PATH)
在可执行程序的链接阶段,不将所调用函数的二进制代码复制到可执行程序中,而只将该函数在共享库中的地址嵌入到调用模块中,因此运行时需要依赖共享库,不能删除,也需要知道共享库的路径。
通过LD_LIBRARY_PATH环境变量中包含共享库所在的路径,以告知链接器在运行时链接动态库。代码如下第1行。
$export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:../mylibss
$./main
3 + 5 = 8
3 - 5 = -2
$
3.6.* LD_LIBRARY_PATH 等3个环境变量
PATH //用于bash敲命令时,找命令
LIBRARY_PATH //用于gcc编译(sel和链接库)时,找库
LD_LIBRARY_PATH //用于链接器在运行程序时找动态库
四、动态库的动态加载
4.0 理论
在程序执行的过程中,开发人员可以动态加载共享库(什么时候用什么时候加载),以提高内存利用效率。
在程序中动态加载共享库需要调用一组函数,他们位于该头文件中 #include<dlfcn.h>
并链接该库 -ldl // libdl.so
4.1 dlopen()
void* dlopen(char const* filename, int flag);
-功能:将共享库载入内存,并过得其访问句柄
-参数:
filename 动态库路径,若只给文件名不给路径,则根据LD_LIBRARY_PATH环境变量搜索
flag 加载方式宏,可取以下值:
RTLD_LAZY 延迟加载,使用动态库中的符号时在真正加载到内存
RTLD_NOW 立即加载
-返回值:成功返回动态库的访问句柄,失败返NULL
-句柄:唯一标识了系统内核所维护的共享库对象,将作为后续函数调用的参数。
4.2 dlsym()
void* dlsym(void *handle, char const* symbol);
-功能:从已被加载的动态库中获取特定名称的符号的地址
-参数:handle 动态库访问句柄
symbol 符号名
-返回值:成功返回给定符号的地址,失败返NULL
-该函数所返回的指针为void*类型,需要造型为与实际目标类型相一致的指针,才能使用。见 代码。
4.3 dlclose()
int dlclose(void* handle);
-功能:从内存中卸载动态库
-参数:handle 动态库句柄
-返回值:成功0,失败非0
-所卸载的共享库未必真的会从内存中立即消失,因为其他程序可能还需要使用该库呢
-只有所有使用该库的程序都显示或隐式地卸载了该库,库占内存空间才会真正释放
-无论所卸载的共享库是否真正被释放,传递给dlclose函数的句柄都会在该函数成功返回后立 即失效
4.4 dlerror()
char* dlerror(void);
-功能:获取在加载、使用和卸载共享库过程中所发生的错误
-返回值:有错误则返回指向错误信息字符串的指针,否则返NULL
-为什么不用perror()?
perror()用于系统调用函数、库函数
dlerror()专用于<dlfcn.h>,动态加载
4.5 load.c代码
//load.c 动态库的动态加载
#include<stdio.h>
#include<dlfcn.h>// dlopen() dlsym() dlclose() dlerror()
int main(void){
//加载库到内存
void* handle = dlopen("/home/tarena/2308/uc/day02/shared/libmath.so",RTLD_NOW);
if(handle == NULL){
fprintf(stderr,"dlopen:%s\n",dlerror());
//printf("dlopen:%s\n",dlerror());
return -1;
}
//获取函数的地址
int (*add)(int,int) = dlsym(handle,"add"); //注意int (*add) 与 int* add
if(add == NULL){
fprintf(stderr,"dlsym:%s\n",dlerror());
return -1;
}
int (*sub)(int,int) = dlsym(handle,"sub");
if(sub == NULL){
fprintf(stderr,"dlsym:%s\n",dlerror());
return -1;
}
void (*show)(int,char,int,int) = dlsym(handle,"show");
if(show == NULL){
fprintf(stderr,"dlsym:%s\n",dlerror());
return -1;
}
//使用函数
int a = 3,b = 5;
show(a,'+',b,add(a,b));
show(a,'-',b,sub(a,b));
//卸载库
dlclose(handle);
return 0;
}
4.5* (*add) 和 *add
int (*add)(int,int)
add是一个指针的名字,精确说是函数指针名,该指针指向的是函数地址。函数没名,形参是 int,int,函数返回int。
int *add(int,int)
add是函数名,该函数形参是int,int,函数返回一个int*类型的指针。
4.6 nm命令
.o .a .so 目标文件、静态库文件、动态库文件均为二进制文件,无法直接查看。
可用nm命令来列出目标文件、可执行程序、静态库或共享库中的符号(如函数名)。
$nm libmath.a //查看库中的函数名
show.o:
U printf
0000000000000000 T show
calc.o:
0000000000000000 T add
0000000000000014 T sub
$
4.7 ldd命令
查看可执行文件或共享库所依赖的共享库
$ldd a.out