Что означает «динамический» в «динамическом деструкторе atexit»? - PullRequest
6 голосов
/ 23 декабря 2009

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

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

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

Вопрос в том, что означает слово «динамический» в «динамическом деструкторе atexit»? Может ли он предоставить мне дополнительную информацию?

1 Ответ

10 голосов
/ 23 декабря 2009

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

из http://www.gershnik.com/tips/cpp.asp (ссылка не работает, см. Ниже)

atexit () и динамические / общие библиотеки

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

Проблема, конечно, в том, что динамические библиотеки имеют свое собственное время жизни, которое, как правило, может заканчиваться раньше, чем у основного приложения. Если код в DLL регистрирует одну из своих собственных функций в качестве обратного вызова atexit (), этот обратный вызов лучше вызывать до выгрузки DLL. В противном случае произойдет сбой или что-то еще худшее во время выхода из основного приложения. (Общеизвестно, что делать неприятные сбои во время выхода из системы сложно, поскольку многие отладчики сталкиваются с проблемами, связанными с умирающими процессами).

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

Пока все хорошо, за исключением того, что некоторые реализации "забыли" расширить тот же механизм до простого старого atexit (). Поскольку стандарт C ++ ничего не говорит о динамических библиотеках, такие реализации технически «правильны», но это не помогает бедному программисту, который по тем или иным причинам должен вызывать 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 (широко используемый для не Itanium-платформ), магическое заклинание называется __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 должна быть встроенной. Таким образом, он выбирает то, что __dso_handle использует вызывающий модуль, и это именно то, что нам нужно.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...