¿Cómo puedo programar un código para funcionar después de todo '_atexit ()' funciones se completan

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

  •  20-09-2019
  •  | 
  •  

Pregunta

Estoy escribiendo un sistema de seguimiento de la memoria y el único problema en realidad me he encontrado es que cuando se cierra la aplicación, las clases estáticas / globales que no se asignan en su constructor, pero se desasignar en su Deconstructor se desasignando después de mi materia de seguimiento de la memoria se ha informado de los datos asignados como una fuga.

Por lo que yo puedo decir, la única manera para mí para resolver adecuadamente esta sería la de cualquiera forzar la colocación de devolución de llamada _atexit del rastreador de memoria en la cabecera de la pila (de modo que se llama pasado) o tiene que ejecutar después de toda la pila _atexit ha sido desenrollada. ¿Es realmente posible llevar a cabo cualquiera de estas soluciones, o hay otra solución que he pasado por alto.

Editar: Estoy trabajando en / desarrollo para Windows XP y compilar con VS2005.

¿Fue útil?

Solución

fin he descubierto la manera de hacer esto en Windows / Visual Studio. Mirando a través de la función de inicio CRT de nuevo (en concreto en el que llama a los inicializadores de variables globales), me di cuenta de que se trataba simplemente ejecutando "punteros de función" que figuraban entre ciertos segmentos. Así que con un poco de conocimiento sobre cómo funciona el enlazador, se me ocurrió esto:

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

que emite:

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

Esto funciona debido a la forma de MS han escrito su biblioteca de tiempo de ejecución. Básicamente, he configurar las siguientes variables en los segmentos de datos:

(aunque esta información es el derecho de autor Creo que esto es el uso justo ya que no devalúa el original y sólo está aquí por referencia)

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

En la inicialización, el programa simplemente itera de '__xN_a' a '__xN_z' (donde N es {i, c, p, t}) y llama a los punteros no nulos que encuentra. Si sólo insertamos nuestro propio segmento entre los segmentos '.CRT $ XnA' y '.CRT $ XnZ' (donde, una vez más, n es {I, C, P, T}), que se llamará junto con todo lo demás que normalmente se vuelve a llamar.

El enlazador simplemente une los segmentos en orden alfabético. Esto hace que sea extremadamente simple para seleccionar cuando nuestras funciones deben ser llamados. Si usted tiene una mirada en defsects.inc (que se encuentra bajo $(VS_DIR)\VC\crt\src\) se puede ver que la EM se han colocado todas las funciones de inicialización de "usuario" (es decir, los que inicializan variables globales en su código) en segmentos que terminan en 'U'. Esto significa que sólo tenemos que poner nuestras inicializadores en un segmento antes de 'U' y ellos serán llamados antes que cualquier otro inicializadores.

Usted debe tener mucho cuidado de no utilizar cualquier funcionalidad que no se ha inicializado hasta después de su colocación seleccionada de los punteros de función (francamente, me gustaría recomendar que acaba de utilizar .CRT$XCT de esa manera su único código sobre la que no se ha inicializado . no estoy seguro de lo que sucederá si se ha enlazado con el código estándar 'C', puede que tenga que colocarlo en el bloque .CRT$XIT en ese caso).

Una cosa que sí descubrieron fue que los "pre-terminadores" y "terminadores" no se almacenan realmente en el ejecutable si enlaza con las versiones de DLL de la biblioteca de tiempo de ejecución. Debido a esto, realmente no se puede utilizar como una solución general. En cambio, la forma en que lo hizo correr mi función específica como la última función de "usuario" se llamar simplemente atexit() dentro de los 'C inicializadores', de esta manera, ninguna otra función podría haber sido añadido a la pila (que se llama en el el orden inverso al que se añaden funciones y es la forma mundial / deconstructores estática están llamados).

Sólo una final (obvio) nota, esto está escrito con la biblioteca de tiempo de ejecución de Microsoft en mente. Es posible que funcione similar en otros plataformas / compiladores (esperemos que será capaz de salirse con sólo cambiar los nombres de los segmentos a lo que utilizan, si utilizan el mismo esquema), pero no cuentes con ello.

Otros consejos

atexit es procesada por el / C ++ de tiempo de ejecución C (CRT). Se ejecuta después de main () ya ha regresado. Probablemente la mejor manera de hacerlo es reemplazar el CRT estándar con el suyo propio.

En Windows tlibc es probablemente un gran lugar para comenzar: http: // www. codeproject.com/KB/library/tlibc.aspx

Mira el ejemplo de código para mainCRTStartup y simplemente ejecutar el código después de la llamada a _doexit ();  pero antes de ExitProcess.

Alternativamente, usted podría recibir una notificación cuando se llama a ExitProcess. Cuando se llama a ExitProcess ocurre lo siguiente (según http: //msdn.microsoft.com/en-us/library/ms682658%28VS.85%29.aspx ):

  1. Todos los subprocesos del proceso, excepto el subproceso de llamada, terminar su ejecución sin recibir una notificación DLL_THREAD_DETACH.
  2. Los estados de todos los hilos terminados en el paso 1 convertido en una señal.
  3. Las funciones de punto de entrada de todas las bibliotecas de vínculos dinámicos (DLL cargadas) se denominan con DLL_PROCESS_DETACH.
  4. Después de todas las DLL adjuntos han ejecutado cualquier código de terminación del proceso, la función ExitProcess termina el proceso actual, incluyendo el subproceso de llamada.
  5. El estado del subproceso de llamada se convierte en una señal.
  6. Todos los identificadores de objetos abierto por el proceso están cerrados.
  7. El estado de terminación del proceso de cambios de STILL_ACTIVE al valor de salida del proceso.
  8. El estado del objeto se convierte en una señal proceso, satisfaciendo todos los hilos que habían estado esperando la terminación del proceso.

Por lo tanto, un método sería crear un archivo DLL y tener esa DLL asociar al proceso. Se recibirá una notificación cuando el proceso finalice, que deben ser después atexit ha sido procesado.

Obviamente, todo esto es más bien hacker, proceder con cuidado.

Este es dependiente de la plataforma de desarrollo. Por ejemplo, Borland C ++ tiene una #pragma que podría ser utilizado para exactamente esto. (De Borland C ++ 5,0, c. 1995)

#pragma startup function-name [priority]
#pragma exit    function-name [priority]
Estos dos pragmas permiten al programa de función (s) que debe ser llamado ya sea al inicio del programa (antes de la función principal se llama), o la salida del programa (justo antes de que el programa termina a través _exit) especifica. La función nombre especificado debe ser una función previamente declarados como:
void function-name(void);
La prioridad opcional debe estar en el rango de 64 a 255, con la más alta prioridad en 0; por defecto es 100. Funciones de mayor prioridad son llamados por primera vez en el arranque y duran en la salida. Prioridades de 0 a 63 son utilizados por las librerías de C, y no deben ser utilizados por el usuario.

Tal vez su compilador C tiene una instalación similar?

He leído varias veces que no se puede garantizar el orden construcción de variables globales ( citar ). Me parece que es bastante seguro para inferir de esto que el orden de ejecución destructor tampoco está garantizada.

Por lo tanto, si su objeto es la memoria de seguimiento mundial, es casi seguro que será incapaz ninguna garantía de que su objeto de seguimiento de memoria conseguirá destructed última (o primera construida). Si no es destruido por última vez, y otras asignaciones son excepcionales, entonces sí que se dará cuenta de las fugas que mencionas.

Además, ¿qué plataforma se define esta función para _atexit?

Tener la limpieza de la memoria de seguimiento ejecutadas última es la mejor solución. La manera más fácil que he encontrado para hacerlo es controlar de forma explícita todo orden de inicialización de las variables globales pertinentes. (Algunas bibliotecas ocultan su estado global en las clases de fantasía o de otra manera, pensando que están siguiendo un patrón, pero lo único que hacen es evitar que este tipo de flexibilidad.)

Ejemplo main.cpp:

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

Cuando el archivo de inicialización global incluye definiciones de objetos y # incluye archivos que no son cabecera similares. Ordenar los objetos en este archivo en el orden que desea que construyen, y que van a ser destruidos en el orden inverso. 18,3 / 8 en C ++ 03 garantiza que la destrucción espejos para la construcción: "Los objetos no-locales, con una duración de almacenamiento estático son destruidos en el orden inverso al de la finalización de su constructor." (Esa sección está hablando de exit(), sino un retorno de la principal es la misma, véase 3.6.1 / 5).

Como beneficio adicional, tiene la garantía de que todas las variables globales (en ese archivo) se inicializan antes de entrar principal. (Algo no está garantizada en la norma, pero permitió que las implementaciones si lo desean.)

He tenido este problema exacto, escribiendo también un rastreador de memoria.

Un par de cosas:

Además de la destrucción, también es necesario para manejar la construcción. Esté preparado para malloc / nuevo a ser llamada antes de que su control de la memoria se construye (suponiendo que se escribe como una clase). Por lo que necesita su clase para saber si se ha construido o destruido todavía!

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

En cada asignación que pone en su tracker, construirlo!

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

Extraño, pero cierto. De todos modos, en la destrucción:

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

Por lo tanto, la destrucción, la salida de sus resultados. Sin embargo, sabemos que no habrá más llamadas. ¿Qué hacer? Bueno, ...

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

Y por último:

  • tener cuidado con rosca
  • tener cuidado de no llamar a malloc / libre / nuevos / borrar dentro de su perseguidor, o ser capaz de detectar la recursividad, etc: -)

EDIT:

  • y me olvidaba, si usted pone su seguidor en una DLL, es probable que necesite a LoadLibrary () (o dlopen, etc) mismo a su cuenta de referencia, por lo que no lo hace se eliminan de la memoria antes de tiempo. Porque a pesar de su clase se le puede llamar después de la destrucción, no puede si el código se ha descargado.
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top