Linux静态链接库与动态链接库的创建和显式与隐式调用

一、可执行程序的参数传递

我们常见的可执行程序有不少都是不带参数直接执行的,特别对于界面交互类应用更是如此。但也有很多命令交互程序是带参数执行的,比如GCC编译指令不仅支持多参数运行,还有参数类型选项(gcc [options] file…),就如同下面的命令:

gcc -g main.cpp -o main

回想下我们常用main函数的参数int main(int argc, char* argv[]),其中argc表示参数个数,argv[]则是保存具体参数的字符串指针数组,默认执行程序名作为第一个参数。比如下面给出一段代码,原样输出所有的参数,按上面给出的gcc命令编译链接后输出可执行程序main,读者可试着加参数运行确认输出结果。

#include <stdio.h>

int main(int argc, char *argv[])
{
    for(int i = 0; i < argc; ++i){
        printf("argv[%d]: %s\n", i, argv[i]);
    }
    return 0;
}

shell script也是可以加参数运行的,具体方法可参考博客:shell中脚本参数传递的两种方式

二、可执行程序的扩展链接库

基于模块化程序设计的原则,一个复杂程序通常由多个模块相互链接而成,每个模块都可以是一个函数库,这便是扩展库。链接库主要有以下好处:

  • 便于共享,开发软件如需相同功能,直接调用即可
  • 便于协作,只需要了解别人开发库的接口,不需过多关注实现细节
  • 便于保密,链接库为二进制文件,源代码不可见

链接库根据链接方式不同,可分为静态链接库(.a, .lib)和动态链接库(.so, .dll),二者区别主要在链接阶段如何处理库:

  • 静态库在程序编译时会被连接到目标代码中,程序运行时将不再需要该静态库,因此体积较大。
  • 动态库在程序编译时并不会被连接到目标代码中,而是在程序运行时才被载入,因此在程序运行时还需要动态库存在,因此代码体积较小。
  • 库文件链接过程

三、Linux静态链接库的创建和使用

一个静态库可以简单看成是一组目标文件(.o/.obj文件)的集合,即很多目标文件经过压缩打包后形成的一个文件。静态库特点总结:

  • 静态库对函数库的链接是放在编译时期完成的。
  • 程序在运行时与函数库再无瓜葛,移植方便。
  • 浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。

3.1 Linux静态库命名规则

Linux静态库命名规范,必须是"lib[your_library_name].a":lib为前缀,中间是静态库名,扩展名为.a。

3.2 Linux静态库创建

  • 首先,编写一些代码文件,用作库文件的函数
// myadd.cpp

#include "mylib.h"

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

// mysub.cpp

#include "mylib.h"

float sub(float a, float b)
{
    return a - b;
}

// mymul.cpp

#include "mylib.h"

float mul(float a, float b)
{
    return a * b;
}

// mylib.h

#ifndef _TEST_H
#define _TEST_H

extern "C" float add(float a, float b);
extern "C" float sub(float a, float b);
extern "C" float mul(float a, float b);

#endif
  • 将代码文件编译成目标文件.o
g++ -c myadd.cpp mymul.cpp mysub.cpp		# -c参数只编译汇编不链接
  • 然后,通过ar工具将目标文件打包成.a静态库文件
ar -crv libstaticmath.a myadd.o mymul.o mysub.o		# -c创建一个库,-r在库中加入或替换成员文件, -v显示操作的附加信息

大一点的项目会编写Makefile文件(CMake等工程管理工具)来生成静态库,省去了输入太多命令的麻烦,本文最后也给出了完整的Makefile代码,想了解Makefile可以参考另一篇文章:VSCode+GCC+Makefile+GitHub项目管理

3.3 Linux静态链接库的使用

  • 先给出一段调用上面库函数的测试代码:
// implicit.cpp	隐式调用测试代码

#include "./lib/mylib.h"
#include <cstdlib>
#include <iostream>

using namespace  std;

int main(int argc, char *argv[])
{
    float a = 3.7, b = 2.9;

    for(int i = 0; i < argc; ++i){
        printf("argv[%d]: %s\n", i, argv[i]);
        if(i == 1)
            a = atof(argv[1]);
        if(i == 2)
            b = atof(argv[2]);
    }

    cout << "a + b = " << add(a, b) << endl;
    cout << "a - b = " << sub(a, b) << endl;
    cout << "a * b = " << mul(a, b) << endl;

    return 0;
}
  • 在编译可执行程序时,指定静态库路径和名称
g++ implicit.cpp -I./lib -L./lib -lstaticmath -static -o testa		# -I指定头文件搜索路径,-L指定库文件搜索路径, -l指定库文件名, -static表示库文件不共享
  • 运行程序结果如下:

静态库调用结果

四、Linux动态链接库的创建和使用

动态链接库的出现,主要是为了解决静态链接库的一些问题,主要有以下两点:

  • 代码虽然可复用,但没法共享,造成空间浪费。

  • 静态库对程序的更新、部署和发布页会带来麻烦。如果静态库liba.lib更新了,所有使用它的应用程序都需要重新编译、发布给用户(对于玩家来说,可能是一个很小的改动,却导致整个程序重新下载,全量更新)。
    动态库共享代码
    由上图可看出,动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行是才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可,增量更新。综上将动态链接库的特点总结如下:

  • 动态库把对一些库函数的链接载入推迟到程序运行的时期。

  • 可以实现进程之间的资源共享。(因此动态库也称为共享库)

  • 将一些程序升级变得简单。(增量更新)

  • 甚至可以真正做到链接载入完全由程序员在程序代码中控制(显式调用)。

4.1 Linux动态库的命名规则

Linux动态库命名规范,必须是"lib[your_library_name].a":lib为前缀,中间是静态库名,扩展名为.so。

4.2 Linux动态库的创建

  • 依然使用上面静态库的示例代码,首先生成目标文件,此时要加编译器选项-fPIC
g++ -fPIC -c myadd.cpp mymul.cpp mysub.cpp		# -fPIC(Position Independent Code)编译为位置独立的代码
  • 生成动态库,此时要加链接器选项-shared
g++ -shared -o libdynamicmath.so myadd.o mymul.o mysub.o		# -shared生成共享目标文件
  • 由于都是g++工具,上面两步可合并为下面一条命令
g++ -fPIC -shared -o libdynamicmath.so myadd.cpp mymul.cpp mysub.cpp

4.3 Linux动态库的使用

测试代码也使用上面静态库时的示例代码:

  • 动态库链接命令与静态库完全一致,命令如下
g++ implicit.cpp -I./lib -L./lib -ldynamicmath -o testso

编译链接生成可执行文件正常,但在运行可执行文件时报错如下,经查询ld动态载入器的定位过程,发现ld默认能找到/lib或/usr/lib下的库文件,如需查找其他目录,还需要将库文件绝对路径添加到/etc/ls.so.conf文件中,并用ldconfig命令重建ld.so.cache文件。

paul@ubuntu:~/Desktop/MyCode$ ./testso
./testso: error while loading shared libraries: libdynamicmath.so: cannot open shared object file: No such file or directory
  • 也通过下面的命令添加动态链接库的绝对路径也可以解决该问题,但仅当前终端有效:
export LD_LIBRARY_PATH=`pwd`			#将当前路径添加到动态库路径环境变量
  • 程序执行结果如下(执行程序如果找不到动态库会报错,推荐一个程序依赖库查询命令:ldd libdynamicmath.so):

动态库隐式调用结果

五、Linux动态链接库的显式调用

上面介绍的动态库使用方法和静态库类似属于隐式调用,编译的时候指定相应的库和查找路径,可执行程序中也需要包含链接库头文件。其实,动态库还可以显式调用,不需要包含链接库的头文件。

Linux显式调用动态库,#include <dlfcn.h>头文件中提供了下面几个接口:

  • void * dlopen( const char * pathname, int mode ):函数以指定模式打开指定的动态连接库文件,并返回一个句柄给调用进程。
  • void* dlsym(void* handle,const char* symbol):dlsym根据动态链接库操作句柄(pHandle)与符号(symbol),返回符号对应的地址。使用这个函数不但可以获取函数地址,也可以获取变量地址。
  • int dlclose (void *handle):dlclose用于关闭指定句柄的动态链接库,只有当此动态链接库的使用计数为0时,才会真正被系统卸载。
  • const char *dlerror(void):当动态链接库操作函数执行失败时,dlerror可以返回出错信息,返回值为NULL时表示操作函数执行成功。

下面给出示例代码,生成动态链接库(libdynamicmath.so)的代码跟前面一致,这里只列出测试代码explicit.cpp如下:

// explicit.cpp不再包含./lib/mylib.h头文件,通过程序内部命令显式加载和释放

#include <cstdlib>
#include <iostream>
#include <dlfcn.h>

using namespace  std;

int main(int argc, char *argv[])
{
    
    if(argc < 2){
        cout << "Argument error." << endl;
        exit(1);
    }
    
    float a = 3.7, b = 2.9;
    char *libname = nullptr;
    char *err = nullptr;

    for(int i = 0; i < argc; ++i){
        printf("argv[%d]: %s\n", i, argv[i]);
        if(i == 1)
            libname = argv[1];     
        if(i == 2)
            a = atof(argv[2]);
        if(i == 3)
            b = atof(argv[3]);
    }

    //open the lib
    void *handle = dlopen(libname, RTLD_NOW);
    if(!handle){
        cout << "Load" << libname << "failed" << dlerror() << endl;
        exit(1);
    }
    //clear error info
    dlerror();
    //get function pointer
    typedef float (*pf_t)(float, float);
    pf_t add = (pf_t)dlsym(handle, "add");
    pf_t sub = (pf_t)dlsym(handle, "sub");
    pf_t mul = (pf_t)dlsym(handle, "mul");
    err = dlerror();
    if(err){
        cout << "Can't find symbol function" << err << endl;
        exit(1);
    }
    //call library function
    cout << "a + b = " << add(a, b) << endl;
    cout << "a - b = " << sub(a, b) << endl;
    cout << "a * b = " << mul(a, b) << endl;
    //close the lib
    dlclose(handle);
    if(dlerror()){
        cout << "Close" << libname << "failed" << dlerror() << endl;
        exit(1);
    }

    return 0;
}

编译生成可执行文件时需要添加-ldl参数声明链接器链接了一个动态库,命令如下:

g++ explicit.cpp -ldl -o testexp		# -ldl显式加载动态库的动态函数库

动态库显式调用结果
从上面的执行结果可以看出示例程序实现了把动态链接库作为参数传递给可执行程序的方式进行显式调用,如果多个动态库包含一个同名函数的不同实现,可以通过传参调用不同的动态库实现多态的效果。读者也可以稍加改动,把函数名也通过参数传递给可执行程序实现选择调用。可以通过nm -D libdynamicmath.so命令或objdump -T libdynamicmath.so查看符号表,从中找到库文件里面的函数名。
nm或objdump查看动态库符号表

六、Makefile文件编写

6.1 Makefile生成静态库与动态库

在包含库源文件和库头文件的目录下./lib新建一个Makefile文件,代码如下:

#自定义变量
MAKE	= make
CC		= g++
AR		= ar
#静态库编译选项,-Wall生成所有警告、-O0不优化、-std=c++11采用c++11标准、-g输出调试信息、-c只编译汇编不链接
CAFLAGS	= -Wall -O0 -std=c++11 -g -c
#动态库编译选项,-fPIC(Position Independent Code)、-shared生成共享目标文件
CSOFLAGS= -fPIC -shared -g
#打包选项,-c创建一个库,-r在库中加入或替换成员文件, -v显示操作的附加信息
ARFLAGS	= -crv

#wildcard为Makefile模式匹配关键字,获取目标目录符合匹配模式的所有文件名
LIBSRCS	= $(wildcard ./*.cpp)
#patsubst为Makefile模式替换关键字,查找字符串SRCS中按空格分开的单词,并将符合模式%.cpp的字符串全部替换成%.o
LIBOBJS	= $(patsubst ./%.cpp, ./%.o, $(LIBSRCS))
LIBA	= libstaticmath.a
LIBSO	= libdynamicmath.so

RM		= rm -f

#默认任务
default:
#默认任务要执行的命令,按上面的变量名替换为变量值后执行
	$(MAKE) liba
	$(MAKE) libso

#模式匹配,冒号前者为目标项,冒号后面为依赖项
liba: $(LIBOBJS)
	$(AR) $(ARFLAGS) $(LIBA) $(LIBOBJS)

libso: $(LIBSRCS)
	$(CC) $(CSOFLAGS) $(LIBSRCS) -o $(LIBSO)

# %模式自动匹配符
%.obj: %.cpp
# $<表示规则中的第一个依赖项、$@表示规则中的目标项
	$(CC) $(CAFLAGS) $< -o $@

#伪目标,声明clean为伪目标或标签,为了避免该清理任务与文件名相同而被错识别
.PHONY: clean
clean:
#清理之前的目标文件,以便下次完整的重新编译
	$(RM) $(LIBOBJS) $(LIBA) $(LIBSO)

make命令执行结果如下:
makefile生成库文件

6.2 Makefile链接生成可执行文件

在测试示例源码目录下新建Makefile文件,编写代码如下:

#自定义变量
CC		= g++
MAKE	= make
#静态链接选项,-g生成调试信息、-I指定头文件搜索路径,-L指定库文件搜索路径、-l静态库名、-static不共享
LDAFLAG	= -g -I./lib -L./lib -lstaticmath -static
#动态编译选项,-g生成调试信息、-I指定头文件搜索路径,-L指定库文件搜索路径、-l动态库名
LDSOFLAG= -g -I./lib -L./lib -ldynamicmath
#显式链接选项,-ldl显式加载动态库的动态函数库
LDEXFLAG= -g -ldl

SRCIMP	= implicit.cpp
SRCEXP	= explicit.cpp
INCLUDE	= ./lib
LIBA	= $(INCLUDE)/libstaticmath.a
LIBSO	= $(INCLUDE)/libdynamicmath.so

RM		= rm -f

#默认任务
default:
#默认任务要执行的命令,按上面的变量名替换为变量值后执行
	$(MAKE) testa
	$(MAKE) testso
	$(MAKE) testexp

#模式匹配,冒号前者为目标项,冒号后面为依赖项
testa: $(LIBA) $(SRCIMP)
	$(CC) $(SRCIMP) $(LDAFLAG) -o $@
#如果执行报错,提示找不到动态库,需要添加环境变量export LD_LIBRARY_PATH=`pwd`/lib
testso: $(LIBSO) $(SRCIMP)
	$(CC) $(SRCIMP) $(LDSOFLAG) -o $@

testexp: $(LIBSO) $(SRCEXP)
	$(CC) $(SRCEXP) $(LDEXFLAG) -o $@
#make -C后跟目标目录,读取目标目录下的Makefile
$(LIBA): $(INCLUDE)
	$(MAKE) -C $(INCLUDE) liba

$(LIBSO): $(INCLUDE)
	$(MAKE) -C $(INCLUDE) libso
#伪目标,声明clean为伪目标或标签,为了避免该清理任务与文件名相同而被错识别
.PHONY: clean
clean:
#清理之前的目标文件,以便下次完整的重新编译
	$(RM) testa testso testexp
	$(MAKE) -C $(INCLUDE) clean

make命令执行结果如下:
makefile链接库生成可执行文件

本文的源代码可以到GitHub下载:https://github.com/StreamAI/LinkLibrary(不熟悉GitHub使用的可以参考文章:GitHub社会化编程)。

如果想了解Windows环境动态库与静态库的创建和使用,可以查看我的另一个博文:Windows静态链接库与动态链接库的创建和显式与隐式调用

如果想深入了解动态库与静态库的链接与调用过程,推荐一本书《程序员的自我修养—链接、装载与库》。

  • 7
    点赞
  • 24
    收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:技术黑板 设计师:CSDN官方博客 返回首页
评论

打赏作者

流云IoT

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值