title: 動態鏈接庫函數內的靜態變數,奇妙的UNIQUE Bind date: 2018 09 28 09:28:22 tags: 介紹 模板函數和內斂函數中的靜態變數,在跨so中的表現,和定義在其他函數中的靜態變數的表現稍微有所不同。使用不慎,會造成預期之外的結果。本文對該現象進行了探討。 多 ...
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