Как я могу запланировать запуск некоторого кода после завершения всех функций '_atexit()'?

StackOverflow https://stackoverflow.com/questions/1753042

  •  20-09-2019
  •  | 
  •  

Вопрос

Я пишу систему отслеживания памяти, и единственная проблема, с которой я на самом деле столкнулся, заключается в том, что при выходе из приложения любые статические/глобальные классы, которые не выделялись в своем конструкторе, но освобождаются в своем деконструкторе, освобождаются после моей памяти. средство отслеживания сообщило об утечке выделенных данных.

Насколько я могу судить, единственный способ правильно решить эту проблему - либо принудительно разместить обратный вызов _atexit трекера памяти в начале стека (чтобы он вызывался последним), либо заставить его выполняться после всего Стек _atexit развернут.Действительно ли возможно реализовать какое-либо из этих решений, или есть другое решение, которое я упустил из виду.

Редактировать:Я работаю/разрабатываю для Windows XP и компилирую с помощью VS2005.

Это было полезно?

Решение

Я наконец понял, как это сделать в Windows/Visual Studio.Снова просматривая функцию запуска crt (в частности, там, где она вызывает инициализаторы глобальных переменных), я заметил, что она просто запускала «указатели на функции», которые содержались между определенными сегментами.Итак, имея лишь немного знаний о том, как работает компоновщик, я пришел к следующему:

#include <iostream>
using std::cout;
using std::endl;

// Typedef for the function pointer
typedef void (*_PVFV)(void);

// Our various functions/classes that are going to log the application startup/exit
struct TestClass
{
    int m_instanceID;

    TestClass(int instanceID) : m_instanceID(instanceID) { cout << "  Creating TestClass: " << m_instanceID << endl; }
    ~TestClass() {cout << "  Destroying TestClass: " << m_instanceID << endl; }
};
static int InitInt(const char *ptr) { cout << "  Initializing Variable: " << ptr << endl; return 42; }
static void LastOnExitFunc() { puts("Called " __FUNCTION__ "();"); }
static void CInit() { puts("Called " __FUNCTION__ "();"); atexit(&LastOnExitFunc); }
static void CppInit() { puts("Called " __FUNCTION__ "();"); }

// our variables to be intialized
extern "C" { static int testCVar1 = InitInt("testCVar1"); }
static TestClass testClassInstance1(1);
static int testCppVar1 = InitInt("testCppVar1");

// Define where our segment names
#define SEGMENT_C_INIT      ".CRT$XIM"
#define SEGMENT_CPP_INIT    ".CRT$XCM"

// Build our various function tables and insert them into the correct segments.
#pragma data_seg(SEGMENT_C_INIT)
#pragma data_seg(SEGMENT_CPP_INIT)
#pragma data_seg() // Switch back to the default segment

// Call create our call function pointer arrays and place them in the segments created above
#define SEG_ALLOCATE(SEGMENT)   __declspec(allocate(SEGMENT))
SEG_ALLOCATE(SEGMENT_C_INIT) _PVFV c_init_funcs[] = { &CInit };
SEG_ALLOCATE(SEGMENT_CPP_INIT) _PVFV cpp_init_funcs[] = { &CppInit };


// Some more variables just to show that declaration order isn't affecting anything
extern "C" { static int testCVar2 = InitInt("testCVar2"); }
static TestClass testClassInstance2(2);
static int testCppVar2 = InitInt("testCppVar2");


// Main function which prints itself just so we can see where the app actually enters
void main()
{
    cout << "    Entered Main()!" << endl;
}

который выводит:

Called CInit();
Called CppInit();
  Initializing Variable: testCVar1
  Creating TestClass: 1
  Initializing Variable: testCppVar1
  Initializing Variable: testCVar2
  Creating TestClass: 2
  Initializing Variable: testCppVar2
    Entered Main()!
  Destroying TestClass: 2
  Destroying TestClass: 1
Called LastOnExitFunc();

Это работает благодаря тому, как MS написала свою библиотеку времени выполнения.По сути, они установили следующие переменные в сегментах данных:

(хотя эта информация защищена авторским правом, я считаю, что это добросовестное использование, поскольку оно не обесценивает оригинал и ПРИВЕДЕНО только для справки)

extern _CRTALLOC(".CRT$XIA") _PIFV __xi_a[];
extern _CRTALLOC(".CRT$XIZ") _PIFV __xi_z[];    /* C initializers */
extern _CRTALLOC(".CRT$XCA") _PVFV __xc_a[];
extern _CRTALLOC(".CRT$XCZ") _PVFV __xc_z[];    /* C++ initializers */
extern _CRTALLOC(".CRT$XPA") _PVFV __xp_a[];
extern _CRTALLOC(".CRT$XPZ") _PVFV __xp_z[];    /* C pre-terminators */
extern _CRTALLOC(".CRT$XTA") _PVFV __xt_a[];
extern _CRTALLOC(".CRT$XTZ") _PVFV __xt_z[];    /* C terminators */

При инициализации программа просто выполняет итерацию от «__xN_a» до «__xN_z» (где N равно {i,c,p,t}) и вызывает любые найденные ненулевые указатели.Если мы просто вставим наш собственный сегмент между сегментами «.CRT$XnA» и «.CRT$XnZ» (где n снова равно {I,C,P,T}), он будет вызван вместе со всем остальным. это обычно вызывается.

Компоновщик просто объединяет сегменты в алфавитном порядке.Это чрезвычайно упрощает выбор времени вызова наших функций.Если вы заглянете defsects.inc (находится под $(VS_DIR)\VC\crt\src\) вы можете видеть, что MS поместила все «пользовательские» функции инициализации (то есть те, которые инициализируют глобальные переменные в вашем коде) в сегменты, заканчивающиеся на «U».Это означает, что нам просто нужно поместить наши инициализаторы в сегмент раньше, чем «U», и они будут вызываться перед любыми другими инициализаторами.

Вы должны быть очень осторожны и не использовать какие-либо функции, которые не инициализированы, пока не будет выбрано размещение указателей функций (честно говоря, я бы рекомендовал вам просто использовать .CRT$XCT таким образом, это только ваш код, который не был инициализирован.Я не уверен, что произойдет, если вы подключите стандартный код «C», возможно, вам придется поместить его в .CRT$XIT в этом случае заблокируйте).

Я обнаружил одну вещь: «претерминаторы» и «терминаторы» на самом деле не сохраняются в исполняемом файле, если вы ссылаетесь на версии DLL библиотеки времени выполнения.Из-за этого вы не можете использовать их в качестве общего решения.Вместо этого я заставил его запустить мою конкретную функцию, поскольку последняя «пользовательская» функция заключалась в простом вызове atexit() Таким образом, внутри «инициализаторов C» в стек не могло быть добавлено ни одной другой функции (которая будет вызываться в порядке, обратном добавлению функций, и именно так вызываются все глобальные/статические деконструкторы).

И последнее (очевидное) замечание: оно написано с учетом библиотеки времени выполнения Microsoft.Это может работать аналогично на других платформах/компиляторах (надеюсь, вам удастся просто изменить имена сегментов на те, которые они используют, ЕСЛИ они используют ту же схему), но не рассчитывайте на это.

Другие советы

Atexit обрабатывается во время выполнения C/C ++ (CRT). Он работает после того, как Main () уже вернулся. Вероятно, лучший способ сделать это - заменить стандартный CRT на свой собственный.

В Windows Tlibc, вероятно, отличное место для начала: http://www.codeproject.com/kb/library/tlibc.aspx

Посмотрите на пример кода для MainCrtStartup и просто запустите свой код после вызова _doExit (); Но перед выходом.

В качестве альтернативы, вы можете просто получить уведомление, когда вызывает ExitProcess. Когда происходит следующее http://msdn.microsoft.com/en-us/library/ms682658%28vs.85%29.aspx):

  1. Все потоки в процессе, кроме вызова, завершают их выполнение, не получая уведомление DLL_THREAD_DETACH.
  2. Состояния всех потоков, заканчиваемых на шаге 1, становятся сигналами.
  3. Функции точки входа из всех загруженных библиотек динамических связей (DLL) вызываются с помощью dll_process_detach.
  4. После того, как все прикрепленные DLL выполнили любой код завершения процесса, функция ExitProcess завершает текущий процесс, включая вызову.
  5. Состояние вызывающего потока становится сигнализированным.
  6. Все ручки объекта, открытые процессом, закрыты.
  7. Статус завершения процесса изменяется от illing_active к значению выхода процесса.
  8. Состояние объекта процесса становится сигнализированным, удовлетворяя любые потоки, которые ждали, когда процесс завершится.

Таким образом, одним из методов было бы создать DLL и иметь этот DLL прикрепить к процессу. Он будет уведомлен, когда процесс выходит, что должно быть после обработки Atexit.

Очевидно, это все довольно хакерское, продолжай тщательно.

Это зависит от платформы разработки. Например, у Borland C ++ есть #Pragma, которая может быть использована именно для этого. (Из Borland C ++ 5,0, ок. 1995)

#pragma startup function-name [priority]
#pragma exit    function-name [priority]
Эти две прагмы позволяют программе указать функции (ы), которые следует вызывать либо после запуска программы (до вызовой основной функции), либо выходом программы (непосредственно перед завершением программы через _exit). Указанная функция имени должна быть ранее объявленной функцией как:
void function-name(void);
Необязательный приоритет должен быть в диапазоне от 64 до 255, с наивысшим приоритетом в 0; По умолчанию 100. Функции с более высокими приоритетами называются первыми при запуске и последним на выходе. Приоритеты от 0 до 63 используются библиотеками C и не должны использоваться пользователем.

Возможно, у вашего компилятора C есть подобное средство?

Я читал несколько раз, вы не можете гарантировать порядок строительства глобальных переменных (цитировать) Я думаю, что из этого довольно безопасно сделать вывод, что заказ на выполнение деструктора также не гарантируется.

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

Кроме того, для какой платформы определяется эта функция _atexit?

Лучшее решение — выполнить очистку трекера памяти последней.Самый простой способ сделать это — явно контролировать порядок инициализации всех соответствующих глобальных переменных.(Некоторые библиотеки скрывают свое глобальное состояние в причудливых классах или иным образом, думая, что следуют шаблону, но все, что они делают, это предотвращают такую ​​гибкость.)

Пример main.cpp:

#include "global_init.inc"
int main() {
  // do very little work; all initialization, main-specific stuff
  // then call your application's mainloop
}

Если файл глобальной инициализации включает определения объектов и #include аналогичные файлы без заголовков.Упорядочите объекты в этом файле в том порядке, в котором вы хотите их построить, и они будут уничтожены в обратном порядке.18.3/8 в C++03 гарантирует, что порядок уничтожения отражает конструкцию:"Нелокальные объекты со статическим сроком хранения уничтожаются в порядке, обратном завершению их конструктора." (В этом разделе речь идет о exit(), но возврат из main тот же, см. 3.6.1/5.)

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

У меня была эта точная проблема, также написал трекер памяти.

Несколько вещей:

Наряду с разрушением, вам также необходимо справиться с конструкцией. Будьте готовы, чтобы Malloc/New был вызван до того, как ваш трекер памяти будет построен (при условии, что он написан как класс). Таким образом, вам нужен ваш класс, чтобы узнать, был ли он построен или разрушен!

class MemTracker
{
    enum State
    {
      unconstructed = 0, // must be 0 !!!
      constructed,
      destructed
    };
    State state;

    MemTracker()
    {
       if (state == unconstructed)
       {
          // construct...
          state = constructed;
       }
    }
};

static MemTracker memTracker;  // all statics are zero-initted by linker

На каждом распределении, которое вызывает ваш трекер, постройте его!

MemTracker::malloc(...)
{
    // force call to constructor, which does nothing after first time
    new (this) MemTracker();
    ...
}

Странно, но это правда. Во всяком случае, на разрушение:

    ~MemTracker()
    {
        OutputLeaks(file);
        state = destructed;
    }

Итак, при разрушении выключите свои результаты. И все же мы знаем, что будет больше звонков. Что делать? Что ж,...

   MemTracker::free(void * ptr)
   {
      do_tracking(ptr);

      if (state == destructed)
      {
          // we must getting called late
          // so re-output
          // Note that this might happen a lot...
          OutputLeaks(file); // again!
       }
   }

И наконец:

  • Будьте осторожны с потоком
  • Будьте осторожны, чтобы не позвонить Malloc/Free/New/Delete внутри вашего трекера, или иметь возможность обнаружить рекурсию и т. Д. :-)

РЕДАКТИРОВАТЬ:

  • И я забыл, если вы поместите свой трекер в DLL, вам, вероятно, нужно будет загрузитьлибрир () (или длопен и т. Д.) самим собой Чтобы увеличить количество ссылок, чтобы вас не удаляли из памяти преждевременно. Потому что, хотя ваш класс все еще может быть вызван после разрушения, это не может, если код был разгружен.
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top