Как я могу запланировать выполнение некоторого кода после завершения всех функций _atexit () - PullRequest
3 голосов
/ 18 ноября 2009

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

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

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

Ответы [ 6 ]

5 голосов
/ 08 июля 2010

Я наконец-то понял, как это сделать в 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 таким образом, только ваш код, который Я не уверен, что произойдет, если вы свяжетесь со стандартным кодом «С», в этом случае вам, возможно, придется поместить его в блок .CRT$XIT).

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

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

1 голос
/ 18 ноября 2009

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

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

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

1 голос
/ 18 ноября 2009

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

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

Посмотрите на пример кода для mainCRTStartup и просто запустите свой код после вызова _doexit (); но до ExitProcess.

Кроме того, вы можете просто получить уведомление, когда вызывается ExitProcess. Когда вызывается 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. Статус завершения процесса изменяется с STILL_ACTIVE на значение выхода процесса.
  8. Состояние объекта процесса становится сигнальным, удовлетворяя все потоки, которые ожидали завершения процесса.

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

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

0 голосов
/ 18 ноября 2009

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

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

Наряду с разрушением, вам также нужно заниматься строительством. Будьте готовы к тому, что 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 внутри вашего трекера или быть в состоянии обнаружить рекурсию и т. Д .: -)

EDIT:

  • и я забыл, если вы поместите свой трекер в DLL, вам, вероятно, потребуется LoadLibrary () (или dlopen и т. Д.) , чтобы увеличить количество ссылок, чтобы вы не получили удален из памяти преждевременно. Потому что, хотя ваш класс все еще можно вызвать после уничтожения, он не может быть, если код был выгружен.
0 голосов
/ 18 ноября 2009

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

Пример 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. (Что-то не гарантировано в стандарте, но разрешено, если выбраны реализации.)

0 голосов
/ 18 ноября 2009

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

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

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

...