Что делает “динамичный” в “динамическом atexit деструкторе”, среднем?

Я портировал свое приложение от VC ++ 7 к VC ++ 9 недавно. Теперь это иногда отказывает в выходе - время выполнения начинает называть деструкторы глобальных объектов, и нарушение прав доступа происходит в одном из них.

Каждый раз, когда я наблюдатель стек вызовов главные функции:

CMyClass::~CMyClass() <- crashes here
dynamic atexit destructor for 'ObjectName'
_CRT_INIT()
some more runtime-related functions follow

Вопрос - то, что значение слова, "динамичного" в "динамическом atexit деструкторе"? Это может предоставить дополнительную информацию мне?

6
задан sharptooth 23 December 2009 в 12:14
поделиться

1 ответ

трудно определить точную проблему без реального кода, но, возможно, Вы сможете найти ее самостоятельно, прочитав его:

из 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 используется вызывающим модулем, а это именно то, что нам нужно.

Стоит ли использовать этот подход вместо многословного и более переносимого, описанного выше? Скорее всего, нет, хотя кто знает, какие требования у вас могут быть. Тем не менее, даже если вы никогда его не используете, это помогает быть в курсе того, как все работает, поэтому он включен сюда.

10
ответ дан 10 December 2019 в 00:39
поделиться
Другие вопросы по тегам:

Похожие вопросы: