3

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

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

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

4

6 回答 6

6

我终于想出了如何在 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 初始化程序”中简单地调用,这样,就不能将其他函数添加到堆栈中(这将被反向调用添加函数的顺序以及调用全局/静态解构函数的方式)。

Just one final (obvious) note, this is written with Microsoft's runtime library in mind. It may work similar on other platforms/compilers (hopefully you'll be able to get away with just changing the segment names to whatever they use, IF they use the same scheme) but don't count on it.

于 2010-07-08T07:18:29.520 回答
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 之后。

显然,这一切都相当骇人听闻,请谨慎进行。

于 2009-11-18T01:41:44.427 回答
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。具有较高优先级的函数在启动时首先调用,最后在退出时调用。C 库使用从 0 到 63 的优先级,用户不应使用。

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

于 2009-11-18T01:52:05.913 回答
0

我已经多次阅读您无法保证全局变量的构造顺序(cite)。我认为从中推断出析构函数的执行顺序也不能保证是非常安全的。

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

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

于 2009-11-18T01:37:42.153 回答
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 类似的非头文件。按照您希望它们构造的顺序对该文件中的对象进行排序,它们将以相反的顺序被破坏。C++03 中的 18.3/8 保证销毁顺序镜像构造:“具有静态存储持续时间的非本地对象按照其构造函数完成的相反顺序销毁。” (该部分正在讨论exit(),但从 main 的返回是相同的,请参见 3.6.1/5。)

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

于 2009-11-18T01:37:59.843 回答
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 等)来增加的引用计数,这样你就不会过早地从内存中删除。因为虽然你的类在销毁后仍然可以被调用,但是如果代码已经被卸载就不能了。
于 2009-11-18T06:17:12.980 回答