跨越DLL邊界傳遞CRT對象潛在的錯誤
翻譯:magictong(童磊)2013年5月
版權:microsoft
原文地址:http://msdn.microsoft.com/en-us/library/ms235460(v=vs.80).aspx
簡介
當你把C運行時(CRT)對象(譬如文件句柄、語言環境和環境變量等等)傳入傳出DLL時(通過調用DLL裏面暴露的一些函數),如果這個DLL加載了一份(與可執行文件)不同的CRT庫,可能發現意向不到的事情。
有一個大家可能遇到過,相似的問題是,如果可執行程序在外面分配一塊內存(顯示的通過new或者malloc分配,或者通過調用strdup,strstreambuf::str等等函數隱式的分配),然後把分配得到的內存指針通過調用DLL暴露的函數傳入DLL裏面去釋放,如果這個DLL加載了一份不同的CRT庫,可能造成內存訪問違例(AV)或者堆破壞。
如果你正在調試程序,這個問題可能在調試輸出窗口打印一條如下的錯誤信息:
HEAP[]: Invalid Address specified to RtlValidateHeap(#,#)
原因
每個CRT庫的副本都有一個單獨並且獨特的狀態,因此,一些類似文件句柄,語言環境和環境變量這樣的CRT對象,僅僅在分配它或者設置它的CRT庫裏面有效。當一個DLL和使用這個DLL的程序分別使用了兩個不同的CRT庫時,如果你把這些CRT對象在DLL和使用DLL的程序兩邊相互傳遞,並希望程序運行正常,那是不太可能的。
另外,因爲每個CRT庫的副本都有一個自己的堆管理器,在一個CRT庫裏面分配的內存傳遞(通過調用DLL暴露的函數)到另外一個CRT庫裏面去釋放可能導致堆破壞。
如果你想設計一個可以傳遞CRT對象的DLL,或者明確的在DLL內部分配內存,而在DLL的外部釋放,那麼你必須限制這個DLL的使用者使用和DLL一樣的CRT庫。而要想DLL的使用者使用和DLL一樣的CRT庫除非二者鏈接到相同版本的CRT動態鏈接庫。如果應用程序和DLL的編譯環境不一樣,譬如應用程序使用Visual C++5.0編譯而DLL使用Visual C++ 4.1編譯或者更早版本,因爲Visual C++ 4.1使用的CRT庫是msvcrt40.dll,而Visual C++5.0使用的是msvcrt.dll,這可能會非常麻煩。你可能無法重新編譯你的應用程序使得和DLL使用的CRT庫一模一樣。
不過,有一個特殊情況。在一些特殊版本的Windows NT 4.0和Windows 2000裏面(譬如,美國英語版本,德國,法國和捷克的本地化版本),使用了一個代理模式的msvcrt40.dll(具體版本是4.20)。在這些環境裏面,雖然DLL鏈接的是msvcrt40.dll,DLL的使用者(應用程序)鏈接的是msvcrt.dll,但是因爲所有對msvcrt40.dll的調用都被轉調用到了msvcrt.dll,因此這種情況二者仍然使用的是同一個CRT庫。
然而,這個代理版本的msvcrt40.dll在Windows 95,Windows 98,Windows Me和其它一些本地化的Windows NT 4.0和Windows 2000(譬如日本,韓國和中國)版本里面是無效的。因此,如果你的應用程序的目標操作系統是這些環境,你需要獲得一個不依賴msvcrt40.dll的升級版本的DLL,或者更改你的應用程序。如果你已經把這個DLL開發出來了,那麼使用Visual C++ 4.2或者更高版本重新編譯一下DLL,如果這個DLL是第三方提供的組件,你可能需要相關開發商提供一個升級。
例子一
描述
這裏是一個跨越DLL邊界傳遞文件句柄的實例。
假如DLL文件和.exe文件都是使用/MD來編譯,那麼它們使用同一份CRT庫,這沒有問題。
如果你使用/MT來重新編譯,使得它們使用不同的CRT庫,然後運行test1Main.exe,馬上就會出現訪問違例(access violation)。
代碼
// test1Dll.cpp
// compile with: /MD /LD
#include <stdio.h>
__declspec(dllexport) void writeFile(FILE *stream)
{
char s[] = "this is a string\n";
fprintf( stream, "%s", s );
fclose( stream );
}
代碼
// test1Main.cpp
// compile with: /MD test1dll.lib
#include <stdio.h>
#include <process.h>
void writeFile(FILE *stream);
int main(void)
{
FILE * stream;
errno_t err = fopen_s( &stream, "fprintf.out", "w" );
writeFile(stream);
system( "type fprintf.out" );
}
輸出
this is a string
例子二
描述
這個例子說明跨DLL傳遞環境變量的問題。
代碼
// test2Dll.cpp
// compile with: /MT /LD
#include <stdio.h>
#include <stdlib.h>
__declspec(dllexport) void readEnv()
{
char *libvar;
size_t libvarsize;
/* Get the value of the MYLIB environment variable. */
_dupenv_s( &libvar, &libvarsize, "MYLIB" );
if( libvar != NULL )
printf( "New MYLIB variable is: %s\n", libvar);
else
printf( "MYLIB has not been set.\n");
free( libvar );
}
代碼
// test2Main.cpp
// compile with: /MT /link test2dll.lib
#include <stdlib.h>
#include <stdio.h>
void readEnv();
int main( void )
{
_putenv( "MYLIB=c:\\mylib;c:\\yourlib" );
readEnv();
}
如果DLL和.EXE文件都使用/MD來編譯,那麼二者使用同一份CRT庫,此時輸出是New MYLIB variable is: c:\mylib;c:\yourlib,而如果二者都使用/MT編譯,那麼輸出將是MYLIB has not been set.
(注:以上兩個實例,譯者magictong並沒有進行過測試,如果有問題,請告訴magictong)
參考文檔
C運行時庫 http://msdn.microsoft.com/en-us/library/abx4dbyh(v=vs.100).aspx