2009-11-18 3 views
3

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

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

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

ответ

5

Я, наконец, выяснил, как это сделать под 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. Это может сработать на других платформах/компиляторах (надеюсь, вам удастся просто сменить названия сегментов на все, что они используют, если они используют ту же схему), но не рассчитывают на это.

0

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

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

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

+0

Вам гарантируется, что конструкция зеркала для разрушения будет создана. – 2009-11-18 01:43:12

+0

Если это так, то, поскольку порядок строительства не гарантируется, вы не можете предсказать порядок зеркального уничтожения. - Doug T. 0 сек назад –

+1

Вы не можете форсировать порядок построения * глобалов в разных единицах перевода *. Решение R.Pate, которое должно помещать все глобалы в одну и ту же блок переводов, гарантирует заказ. –

0

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

Пример main.cpp:

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

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

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

+1

Некоторые библиотеки скрывают свое глобальное состояние в переменных локального диапазона со статической продолжительностью хранения. Это также позволяет вам контролировать порядок, определяя, какой порядок вызывать функции, содержащие эти статические переменные. Конечно, есть проблемы безопасности потоков. –

+0

@steve: Мне нравится этот трюк для фиксации глобального заказа на строительство. Однако это не влияет на порядок уничтожения. локальный порядок статической переменной уничтожения является полным бесплатным для всех. –

+0

Они уничтожены в обратном порядке завершения их конструкторов (3.6.3). Это относится ко всем переменным со статической продолжительностью хранения, независимо от их объема. Таким образом, порядок уничтожения является свободным для всех, если (и только если) порядок строительства. Конечно, если не все используют одну и ту же схему, глобальные глобальные пространства имен могут по-прежнему создавать вещи до того, как main() начнет в непредсказуемом порядке, если они находятся в разных единицах перевода, и вы потеряете контроль. –

1

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

В 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.

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

1

Это зависит от платформы разработки. Например, 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 имеет аналогичный объект?

+0

Я действительно ищу это на VC++ и g ++, большое совпадение. Надеюсь, что кто-то сможет разработать. – GManNickG

+0

Я просмотрел документы VS2008, но ничего не видел. Я даже пытался использовать help на «# прагма», но он разбил средство справки. Sheesh! – wallyk

+0

Спасибо, что посмотрели. :) Я знаю, что это возможно, я видел это где-то однажды. Если я найду это, я дам вам знать. – GManNickG

0

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

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

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

И, наконец:

  • быть осторожным с резьб
  • быть осторожным, чтобы не называть таНос/свободный/новый/удалять в АПУ, или быть в состоянии обнаружить рекурсии и т.д. :-)

EDIT:

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

Вы можете полностью избежать проблемы с созданием, поставив статическую переменную в функцию, которая впоследствии возвращает ее. Затем он создается при первом вызове функции. Все, что осталось обработать, это события, которые происходят после вызова деструктора. Вот хороший обзор метода: http://www.entropygames.net/index.php?option=com_content&view=article&id=52:global-dependencies-made-easy&catid=37:articles&Itemid=56 –

+0

Хм, конечно, вы правильно. Должно быть, я смешиваю свою память с другим случаем - я думаю, что в этом случае мне пришлось беспокоиться о потолочной обработке, и мне нужно было быть более осторожным со строительством. Это было несколько лет назад. В любом случае, «после уничтожения, выхода каждый раз, когда вы называетесь», определенно была техника, которую я использовал для конкретной проблемы. Может быть, я должен был остаться на тему. – tony