动态链接库函数内的静态变量,奇妙的UNIQUE Bind
title: 动态链接库函数内的静态变量,奇妙的UNIQUE Bind
date: 2018-09-28 09:28:22
tags:
介绍
模板函数和内敛函数中的静态变量,在跨so中的表现,和定义在其他函数中的静态变量的表现稍微有所不同。使用不慎,会造成预期之外的结果。本文对该现象进行了探讨。
多共享动态库的静态变量问题
最近遇到一个使用多个共享动态库时,由于静态变量导致的逻辑问题。考虑如下一个问题,主模块要打开A.so和B.so两个动态库,两个动态库的代码使用到了同一个模板函数,而该模板函数有一个静态变量。那么,当两个动态库都加载到内存时,这两个函数间会产生联系吗?
头文件和so的示例代码如下:
//so_test.h
#include <stdio.h>
template<typename T> void print_msg(T) {
static int num = 0;
num++;
printf("msg form , num = %d, \n", num );
printf("-----------------------\n");
}
#define EXPORT_DYN_SYM __attribute__ ((visibility ("default")))
extern "C" {
EXPORT_DYN_SYM void test_a();
EXPORT_DYN_SYM void test_b();
EXPORT_DYN_SYM void test_c();
}
//A.so
#include "so_test.h"
void test_a()
{
printf("this is in test_a...\n");
print_msg();
}
//B.so
#include "so_test.h"
void test_b()
{
printf("this is in test_b...\n");
print_msg();
}
加载模块的代码如下,动态加载两个so,并调用两个函数,RTLD_LOCAL属性表示调用函数时应该尽量在本地so寻找符号。
#include "so_test.h"
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#define LIB_A_PATH "./liba.so"
#define LIB_B_PATH "./libb.so"
typedef void (*DYN_FUNC)();
int main()
{
//test_a();
//test_b();
void *handle,* handle2;
char *error;
DYN_FUNC func_a = NULL;
DYN_FUNC func_b = NULL;
//打开动态链接库
handle = dlopen(LIB_A_PATH, RTLD_LAZY |RTLD_LOCAL); //错误处理过程已省略
handle2 = dlopen(LIB_B_PATH, RTLD_LAZY | RTLD_LOCAL);//错误处理过程已省略
func_a = (DYN_FUNC)dlsym(handle, "test_a" );//错误处理过程已省略
func_b = (DYN_FUNC)dlsym(handle2, "test_b");//错误处理过程已省略
func_b();
func_a();
return 0;
}
使用如下的命令编译该代码:
g++ -fPIC -shared -g -o liba.so so_a.c
g++ -fPIC -shared -g -o libb.so so_b.c
g++ -o test test.c -g -ldl
程序执行后结果如下:
this is in test_b...
msg , num = 1,
-----------------------
this is in test_a...
msg , num = 2,
-----------------------
从程序执行结果看,这两个so中的同名函数产生了联系,这种联系是怎么产生的呢?是因为调用了同一个print_msg函数,还是因为使用了相同的静态变量呢?
模板函数中的静态变量分析
在第一节中,发现不同so中实例化的同名模板函数之间产生了联系。
在往下分析之前,首先要了解两个事实:
- 这个模板函数在a.so和b.so分别实例化了一份代码,可以通过
readelf -sW liba.so
看到两个的函数各自的符号。 - 如果不适用模板,而是将
print_msg
分别在a.c和b.c中各定义一份,此时编译生成so之后,执行结果的两个函数间是没有联系的,也就是打印结果是两个num=1
。该测试这里不再详细描述。
那么为什么两个不同的函数中的num++会互相影响呢?
- 是因为plt调用了同一个print_msg函数吗?
- 还是因为两个print_msg函数使用了同一个静态变量。
首先看问题1, 是不是因为plt调用了同一个print_msg函数。可以在gdb中下断点观察函数的地址,也可以在代码中添加print打印函数的地址。
在代码中打印print_msg函数的代码如下,将这两行代码分别添加到test_a和test_b函数中。
void (*p)(int) = print_msg<int>;
printf("print_msg is %p\n", p);
同时,在so_test.h的print_msg函数中打印静态变量的地址。
printf("msg ,num addess %p, num = %d, \n", &num, num );
更改之后,编译,执行,结果如下
print_msg is 0x7f42a8d94902
this is in test_b...
msg ,num addess 0x7f42a919704c, num = 1,
-----------------------
print_msg is 0x7f42a8f96812
this is in test_a...
msg ,num addess 0x7f42a919704c, num = 2,
-----------------------
从结果看,a.so和b.so之间的调用的print_msg是不同的地址,这两个print_msg是不同的函数,但是静态变量num的地址是相同的。这是不寻常的。
不熟悉的人可能会认为同名函数的静态变量本来应该是一个。实际上,如果没有使用模板函数的模板化,而是各自定义相同代码的print_msg,甚至加载相同的so,两个so间同名函数使用的同名静态变量,也是不同的。可以将上文的print_msg从模板函数改为本地函数得到验证.
在上文中的main函数里,使用了dlopen和dlsym来动态加载函数,而没有在编译是用
-L./ -la -lb
选项链接a.so和b.so,并直接调用test_a和test_b,是因为如果在编译时就指定了链接的话,print_msg将从plt表中获取,此时test_a和test_b将调用的是同一个print_msg函数。
首先,我们知道对于加了选项 -fpic或 -fPIC的共享库,全局变量的地址都存放在该共享库的全局偏移表(Global Offset Table,GOT)中,那么这个静态变量是不是这样呢,使用objdump或者 readelf命令分析共享库a.so结果如下。_ZZ9print_msgIiEvT_E3num就是我们模板函数中的静态变变量(c++ name mangling后的符号名),现在在GOT表中。
$objdump -x -R libb.so | grep num
0000000000201060 l O .bss 0000000000000004 _ZZ11local_printvE3num
0000000000201068 u O .bss 0000000000000004 _ZZ9print_msgIiEvT_E3num
0000000000200fd8 R_X86_64_GLOB_DAT _ZZ9print_msgIiEvT_E3num@@Base
这就解释我们的问题了吗?不,虽然_ZZ9print_msgIiEvT_E3num在GOT表中,但是这并不能解释为什么模板函数和普通函数的静态变量表现不同。即使我们在两个so中定义了同名的全局变量,全局变量也一样出现在GOT表中,但是这两个全局变量仍然会指向两个不同的地址。不同的so间同名全局变量不会相互干扰。
接下来,使用readelf工具查看这个静态变量到底有什么不同之处。
$readelf -sW liba.so
Num: Value Size Type Bind Vis Ndx Name
....
10: 0000000000201054 4 OBJECT UNIQUE DEFAULT 23 _ZZ9print_msgIiEvT_E3num
60: 0000000000201054 4 OBJECT UNIQUE DEFAULT 23 _ZZ9print_msgIiEvT_E3num
从结果看,_ZZ9print_msgIiEvT_E3num就是我们要找的静态变量。这两行的结果分别是'.dynsym'节区和'.symtab'节区的内容。如果对ELF文件的格式熟悉的话,会注意到,常见的函数Bind Type一般是LOCAL、GLOBAL或者WEAK。就算是全局变量,Bind类型也是GLOBAL。这里出现了UNIQUE,UNIQUE
是什么,它又表示什么意思?
STB_GNU_UNIQUE的Bind属性
上一节中最后提到的UNIQUE属性全名是STB_GNU_UNIQUE
。该属性表示了符号在动态链接过程中的一种类型,它的工作模式并不是很直观。这里找到了一份dllookup的代码。在处理STB_GNU_UNIQUE
时的注释如下:
307 case STB_GNU_UNIQUE:;
308 /* We have to determine whether we already found a
309 symbol with this name before. If not then we have to
310 add it to the search table. If we already found a
311 definition we have to use it. */
大致意思是说,在处理该属性的符号时,会先查找搜索表内容,如果搜索表中已经存在该符号,则使用已经存在的符号,否则将其加入搜索表。
到这里,已经大致能够猜到,STB_GNU_UNIQUE
属性的符号,在链接时只会有一份,即使这些符号分布在不同的so之间。就算由于模板函数中的静态变量是STB_GNU_UNIQUE
属性,导致改模板函数即使在不同的so中各实例化了一份代码,也要使用同一个静态变量。
而且,通过在谷歌搜索STB_GNU_UNIQUE
,发现STB_GNU_UNIQUE
还有导致一个其他的更为常见的问题:无法使用dlclose卸载含有STB_GNU_UNIQUE
变量的动态库。
在StackOverFlow有这么一个问题dlclose() doesn't work with factory function & complex static in function?。其中一个回答的内容是
What's happening is that there is a STB_GNU_UNIQUE symbol in libempty.so:
readelf -Ws libempty.so | grep _ZGVZN3Foo4initEvE2ns
91: 0000000000203e80 8 OBJECT UNIQUE DEFAULT 25 _ZGVZN3Foo4initEvE2ns
77: 0000000000203e80 8 OBJECT UNIQUE DEFAULT 25 _ZGVZN3Foo4initEvE2ns
The problem is that STB_GNU_UNIQUE symbols work quite un-intuitively, and persist across dlopen/dlclose calls.
The use of that symbol forces glibc to mark your library as non-unloadable here.
There are other surprises with GNU_UNIQUE symbols as well. If you use sufficiently recent gold linker, you can disable the GNU_UNIQUE with --no-gnu-unique flag.
可以知道,STB_GNU_UNIQUE
将会强制标记动态库为不可使用dlcose卸载。如果不希望生成该类型的符号,则需要在编译时使用--no-gnu-unique
选项。
inline函数的静态符号
除了第一个节使用的模板函数外,在inline函数中使用静态符号,也会生成UNIQUE类型的变量符号。
使用如下的代码
inline int goo() {
static int xyz;
return xyz++;
}
void test_b()
{
print_msg<int>(1);
goo();
}
g++ -fPIC -shared -g -o liba.so so_a.c
编译生成so文件后,使用readelf查看xyz变量的属性。
$readelf -sW libb.so | grep xyz
13: 0000000000201064 4 OBJECT UNIQUE DEFAULT 23 _ZZ3goovE3xyz
67: 0000000000201064 4 OBJECT UNIQUE DEFAULT 23 _ZZ3goovE3xyz
可以看到,xyz对应的符号_ZZ3goovE3xyz属性也是UNIQUE。根据上一节的分析,不同so之间使用该inline函数,也会使用同一个静态变量符号。而且,使用了这个inline函数后,也会导致编译生成的动态库不可卸载。
避开UNIQUE
有的时候,我们不希望不同so之间的同名函数互相影响,或者希望能够动态加载和卸载动态库,但又不得不让该变量继续是static。除了上文中提到过的--no-gnu-unique
编译选项,还有什么办法可以避开STB_GNU_UNIQUE
属性呢?
有一个方法是使用static。不是说static变量导致了该属性吗?怎么还要使用static。这一次的static使用在函数前,而不是变量前。例如上一节的内敛函数,可以使用static声明。
static inline int goo() {
static int xyz;
return xyz++;
}
之后再次使用该函数时,生成的符号属性则如下所示。
$readelf -sW libb.so | grep xyz
45: 0000000000201058 4 OBJECT LOCAL DEFAULT 23 _ZZL3goovE3xyz
处理发现变量的Bind从UNIQUE编程了LOCAL以外,还会发现,前边readelf都会发现该变量有两行结果,一个在'.dynsym'节区,一个在'.symtab'节区。而这次只剩下了一行结果。这是因为'.dynsym'节区没有这个符号了,只剩下了'.symtab'节区的符号。
此外,在编译选项中使用--visibility=hidden
,也会将该符号变为LOCAL。
参考资料
- https://www.ibm.com/developerworks/cn/linux/l-cn-sdlstatic/index.html
- https://stackoverflow.com/questions/11050693/dlclose-doesnt-work-with-factory-function-complex-static-in-function
- https://sourceware.org/git/?p=glibc.git;a=blob;f=elf/dl-lookup.c;h=a2a699b48f5f188da2528ed163b7befffed586ee;hb=HEAD#l445
动态链接库函数内的静态变量,奇妙的UNIQUE Bind的更多相关文章
- 成员函数内定义static变量(不安全,各对象之间共享)
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明.本文链接:https://blog.csdn.net/u012317833/article/de ...
- 【ThinkingInC++】52、函数内部的静态变量
/** * 书本:[ThinkingInC++] * 功能:函数内部的静态变量 * 时间:2014年9月17日18:06:33 * 作者:cutter_point */ #include " ...
- 实例甜点 Unreal Engine 4迷你教程(5)之函数中的静态变量
本小节的教程无前置教程,可直接学习,篇幅很短. 本教程浓缩起来就是一句话:函数中的静态变量在调试过程中保留值.所以需要谨慎对待. 什么意思?请先不要一步一步对着做,而整体地看一遍下面的过程: 第一步: ...
- PHP引用操作以及外部操作函数的局部静态变量的方法
通过引用方式在外部操作函数或成员方法内部的静态变量 下面举个简单的例子,说明三个关于引用方面的问题: 1. 参数引用后函数内进行类型转换同样是地址操作 2. 参数引用后再传递给其他函数时需要再次添加引 ...
- PHP笔记4__函数/全局、静态变量/函数参数/加载函数库/,,
<?php header("Content-type: text/html; charset=utf-8"); echo table(5,5); function table ...
- var声明的成员变量和函数内声明的变量区别
1.函数内部,有var声明的是局部变量,没var的,声明的全局变量. 2.在全局作用域内声明变量时,有var 和没var声明的都是全局变量,是window的属性.通过变量var声明全局对象的属性无法通 ...
- js函数内未声明变量
<script> function test(){ testd = "Hello"; } test(); alert(testd); </script> 当 ...
- C++中如何可以修改const函数内的成员变量的值?
呵呵,你使用mutable关键字来定义变量就可以了.下面举例说明 C++关键字mutable Mutable (1)mutable的意思是"可变的,易变的",跟C++中的const ...
- C++ 全局变量 静态变量 全局函数 静态函数
1. static 变量 静态变量的类型 说明符是static. 静态变量当然是属于静态存储方式,但是属于静态存储方式的量不一定就是静态变量. 例如外部变量虽属于静态存储方式,但不一定是静态变量,必须 ...
随机推荐
- Azkaban调度器
Azkaban介绍 Azkaban 是由 Linkedin 公司推出的一个批量工作流任务调度器,用于在一个工作流内以一个特定的顺序运行一组工作和流程.Azkaban 使用 job 配置文件建立任务之间 ...
- sqlserver索引维护(重新组织生成索引)
sqlserver索引的维护 1:查看索引碎片大于百分三十以上的索引 select object_id= object_id,indexid = index_id,partitionnum = par ...
- C#调用Excel VBA宏[转载]
原文地址:https://www.cnblogs.com/heekui/archive/2008/03/30/1129355.html 近日的一系列工作是做网站的营运维护,因此做了大量的支持工具.有E ...
- How to import .sql script
How to import .sql script 1.Export .sql from pl/sql developer you can reference to other document in ...
- python 中的list&tuple
list Python内置的一种数据类型是列表:list>>> classmates = ['Michael', 'Bob', 'Tracy']>>> classm ...
- 工作好搭档(一):松林 SL-B3 人体工学椅
本人从事码农这行职业,已经整整十年零九天,十年一觉如旧梦,仿佛昨天还在SARS. 2008年,我累到腰痛,脖子痛,怎么休息也不见好,去中医院检查,医生诊断,坐的太久,坐姿不对,运动少,轻度颈椎,腰肌劳 ...
- C#配置IIS搭建网站的工具类
public class IISWorker { public static string HostName = "localhost"; /// <summary> ...
- SpringMVC WEB应用上传照片的实现
使用是SpringMVC+Hibernate搭建的WEB应用,使用jsp写的前端页面. 如何将文件上传到服务器呢?我这里使用的是Multipart的形式将文件上传. 这里有两大步:一是配置multip ...
- Mysql数据库,表中有中文时,select出来好多问号(?)的解决方法
在QQ群里问了一些高手,同时参考了这篇文章:http://huangyunbin.iteye.com/blog/1113983,终于把这个问题搞定了. 首先,我用的是zip包的Mysql,直接解压使用 ...
- tp3.2中前台模板中日期时间的转换
{$vo.create_time|date='Y-m-d',###} 其中###是占位符