Pregunta

Estoy escribiendo una biblioteca que me gustaría ser portátil. Por lo tanto, no debe depender de las extensiones de glibc o Microsoft o cualquier otra cosa que no esté en el estándar. Tengo una buena jerarquía de clases derivada de std :: excepción que utilizo para manejar errores en lógica y entrada. Saber que se lanzó un tipo particular de excepción a un archivo en particular y el número de línea es útil, pero saber cómo se realizó la ejecución sería potencialmente mucho más valioso, por lo que he estado buscando formas de adquirir el seguimiento de la pila.

Soy consciente de que esta información está disponible cuando se compila contra glibc usando las funciones en execinfo.h (vea question 76822 ) y a través de la interfaz StackWalk en la implementación de C ++ de Microsoft (consulte question 126450 ), pero me gustaría mucho evitar cualquier cosa que no sea portátil.

Estaba pensando en implementar esta funcionalidad yo mismo de esta forma:

class myException : public std::exception
{
public:
  ...
  void AddCall( std::string s )
  { m_vCallStack.push_back( s ); }
  std::string ToStr() const
  {
    std::string l_sRet = "";
    ...
    l_sRet += "Call stack:\n";
    for( int i = 0; i < m_vCallStack.size(); i++ )
      l_sRet += "  " + m_vCallStack[i] + "\n";
    ...
    return l_sRet;
  }
private:
  ...
  std::vector< std::string > m_vCallStack;
};

ret_type some_function( param_1, param_2, param_3 )
{
  try
  {
    ...
  }
  catch( myException e )
  {
    e.AddCall( "some_function( " + param_1 + ", " + param_2 + ", " + param_3 + " )" );
    throw e;
  }
}

int main( int argc, char * argv[] )
{
  try
  {
    ...
  }
  catch ( myException e )
  {
    std::cerr << "Caught exception: \n" << e.ToStr();
    return 1;
  }
  return 0;
}

¿Es esta una idea terrible? Significaría mucho trabajo agregar bloques try / catch a cada función, pero puedo vivir con eso. No funcionaría cuando la causa de la excepción es la corrupción de la memoria o la falta de memoria, pero en ese punto, de todos modos, estás bastante jodido. Puede proporcionar información engañosa si algunas funciones de la pila no detectan excepciones, se agregan a la lista y se vuelven a generar, pero al menos puedo proporcionar una garantía de que todas las funciones de mi biblioteca lo hacen. A diferencia de un " real " rastreo de pila No obtendré el número de línea en las funciones de llamada, pero al menos tendría algo.

Mi principal preocupación es la posibilidad de que esto provoque una ralentización incluso cuando no se produzcan excepciones. ¿Todos estos bloques try / catch requieren una configuración y un desmontaje adicionales en cada invocación de función, o se manejan de alguna manera en tiempo de compilación? ¿O hay otros temas que no he considerado?

¿Fue útil?

Solución

Creo que esta es una muy mala idea.

La portabilidad es un objetivo muy valioso, pero no cuando se traduce en una solución intrusiva, que agota el rendimiento y una implementación inferior.

Cada plataforma (Windows / Linux / PS2 / iPhone / etc) en la que he trabajado ha ofrecido una manera de recorrer la pila cuando ocurre una excepción y hacer coincidir las direcciones con los nombres de las funciones. Sí, ninguno de estos dispositivos es portátil, pero el marco de informes puede serlo y, por lo general, se necesita menos de uno o dos días para escribir una versión específica de la plataforma de código para caminar de la pila.

No solo es menos tiempo del necesario para crear / mantener una solución multiplataforma, sino que los resultados son mucho mejores;

  • No es necesario modificar las funciones
  • Las trampas se bloquean en bibliotecas estándar o de terceros
  • No hay necesidad de probar / capturar en todas las funciones (lentas y con gran cantidad de memoria)

Otros consejos

Busque Contexto de diagnóstico anidado una vez. Aquí hay una pequeña pista:

class NDC {
public:
    static NDC* getContextForCurrentThread();
    int addEntry(char const* file, unsigned lineNo);
    void removeEntry(int key);
    void dump(std::ostream& os);
    void clear();
};

class Scope {
public:
    Scope(char const *file, unsigned lineNo) {
       NDC *ctx = NDC::getContextForCurrentThread();
       myKey = ctx->addEntry(file,lineNo);
    }
    ~Scope() {
       if (!std::uncaught_exception()) {
           NDC *ctx = NDC::getContextForCurrentThread();
           ctx->removeEntry(myKey);
       }
    }
private:
    int myKey;
};
#define DECLARE_NDC() Scope s__(__FILE__,__LINE__)

void f() {
    DECLARE_NDC(); // always declare the scope
    // only use try/catch when you want to handle an exception
    // and dump the stack
    try {
       // do stuff in here
    } catch (...) {
       NDC* ctx = NDC::getContextForCurrentThread();
       ctx->dump(std::cerr);
       ctx->clear();
    }
}

La sobrecarga está en la implementación del NDC. Estaba jugando con una versión perezosamente evaluada, así como con una que solo mantenía un número fijo de entradas también. El punto clave es que si usas constructores y destructores para manejar la pila de modo que no necesites todos esos desagradables try / catch y la manipulación explícita en todas partes.

El único dolor de cabeza específico de la plataforma es el método getContextForCurrentThread () . Puede usar una implementación específica de la plataforma utilizando el almacenamiento local de subprocesos para manejar el trabajo en la mayoría de los casos, si no en todos.

Si está más orientado al rendimiento y vive en el mundo de los archivos de registro, cambie el alcance para mantener un puntero al nombre del archivo y el número de línea y omita la NDC:

class Scope {
public:
    Scope(char const* f, unsigned l): fileName(f), lineNo(l) {}
    ~Scope() {
        if (std::uncaught_exception()) {
            log_error("%s(%u): stack unwind due to exception\n",
                      fileName, lineNo);
        }
    }
private:
    char const* fileName;
    unsigned lineNo;
};

Esto le dará un buen seguimiento de pila en su archivo de registro cuando se lance una excepción. No hay necesidad de caminar en la pila real, solo un pequeño mensaje de registro cuando se lanza una excepción;)

No creo que haya una " plataforma independiente " forma de hacer esto: después de todo, si existiera, no habría necesidad de StackWalk o de las funciones especiales de rastreo de pila gcc que mencionas.

Sería un poco desordenado, pero la forma en que lo implementaría sería crear una clase que ofrezca una interfaz consistente para acceder al seguimiento de la pila, luego tener #ifdefs en la implementación que use los métodos específicos de la plataforma apropiados para en realidad poner el seguimiento de la pila juntos.

De esa manera, su uso de la clase es independiente de la plataforma, y ??solo esa clase tendría que ser modificada si quisiera apuntar a otra plataforma.

En el depurador:

Para obtener el seguimiento de la pila de donde se produce una excepción, simplemente puse el punto de ruptura en std :: exception constructor.

Por lo tanto, cuando se crea la excepción, el depurador se detiene y, a continuación, puede ver el seguimiento de la pila en ese punto. No es perfecto, pero funciona la mayor parte del tiempo.

La gestión de la pila es una de esas cosas simples que se complican muy rápidamente. Mejor dejarlo para bibliotecas especializadas. ¿Has probado libunwind? Funciona muy bien y AFAIK es portátil, aunque nunca lo he probado en Windows.

Esto será más lento pero parece que debería funcionar.

Por lo que entiendo, el problema al hacer un seguimiento rápido y portátil de la pila es que la implementación de la pila es tanto específica del sistema operativo como de la CPU, por lo que es implícitamente un problema específico de la plataforma. Una alternativa sería usar las funciones MS / glibc y usar #ifdef y las definiciones de preprocesador apropiadas (por ejemplo, _WIN32) para implementar las soluciones específicas de la plataforma en diferentes versiones.

Dado que el uso de la pila es altamente dependiente de la plataforma y la implementación, no hay forma de hacerlo directamente que sea completamente portátil. Sin embargo, podría crear una interfaz portátil para una plataforma y una implementación específica del compilador, ubicando los problemas tanto como sea posible. En mi humilde opinión, este sería su mejor enfoque.

La implementación del trazador se vincularía a las bibliotecas auxiliares específicas de la plataforma que estén disponibles. Entonces funcionaría solo cuando se produce una excepción, e incluso entonces solo si lo llama desde un bloque catch. Su API mínima simplemente devolverá una cadena que contenga la traza completa.

Requerir que el codificador inyecte el procesamiento de retoque y retoque en la cadena de llamadas tiene costos de tiempo de ejecución significativos en algunas plataformas e impone un gran costo de mantenimiento futuro.

Dicho esto, si elige usar el mecanismo de captura / lanzamiento, no olvide que incluso C ++ todavía tiene el preprocesador de C disponible, y que las macros __FILE__ y __LINE __ están definidos. Puede usarlos para incluir el nombre del archivo de origen y el número de línea en su información de rastreo.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top