1. 程式人生 > >跨模組傳引數的教訓

跨模組傳引數的教訓

今天遇到一個比較奇怪的crash問題,這裡記錄下。這個crash是由QA設定了一些不合理的引數引起的,還好QA當時儲存了Dump檔案,讓我們可以慢慢分析,從而找出程式碼中隱藏的問題。
    
這裡先簡單介紹下ATL/WTL裡字串的設計:
(1)每個CString都有自己的串頭(內含引用計數,資料長度,已分配記憶體長度),緊接著後面是真正的資料。
因為是基於引用計數,所以相同的多個CString可以共享同一份資料。
struct CStringData
{
    long nRefs;     // reference count    int nDataLength;
    int nAllocLength;
    // TCHAR data[nAllocLength]
    TCHAR* data()
    { return (TCHAR*)(this + 1); }
};

(2)每個未初始化CString都會指向同一固定的全域性資料,內部引用計數、資料長度、已分配記憶體長度、內容分別為-1,0,0,0//
 Globals

// For an empty string, m_pchData will point here
// (note: avoids special case of checking for NULL m_pchData)
// empty string data (and locked)_declspec(selectany) int rgInitData[] = { -1, 0, 0, 0 };
_declspec(selectany) CStringData* _atltmpDataNil = (CStringData*)&rgInitData;
_declspec(selectany) LPCTSTR _atltmpPchNil = (LPCTSTR)(((BYTE*)&rgInitData) + sizeof
(CStringData));

inline CString::CString(){Init();}inline void CString::Init(){ m_pchData = _GetEmptyString().m_pchData; }

static const CString& __stdcall _GetEmptyString(){return *(CString*)&_atltmpPchNil;}
(3)字串析構時會檢測是否已經分配記憶體,是否其他沒有人用(引用計數小於0),都滿足後才會最終釋放記憶體。inline CString::~CString()
//  free any attached data
{
    if (GetData() != _atltmpDataNil)
    {
        if (InterlockedDecrement(&GetData()->nRefs) <= 0)
          delete[] (BYTE*)GetData();
    }
}
 用Windbg開啟Dump檔案,輸入!analyze -v 讓它自動分析Crash時的情況,最終發現Crash在ATL/WTL字串的解構函式~CString()裡的delete語句, 然後我們通過分析傳入引數,發現外部傳入的是一個沒有初始化的CString,既然是沒有初始化的CString,那應該都是指向初始字串的固定記憶體,也就不會滿足條件 if (GetData() != _atltmpDataNil),為什麼會跑到裡面去呢?

這裡關鍵原因就是這個CString是跨模組傳遞過來的,比如你DLL裡有個匯出函式void SetValue(CString strValue), 然後你外部Exe傳遞一個未出始化的字串CString str; SetValue(str); 這時就會Crash。根本原因是因為傳入的字串是在Exe裡構造,但是在DLL裡析構,Exe裡的未初始化str指向的是Exe模組自己的全域性初始值Exe!_atltmpDataNil, 而DLL內CString的全域性初始值是Dll自己的Dll!_atltmpDataNil, 兩者比較當然不相等,而後面的if (InterlockedDecrement(&GetData()->nRefs) <= 0)又會把引用計數從-1改成-2, 接下來就會試圖delete這塊不是new出來的全域性記憶體,當然會Crash了。

這個Bug一直沒有發現的原因是QA一直設定的都是有效引數,也就不會引起傳入未初始化的CString的情況,但這次意外卻暴露了我們程式碼中隱藏的問題。

知道了原因,接下來就是如何改了?方法很多,可以用傳引用的方式CString&;也可以傳C方式的字串LPCTSTR;也可以還是傳CString, 但是在傳之前先做下長度判斷,以確保已經出始化。

另外提醒下如果要在模組(DLL)之間傳遞記憶體,要確保C/C++執行庫要用DLL的方式(MD), 這樣跨模組new和delete時他們會共享同一個記憶體堆,不同模組之間相互new和delete才不會有問題。

測試工程: DllStringTest
posted on 2012-07-13 21:27 Richard Wei 閱讀(2884) 評論(4)  編輯 收藏 引用 所屬分類: windbg