我正在编写一个内存跟踪系统,我实际遇到的唯一问题是当应用程序退出时,任何未在其构造函数中分配但在其解构函数中释放的静态/全局类正在释放在我的内存跟踪工具将分配的数据报告为泄漏之后。
据我所知,正确解决此问题的唯一方法是强制将内存跟踪器的 _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 版本,“预终止符”和“终止符”实际上并未存储在可执行文件中。因此,您不能真正将它们用作通用解决方案。相反,我让它作为最后一个“用户”函数运行我的特定函数的方式是简单地在“C 初始值设定项”中调用 atexit()
,这样,就没有其他函数可以添加到堆栈(将按照添加函数的相反顺序调用,这也是调用全局/静态解构函数的方式)。
只是最后一个(明显的)说明,这是在考虑 Microsoft 的运行时库的情况下编写的。它可能在其他平台/编译器上工作类似(希望您能够将段名称更改为它们使用的任何名称,如果它们使用相同的方案)但不要指望它。
关于c++ - 如何安排一些代码在所有 '_atexit()' 功能完成后运行,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/1753042/