程序编译、运行使用动态库的一个示例

背景介绍

       在一个工作项目中我和另外几个同事各负责一个进程的代码编程,这就必然涉及到进程间通信的问题,而我们公司针对这一平台已经开发出了完善的进程间消息传递API接口,包含了各种消息的发送和接收接口函数,以及数据结构等。因此我在编程中只需要调用相应的接口就可以给其他进程发送数据或是接收其他进程发送过来的数据,正式编程伊始我写了两个简单的测试用例(进程),用以验证两个进程是否能成功实现数据交互。由于涉及到动态库的使用问题,这是一个比较常见的软件开发场景,在此次程序编译调试过程中弥补了不少这方面知识上的空白,是一个非常值得总结的案例。

静态库与动态库

       首先使用API需要提供的头文件和库文件如下所示: 

图1 例程使用的头文件和库文件

       本次测试程序用到了三个接口函数,我们知道头文件提供的是函数的声明,它只起描述性作用,如函数功能、输入、输出和返回值的阐释,用户据此调用相关函数或变量。库文件就是存放函数的具体实现代码,只不过是以二进制形式提供给用户使用的

什么是库?

       库是写好的现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常

       本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。库有两种:静态库和动态库。在widows平台下,静态链接库是.lib文件,动态库文件是.dll文件;在Linux平台下,静态链接库是.a文件,动态链接库是.so文件。本文是基于Linux环境下的动态库使用案例。

       从上图中库文件后缀.so可以看出这是一个动态库,在了解动态库之前,首先必须知道一个程序编译形成可执行程序一般需要经过预处理—>编译—>汇编—>链接几个步骤,链接操作就是将编译、汇编形成的多个目标文件(Linux平台下是.o文件,windows平台下是.obj文件)和链接库文件进行链接,最终合并成可执行文件。所谓静态、动态就是链接时如何处理库,是动态链接方式还是静态链接方式。

1. 静态库

       在应用中,有一些公共代码是需要反复使用,就把这些代码编译为“库”文件;在使用静态库的情况下,在链接步骤中,链接器是从库中复制所需的代码和其他目标文件(.o文件)一起链接打包到最终的可执行文件中。当发布产品时,只需要发布这个可执行文件,并不需要发布被使用的静态库。因此静态链接方式,这种库被称之为静态库,其特点是可执行文件中包含了库代码的一份完整拷贝,且被多次使用就会有多份冗余拷贝。静态库的代码在编译过程中已经被载入可执行程序,因此体积较大。

       试想一下,静态库与汇编生成的目标文件一起链接为可执行文件,那么静态库必定跟.o文件格式相似。其实一个静态库可以简单看成是一组目标文件(.o/.obj文件)的集合,即很多目标文件经过压缩打包后形成的一个文件。静态库特点总结如下:

       ①静态库对函数库的链接是在编译时期完成的。

       ②程序在运行时与函数库再无瓜葛,移植方便。

       ③浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。

       ④静态库对程序的更新、部署和发布会带来麻烦,如果静态库更新了,所有使用它的应用程序都需要重新编译、发布给用户(可能是一个很小的改动,却导致整个程序重新下载,全量更新)。

2. 动态库

       动态库英文为DLL,是Dynamic Link Library 的缩写形式,DLL是一个包含可由多个程序同时使用的代码和数据的库,DLL不是可执行文件。动态链接提供了一种方法,使进程可以调用不属于其可执行代码的函数。函数的可执行代码位于一个 DLL 中,该 DLL 包含一个或多个已被编译、链接并与使用它们的进程分开存储的函数。DLL 还有助于共享数据和资源。多个应用程序可同时访问内存中单个DLL 副本的内容。DLL 是一个包含可由多个程序同时使用的代码和数据的库,Windows下动态库为.dll后缀,Linux下为.so后缀。

       动态库在链接阶段没有被复制到程序中,而是程序在运行时由系统动态加载到内存中供程序调用。因此在发布产品时,除了发布可执行文件以外,同时还需要发布该程序将要调用的动态链接库。动态库的代码是在可执行程序运行时才载入内存的,在编译过程中仅简单的引用,因此代码体积较小;还有一个好处就是,不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,不同的程序可以得到内存中相同的动态库的副本,因此节省了很多内存,规避了空间浪费问题。动态库特点总结如下:

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

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

       ③将一些程序升级变得简单,用户只需要更新动态库即可,增量更新。。

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

3. 动态库与静态库对比

       动态库和静态库的本质区别就是库中代码被载入的时刻不同。静态库在程序的链接阶段被复制到了程序中,和程序运行的时候没有关系;动态库在链接阶段没有被复制到程序中,而是程序在运行时由系统动态加载到内存中供程序调用。正是由于此两者有以下不同:

       ①可执行文件大小不一样:静态链接的可执行文件要比动态链接的可执行文件要大得多,因为它将需要用到的代码从二进制文件中“拷贝”了一份,而动态库仅仅是复制了一些重定位和符号表信息。

       ②占用内存大小不一样:如果有一个库中函数,那么静态库中的同一个函数的代码就会被复制多份,而动态库只有一份,因此使用静态库占用的磁盘空间相对比动态库要大。

       ③扩展性与兼容性不一样:如果静态库中某个函数的实现变了,那么可执行文件必须重新编译,而对于动态链接生成的可执行文件,只需要更新动态库本身即可,不需要重新编译可执行文件。正因如此,使用动态库的程序方便升级和部署。

       ④依赖不一样:静态链接的可执行文件不需要依赖其他的内容即可运行,而动态链接的可执行文件必须依赖动态库的存在。所以如果你在安装一些软件的时候,提示某个动态库不存在的时候也就不奇怪了。

       ⑤复杂性不一样:相对来讲,动态库的处理要比静态库要复杂,例如,如何在运行时确定地址?多个进程如何共享一个动态库?当然,作为调用者我们不需要关注。另外动态库版本的管理也是一项技术活。

       ⑥加载速度不一样:由于静态库在链接时就和可执行文件在一块了,而动态库在加载或者运行时才链接,因此,对于同样的程序,静态链接的要比动态链接加载更快。所以选择静态库还是动态库是空间和时间的考量。但是通常来说,牺牲这点性能来换取程序在空间上的节省和部署的灵活性时值得的。

编译链接动态库

       关于静态库和动态库本文只简单介绍这些(本人目前暂时只了解这么多),了解这些基本信息应该可以帮助我们理解并解决一些编译问题。言归正传,回到动态库的使用示例,首先在库文件目录下新建一个文件夹用于存放两个进程源文件,编程时要包含相关接口函数的头文件,这里需注意相关头文件在上一级目录include文件夹中,所以此时包含头文件需要使用" ../ ",进程1和进程2简单地调用三个接口函数实现互相收发数据功能,进程1源文件代码如下:

#include "../include/mbmessage.h"
#include "../include/mbconf.h"
#include "../include/mbrealapi.h"
#include "../include/shared_mm.h"
#include "../include/mbhis.h"
#include "../include/apim6y/declare.h"
#include "../include/apim6y/api.h"
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>


void Api_Rec_MSG_Process(LPMSGINFO lpMsgInfo);
int GeneralRevFromOtherPro(LPMSGINFO lpMsgInfo);

int main(int argc, char *argv[])
{
	char str[20] = "hello,jdp";
    GENERALMSG generalMsg;

    sleep(3);       //延迟进程启动5s
	printf("process 1(pid = %d) running",getpid());
    while(kh_publisherMsg())
	{
		printf("rcd_dj200: kh_publisherMsg error .\n");
		usleep(2000);
	}
    while(kh_subscriberMsg_async(Api_Rec_MSG_Process))
	{
		printf("rcd_djc200: kh_subscriberMsg_async error .\n");
		usleep(2000);
	}
    while(1)
    {
        printf("进程1等待接收消息···\n");
        sleep(15);
        generalMsg.type = 520;
        generalMsg.dataLen = 20;
        memcpy(&generalMsg.buf[0],str,20);
        kh_sendGeneralMsg(&generalMsg);
        printf("进程1发送消息成功\n");
    }
	
    return 0;    
}

void Api_Rec_MSG_Process(LPMSGINFO lpMsgInfo)	//消息接收处理
{
	//代码添加处
	switch(lpMsgInfo->msgType)
	{
		case MSGFILE_TYPE:
			break;
		case SOEINFO_TYPE:				// 2:接收SOE消息
			break;
		case PROTECTINFO_TYPE:
			break;
		case SBODOSAVEINFO_TYPE:
			break;
		case GENERALMSG_TYPE:			// 5: 接收通用消息
			GeneralRevFromOtherPro(lpMsgInfo);//Api_Rec_MSG_GENERAL(lpMsgInfo);
			break;
		case SBOCTRL_TYPE:
			break;
		case SBOCTRLRET_TYPE:
			break;
		case DIGITALOUT_TYPE:
			break;
		case DIGITALOUTRET_TYPE:
			break;
		default:
			break;
	}
	return ;
}

int GeneralRevFromOtherPro(LPMSGINFO lpMsgInfo)
{
	GENERALMSG *pData;

    if(lpMsgInfo->msgType == 5)
    {
        printf("进程1接收到消息,类型为 %d:通用消息\n",lpMsgInfo->msgType);
        pData = (GENERALMSG *)&lpMsgInfo->msgInfo;
        printf("进程1接收到通用消息类型: %d\n",pData->type);
        printf("进程1接收到通用消息长度: %d\n",pData->dataLen);
        printf("进程1接收到通用消息内容: %s\n",&pData->buf[0]);
    }
    else
        printf("进程1接收到数据但是数据错误\n");
    
    return 0;
}

       进程2与进程1代码相差无几,至此已经具备两个进程的源码,接下来分别编译得到两个可执行文件。由于只有一个源文件,因此没有必要编写Makefile,直接单独编译,首先我们用编译器直接编译源程序,结果如下:

图2 编译报错

       可以看到编译报错三个函数未定义,这正是例程中调用的三个接口函数,也就是编译器找不到这三个函数的定义,对于"undefined reference to XXX"这种编译报错,一般由以下几个原因造成:

       ①链接时缺少定义了XXX的源文件或者目标文件或者库文件:例如有两个源文件main.c和test.c,main.c依赖test.c(即main.c中所调用函数在test.c中定义),此时如果只编译main.c一个文件就会报此错误(缺少源文件);若先前已经单独编译test.c得到test.o目标文件,但是编译main.c时没有链接test.o也会报此错误(缺少目标文件);若把test.c编译成动态库得到.so文件,但是在编译main.c时没有链接到库也会报此错误(缺少库文件)。

       ②链接顺序不对:在给编译器输入源文件、目标文件或者动态静态库文件时,如果A文件依赖于B文件中的内容,那么A文件应该放在B文件的左边。

       ③函数符号修饰不一样:一般是函数定义和声明不一致。

       详细的"undefined reference to XXX"编译报错原因和解决办法的介绍可以参考此博客:

"undefined reference to XXX"问题总结 - 知乎 (zhihu.com)

       显而易见,我这里报错是因为编译链接时找不到库文件,将例程源文件与动态库.so文件链接生成可执行文件命令如下:

图3 编译链接动态库

-L../lib:-L参数是指定库的位置

-l:-ltest表示要链接libtet.so

运行链接动态库

       至此已经成功得到两个进程的可执行文件了,那么它们能否直接运行呢?把两个可执行文件导入开发板中尝试让其直接运行,结果如下:

图4 运行出错打印1

       显然不能成功运行,提示找不到共享库(动态库)。原因很简单,上文中已经提及动态库是在程序在运行时才加载到内存中供程序调用,因此在发布产品时,除了发布可执行文件以外,同时还需要发布该程序将要调用的动态链接库。这里我们并没有把动态库跟可执行文件一起导入到开发板中,程序在运行时自然找不到动态库。

       运行由动态链接库生成的可执行文件时,必须确保程序在运行时可以找到这个动态链接库。如果我把lib和可执行文件整个文件夹导入开发板中,此时能否正常运行?

图5 运行出错打印2

       可以看到仍提示找不到链接库,这是因为在执行程序时没有指定动态库的路径,虽然在编译时链接动态库指定路径可以通过编译,但是-L选项指定的路径只在编译时有效,编译出来的可执行文件不知道-L选项后面的值,当然找不到。

Linux下程序运行时动态库搜索路径顺序如下(按优先级排序):

1. 编译目标代码时指定的动态库搜索路径(在makefile中,一般使用“-Wl -rpath”来指明程序运行时到哪个路径去找库);

2. 环境变量LD_LIBRARY_PATH指定的路径(可以使用 echo LD_LIBRARY_PATH查看。一般初始时/lib和/user/lib库包含在里面。用户可以往里面添加);

3. 配置文件/etc/ld.so.conf中指定的动态库搜索路径( /etc/ld.so.conf的第一行有一个引用命令:include ld.so.conf.d/*.conf, 所以可以通过修改/etc/ls.so.conf这个配置文件来增删路径,也可以增加一个.conf文件来配置特有的动态库路径。直接将寻库路径加进来即可,保存后需要运行一下ldconfig重载一下);

4. 默认动态库搜索路径/lib和/usr/lib(这两个路径是系统最初就添加在LD_LIBRARY_PATH中,所以将库移动到这里亦可以寻找到)。

参考博客:linux环境下程序搜索动态库路径和加载相关操作 - 王清河 - 博客园 (cnblogs.com)

       程序运行时不会把当前目录作为默认的寻库路径,除非在编译时指定;目前我们要用的动态库路径不符于上述搜索逻辑,故需要在系统中增添我们使用的动态库路径,以确保程序运行时能够找到动态库。依据此有以下几种解决办法:

       ①在LD_LIBRARY_PATH中增加路径

       在终端输入:export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:xxx,其中xxx为动态链接库文件的绝对存储路径(此方式临时有效,终端重启后就失效,可在开机自启动脚本使用)。

LIBRARY_PATH环境变量:指定程序静态链接库文件搜索路径。

LD_LIBRARY_PATH环境变量:指定程序动态链接库文件搜索路径。

       ②编译时(或者在Makefile)添加-Wl,-rpath,指定程序运行时的动态库搜索路径。

       ③修改~./bashrc或~/.bash_profile或系统级别的/etc/profile,然后执行source .bashrc命令 (source命令通常用于重新执行刚修改的初始化文件,使之立刻生效,而不必注销并重新登录)。

       ④在/etc/ld.so.conf中增加路径,或者在/etc/文件目录下增加 *.conf文件去配置自己独有的寻库路径。修改之后要ldconfig一下。

       我只是简单的测试一下,修改系统内容不合适,所以只试试前两种方法。首先是在终端中输入命令指定动态链接库文件搜索路径,然后运行程序,结果如下:

图6 程序运行成功打印

       第二种解决方法就是在编译时添加-Wl,-rpath指定程序运行时动态库搜索路径,编译指令如下所示:

图7 编译指定动态路径

       这样就可以直接运行程序了,不用在终端输入命令指定路径,演示结果如下:

动态库编译时指定路径直接运行测试结果

综上可知:程序用到动态库时,编译和运行都必需要链接到动态库!

两个常用的可执行程序查看工具

1. ldd命令

       使用ldd工具,查看可执行程序依赖哪些动态库或着动态库依赖于哪些动态库。例如我编写一个简单的小程序,里面仅调用一个标准库函数printf,代码如下:

#include<stdio.h>

int main()
{
    printf("ldd test\n");

    return 0;
}

       先编译得到可执行程序,然后再使用ldd命令查看可执行程序依赖哪些动态库(也就是printf函数依赖于哪些动态库) ,在ubuntu的linux终端查看结果如下:

图8 ldd命令查看结果(异常)

       提示not a dynamic executable,这是因为我经常用的是交叉编译工具,编译生成的是在开发板上运行的可执行程序,在ubuntu系统上运行不了,自然就查看不了依赖哪些库。改为用gcc编译工具编译就可以了,结果如下:

图9 ldd命令查看结果

2. nm命令

       有时候可能需要查看一个库中到底有哪些函数,nm命令可以打印出库中的涉及到的所有符号。库既可以是静态的也可以是动态的。nm列出的符号有很多,常见的有三种:

       ①一种是在库中被调用,但并没有在库中定义(表明需要其他库支持),用U表示;

       ②一种是库中定义的函数,用T表示,这是最常见的;

       ③一种是所谓的弱态"符号,它们虽然在库中被定义,但是可能被其他库中的同名符号覆盖,用W表示。

       例如查看上文例程中用到的libmagicboxapi.so库中的函数,发现就有我们使用的接口函数,属于T类,即在库中定义的函数。 

图10 nm命令查看结果
  • 23
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值