Потенциальные ошибки при передаче объектов CRT через границы DLL

Код может иметь ошибки при передаче объектов среды выполнения C (CRT), таких как дескриптор файлов, языковые параметры и переменные среды в библиотеку DLL или из нее. Вызовы функций через границу DLL могут привести к непредвиденному поведению, если библиотека DLL и все файлы, вызывающие библиотеки DLL, используют разные копии библиотек CRT.

Связанная проблема может возникать при выделении памяти (явно или неявно с newmallocstrdup, strstreambuf::strа также т. д.) и последующей передаче указателя через границу БИБЛИОТЕКи DLL, где она освобождена. Такие указатели могут привести к нарушению доступа к памяти или повреждению кучи, если библиотека DLL и ее потребители используют разные копии библиотек CRT.

Еще одним признаком этой проблемы является ошибка в окне вывода во время отладки, например HEAP[]: Invalid Address specified to RtlValidateHeap(#,#)

Причины

Каждая копия библиотеки CRT имеет определенное собственное состояние, которое хранится приложением или библиотекой DLL в локальном хранилище потока приложения.

Объекты CRT, такие как дескриптора файлов, переменные среды и языковые параметры, допустимы только для копии CRT в приложении или библиотеке DLL, где эти объекты были выделены или заданы. Если библиотека DLL и ее клиенты используют разные копии библиотеки CRT, эти объекты CRT нельзя использовать правильно при передаче через границу библиотеки DLL.

Это особенно верно для версий CRT до универсальной CRT в Visual Studio 2015 и более поздних версий. В Visual Studio 2013 и более ранних версиях создавались специальные библиотеки CRT для каждой версии Visual Studio. Внутренние сведения о реализации CRT, такие как структуры данных и соглашения об именовании, отличаются в каждой версии. Динамическое связывание кода, скомпилированного для одной версии CRT, к другой версии библиотеки DLL CRT никогда не поддерживается. Иногда это будет работать, но из-за удачи, а не дизайна.

Каждая копия библиотеки CRT имеет собственный диспетчер куч. Это может привести к повреждению кучи, если вы выделяете память в одной библиотеке CRT и передаете указатель через границу DLL, чтобы освободить другую копию библиотеки CRT. Если библиотека DLL передает объекты CRT через границу библиотеки DLL или выделяет память, освобожденную за пределами библиотеки DLL, клиенты библиотеки DLL должны использовать ту же копию библиотеки CRT, что и библиотека DLL.

Библиотека DLL и ее клиенты используют одну и ту же копию библиотеки CRT, только если при загрузке они будут связаны с одной и той же версией DLL CRT. Так как библиотека DLL универсальной библиотеки CRT, используемая Visual Studio 2015 и более поздних версий, теперь является централизованно развернутой компонентом Windows (ucrtbase.dll), это то же самое для приложений, созданных с помощью Visual Studio 2015 и более поздних версий. Однако даже если код CRT идентичен, вы не можете предоставить памяти, выделенной в одной куче компоненту, использующим другую кучу.

Пример. Передача дескриптора файлов через границу DLL

Description

В этом примере дескриптор файла передается через границу библиотеки DLL.

Файлы DLL и EXE создаются с /MDпомощью , чтобы они совместно используют одну копию CRT.

При перестроении с /MT тем, чтобы они использовали отдельные копии CRT, выполнение результирующего test1Main.exe результата приведет к нарушению доступа.

Исходный файл test1Dll.cppDLL:

// test1Dll.cpp
// compile with: cl /EHsc /W4 /MD /LD test1Dll.cpp
#include <stdio.h>
__declspec(dllexport) void writeFile(FILE *stream)
{
   char   s[] = "this is a string\n";
   fprintf( stream, "%s", s );
   fclose( stream );
}

Исходный файл исполняемого файла test1Main.cpp:

// test1Main.cpp
// compile with: cl /EHsc /W4 /MD test1Main.cpp 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

Description

В этом примере переменные среды передаются через границу библиотеки DLL.

Исходный файл test2Dll.cppDLL:

// test2Dll.cpp
// compile with: cl /EHsc /W4 /MT /LD test2Dll.cpp
#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:

// test2Main.cpp
// compile with: cl /EHsc /W4 /MT test2Main.cpp test2dll.lib
#include <stdlib.h>
#include <stdio.h>

void readEnv();

int main( void )
{
   _putenv( "MYLIB=c:\\mylib;c:\\yourlib" );
   readEnv();
}
MYLIB has not been set.

Если вы создаете файлы DLL и EXE с помощью /MD, поэтому используется только одна копия CRT, программа успешно выполняется и выдает следующие выходные данные:

New MYLIB variable is: c:\mylib;c:\yourlib

См. также

Файлы среды выполнения C (CRT) и стандартной библиотеки C++ (STL) .lib