如何在完成所有'_atexit()'函数后安排一些代码运行

时间:2009-11-18 01:28:02

标签: c++ allocation atexit

我正在编写一个内存跟踪系统,我实际遇到的唯一问题是,当应用程序退出时,任何未在其构造函数中分配但在解构器中解除分配的静态/全局类都会解除分配在我的内存跟踪内容之后,已将分配的数据报告为泄漏。

据我所知,正确解决这个问题的唯一方法是强制将内存跟踪器的_atexit回调放置在堆栈的头部(以便最后调用它)或让它执行整个_atexit堆栈解除后。实际上是否可以实现这些解决方案中的任何一种,或者是否存在我忽略的另一种解决方案。

编辑: 我正在开发/开发Windows XP并使用VS2005进行编译。

6 个答案:

答案 0 :(得分:5)

我终于想出了如何在Windows / Visual Studio下执行此操作。再次查看crt启动函数(特别是它调用globals的初始值设定项),我注意到它只是运行某些段之间包含的“函数指针”。因此,只需了解链接器的工作原理,我想出了这个:

#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编写运行时库的方式。基本上,他们在数据段中设置了以下变量:

(虽然这个信息是版权所有,但我认为这是合理使用,因为它不会使原件贬值而且IS仅供参考)

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”的段中,它们将在任何其他初始化器之前调用。

你必须非常小心,不要在你选择放置函数指针之后使用任何未初始化的功能(坦率地说,我建议你只使用.CRT$XCT这样只有你的代码才能使用我已经初始化了。我不确定如果你与标准'C'代码链接会发生什么,在这种情况下你可能不得不把它放在.CRT$XIT块中。

我发现的一件事是,如果链接到运行时库的DLL版本,“预终结符”和“终结符”实际上并不存储在可执行文件中。因此,您无法真正将它们用作通用解决方案。相反,我使用它的方式作为最后一个“用户”函数运行我的特定功能是简单地在'C初始化器'中调用atexit(),这样,没有其他功能可以添加到堆栈中(这将以与添加函数相反的顺序调用,以及如何调用全局/静态解构器)。

最后一个(显而易见的)注释,这是用Microsoft的运行时库编写的。它可能在其他平台/编译器上的工作方式类似(希望您只需将段名更改为他们使用的任何内容,如果他们使用相同的方案),但不要依赖它。

答案 1 :(得分:1)

atexit由C / C ++运行时(CRT)处理。它在main()已经返回后运行。可能最好的方法是用您自己的CRT替换标准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_PROCESS_DETACH调用所有加载的动态链接库(DLL)的入口点函数。
  4. 在所有附加的DLL执行了任何进程终止代码之后,ExitProcess函数终止当前进程,包括调用线程。
  5. 调用线程的状态变为信号。
  6. 该进程打开的所有对象句柄都将关闭。
  7. 流程的终止状态从STILL_ACTIVE变为流程的退出值。
  8. 进程对象的状态变为信号,满足任何等待进程终止的线程。
  9. 因此,一种方法是创建一个DLL并将该DLL附加到进程。它将在进程退出时收到通知,这应该在atexit处理完毕后通知。

    显然,这都是相当hackish,小心行事。

答案 2 :(得分:1)

这取决于开发平台。例如,Borland C ++有一个#pragma,可以用于此。 (摘自Borland C ++ 5.0,c.1995)

#pragma startup function-name [priority]
#pragma exit    function-name [priority]
这两个pragma允许程序指定应该在程序启动时(在调用main函数之前)或程序退出(在程序通过_exit终止之前)调用的函数。 指定的函数名必须是先前声明的函数:
void function-name(void);
可选优先级应在64到255之间,最高优先级为0;默认值为100.具有较高优先级的函数在启动时首先调用,最后在退出时调用。从0到63的优先级由C库使用,不应由用户使用。

也许你的C编译器有类似的功能?

答案 3 :(得分:0)

我多次读过你不能保证全局变量(cite)的构造顺序。我认为从中推断析构函数执行顺序也不能得到保证是非常安全的。

因此,如果您的内存跟踪对象是全局的,那么您几乎肯定无法保证内存跟踪器对象最后会被破坏(或首先构建)。如果它最后没有被破坏,而其他分配是未完成的,那么它会注意到你提到的泄漏。

此外,这个_atexit函数定义的平台是什么?

答案 4 :(得分:0)

最后执行内存跟踪器清理是最佳解决方案。我发现这样做的最简单方法是显式控制所有相关的全局变量的初始化顺序。 (有些图书馆在花哨的课程中隐藏自己的全球状态,或者认为他们遵循一种模式,但他们所做的只是阻止这种灵活性。)

示例main.cpp:

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

全局初始化文件包含对象定义和#includes类似的非头文件。按照您希望它们构造的顺序对此文件中的对象进行排序,它们将以相反的顺序被破坏。 18.3 / 8 in C ++ 03保证销毁顺序镜像构造:“具有静态存储持续时间的非本地对象以其构造函数完成的相反顺序被销毁。” (该部分讨论的是exit(),但是从main返回是相同的,见3.6.1 / 5。)

作为奖励,您可以保证在进入main之前初始化所有全局变量(在该文件中)。 (标准中不保证某些内容,但如果实现选择则允许。)

答案 5 :(得分: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!
       }
   }

最后:

  • 小心线程
  • 注意不要在你的跟踪器中调用malloc / free / new / delete,或者能够检测到递归等等: - )

编辑:

  • 我忘了,如果你把你的跟踪器放在一个DLL中,你可能需要LoadLibrary()(或dlopen等)你自己来提高你的引用数量,这样你就不会过早地从记忆中删除。因为虽然你的类仍然可以在破坏后被调用,但是如果代码已被卸载则不能。