Comment puis-je planifier un code à courir après tout '_atexit () fonctions sont complétées

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

  •  20-09-2019
  •  | 
  •  

Question

Je suis en train d'écrire un système de suivi de la mémoire et le seul problème que je l'ai fait courir dans est que lorsque les sorties de l'application, toutes les classes statiques / mondiales qui n'a pas affecté leur constructeur, mais Désallocation dans leur deconstructor sont Désallocation après mon truc de suivi de la mémoire a signalé les données affectées comme une fuite.

Pour autant que je peux dire, la seule façon pour moi de résoudre correctement ce serait soit forcer le placement de rappel _atexit du suivi de mémoire à la tête de la pile (de sorte qu'il est appelé en dernier) ou avoir exécuter après l'ensemble de la pile de _atexit a été déroulée. Est-il réellement possible de mettre en œuvre l'une de ces solutions, ou est-il une autre solution que j'ai oublié.

Edit: Je travaille sur / développement pour Windows XP et la compilation avec VS2005.

Était-ce utile?

La solution

Je l'ai enfin compris comment faire sous Windows / Visual Studio. En regardant à travers la fonction de démarrage crt à nouveau (en particulier où il appelle les initialisations pour globals), j'ai remarqué qu'il était tout simplement en cours d'exécution « pointeurs de fonction » qui ont été comprises entre certains segments. Donc, avec un peu de connaissances sur la façon dont fonctionne l'éditeur de liens, je suis venu avec ceci:

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

sorties qui:

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

Cela fonctionne en raison de la façon dont les États membres ont écrit leur bibliothèque d'exécution. En gros, ils ont configurer les variables suivantes dans les segments de données:

(même si cette information est le droit d'auteur, je crois que c'est l'utilisation équitable car il n'a pas dévaluer l'original et n'est ici pour référence)

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

Lors de l'initialisation, le programme simplement itère de '__xN_a' à '__xN_z' (où N est {i, c, p, t}) et appelle tous les pointeurs non nuls qu'il trouve. Si nous suffit d'insérer notre propre segment entre les segments de .CRT $ XNA 'et « .CRT $ XnZ » (où, encore une fois n est {I, C, P, T}), il sera appelé avec tout le reste qui obtient normalement appelé.

L'éditeur de liens rejoint simplement les segments dans l'ordre alphabétique. Cela rend extrêmement simple pour sélectionner lorsque nos fonctions doivent être appelées. Si vous avez un coup d'oeil dans defsects.inc (qui se trouve sous $(VS_DIR)\VC\crt\src\) vous pouvez voir que les États membres ont placé toutes les fonctions d'initialisation « utilisateur » (qui est, ceux qui initialisent GLOBALS dans votre code) dans les segments se terminant par « U ». Cela signifie que nous avons juste besoin de placer nos initializers dans un segment plus tôt que « U » et ils seront appelés avant toute autre initializers.

Vous devez être très prudent de ne pas utiliser toutes les fonctionnalités qui ne sont pas initialisé qu'après votre placement sélectionné des pointeurs de fonction (franchement, je vous recommande d'utiliser simplement .CRT$XCT cette façon son seul code qui n'a pas été initialisé . Je ne sais pas ce qui se passera si vous avez lié avec le code standard « C », vous devrez peut-être placer dans le bloc .CRT$XIT dans ce cas).

Une chose que j'ai découvert était que les « pré-terminateurs » et « terminateurs » ne sont pas réellement stockées dans le fichier exécutable si vous liez contre les versions de DLL de la bibliothèque d'exécution. En raison de cela, vous ne pouvez pas vraiment les utiliser comme une solution générale. Au lieu de cela, la façon dont je l'ai fait exécuter ma fonction spécifique comme la dernière fonction « utilisateur » a été d'appeler simplement atexit() dans les « C initializers », de cette façon, aucune autre fonction aurait pu être ajouté à la pile (qui sera appelé dans la l'ordre inverse auquel les fonctions sont ajoutées et comment déconstructeurs global / statique sont tous appelés).

Juste un dernier (évident) la note, ceci est écrit avec la bibliothèque d'exécution de Microsoft à l'esprit. Il peut fonctionner même sur d'autres plates-formes / compilateurs (nous espérons que vous serez en mesure de sortir avec changeant seulement les noms de segment à ce qu'ils utilisent, si elles utilisent le même schéma), mais ne comptez pas sur elle.

Autres conseils

atexit est traitée par le moteur d'exécution C / C ++ (CRT). Il court après principal () a déjà retourné. Probablement la meilleure façon de le faire est de remplacer le tube cathodique standard avec votre propre.

Sous Windows tlibc est probablement un excellent endroit pour commencer: http: // www. codeproject.com/KB/library/tlibc.aspx

Regardez l'exemple de code pour mainCRTStartup et il suffit d'exécuter votre code après l'appel à _doexit ();  mais avant ExitProcess.

Vous pourriez être averti quand ExitProcess est appelé. Lorsque ExitProcess est appelée ci-après se produit (selon http: //msdn.microsoft.com/en-us/library/ms682658%28VS.85%29.aspx ):

  1. Tous les threads du processus, à l'exception du thread appelant, mettre fin à leur exécution sans recevoir une notification DLL_THREAD_DETACH.
  2. Les états de tous les fils terminés dans l'étape 1 devenir signalé.
  3. Les fonctions point d'entrée de toutes les bibliothèques de liens dynamiques (DLL chargées) sont appelés avec DLL_PROCESS_DETACH.
  4. Une fois que toutes les DLL joints ont exécuté un code de terminaison de processus, la fonction de ExitProcess met fin au processus en cours, y compris le thread appelant.
  5. L'état du thread appelant devient signe.
  6. Toutes les poignées de l'objet ouvert par le processus sont fermés.
  7. L'état de cessation de l'évolution du processus de STILL_ACTIVE à la valeur de sortie du processus.
  8. L'état de l'objet de processus devient signalé, satisfaisant les fils qui ont été en attente pour l'arrêt du processus.

Ainsi, une méthode serait de créer une DLL et ont cette DLL attacher au processus. Il sera averti lorsque le processus se termine, ce qui devrait être après atexit a été traité.

De toute évidence, cela est plutôt hackish, agir avec prudence.

Cela dépend de la plate-forme de développement. Par exemple, Borland C ++ a un #pragma qui pourrait être utilisé pour exactement cela. (De Borland C ++ 5.0, c. 1995)

#pragma startup function-name [priority]
#pragma exit    function-name [priority]
Ces deux pragmas permettent au programme de préciser la fonction (s) qui doit être appelé soit au démarrage du programme (avant la principale fonction est appelée), ou la sortie du programme (juste avant que le programme se termine par _exit). La fonction nom spécifié doit être une fonction précédemment déclarée comme:
void function-name(void);
La priorité en option doit être comprise entre 64 et 255, avec la plus haute priorité à 0; par défaut est 100. Fonctions avec des priorités plus élevées sont appelées première au démarrage et à la sortie durer. Les priorités de 0 à 63 sont utilisés par les bibliothèques C et ne doivent pas être utilisés par l'utilisateur.

Peut-être votre compilateur C a une structure similaire?

J'ai lu plusieurs fois, vous ne pouvez pas garantir l'ordre de construction des variables globales ( cite ). Je pense qu'il est assez sûr de déduire que l'ordre d'exécution de destructor est pas non plus garantie.

Par conséquent, si votre objet de suivi de la mémoire est globale, vous serez certainement pas toutes les garanties que votre objet de suivi de mémoire se DESTRUCTED dernier (ou construit en premier). Si ce n'est pas destructed dernière, et d'autres allocations sont en circulation, alors oui, il remarquera les fuites que vous mentionnez.

En outre, quelle plate-forme est cette fonction _atexit définie pour?

Avoir le nettoyage du suivi de mémoire exécuté dernier est la meilleure solution. La meilleure façon que je l'ai trouvé à faire est de contrôler explicitement tout l'ordre d'initialisation variables globales pertinentes. (Certaines bibliothèques cachent leur état global dans les classes de fantaisie ou autrement, en pensant qu'ils suivent un modèle, mais tout ce qu'ils font est d'éviter ce genre de flexibilité.)

Exemple main.cpp:

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

Lorsque le fichier global-initialisation comprend les définitions d'objet et #Includes similaires fichiers non-tête. Commandez les objets dans ce fichier dans l'ordre que vous voulez les construire, et ils seront détruits dans l'ordre inverse. 18,3 / 8 à 03 garanties C de l'ordre de destruction miroirs construction: « objets non locales avec une durée de stockage statique sont détruites dans l'ordre inverse de l'achèvement de leur constructeur. » (Cette section parle exit(), mais un retour de la principale est le même, voir 3.6.1 / 5).

En prime, vous êtes assuré que tous les globals (dans ce fichier) sont initialisés avant d'entrer principale. (Quelque chose pas garanti dans la norme, mais autorisée si les implémentations choisir.)

J'ai eu ce problème exact, écrit aussi un suivi de mémoire.

Quelques choses:

Avec la destruction, vous devez également gérer la construction. Préparez-vous à malloc / nouveau pour être appelé avant votre suivi de mémoire est construit (en supposant qu'il est écrit en tant que classe). Vous avez donc besoin de votre classe pour savoir si elle a été construite ou encore destructed!

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

Sur chaque allocation qui appelle dans votre tracker, construire!

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

Etrange, mais vrai. Quoi qu'il en soit, sur la destruction:

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

Ainsi, sur la destruction, la production de vos résultats. Pourtant, nous savons qu'il y aura plus d'appels. Que faire? Eh bien, ...

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

Et enfin:

  • être prudent avec filetage
  • attention à ne pas appeler malloc / gratuit / nouveau / supprimer l'intérieur de votre tracker, ou être en mesure de détecter la récursivité, etc: -)

EDIT:

  • et j'ai oublié, si vous mettez votre suivi dans une DLL, vous aurez probablement besoin de LoadLibrary () (ou dlopen, etc) vous pour votre compte de référence, de sorte que vous ne faites pas supprimeront de la mémoire prématurément. Parce que même si votre classe peut encore être appelé après la destruction, il ne peut pas si le code a été déchargé.
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top