毕竟“_atexit()”之后我怎样才能安排一些代码运行呢?功能已完成

发布于 2024-08-11 17:54:33 字数 276 浏览 10 评论 0原文

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

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

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

I'm writing a memory tracking system and the only problem I've actually run into is that when the application exits, any static/global classes that didn't allocate in their constructor, but are deallocating in their deconstructor are deallocating after my memory tracking stuff has reported the allocated data as a leak.

As far as I can tell, the only way for me to properly solve this would be to either force the placement of the memory tracker's _atexit callback at the head of the stack (so that it is called last) or have it execute after the entire _atexit stack has been unwound. Is it actually possible to implement either of these solutions, or is there another solution that I have overlooked.

Edit:
I'm working on/developing for Windows XP and compiling with VS2005.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

云归处 2024-08-18 17:54:34

我已经读过多次,你不能保证全局变量的构造顺序(引用)。我认为可以很安全地推断析构函数的执行顺序也不能得到保证。

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

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

I've read multiple times you can't guarantee the construction order of global variables (cite). I'd think it is pretty safe to infer from this that destructor execution order is also not guaranteed.

Therefore if your memory tracking object is global, you will almost certainly be unable any guarantees that your memory tracker object will get destructed last (or constructed first). If it's not destructed last, and other allocations are outstanding, then yes it will notice the leaks you mention.

Also, what platform is this _atexit function defined for?

风吹雨成花 2024-08-18 17:54:34

最后执行内存跟踪器的清理是最好的解决方案。我发现最简单的方法是显式控制所有相关全局变量的初始化顺序。 (有些库将其全局状态隐藏在奇特的类中或以其他方式,认为它们遵循某种模式,但它们所做的只是阻止这种灵活性。)

示例 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 之前进行初始化。 (标准中没有保证某些内容,但如果实现选择则允许。)

Having the memory tracker's cleanup executed last is the best solution. The easiest way I've found to do that is to explicitly control all the relevant global variables' initialization order. (Some libraries hide their global state in fancy classes or otherwise, thinking they're following a pattern, but all they do is prevent this kind of flexibility.)

Example main.cpp:

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

Where the global-initialization file includes object definitions and #includes similar non-header files. Order the objects in this file in the order you want them constructed, and they'll be destructed in the reverse order. 18.3/8 in C++03 guarantees that destruction order mirrors construction: "Non-local objects with static storage duration are destroyed in the reverse order of the completion of their constructor." (That section is talking about exit(), but a return from main is the same, see 3.6.1/5.)

As a bonus, you're guaranteed that all globals (in that file) are initialized before entering main. (Something not guaranteed in the standard, but allowed if implementations choose.)

不疑不惑不回忆 2024-08-18 17:54:34

我也遇到过这个问题,还写了一个内存跟踪器。

有几件事:

除了破坏之外,您还需要处理建设。在构建内存跟踪器之前,准备好调用 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 等)来增加引用计数,这样您就不会过早地从内存中删除。因为虽然你的类在销毁后仍然可以被调用,但如果代码已被卸载,则不能调用。

I've had this exact problem, also writing a memory tracker.

A few things:

Along with destruction, you also need to handle construction. Be prepared for malloc/new to be called BEFORE your memory tracker is constructed (assuming it is written as a class). So you need your class to know whether it has been constructed or destructed yet!

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

On every allocation that calls into your tracker, construct it!

MemTracker::malloc(...)
{
    // force call to constructor, which does nothing after first time
    new (this) MemTracker();
    ...
}

Strange, but true. Anyhow, onto destruction:

    ~MemTracker()
    {
        OutputLeaks(file);
        state = destructed;
    }

So, on destruction, output your results. Yet we know that there will be more calls. What to do? Well,...

   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!
       }
   }

And lastly:

  • be careful with threading
  • be careful not to call malloc/free/new/delete inside your tracker, or be able to detect the recursion, etc :-)

EDIT:

  • and I forgot, if you put your tracker in a DLL, you will probably need to LoadLibrary() (or dlopen, etc) yourself to up your reference count, so that you don't get removed from memory prematurely. Because although your class can still be called after destruction, it can't if the code has been unloaded.
硪扪都還晓 2024-08-18 17:54:33

我终于弄清楚如何在 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 运行时库的情况下编写的。它在其他平台/编译器上的工作方式可能类似(希望您只需将段名称更改为它们使用的任何名称即可,如果它们使用相同的方案),但不要指望它。

I've finally figured out how to do this under Windows/Visual Studio. Looking through the crt startup function again (specifically where it calls the initializers for globals), I noticed that it was simply running "function pointers" that were contained between certain segments. So with just a little bit of knowledge on how the linker works, I came up with this:

#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;
}

which outputs:

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();

This works due to the way MS have written their runtime library. Basically, they've setup the following variables in the data segments:

(although this info is copyright I believe this is fair use as it doesn't devalue the original and IS only here for reference)

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 */

On initialization, the program simply iterates from '__xN_a' to '__xN_z' (where N is {i,c,p,t}) and calls any non null pointers it finds. If we just insert our own segment in between the segments '.CRT$XnA' and '.CRT$XnZ' (where, once again n is {I,C,P,T}), it will be called along with everything else that normally gets called.

The linker simply joins up the segments in alphabetical order. This makes it extremely simple to select when our functions should be called. If you have a look in defsects.inc (found under $(VS_DIR)\VC\crt\src\) you can see that MS have placed all the "user" initialization functions (that is, the ones that initialize globals in your code) in segments ending with 'U'. This means that we just need to place our initializers in a segment earlier than 'U' and they will be called before any other initializers.

You must be really careful not to use any functionality that isn't initialized until after your selected placement of the function pointers (frankly, I'd recommend you just use .CRT$XCT that way its only your code that hasn't been initialized. I'm not sure what will happen if you've linked with standard 'C' code, you may have to place it in the .CRT$XIT block in that case).

One thing I did discover was that the "pre-terminators" and "terminators" aren't actually stored in the executable if you link against the DLL versions of the runtime library. Due to this, you can't really use them as a general solution. Instead, the way I made it run my specific function as the last "user" function was to simply call atexit() within the 'C initializers', this way, no other function could have been added to the stack (which will be called in the reverse order to which functions are added and is how global/static deconstructors are all called).

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.

木緿 2024-08-18 17:54:33

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) 的入口点函数均通过 DLL_PROCESS_DETACH 调用。
  4. 在所有附加的 DLL 执行完任何进程终止代码后,ExitProcess 函数将终止当前进程,包括调用线程。
  5. 调用线程的状态变为有信号状态。
  6. 进程打开的所有对象句柄都将关闭。
  7. 进程的终止状态从 STILL_ACTIVE 更改为进程的退出值。
  8. 进程对象的状态变为有信号状态,满足任何一直在等待进程终止的线程。

因此,一种方法是创建一个 DLL 并将该 DLL 附加到进程。当进程退出时,它会收到通知,这应该是在 atexit 被处理之后。

显然,这一切都相当黑客,请小心行事。

atexit is processed by the C/C++ runtime (CRT). It runs after main() has already returned. Probably the best way to do this is to replace the standard CRT with your own.

On Windows tlibc is probably a great place to start: http://www.codeproject.com/KB/library/tlibc.aspx

Look at the code sample for mainCRTStartup and just run your code after the call to _doexit();
but before ExitProcess.

Alternatively, you could just get notified when ExitProcess gets called. When ExitProcess gets called the following occurs (according to http://msdn.microsoft.com/en-us/library/ms682658%28VS.85%29.aspx):

  1. All of the threads in the process, except the calling thread, terminate their execution without receiving a DLL_THREAD_DETACH notification.
  2. The states of all of the threads terminated in step 1 become signaled.
  3. The entry-point functions of all loaded dynamic-link libraries (DLLs) are called with DLL_PROCESS_DETACH.
  4. After all attached DLLs have executed any process termination code, the ExitProcess function terminates the current process, including the calling thread.
  5. The state of the calling thread becomes signaled.
  6. All of the object handles opened by the process are closed.
  7. The termination status of the process changes from STILL_ACTIVE to the exit value of the process.
  8. The state of the process object becomes signaled, satisfying any threads that had been waiting for the process to terminate.

So, one method would be to create a DLL and have that DLL attach to the process. It will get notified when the process exits, which should be after atexit has been processed.

Obviously, this is all rather hackish, proceed carefully.

尴尬癌患者 2024-08-18 17:54:33

这取决于开发平台。例如,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 编译器有类似的功能?

This is dependent on the development platform. For example, Borland C++ has a #pragma which could be used for exactly this. (From Borland C++ 5.0, c. 1995)

#pragma startup function-name [priority]
#pragma exit    function-name [priority]

These two pragmas allow the program to specify function(s) that should be called either upon program startup (before the main function is called), or program exit (just before the program terminates through _exit).

The specified function-name must be a previously declared function as:

void function-name(void);

The optional priority should be in the range 64 to 255, with highest priority at 0; default is 100. Functions with higher priorities are called first at startup and last at exit. Priorities from 0 to 63 are used by the C libraries, and should not be used by the user.

Perhaps your C compiler has a similar facility?

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文