Я портировал свое приложение от VC ++ 7 к VC ++ 9 недавно. Теперь это иногда отказывает в выходе - время выполнения начинает называть деструкторы глобальных объектов, и нарушение прав доступа происходит в одном из них.
Каждый раз, когда я наблюдатель стек вызовов главные функции:
CMyClass::~CMyClass() <- crashes here
dynamic atexit destructor for 'ObjectName'
_CRT_INIT()
some more runtime-related functions follow
Вопрос - то, что значение слова, "динамичного" в "динамическом atexit деструкторе"? Это может предоставить дополнительную информацию мне?
трудно определить точную проблему без реального кода, но, возможно, Вы сможете найти ее самостоятельно, прочитав его:
из http://www.gershnik.com/tips/cpp.asp (ссылка теперь мертва, см. ниже)
atexit() и стандартные библиотеки dynamic/shared
C и C++ включают в себя иногда полезную функцию: atexit(). Она позволяет вызывающему абоненту регистрировать обратный вызов, который будет вызван при выходе приложения (обычно). В C++ она также интегрирована с механизмом, который вызывает деструкторы глобальных объектов, так что вещи, которые были созданы до данного вызова atexit(), будут уничтожены до обратного вызова и наоборот. Все это должно быть хорошо известно и прекрасно работает до тех пор, пока DLL или разделяемые библиотеки не войдут в картину.
Проблема, конечно, в том, что динамические библиотеки имеют свое собственное время жизни, которое, в общем случае, может закончиться до того, как основное приложение выйдет из строя. Если код в DLL регистрирует одну из своих собственных функций в качестве обратного вызова atexit(), то лучше вызвать этот вызов до выгрузки DLL. Иначе произойдет падение или что-то похуже во время выхода из основного приложения. (Известно, что делать неприятные сбои при выходе очень сложно, так как многие отладчики имеют проблемы, связанные с умирающими процессами)
Эта проблема гораздо лучше известна в контексте деструкторов глобальных объектов C++ (которые, как упоминалось выше, являются братьями atexit()). Очевидно, что любая реализация Си++ на платформе, поддерживающей динамические библиотеки, должна была решить эту проблему, и единогласным решением было обращение к глобальным деструкторам либо при выгрузке разделяемой библиотеки, либо при выходе из приложения, в зависимости от того, что наступит раньше.
Пока все хорошо, за исключением того, что в некоторых реализациях "забыли" распространить тот же механизм на обычную старую atexit(). Так как в стандарте Си++ про динамические библиотеки ничего не сказано, то такие реализации технически "правильны", но это не помогает бедному программисту, которому по той или иной причине приходится вызывать atexit(), передавая обратный вызов, находящийся в DLL.
На платформах я знаю, что ситуация следующая. MSVC на Windows, GCC на Linux и Solaris и SunPro на Solaris имеют "правый" atexit(), который работает так же, как и глобальные деструкторы. Однако, GCC на FreeBSD на момент написания этой статьи имеет "сломанный", который всегда регистрирует обратные вызовы для выполнения приложения, а не выход из разделяемой библиотеки. Однако, как и обещано, глобальные деструкторы прекрасно работают даже на FreeBSD.
Что делать в переносимом коде? Одним из решений, конечно, является полное исключение функции atexit(). Если вам нужна его функциональность, то легко заменить его на C++ деструкторы следующим образом
//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();
...
Это работает за счет большого количества опечаток и неинтуитивно понятного интерфейса. Важно отметить, что по сравнению с версией atexit() функциональность не теряется. Деструктор обратного вызова не может бросать исключения, но и функции, вызываемые atexit. Функция callback::register() может быть небезопасна для потоков на данной платформе, но также и atexit() (в настоящее время в стандарте C++ ничего не сказано о потоках, так что вопрос о том, следует ли реализовывать atexit() безопасным для потоков способом, зависит от реализации)
Что, если вы хотите избежать всех вышеописанных опечаток? Обычно есть способ, и он опирается на простой трюк. Вместо того, чтобы вызывать сломанную функцию atexit(), мы должны делать все, что делает компилятор C++ для регистрации глобальных деструкторов. В GCC и других компиляторах, реализующих так называемый Itanium ABI (широко используемый для неитановых платформ), волшебное заклинание называется __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 следующий. Она регистрирует обратный вызов в одном глобальном списке так же, как atexit(), не знающая DLL. Однако она также связывает с ней два других параметра. Второй параметр просто приятен. Он позволяет передавать обратный вызов некоторому контексту (как в случае с каким-либо объектом this), и поэтому один обратный вызов может быть повторно использован для многократной очистки. Третий параметр - это тот, который нам действительно нужен. Это просто "cookie", который идентифицирует разделяемую библиотеку, которая должна быть связана с обратным вызовом. Когда любая разделяемая библиотека выгружается, ее код очистки проходит через список вызовов atexit и вызывает (и удаляет) любые вызовы, имеющие cookie-файл, соответствующий тому, который связан с выгружаемой библиотекой. Каким должно быть значение куки? Это не стартовый адрес DLL и не его dlopen() хэндл, как можно предположить. Вместо этого хэндл хранится в специальной глобальной переменной __dso_handle, обслуживаемой C++ во время выполнения.
Функция safe_atexit должна быть inline. Таким образом, она выбирает, какой __dso_handle используется вызывающим модулем, а это именно то, что нам нужно.
Стоит ли использовать этот подход вместо многословного и более переносимого, описанного выше? Скорее всего, нет, хотя кто знает, какие требования у вас могут быть. Тем не менее, даже если вы никогда его не используете, это помогает быть в курсе того, как все работает, поэтому он включен сюда.