2012年4月19日 星期四

跨函式庫進行記憶體配置與釋放需考慮 Runtime 連結方式

最近發現在 Windows 中跨函式庫進行專案開發時, 只要是在函式庫中配置出 (new, alloc) 的記憶體, 不能在其它地方釋放 (delete, free). 不然馬上會有作業系統發出的例外通知. 特別的是這種情況只會發生在與 CRT 靜態連結的時候 (/MTd or /MT), 反之若是動態連結 CRT (/MDd or /MD) 就不會有這個問題.

網路上找了一些資訊, 以這幾篇提到的最為相關:

具體來說, 記憶體的配置與釋放是依賴 Runtime 在其所屬的 Heap 中進行操作的. 當主程式與函式庫分別都與 Runtime 做靜態連結時, Heap 會是各自擁有, 因此有某一方在其 Heap 中配置了記憶體區塊, 一定也只能在其中釋放, 另一方的 Heap 是全然無記錄的.

解決方法在上述的討論中已提到了很多, 就不再提及. 不過討論中有個點我蠻在意, 有位老兄提到這是共通的平台問題, 其它作業系統也會發生的. 由於我之前大都在 Linux 環境中工作, 但一直沒注意到這問題, 實在好奇就自己在 Linux 下做了一些實驗.

首先寫了一個簡單的函式庫, 它只有一個函數, 配置一塊記憶體空間後返回其指針.
#include <stdlib.h>

char* func() {
    return (char*)malloc(100);
}

編譯時分別編出與 Runtime (libc) 動態與靜態連結的版本:
gcc -o libtestlib.so -fpic -shared testlib.c          // 動態
gcc -o libtestlibs.so -fpic -shared -static testlib.c  // 靜態

接著是調用該函式庫的主程式, 這裡用的是顯式連結(Explicit Linking)而非隱式(Implicit Linking), 原因後述.
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>

int main(int argc, char *argv[]) {
    int* (*func_ptr)();
    if (argc <= 1) return 0;

    // Load library specified as argv[1]
    void* handle = dlopen(argv[1], RTLD_NOW); 
    if (!handle) { 
        printf("failed to load library.\n"); 
        return 0;
    }

    // Get function ptr
    func_ptr = dlsym(handle, "func"); 
    if (!func_ptr) { 
        printf("null func_ptr!\n"); 
        dlclose(handle); 
        return 0;
    }

    // Invoke
    int* a = func_ptr(); 
    // Try to free the memory block alloced in library.
    free(a); 

    return 0;
}

編譯時一樣分別編出與 libc 動態與靜態連結的版本:
gcc -o test -ldl test.c           // 動態
gcc -o tests -ldl -static test.c  // 靜態

執行後會發現, 與 Runtime 靜態連結的版本 (tests) 在試圖釋放指針時, 會產生 double free 的例外. 但動態連結的版本 (test) 可以正常執行完畢. 發現這問題的確是平台共通的問題.

為何不用隱式連結? 我發現原因跟我之前為何從未注意到這問題有關係. Linux 下的 GCC toolchain 在進行編譯連結時, 對於函式庫的動態與靜態連結, 只會給你兩個大方向: 要嘛就全靜態, 要嘛就全動態, 關鍵看你有沒有指定 -static 這個參數. 而在 Windows 上遇到的這個問題, 是函式庫本身與 Runtime 靜態連結, 但函式庫和主程式之間卻是動態連結. 我在 Linux 上重現這樣的連結方式, 發現一定要先單獨將函式庫編好, 然後主程式中用顯示連結的方式來呼叫才有辦法做到. 若是直接指定 -static, 那麼連結器就會直接找函式庫的 .a 檔而非 .so 檔了. 可以說某種程度上, GCC toolchain 的參數設定方式簡化了連結方式的可能性, 這大概能解釋為何我 (也或許大部分先接觸 Unix 的人) 不曾在 Linux 上注意過此類問題吧.

沒有留言:

張貼留言