Переносная трассировка стека C ++ при исключении

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

  •  03-07-2019
  •  | 
  •  

Вопрос

Я пишу библиотеку, которую хотел бы сделать переносимой.Таким образом, это не должно зависеть от glibc, расширений Microsoft или чего-либо еще, чего нет в стандарте.У меня есть хорошая иерархия классов, производных от std::exception, которую я использую для обработки ошибок в логике и вводе.Знание того, что определенный тип исключения был сгенерирован для определенного файла и номера строки, полезно, но знание того, как туда попало выполнение, потенциально было бы гораздо более ценным, поэтому я рассматривал способы получения трассировки стека.

Я знаю, что эти данные доступны при сборке на основе glibc с использованием функций в execinfo.h (см. вопрос 76822) и через интерфейс StackWalk в реализации Microsoft на C++ (см. вопрос 126450), но я бы очень хотел избегать всего, что не является переносимым.

Я подумывал о том, чтобы самому реализовать эту функциональность в такой форме:

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

Неужели это ужасная идея?Это означало бы много работы по добавлению блоков try / catch к каждой функции, но я могу с этим смириться.Это не сработало бы, если причиной исключения является повреждение памяти или нехватка памяти, но в этот момент вы все равно в значительной степени облажались.Это может привести к вводящей в заблуждение информации, если некоторые функции в стеке не перехватывают исключения, не добавляют себя в список и не отбрасывают заново, но я могу, по крайней мере, предоставить гарантию, что все мои библиотечные функции делают это.В отличие от "реальной" трассировки стека, я не получу номер строки при вызове функций, но, по крайней мере, у меня было бы что-то.

Моя главная проблема заключается в возможности того, что это вызовет замедление, даже если на самом деле никаких исключений не генерируется.Требуют ли все эти блоки try / catch дополнительной настройки и демонтажа при каждом вызове функции или каким-то образом обрабатываются во время компиляции?Или есть другие вопросы, которые я не рассматривал?

Это было полезно?

Решение

Я думаю, что это действительно плохая идея.

Переносимость - очень достойная цель, но не тогда, когда она приводит к навязчивому решению, снижающему производительность и некачественной реализации.

Каждая платформа (Windows / Linux / PS2 / iPhone / etc) , над которой я работал, предлагала способ обхода стека при возникновении исключения и сопоставления адресов с именами функций.Да, ни один из них не является переносимым, но платформа создания отчетов может быть переносимой, и обычно требуется менее одного-двух дней, чтобы написать версию stack walking code для конкретной платформы.

Это не только занимает меньше времени, чем потребовалось бы на создание / поддержку кроссплатформенного решения, но и дает намного лучшие результаты;

  • Нет необходимости изменять функции
  • Ловушки сбоев в стандартных или сторонних библиотеках
  • Нет необходимости в try / catch в каждой функции (медленной и требующей много памяти)

Другие советы

Просмотрите вложенный диагностический контекст один раз. Вот маленький намек:

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

Затраты на реализацию NDC. Я играл с лениво оцененной версией, а также с версией, в которой оставалось только фиксированное количество записей. Ключевым моментом является то, что если вы используете конструкторы и деструкторы для обработки стека, так что вам не нужны все эти грязные блоки try / catch и явные манипуляции везде.

Единственная специфическая для платформы головная боль - это метод getContextForCurrentThread () . В большинстве, если не во всех случаях, вы можете использовать реализацию, специфичную для платформы, с использованием локального хранилища потоков, чтобы справиться с заданием.

Если вы в большей степени ориентированы на производительность и живете в мире файлов журналов, измените область, чтобы указатель содержал указатель на имя файла и номер строки, и вообще не указывайте элемент 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;
};

Это даст вам хорошую трассировку стека в вашем журнале при возникновении исключения Нет необходимости в реальном стэке, просто небольшое сообщение в журнале, когда выдается исключение;)

Я не думаю, что есть "независимый от платформы" способ сделать это - в конце концов, если бы это было так, не было бы необходимости в StackWalk или специальных функциях трассировки стека gcc, о которых вы упомянули.

Это было бы немного беспорядочно, но я бы реализовал это, создав класс, который предлагает согласованный интерфейс для доступа к трассировке стека, а затем включил в реализацию #ifdefs, использующие соответствующие методы, специфичные для платформы. на самом деле положить трассировку стека.

Таким образом, использование вами класса не зависит от платформы, и нужно изменить только этот класс, если вы хотите использовать другую платформу.

В отладчике:

Чтобы получить трассировку стека, из которой выбрасывается исключение, я просто указываю точку останова в конструкторе std :: exception.

Таким образом, когда создается исключение, отладчик останавливается, и вы можете увидеть трассировку стека в этой точке. Не идеально, но это работает большую часть времени.

Управление стеками - одна из тех простых вещей, которые очень быстро усложняются. Лучше оставить его для специализированных библиотек. Вы пробовали libunwind? Прекрасно работает и AFAIK это портативно, хотя я никогда не пробовал это на Windows.

Это будет медленнее, но похоже, что оно должно работать.

Из того, что я понимаю, проблема создания быстрой, переносимой трассировки стека заключается в том, что реализация стека зависит как от операционной системы, так и от процессора, поэтому она неявно связана с конкретной платформой. Альтернативой может быть использование функций MS / glibc и использование #ifdef и соответствующих определений препроцессора (например, _WIN32) для реализации решений, специфичных для платформы, в разных сборках.

Поскольку использование стека сильно зависит от платформы и реализации, нет способа сделать это напрямую, что является полностью переносимым. Однако вы можете создать переносимый интерфейс для конкретной реализации платформы и компилятора, максимально локализуя проблемы. ИМХО, это был бы ваш лучший подход.

Затем реализация трассировщика будет ссылаться на любые доступные вспомогательные библиотеки платформы. Тогда он будет работать только тогда, когда возникает исключение, и даже тогда, только если вы вызываете его из блока catch. Его минимальный API просто возвращает строку, содержащую весь след.

Требование кодировщика ввести обработку перехвата и повторного выброса в цепочке вызовов имеет значительные затраты времени выполнения на некоторых платформах и влечет за собой большие затраты на техническое обслуживание в будущем.

Тем не менее, если вы решите использовать механизм catch / throw, не забывайте, что даже в C ++ все еще есть доступный препроцессор C и что макросы __ FILE __ и __ LINE __ определены. Вы можете использовать их для включения имени исходного файла и номера строки в информацию о трассировке.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top