كيف يمكنني جدولة بعض التعليمات البرمجية لتشغيلها بعد كل وظائف "_atexit ()"

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

  •  20-09-2019
  •  | 
  •  

سؤال

أنا أكتب نظام تتبع الذاكرة والمشكلة الوحيدة التي أديرها بالفعل هي أنه عندما يخرج التطبيق ، فإن أي فئات ثابتة/عالمية لم تخصص في مُنشئها ، ولكن يتم توسيع نطاقها في تفككها. أبلغ تتبع الأشياء عن البيانات المخصصة كتسرب.

بقدر ما أستطيع أن أقول ، فإن الطريقة الوحيدة بالنسبة لي لحل هذا الأمر هي إما إما فرض وضع رد اتصال متعقب الذاكرة على رأس المكدس (بحيث يطلق عليه آخر) أو يتم تنفيذه بعد كامل _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" و. هذا عادة ما يسمى.

الرابط ينضم ببساطة إلى الأجزاء بالترتيب الأبجدي. هذا يجعل من السهل للغاية تحديد متى يجب استدعاء وظائفنا. إذا كان لديك نظرة defsects.inc (وجدت تحت $(VS_DIR)\VC\crt\src\) يمكنك أن ترى أن MS قد وضعت جميع وظائف التهيئة "المستخدم" (أي تلك التي تهيئة الكرات في الكود الخاص بك) في الأجزاء التي تنتهي بـ "U". هذا يعني أننا نحتاج فقط إلى وضع المهيمنات لدينا في شريحة في وقت سابق من "U" وسيتم استدعاؤها قبل أي مُهيئات أخرى.

يجب أن تكون حريصًا حقًا على عدم استخدام أي وظيفة لم يتم تهيئتها حتى بعد وضعك المختار من مؤشرات الوظيفة (بصراحة ، أوصيك فقط بالاستخدام .CRT$XCT وبهذه الطريقة ، لم يتم تهيئة الكود الخاص بك فقط. لست متأكدًا مما سيحدث إذا كنت مرتبطًا برمز "C" القياسي ، فقد تضطر إلى وضعه في .CRT$XIT حظر في هذه الحالة).

أحد الأشياء التي اكتشفتها هو أن "المصممين قبل" و "Terminators" لم يتم تخزينهم فعليًا في القابل للتنفيذ إذا قمت بالربط مع إصدارات DLL من مكتبة وقت التشغيل. بسبب هذا ، لا يمكنك استخدامها كحل عام. بدلاً من ذلك ، كانت الطريقة التي جعلتها تعمل بتشغيل وظيفتي المحددة كوظيفة "المستخدم" الأخيرة هي ببساطة الاتصال atexit() ضمن "المبدعين C" ، بهذه الطريقة ، لم يكن من الممكن إضافة أي وظيفة أخرى إلى المكدس (والتي سيتم استدعاؤها بالترتيب العكسي الذي تتم إضافته إلى الوظائف وهي كيف تسمى جميع الفكات العالمية/الثابتة).

ملاحظة أخيرة فقط (واضحة) ، يتم كتابة هذا مع وضع مكتبة وقت تشغيل Microsoft في الاعتبار. قد يعمل الأمر مشابهًا على المنصات/المجمعين الآخرين (نأمل أن تكون قادرًا على الابتعاد عن تغيير أسماء القطاعات إلى كل ما يستخدمونه ، إذا استخدموا نفس المخطط) ولكن لا تعتمد عليه.

نصائح أخرى

تتم معالجة 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. يتم استدعاء وظائف نقطة الدخول لجميع مكتبات الارتباط الديناميكي المحملة (DLLs) مع dll_process_detach.
  4. بعد كل شيء ، قامت DLLs المرفقة بتنفيذ أي رمز إنهاء العملية ، تنهي وظيفة 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 الخاص بك منشأة مماثلة؟

لقد قرأت عدة مرات لا يمكنك ضمان ترتيب بناء المتغيرات العالمية (استشهد). أعتقد أنه من الآمن أن نستنتج من هذا أمر تنفيذ Destructor غير مضمون أيضًا.

لذلك ، إذا كان كائن تتبع الذاكرة عالميًا ، فمن المؤكد أنك لن تتمكن من أي ضمانات بأن كائن متتبع الذاكرة الخاص بك سيتم تدميره أخيرًا (أو تم بناؤه أولاً). إذا لم يتم تدميرها أخيرًا ، والمخصصات الأخرى رائعة ، فأنت نعم ستلاحظ التسريبات التي ذكرتها.

أيضا ، ما هي المنصة التي يتم تعريف وظيفة _atexit هذه؟

إن تنفيذ تنظيف Tracker الخاص بالذاكرة هو الحل الأفضل. أسهل طريقة وجدت أن تفعل ذلك هي التحكم بشكل صريح في جميع أمر التهيئة العالمي ذات الصلة. (تخفي بعض المكتبات حالتها العالمية في فصول فاخرة أو غير ذلك ، معتقدًا أنها تتبع نمطًا ، لكن كل ما تفعله هو منع هذا النوع من المرونة.)

مثال main.cpp:

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

حيث يتضمن ملف التخصيص العالمي تعريفات الكائنات و #includes ملفات غير رأس مماثلة. اطلب الكائنات الموجودة في هذا الملف بالترتيب الذي تريده بناء ، وسيتم تدميرهم بالترتيب العكسي. 18.3/8 في C ++ 03 يضمن أن ترتيب التدمير يعكس بناء: "يتم تدمير الكائنات غير المحلية ذات مدة التخزين الثابت في الترتيب العكسي لإنجاز مُنشئها." (هذا القسم يتحدث عنه exit(), ، لكن العودة من الرئيسية هي نفسها ، انظر 3.6.1/5.)

كمكافأة ، أنت مضمون أن يتم تهيئة جميع الكرات (في هذا الملف) قبل الدخول الرئيسي. (شيء غير مضمون في المعيار ، ولكن مسموح به إذا اختارت التطبيقات.)

لقد واجهت هذه المشكلة الدقيقة ، وأيضًا كتابة متتبع الذاكرة.

أشياء قليلة:

جنبا إلى جنب مع الدمار ، تحتاج أيضا إلى التعامل مع البناء. كن مستعدًا لـ 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 ، فربما ستحتاج إلى تحميل الكلى () (أو dlopen ، إلخ) نفسك لرفع عدد المرجع الخاص بك ، بحيث لا تتم إزالتها من الذاكرة قبل الأوان. لأنه على الرغم من أنه لا يزال من الممكن استدعاء فصلك بعد التدمير ، إلا أنه لا يمكن أن يتم تفريغ الكود.
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top