모든 '_atexit()' 함수가 완료된 후 일부 코드가 실행되도록 예약하려면 어떻게 해야 합니까?

StackOverflow https://stackoverflow.com/questions/1753042

  •  20-09-2019
  •  | 
  •  

문제

저는 메모리 추적 시스템을 작성 중인데 실제로 겪게 된 유일한 문제는 응용 프로그램이 종료될 때 생성자에서는 할당되지 않았지만 해체자에서는 할당이 해제되는 모든 정적/전역 클래스가 내 메모리 이후에 할당이 해제된다는 것입니다. 추적 자료에서 할당된 데이터가 누출되었다고 보고했습니다.

내가 알 수 있는 한, 이 문제를 올바르게 해결하는 유일한 방법은 메모리 추적기의 _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 버전에 연결되면 "사전 터미네이터"와 "터미네이터"가 실제로 실행 파일에 저장되지 않는다는 것입니다. 이로 인해 실제로 일반 솔루션으로 사용할 수는 없습니다. 대신, 마지막 "사용자"기능으로 내 특정 기능을 실행하는 방식은 단순히 전화하는 것이 었습니다. atexit() 'C 이니셜 라이저'내 에서이 방식으로 스택에 다른 함수가 추가되지 않았을 수 있습니다 (기능이 추가되는 역 순서로 호출 될 것이며 글로벌/정적 디 구성자가 모두 호출되는 방법).

최종 (명백한) 메모는 Microsoft의 런타임 라이브러리를 염두에두고 작성되었습니다. 다른 플랫폼/컴파일러에서 유사하게 작동 할 수 있습니다 (동일한 체계를 사용하는 경우 세그먼트 이름을 사용하는 모든 것으로 변경하여 도망 갈 수 있기를 바랍니다).

다른 팁

ATEXIT는 C/C ++ 런타임 (CRT)에 의해 처리됩니다. main ()가 이미 돌아온 후에 실행됩니다. 아마도이 작업을 수행하는 가장 좋은 방법은 표준 CRT를 자신의 것으로 바꾸는 것입니다.

Windows에서 TLIBC는 아마도 시작하기에 좋은 장소 일 것입니다. http://www.codeproject.com/kb/library/tlibc.aspx

maincrtStartup의 코드 샘플을보고 _doexit ()로 호출 한 후 코드를 실행하십시오. 그러나 외출하기 전에.

또는 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이 처리 된 후에야합니다.

분명히, 이것은 다소 해킹되며, 신중하게 진행합니다.

이것은 개발 플랫폼에 따라 다릅니다. 예를 들어, Borland C ++에는 #Pragma가있어 정확히 사용될 수 있습니다. (Borland C ++ 5.0, c. 1995)

#pragma startup function-name [priority]
#pragma exit    function-name [priority]
이 두 pragmas는 프로그램이 프로그램 시작시 (기본 함수가 호출되기 전에) 또는 프로그램 종료 (프로그램이 _exit을 통해 종료되기 직전)를 지정해야합니다. 지정된 함수 이름은 이전에 선언 된 기능이어야합니다.
void function-name(void);
선택적 우선 순위는 64 ~ 255 범위에 있어야하며 0에서 우선 순위가 가장 높습니다. 기본값은 100입니다. 우선 순위가 높은 기능은 시작시 첫 번째로, 마지막으로 출구에서 호출됩니다. 0에서 63의 우선 순위는 C 라이브러에서 사용되며 사용자가 사용해서는 안됩니다.

아마도 C 컴파일러에 비슷한 시설이 있습니까?

여러 번 읽었습니다. 글로벌 변수의 구성 순서를 보장 할 수 없습니다.인용하다). 나는 파괴자 실행 순서도 보장되지 않는다는 것을 이것으로부터 추론하는 것이 매우 안전하다고 생각합니다.

따라서 메모리 추적 객체가 글로벌 인 경우 메모리 추적 객체가 마지막으로 파괴되거나 먼저 구성 될 수 있다는 보장이 거의 없습니다. 그것이 마지막으로 파괴되지 않았고 다른 할당이 뛰어나면, 그렇습니다. 누출이 언급 될 것입니다.

또한이 _atexit 함수는 어떤 플랫폼으로 정의됩니까?

메모리 추적기의 정리를 마지막에 실행하는 것이 가장 좋은 솔루션입니다.제가 찾은 가장 쉬운 방법은 관련된 모든 전역 변수의 초기화 순서를 명시적으로 제어하는 ​​것입니다.(일부 라이브러리는 패턴을 따르고 있다고 생각하여 고급 클래스 등에서 전역 상태를 숨기지만, 그들이 하는 일은 이러한 종류의 유연성을 방지하는 것뿐입니다.)

예 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(), 그러나 메인으로부터의 복귀는 동일합니다. 3.6.1/5를 참조하세요.)

보너스로, main에 들어가기 전에 해당 파일의 모든 전역 변수가 초기화된다는 것을 보장합니다.(표준에서는 보장되지 않지만 구현이 선택하는 경우 허용됩니다.)

이 정확한 문제가 있었으며 메모리 추적기도 작성했습니다.

몇 가지:

파괴와 함께 건축도 처리해야합니다. 메모리 추적기가 구성되기 전에 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/Tracker 내부에서 삭제하거나 재귀 등을 감지 할 수 있도록주의하십시오. :-)

편집하다:

  • 그리고 트래커를 DLL에 넣으면 loadlibrary () (또는 dlopen 등)를 잊어 버렸습니다. 당신 자신 메모리에서 조기에 제거되지 않도록 참조 수를 올리십시오. 파괴 후에도 수업이 여전히 호출 될 수 있지만 코드가 언로드되었는지 여부는 할 수 없습니다.
라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top