Переносная трассировка стека C ++ при исключении
-
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 __ код> определены. Вы можете использовать их для включения имени исходного файла и номера строки в информацию о трассировке. Р>