動態鏈接庫函數內的靜態變數,奇妙的UNIQUE Bind

来源:https://www.cnblogs.com/your2b/archive/2018/09/29/9724488.html
-Advertisement-
Play Games

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中實例化的同名模板函數之間產生了聯繫。
在往下分析之前,首先要瞭解兩個事實:

  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。

參考資料

  • 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

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 0.背景 在項目開發之中,前期可能主要以保證任務完成為主,對於性能優化主要在於開發完成之後再來進行。可能在測試的時候發現部分介面的代碼執行時間過長,但是又毫無頭緒,這個時候你就需要性能分析工具來協助你排查問題了。 常規性能分析藉助於 Visual Studio 強大的性能測試工具就可以進行分析,但是 ...
  • 昨天晚上寫代碼的時候偶然發現 DateTime 里出現了星期幾,當時一陣凌亂,去網上百度沒有詳細解決辦法,很多人說可以用用 ToString 解決。 也有部分人說可以修改系統時間的顯示,我打算試一下看看行不行。 DateTime 的時間上帶有星期,我去系統時間里發現。 把短日期修改後 再看代碼,星期 ...
  • DotNetty網路通信框架學習之源碼分析 有關DotNetty框架,網上的詳細資料不是很多,有不多的幾個博友做了簡單的介紹,也沒有做深入的探究,我也根據源碼中提供的demo做一下記錄,方便後期查閱。 github地址:https://github.com/Azure/DotNetty 源碼的src ...
  • 1.什麼是docker: 可以理解為一個可移植的集裝箱容器,開發者可以打包他們的應用以及依賴包到一個可移植的容器中。 2.docker安裝及使用(ubuntu16.04) (1)首先通過apt-get命令安裝docker: sudo apt-get install docker.io (2) doc ...
  • 一、書籍介紹 入門書籍:Linux系統命令及Shell腳本實踐指南,該書需要細讀以及實操。 進階書籍:鳥哥的 Linux 私房菜 基礎篇 第四版,該書建議在入門後根據實際需要選擇性翻閱。 二、Linux常用工具 secureCRT:windows下的遠程連接Linux的ssh客戶端,個人傾向於這個工 ...
  • 2.1 pwd (print work directory)列印工作目錄(顯示當前所在路徑) 後面可以接 -L 預設情況下接的也是-L(logical)的 此種情況顯示的是邏輯路徑(相當於win的一樣) -P(physical)的話,就會把真實的物理路徑全部顯示出來 2.2 cd (change d ...
  • gitblit是當作git伺服器,也就是作為私有的代碼倉庫,用法類似於Github Jenkins 是自動構建工具,幫忙將倉庫中的代碼更新到伺服器上。可以設置為定時自動構建。 詳細摸索了我現在公司的用法, 先簡單描述下工作過程: 平常我們寫好代碼之後,會git push到倉庫,然後到 Jenkins ...
  • 本文參考鏈接:http://www.runoob.com/linux/linux-shell.html 基本說明 Shell腳本(shell script)是一種為shell編寫的腳本程式。其中shell常見的種類包括: Bourne Shell:對應的命令(/usr/bin/sh或/bin/sh) ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...