Come posso programmare il codice per eseguire dopo tutto '_atexit ()' funzioni sono completate

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

  •  20-09-2019
  •  | 
  •  

Domanda

Sto scrivendo un sistema di monitoraggio della memoria e l'unico problema che ho effettivamente incontrato è che alla chiusura dell'applicazione, eventuali / classi globali statiche che non assegnava nella loro costruttore, ma sono deallocando nella loro decostruttore sono deallocando dopo la mia roba di monitoraggio della memoria ha riportato i dati allocati come una perdita.

Per quanto posso dire, l'unico modo per me per risolvere correttamente questo sarebbe né forzare il posizionamento di callback _atexit del tracker di memoria a capo della pila (in modo che si chiama ultimo) o far eseguire dopo l'intero stack _atexit è stato svolto. E 'davvero possibile implementare una di queste soluzioni, o c'è un'altra soluzione che ho trascurato.

Modifica: Sto lavorando a / in via di sviluppo per Windows XP e la compilazione con VS2005.

È stato utile?

Soluzione

Ho finalmente capito come fare questo in ambiente Windows / Visual Studio. Guardando attraverso la funzione di avvio CRT di nuovo (in particolare in cui chiede le inizializzatori per globali), ho notato che era semplicemente in esecuzione "puntatori a funzione", che erano contenuti tra alcuni segmenti. Così, con solo un po 'di conoscenza su come funziona il linker, mi si avvicinò con questo:

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

che fornisce in uscita:

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

Questo funziona a causa del modo in cui MS hanno scritto la loro libreria di runtime. In sostanza, essi hanno impostare le seguenti variabili nei segmenti di dati:

(anche se questa informazione è copyright Credo che questo sia fair use in quanto non svalutare l'originale ed è qui solo per riferimento)

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

In inizializzazione, il programma scorre semplicemente da '__xN_a' a '__xN_z' (dove N è {i, c, p, t}) e chiede eventuali puntatori non nulli che trova. Se abbiamo appena inseriamo il nostro segmento tra i segmenti '.CRT $ XNA' e '.CRT $ XnZ' (dove, ancora una volta è n {I, C, P, T}), sarà chiamato insieme a tutto il resto che normalmente viene chiamato.

Il linker unisce semplicemente i segmenti in ordine alfabetico. Questo lo rende estremamente semplice da selezionare quando le nostre funzioni dovrebbero essere chiamati. Se si dispone di uno sguardo in defsects.inc (che si trova sotto $(VS_DIR)\VC\crt\src\) si può vedere che gli Stati membri hanno messo tutte le funzioni di inizializzazione "utente" (vale a dire, quelli che inizializzano globali nel codice) in segmenti che terminano con 'U'. Questo significa che abbiamo solo bisogno di collocare i nostri initializers in un segmento prima di 'U' e saranno chiamati prima di qualsiasi altra inizializzatori.

È necessario essere molto attenti a non utilizzare qualsiasi funzionalità che non viene inizializzato solo dopo il posizionamento selezionato dei puntatori a funzione (francamente, vi consiglio di usare solo .CRT$XCT in questo modo il suo unico codice che non è stato inizializzato . non sono sicuro di cosa accadrà se hai collegato con il codice standard 'C', potrebbe essere necessario metterlo nel blocco .CRT$XIT in quel caso).

Una cosa che ha scoperto è che le "pre-terminazioni" e "terminatori" non vengono effettivamente memorizzati nel file eseguibile se ci si collega contro le versioni DLL della libreria di runtime. A causa di questo, non si può davvero usarli come una soluzione generale. Al contrario, il modo in cui ho fatto funzionare la mia funzione specifica come ultima funzione "utente" è stata di chiamare semplicemente atexit() entro i 'inizializzatori C', in questo modo, nessun altra funzione potrebbe essere stato aggiunto alla pila (che si chiamerà in ordine inverso a cui si aggiungono le funzioni ed è il modo globale / deconstructors statica sono tutti chiamati).

Un solo finale (ovvio) nota, questo è scritto con libreria di runtime di Microsoft in mente. Si può lavorare simile su altre piattaforme / compilatori (si spera che sarete in grado di cavarsela con solo cambiare i nomi dei segmenti a tutto ciò che usano, se usano lo stesso schema), ma non contate su di esso.

Altri suggerimenti

atexit viene elaborato dal / C ++ runtime C (CRT). Si corre dietro main () è già tornato. Probabilmente il modo migliore per farlo è quello di sostituire il tubo catodico di serie con il proprio.

In Windows tlibc è probabilmente un ottimo posto per iniziare: http: // www. codeproject.com/KB/library/tlibc.aspx

Guardate il codice di esempio per mainCRTStartup e basta eseguire il codice dopo la chiamata a _doexit ();  ma prima ExitProcess.

In alternativa, si può solo ottenere una notifica quando viene chiamato ExitProcess. Quando viene chiamato ExitProcess si verifica quanto segue (in base al http: //msdn.microsoft.com/en-us/library/ms682658%28VS.85%29.aspx ):

  1. Tutti i thread nel processo, se il thread chiamante, terminare la loro esecuzione senza ricevere una notifica DLL_THREAD_DETACH.
  2. Gli stati di tutti i thread terminati nel passaggio 1 diventare segnalato.
  3. Le funzioni di punto di ingresso di tutte le librerie a collegamento dinamico (DLL) caricati vengono richiamati con DLL_PROCESS_DETACH.
  4. Dopo che tutte le DLL collegate hanno eseguito qualsiasi codice terminazione del processo, la funzione ExitProcess termina il processo corrente, compreso il thread chiamante.
  5. Lo stato del thread chiamante viene segnalato.
  6. Tutti dell'oggetto handle aperti dal processo sono chiusi.
  7. Lo stato di terminazione dei cambiamenti di processo da STILL_ACTIVE al valore di uscita del processo.
  8. Lo stato del processo oggetto diventa segnalato, soddisfacendo eventuali fili che avevano atteso per il terminazione del processo.

Quindi, un metodo sarebbe quello di creare una DLL e che hanno DLL connettersi al processo. Sarà per sapere quando è stato elaborato le uscite di processo, che dovrebbe essere dopo atexit.

Ovviamente, questo è tutto piuttosto hacker, procedere con cautela.

Questa è dipendente dalla piattaforma di sviluppo. Ad esempio, Borland C ++ ha un #pragma che potrebbe essere usato esattamente a questo. (Da Borland C ++ 5.0, c. 1995)

#pragma startup function-name [priority]
#pragma exit    function-name [priority]
Queste due pragma consentono al programma di specificare funzione (s) che deve essere chiamato sia all'avvio del programma (prima che la funzione principale si chiama), o l'uscita programma (poco prima che il programma termina per _exit). La funzione-nome specificato deve essere una funzione in precedenza dichiarate:
void function-name(void);
La priorità opzionale dovrebbe essere all'intervallo 64 e 255, con la massima priorità a 0; predefinito è 100. Le funzioni di priorità più alta sono chiamati prima all'avvio e durano all'uscita. Le priorità da 0 a 63 vengono utilizzati dalle librerie C, e non dovrebbero essere utilizzati dall'utente.

Forse il compilatore C ha una struttura simile?

ho letto più volte non è possibile garantire l'ordine costruzione di variabili globali ( cite ). Mi piacerebbe pensare che sia abbastanza sicuro per dedurre che l'ordine di esecuzione distruttore non è garantita.

Quindi se l'oggetto di monitoraggio della memoria è globale, sarà quasi certamente incapace alcuna garanzia che l'oggetto inseguitore memoria otterrà distrutte della ultima (o costruiti prima). Se non è distrutto scorso, e gli altri accantonamenti sono eccezionali, allora sì si noterà le perdite di cui parli.

Inoltre, quale piattaforma è questa funzione _atexit definita per?

Avere la pulizia del tracker di memoria eseguito ultima è la soluzione migliore. Il modo più semplice che ho trovato per farlo è quello di controllare in modo esplicito ogni ordine di inizializzazione delle variabili globali rilevanti. (Alcune librerie nascondono il loro stato globale nelle classi di fantasia o in altro modo, pensando che stanno seguendo un modello, ma tutto quello che fanno è impedire questo tipo di flessibilità.)

Esempio main.cpp:

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

Se il file globale inizializzazione comprende definizioni di oggetti e simili # include file non-header. Ordinare gli oggetti in questo file nell'ordine in cui si desidera vengano costruiti, e saranno distrutti in ordine inverso. 18.3 / 8 in C ++ 03 garantisce che l'ordine distruzione rispecchia costruzione: "oggetti non locali con durata di conservazione statica vengono distrutti in ordine inverso del completamento della loro costruzione." (Quella sezione sta parlando exit(), ma un ritorno dalla principale è lo stesso, vedi 3.6.1 / 5).

Come bonus, avrete la garanzia che tutte le variabili globali (in quel file) vengono inizializzate prima di entrare principale. (Qualcosa non garantita dallo standard, ma ha permesso se le implementazioni scelgono.)

Ho avuto questo problema esatto, anche scrivendo un tracker di memoria.

Un paio di cose:

Con la distruzione, è inoltre necessario per gestire la costruzione. Essere preparati per malloc / new per essere chiamato prima il vostro inseguitore di memoria è costruito (ammesso che è scritto come una classe). Quindi è necessario la classe per sapere se è stato costruito o ancora distrutto!

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

In ogni allocazione che chiama nel vostro inseguitore, costruirlo!

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

Strano, ma vero. In ogni caso, sulla distruzione:

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

Quindi, la distruzione, in uscita i risultati. Eppure sappiamo che ci saranno più chiamate. Cosa fare? Beh, ...

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

E infine:

  • essere cauti con filettatura
  • fare attenzione a non chiamare malloc / libero / new / delete all'interno del vostro inseguitore, o essere in grado di rilevare la ricorsione, ecc: -)

EDIT:

  • e ho dimenticato, se mettete il vostro inseguitore in una DLL, si avrà probabilmente bisogno di LoadLibrary () (o dlopen, ecc) voi per fino il valore di riferimento, in modo che non si ottenere rimosso dalla memoria prematuramente. Perché anche se la vostra classe può ancora essere chiamato dopo la distruzione, non può se il codice è stato scaricato.
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top