Портативная трассировка стека C ++ при исключении - PullRequest
13 голосов
/ 06 марта 2009

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

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

Я думал о реализации этой функции сам в этой форме:

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 требуют дополнительной настройки и завершения при каждом вызове функции, или они каким-то образом обрабатываются во время компиляции? Или есть другие вопросы, которые я не рассматривал?

Ответы [ 7 ]

21 голосов
/ 06 марта 2009

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

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

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

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

  • Нет необходимости изменять функции
  • Сбои ловушек в стандартных или сторонних библиотеках
  • Нет необходимости в попытке / перехвате каждой функции (медленная и интенсивная память)
6 голосов
/ 06 марта 2009

Посмотрите Nested Diagnostic Context один раз. Вот маленький намек:

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

Накладные расходы при реализации НДЦ. Я играл с лениво оцененной версией, а также с версией, в которой оставалось только фиксированное количество записей. Ключевым моментом является то, что если вы используете конструкторы и деструкторы для обработки стека, так что вам не нужны все эти грязные 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;
};

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

2 голосов
/ 06 марта 2009

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

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

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

1 голос
/ 12 декабря 2012

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

1 голос
/ 06 марта 2009

В отладчике:

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

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

0 голосов
/ 06 марта 2009

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

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

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

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

0 голосов
/ 06 марта 2009

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

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

...