天天看點

Linux動态庫(.so)連結探究同一個動态庫被間接連結多次時類内靜态變量使用問題(附帶c++源碼解釋)

  介紹一種場景,有一個抽象基類生成了一個動态庫,記為libcompo.so,繼承該基類的具體實作有兩個元件,也各自生成了動态庫libcompo1.so和libcompo2.so,由于使用到了基類,是以這兩個庫也是會分别連結基類庫的,也就是libcompo.so,然後在可執行檔案裡,我們為了軟體各個元件之間的解耦通過反射技術注冊的方式動态加載具體元件,是以可執行檔案隻用到了該抽象基類的指針,隻需要在編譯時連結libcompo.so即可,在運作時通過dlopen()加載生成的動态庫即可完成工作。

  最終軟體的組織架構可以表示為如下:

compo.h

//抽象基類與元件工廠
#pragma once
#include <string>
#include <map>
using namespace std;

class AbstractComponent
{
public:
    ~AbstractComponent() {}
};

class ComponentFactory
{
public:
    ComponentFactory() {}
    static map<string, AbstractComponent *> componentMap_;
};

#define REGISTER_COMPONENT(name, component)                                     \
    namespace                                                                   \
    {                                                                           \
        struct ProxyType##component                                             \
        {                                                                       \
            ProxyType##component()                                              \
            {                                                                   \
                ComponentFactory::componentMap_.insert({name, new component()});\
            }                                                                   \
        };                                                                      \
        static ProxyType##component register_class_##component;                 \
    }
           

compo.cpp

#include "compo.h"
map<string, AbstractComponent *> ComponentFactory::componentMap_;
           

compo1和compo2的頭檔案和源檔案是除了命名完全一樣的,是以為了簡單隻展示一個

compo1.h

#include "compo.h"
class Compo1 : public AbstractComponent
{
public:
    Compo1() : AbstractComponent() {}
    ~Compo1() {}
};

REGISTER_COMPONENT("compo1", Compo1)
           

compo1.cpp(為示範沒有填入沒有任何内容)

此時CMakeLists可以寫為

cmake_minimum_required(VERSION 3.2)
project(test)

add_compile_options(-fPIC -g -std=c++11 -Wall )
add_library(compo SHARED src/exe.cpp)

add_library(compo1 SHARED src/lib1.cpp)
target_link_libraries(compo1 compo)

add_library(compo2 SHARED src/lib2.cpp)
target_link_libraries(compo2 compo)

add_executable(${PROJECT_NAME} src/main.cpp)
target_link_libraries(${PROJECT_NAME} compo dl)
           

主函數運作時加載這兩個動态庫,并且檢視元件的注冊情況

#include "exe.h"
#include <dlfcn.h>
#include <iostream>

int main()
{    
    dlopen("./libcompo1.so", RTLD_NOW | RTLD_GLOBAL);
    dlopen("./libcompo2.so", RTLD_NOW | RTLD_GLOBAL);
    for(auto i : ComponentFactory::componentMap_)
    {
        cout << i.first << endl;
    }
	return 0;
}
           

  到此處,描述的場景已經用代碼表示完成,但我們可以發現一個動态庫出現了一個很有意思的現象———libcompo.so被動态連結多次,被生成的可執行程式動态連結了一次,被生成的另外兩個動态庫libcompo1.so和libcompo2.so各連接配接一次,最後在主函數運作時,利用dlopen函數加載libcompo1.so和libcompo2.so動态庫時實際上又間接加載了兩次。對于這種動态庫,我們就需要格外小心了!一些細節錯誤可能會引起難以排查的問題,在這裡先說結論:

           類的靜态變量的定義一定要放在.cpp檔案内

  當componentFactory類的類内靜态變量定義在.h檔案内時,連結了libcompo.so的可執行檔案内有他的一份定義,libcompo1.so和libcompo1.so有他的一份定義,當他們沒有同時出現在一個程序裡時是沒有問題的,但是如果照着現在的main函數執行,你會發現這三份定義并沒有出錯(可能是應用了RTLD_GLOBAL來處理symbols),且你會發現這個靜态變量三份定義的記憶體位址一樣,但是他們卻不是同一個變量,當你檢查ComponentFactory::componentMap_這個map内的元素時,你會發現有兩個元件注冊了但是其大小并不為2,而是為1,每次在不同的定義的代碼塊内(三個定義)對這個map進行寫入都會對其進行覆寫清空再重新寫入,出現這種情況自然就無法使得使用者通過該map來檢測有哪些子產品完成了注冊。為了避免這樣的情況,我們需要使得該靜态變量隻有一份定義,将其定義在cpp檔案内,則隻會在連結libcompo.so的可執行檔案内會有該map的定義,而libcompo1.so和libcompo2.so隻有他的聲明,這個時候這三個地方仍然可以對這個map進行修改,但是不會出現異常了,他們的修改都會反映在這個唯一的map内部。

  結語:當我們使用動态庫和編寫動态庫時,一定要時刻謹記,動态庫内的變量或函數如果和使用者的程式重名了該怎麼處理?這種情況往往可以編譯通過并且運作使得問題難以排查。

  首先需要謹記的是,把類内靜态變量的定義放置在cpp檔案内,這樣使用者連結動态庫時仍然可以修改該靜态變量(可以拿到該變量的聲明),但是最終的程式就算使用了dlopen()整個程序内部也隻有該map的一份定義,多個元件可以修改它,但修改的是同一個東西。如果你使用的動态庫内部的變量定義和你的程式重名了,可以思考一下該動态庫的品質到底如何才犯了如此低級的錯誤,是否還有繼續使用的必要性?

繼續閱讀