2009-12-23 30 views
6

我最近將應用程序從VC++ 7移植到VC++ 9。現在,它有時在退出時崩潰 - 運行時開始調用全局對象析構函數,並在其中一箇中發生訪問衝突。「動態分析析構函數」中的「動態」是什麼意思?

每當我觀察調用堆棧頂部的功能是:

CMyClass::~CMyClass() <- crashes here 
dynamic atexit destructor for 'ObjectName' 
_CRT_INIT() 
some more runtime-related functions follow 

的問題是,什麼是「動態的atexit析構」字「動態」的含義是什麼?它可以提供任何額外的信息給我嗎?

回答

7

其難以完全無需實際的代碼確切的問題,但也許你可以看完這個自己找到它:

http://www.gershnik.com/tips/cpp.asp(鏈接現在死了見下文)

的atexit()和動態/共享庫

C和C++標準庫包含一個有時可用的函數:atexit()。它允許調用者註冊在應用程序退出時(通常情況下)將要調用的回調。在C++中,它還與調用全局對象的析構函數的機制集成,因此在給定的atexit()調用之前創建的事件將在回調之前銷燬,反之亦然。所有這些都應該是衆所周知的,並且在DLL或共享庫進入圖片之前它可以很好地工作。

問題是,當然,動態庫有自己的生命週期,一般來說,它們可能會在主應用程序之前結束。如果DLL中的代碼將其自己的函數中的一個函數註冊爲atexit()回調函數,則應該在卸載DLL之前調用該回調函數。否則,在主應用程序退出期間會發生崩潰或更糟糕的情況。 (爲了避免在退出時出現令人討厭的崩潰,很難調試,因爲許多調試器在處理死亡進程時遇到問題)。

這個問題在C++全局對象(如上所述,是atexit()的兄弟)的析構函數的上下文中更好得知。很顯然,支持動態庫的平臺上的任何C++實現都必須處理這個問題,而一致的解決方案是在共享庫卸載或應用程序退出時調用全局析構函數,以先到者爲準。

迄今爲止,除了一些實現「忘記」將相同的機制擴展到普通的舊atexit()之外。由於C++標準沒有對動態庫進行任何說明,所以這些實現在技術上是「正確的」,但是這並不能幫助那些由於某種原因需要調用atexit()傳遞駐留在DLL中的回調函數的窮程序員。

我在平臺上了解情況如下。 Windows上的MSVC,Linux和Solaris上的GCC以及Solaris上的SunPro都有一個「正確的」atexit(),它與全局析構函數的工作方式相同。然而,寫這篇文章時FreeBSD上的GCC有一個「破損」的,它總是註冊要在應用程序上執行的回調,而不是共享庫出口。但是,正如所承諾的,即使在FreeBSD上,全局析構函數也能正常工作。

你應該在可移植代碼中做什麼?當然,一個解決方案是完全避免atexit()。如果你需要它的功能很容易通過以下方式

//Code with atexit() 

void callback() 
{ 
    //do something 
} 

... 
atexit(callback); 
... 

//Equivalent code without atexit() 

class callback 
{ 
public: 
    ~callback() 
    { 
     //do something 
    } 

    static void register(); 
private: 
    callback() 
    {} 

    //not implemented 
    callback(const callback &); 
    void operator=(const callback &); 
}; 

void callback::register() 
{ 
    static callback the_instance; 
} 

... 
callback::register(); 
... 

這個工作在很多打字和非直觀的界面爲代價與C++析構函數來代替它。請注意,與atexit()版本相比,不存在功能損失。回調析構函數不能拋出異常,但是atexit調用的函數也是如此。 callback :: register()函數在給定的平臺上可能不是線程安全的,但atexit()也是如此(C++標準目前在線程上保持沉默,因此是否以線程安全的方式實現atexit()取決於實現)

如果你想避免上面所有的輸入,該怎麼辦?通常有一種方法,它依賴於一個簡單的技巧。我們不需要調用破壞的atexit(),而是需要執行C++編譯器註冊全局析構函數的操作。使用實現所謂的Itanium ABI(廣泛用於非Itanium平臺)的GCC和其他編譯器時,這個魔法咒語被稱爲__cxa_atexit。以下是如何使用它。首先將下面的代碼放在一些公用報頭中

#if defined(_WIN32) || defined(LINUX) || defined(SOLARIS) 

    #include <stdlib.h> 

    #define SAFE_ATEXIT_ARG 

    inline void safe_atexit(void (*p)(SAFE_ATEXIT_ARG)) 
    { 
     atexit(p); 
    } 

#elif defined(FREEBSD) 

    extern "C" int __cxa_atexit(void (*func) (void *), void * arg, void * dso_handle); 
    extern "C" void * __dso_handle;  


    #define SAFE_ATEXIT_ARG void * 

    inline void safe_atexit(void (*p)(SAFE_ATEXIT_ARG)) 
    { 
     __cxa_atexit(p, 0, __dso_handle); 
    } 

#endif 
And then use it as follows 


void callback(SAFE_ATEXIT_ARG) 
{ 
    //do something 
} 

... 
safe_atexit(callback); 
... 

__cxa_atexit的工作方式如下。它將回調註冊到一個全局列表中,就像非DLL知道atexit()一樣。但是它也將其他兩個參數與它相關聯。第二個參數只是一個很好的東西。它允許回調傳遞給某個上下文(就像某個對象的這樣),所以一個回調可以重用於多個清理。第三個參數是我們真正需要的參數。它只是一個「cookie」,用於標識應該與回調關聯的共享庫。當任何共享庫被卸載時,其清理代碼遍歷atexit回調列表,並調用(並刪除)任何具有與正在卸載的庫相關聯的cookie的回調的回調。 cookie的價值應該是什麼?它不是DLL的起始地址,也不是它可能假設的dlopen()句柄。相反,句柄存儲在由C++運行時維護的特殊全局變量__dso_handle中。

safe_atexit函數必須內聯。這樣它就會選擇調用模塊所使用的__dso_handle,這正是我們所需要的。

您是否應該使用這種方法而不是上面的詳細和更便攜的方法?可能不會,但誰知道你可能有什麼要求。儘管如此,即使你從未使用過它,它也有助於瞭解事情是如何工作的,所以這就是它包含在這裏的原因。

+0

這是否意味着「動態」來自「動態加載庫」? – sharptooth 2009-12-30 12:13:24

+0

否這個術語是指運行時動態註冊atexit函數回調,這與在編譯時完成靜態註冊相反。它對於動態加載庫非常有用,因爲如果在應用程序決定卸載之前加載的dll的某個任意點,您可以從atexit列表中刪除回調函數,在這種情況下,您還可以手動調用任何清理代碼,而無需中繼ateixt。 – Alon 2009-12-31 07:19:25

+0

鏈接已死亡。您應該考慮將相關信息複製到您的答案中。 – 2015-12-02 15:52:07