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中实例化的同名模板函数之间产生了联系。

在往下分析之前,首先要了解两个事实:

  1. 这个模板函数在a.so和b.so分别实例化了一份代码,可以通过readelf -sW liba.so看到两个的函数各自的符号。
  2. 如果不适用模板,而是将print_msg分别在a.c和b.c中各定义一份,此时编译生成so之后,执行结果的两个函数间是没有联系的,也就是打印结果是两个num=1。该测试这里不再详细描述。

那么为什么两个不同的函数中的num++会互相影响呢?

  1. 是因为plt调用了同一个print_msg函数吗?
  2. 还是因为两个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。

参考资料

动态链接库函数内的静态变量,奇妙的UNIQUE Bind的更多相关文章

  1. 成员函数内定义static变量(不安全,各对象之间共享)

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明.本文链接:https://blog.csdn.net/u012317833/article/de ...

  2. 【ThinkingInC++】52、函数内部的静态变量

    /** * 书本:[ThinkingInC++] * 功能:函数内部的静态变量 * 时间:2014年9月17日18:06:33 * 作者:cutter_point */ #include " ...

  3. 实例甜点 Unreal Engine 4迷你教程(5)之函数中的静态变量

    本小节的教程无前置教程,可直接学习,篇幅很短. 本教程浓缩起来就是一句话:函数中的静态变量在调试过程中保留值.所以需要谨慎对待. 什么意思?请先不要一步一步对着做,而整体地看一遍下面的过程: 第一步: ...

  4. PHP引用操作以及外部操作函数的局部静态变量的方法

    通过引用方式在外部操作函数或成员方法内部的静态变量 下面举个简单的例子,说明三个关于引用方面的问题: 1. 参数引用后函数内进行类型转换同样是地址操作 2. 参数引用后再传递给其他函数时需要再次添加引 ...

  5. PHP笔记4__函数/全局、静态变量/函数参数/加载函数库/,,

    <?php header("Content-type: text/html; charset=utf-8"); echo table(5,5); function table ...

  6. var声明的成员变量和函数内声明的变量区别

    1.函数内部,有var声明的是局部变量,没var的,声明的全局变量. 2.在全局作用域内声明变量时,有var 和没var声明的都是全局变量,是window的属性.通过变量var声明全局对象的属性无法通 ...

  7. js函数内未声明变量

    <script> function test(){ testd = "Hello"; } test(); alert(testd); </script> 当 ...

  8. C++中如何可以修改const函数内的成员变量的值?

    呵呵,你使用mutable关键字来定义变量就可以了.下面举例说明 C++关键字mutable Mutable (1)mutable的意思是"可变的,易变的",跟C++中的const ...

  9. C++ 全局变量 静态变量 全局函数 静态函数

    1. static 变量 静态变量的类型 说明符是static. 静态变量当然是属于静态存储方式,但是属于静态存储方式的量不一定就是静态变量. 例如外部变量虽属于静态存储方式,但不一定是静态变量,必须 ...

随机推荐

  1. windows常用快捷命令

    打开控制面板 control.exe 1.操作中心 wscui.cpl 2.Windows防火墙 Firewall.cpl 3.设备管理器 hdwwiz.cpl 4.Internet属性 inetcp ...

  2. drag与drop事件

    为了支持网页上一些元素的拖动效果,可以使用drag和drog事件. 目前ie 5.0+, firefox 3.5+等都支持这些事件,ECMA Script第5版正式将其纳入标准. 对于被拖动的元素来说 ...

  3. 《SQL必知必会》总结

    目录   第1章 了解SQL 第2章 检索数据 第3章 排序检索数据 第4章 过滤数据 第5章 高级数据过滤 第6章 用通配符进行过滤 第7章 创建计算字段 第8章 使用数据处理函数 第9章 汇总数据 ...

  4. vue记录

    vue项目中使用默认图片代替异常图片 第一种方法 <img onerror="javascript:this.src='../../static/custom.png';" ...

  5. C#图解教程读书笔记(第3章 类型、存储及变量)

    1.C#的中的数值不具有bool特性. 2.dynamic在使用动态语言编写的程序集时使用,这个不太明白,看到后面需要补充!! 动态化的静态类型 3.对于引用类型,引用是存放在栈中,而数据是存放在堆里 ...

  6. nginx里配置跨域

    发布于 881天前  作者 wendal  1404 次浏览  复制  上一个帖子  下一个帖子  标签: nginx 跨域 if ($request_method = OPTIONS ) { add ...

  7. linux一切皆文件之文件描述符

    一.知识准备 1.在linux中,一切皆为文件,所有不同种类的类型都被抽象成文件.如:普通文件.目录.字符设备.块设备.套接字等2.当一个文件被进程打开,就会创建一个文件描述符.这时候,文件的路径就成 ...

  8. Struts2注解 及 约定优于配置

    Struts2注解 1 Struts2注解的作用 使用注解可以用来替换struts.xml配置文件!!! 2 导包 必须导入struts2-convention-plugin-2.3.15.jar包, ...

  9. 解决weblogic错误:java.sql.SQLRecoverableException: IO Error: Broken pipe

    首先说一下系统基础架构: 服务器:weblogic11g集群 数据库:oracle数据库Rac 出错信息: 1.java.sql.SQLRecoverableException: Closed Con ...

  10. 牛客网多校训练第一场 I - Substring(后缀数组 + 重复处理)

    链接: https://www.nowcoder.com/acm/contest/139/I 题意: 给出一个n(1≤n≤5e4)个字符的字符串s(si ∈ {a,b,c}),求最多可以从n*(n+1 ...