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}) まで反復し、見つかった null 以外のポインターを呼び出します。セグメント '.CRT$XnA' と '.CRT$XnZ' (ここでも、n は {I,C,P,T} です) の間に独自のセグメントを挿入すると、他のすべてのセグメントと共に呼び出されます。通常は呼び出されます。

リンカは単にセグメントをアルファベット順に結合します。これにより、関数を呼び出すタイミングを非常に簡単に選択できます。defsects.inc( の下にあります$(VS_DIR)\VC\crt\src\) を見ると、MS がすべての「ユーザー」初期化関数 (つまり、コード内のグローバルを初期化する関数) を「U」で終わるセグメントに配置していることがわかります。これは、イニシャライザを「U」より前のセグメントに配置するだけでよく、他のイニシャライザの前に呼び出されることを意味します。

関数ポインターの選択した配置が完了するまで、初期化されていない機能を使用しないように十分に注意する必要があります (率直に言って、.CRT$XCT初期化されていないコードのみをそのように使用することをお勧めします。標準の「C」コードとリンクした場合に何が起こるかわかりません.CRT$XIT。その場合、ブロックに配置する必要があるかもしれません)。

私が発見したことの 1 つは、ランタイム ライブラリの DLL バージョンに対してリンクすると、「プレターミネーター」と「ターミネーター」が実際には実行可能ファイルに格納されないことです。このため、一般的なソリューションとして実際に使用することはできません。代わりに、最後の「ユーザー」関数として特定の関数を実行する方法は、単にatexit()「C イニシャライザー」内で呼び出すことでした。この方法では、スタックに他の関数を追加することはできませんでした (逆に呼び出されます)。関数が追加される順序と、グローバル/静的デコンストラクターがすべて呼び出される方法です)。

最後に 1 つだけ (明白な) 注意点として、これは Microsoft のランタイム ライブラリを念頭に置いて書かれています。他のプラットフォーム/コンパイラでも同様に機能する可能性があります(同じスキームを使用している場合は、セグメント名を使用するものに変更するだけで済むことを願っています)が、それを当てにしないでください.

于 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. プロセス オブジェクトの状態がシグナル状態になり、プロセスの終了を待っていたすべてのスレッドが満たされます。

したがって、1 つの方法は、DLL を作成し、その DLL をプロセスにアタッチすることです。プロセスが終了すると通知されます。これは atexit が処理された後である必要があります。

明らかに、これはかなりハックです。慎重に進めてください。

于 2009-11-18T01:41:44.427 に答える
1

これは、開発プラットフォームに依存します。たとえば、Borland C++ には、まさにこれに使用できる #pragma があります。(Borland C++ 5.0 から、1995 年頃)

#pragma startup function-name [priority]
#pragma exit    function-name [priority]
これらの 2 つのプラグマにより、プログラムは、プログラムの起動時 (メイン関数が呼び出される前) またはプログラムの終了時 (プログラムが _exit によって終了する直前) に呼び出される関数を指定できます。指定された関数名は、次のように事前に宣言された関数でなければなりません。
void function-name(void);
オプションの優先度は 64 ~ 255 の範囲で、最高の優先度は 0 です。デフォルトは 100 です。優先度の高い関数は、起動時に最初に呼び出され、終了時に最後に呼び出されます。0 から 63 までの優先度は C ライブラリによって使用され、ユーザーは使用しないでください。

おそらく、あなたの C コンパイラにも同様の機能がありますか?

于 2009-11-18T01:52:05.913 に答える
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 に答える
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
}

グローバル初期化ファイルにはオブジェクト定義が含まれ、#include には同様の非ヘッダー ファイルが含まれます。このファイル内のオブジェクトを構築したい順序で並べると、逆の順序で破棄されます。C++03 の 18.3/8 では、破棄順序が構築を反映することが保証されています。(そのセクションは について話してexit()いますが、main からの戻りも同じです。3.6.1/5 を参照してください。)

おまけとして、(そのファイル内の) すべてのグローバルが main に入る前に初期化されることが保証されます。(標準では保証されていませんが、実装が選択した場合は許可されます。)

于 2009-11-18T01:37:59.843 に答える