动态链接库相关知识
背景
目前欢乐游戏后台服务器都是由bin+so的方式构成,ServerFrame(bin)提供网络通信,内存管理,配置管理等基础的通用服务,so实现各个服务器的特有逻辑。bin文件和So公用一个全局变量G进行数据共享,在G在bin中读取相应的配置,so中根据G中的参数进行一系列的操作。bin和so还公用一些基础库的代码。
最近一件事情引起了后台同学的注意,服务器so更新后,同学执行bin文件,加载so后会发生错误,导致服务器会出现异常。
//TODO::确认一下修改代码的调用点到底是so还是bin,在so是直接调用还是间接调用
经过查看svn代码,发现了几个可疑的地方:
1.在bin和so公用的代码库里,有一个函数的定义发生了变化,增加了一个带有默认值的参数。这个函数会在so中被调用,由于带有默认参数,因此调用方式与从前一样,并没有发生改变。
2.最近的代码提交中,G的定义发生了变化,在其定义的中间添加了几个变量。
因为so编译时用的是最新代码,而bin文件并没有重新编译,因此其中的函数以及结构体的定义还都是老版本的定义,因此现在程序的结构图大致如下所示:
问题
Q1:因为调用除的代码不会变,是否可能so中队函数的调用依旧会走到了bin中所定义的旧版本的函数呢?
A1:不会。程序中调用哪一个函数是在编译时决定的。默认参数一般只定义在头文件中,只有编译器看到了函数有默认参数的声明或定义之后,才会在函数调用处根据情况,添加默认的参数,否则可能会出现编译问题。
在so中编译时确定了调用名为foo且有两个int类型参数的函数后,是不会再调用到bin中定义的foo函数的。
关于带默认值的参数,还需要注意一点:Never redefine an inherited default parameter valueQ2:现在两边看到的定义已经不同了,那么程序会如何运行。
A2:bin会按照老的偏移去给变量赋值,而so会依照新的偏移去获取数值并使用。因为中间添加了新的变量,导致取出的一部分变量没问题,一部分变量数值不正确。也正是因为这个问题,导致了更换so之后,服务器无法正常启动。
临时解决方案
因为问题的根本原因是由于数据结构的改变引起的不兼容,想要解决问题就必须更新bin文件,用最新的版本的数据结构定义编译后内存才能够匹配上。
延伸问题
这次问题的出现使一个没有注意到的问题浮出水面:
由于bin文件和so公用一些基础的代码库。而两者又是分开编译的,如果编译So时一个公用的函数已经更新,那么bin和so中的函数执行就可能出现问题。
实例:
橙色函数表示foo的新版本,期望的运行方式如图:
![]()
但实际可能出现的运行方式有可能是一下两种:
为了明白上面的问题,我们要搞清楚一下几个问题:
为什么在bin文件中和so中函数原型相同的两个函数有着不同的实现还能够正常的链接和运行。
我们的bin文件和so在编译时互相并不知道对方的存在。bin中通过dlopen打开so,除了在bin中调用的几个有限的接口之外,编译时不会知道so中其他的信息。因此在编译时,两者都可以编译通过。
那么为什么通过dlopen打开之后,bin和so中明明有相同函数原型的函数,却不会出现在编译时经常可以看到的错误信息呢?symbol “x” redefined: first defined in “./main.obj”; redefined in *.c
是什么决定了bin或者so调用到哪一个函数。
根据分析,既然运行时有多个定义,那么bin或者so又是如何决定选取哪一个定义,我们又是否有办法来控制bin和so的行为,让他们按照我们的希望选择相应的定义呢?
动态链接库相关知识
Position Independent Code
目前我们编译so时都会用到-fPIC选项,这表示生成的动态链接库(SO)是地址无关代码(Position Independent Code),那么地址有关无关到底有什么关系呢?
//libdep.c
int g = 1;
int foo(int a){
return g + a ;
}
由于动态链接库无法知道自己将会被加载到内存的哪一个位置,因此也就无法在编译时决定g的地址。对于非PIC的so,当libdep.so被不同的程序都用到的时候,g的地址也就不一样,导致ptr的赋值代码会不同。
如果so代码在不同的程序都不同,因此这种方式没有办法做到同一份指令被多个进程所共享。
而地址无关代码的so的内存结构如下所示:
地址无关的实现
PIC的实现基本想法是把指令中那些需要被修改的部分分离出来,跟数据放在一起,这样指令部分就可以保持不变,而数据部分可以在各进程中拥有一个副本。
这个想法之所以能够实现的前提是,在链接阶段,链接器可以知道数据段和程序段的相对偏移。
还是这段代码为例
//libdep.c
int g = 1;
int foo(int a){
return g + a ;
}
如果不是地址无关代码,那么生成的程序可能会是这么描述获取g值的过程。
将1234地址的内容放到ax寄存器。
而PIC生成的代码会是这样的
从数据段中用来实现PIC的数据结构中获取g的地址,并存到bx寄存器
将bx中地址的内容放到ax
图示如下:
数据段中用来实现PIC的数据结构有一个名字叫做全局偏移表(Global Offfset Table)。
GOT中包含哪些内容
我们可以把共享库中对地址的引用分为四类
- 模块内部的函数调用,跳转等
- 模块内部的数据访问,包括模块内定义的全局变量,静态变量
- 模块外部的函数调用,跳转等
- 模块外部的数据访问,比如定义在其他模块中定义的全局变量
static int