모든 '_atexit()' 함수가 완료된 후 일부 코드가 실행되도록 예약하려면 어떻게 해야 합니까?
-
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):
- 호출 스레드를 제외한 프로세스의 모든 스레드는 DLL_THREAD_DETACH 알림을받지 않고 실행을 종료합니다.
- 1 단계에서 종료 된 모든 실의 상태가 신호가된다.
- 모든로드 된 다이나믹 링크 라이브러리 (DLL)의 진입 기능은 DLL_PROCESS_DETACH로 호출됩니다.
- 첨부 된 DLL이 모든 프로세스 종료 코드를 실행 한 후 ExitProcess 함수는 호출 스레드를 포함하여 현재 프로세스를 종료합니다.
- 호출 스레드의 상태가 신호됩니다.
- 프로세스에서 열린 모든 객체 핸들이 닫힙니다.
- 프로세스의 종료 상태는 Still_active에서 프로세스의 종료 값으로 변경됩니다.
- 프로세스 객체의 상태는 신호가되어 프로세스가 끝나기를 기다리는 스레드를 만족시킵니다.
따라서 한 가지 방법은 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 등)를 잊어 버렸습니다. 당신 자신 메모리에서 조기에 제거되지 않도록 참조 수를 올리십시오. 파괴 후에도 수업이 여전히 호출 될 수 있지만 코드가 언로드되었는지 여부는 할 수 없습니다.