简单的 C++ 函数调用堆栈跟踪实用程序






3.02/5 (23投票s)
如何记录函数调用堆栈以进行调试
引言
在 UNIX/Linux 平台上,程序崩溃时会生成一个 core 文件,我们可以分析 core 文件来确定程序的问题所在。core 文件记录了函数调用堆栈(例如,在 gdb 中可以使用 bt 命令查看回溯),这对于我们进行故障排除非常有帮助。
当我调试程序时,我尝试将函数调用堆栈记录到程序调试日志中,以帮助我诊断程序缺陷。幸运的是,C++ 提供了一种简单的方法来实现这一点。
原理
只需使用 C++ 构造函数/析构函数的语义,我们就可以创建自己的跟踪类。在函数入口处,我们声明一个局部变量,它会立即调用构造函数,在离开函数时,局部变量会被销毁并调用析构函数。就是这样。我们需要做的就是实现一个类的构造函数和析构函数。这个原理很简单,但有时可以帮助我们很多。
完整的代码如下
// trace.hpp
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <stdarg.h>
#include <string>
#define CM_TRACE_FILE(trace_file) cm::Trace::LogToFile(trace_file)
#define CM_TRACE_FUNC(func_name) cm::Trace __CM_TRACE__(func_name, "()")
#define CM_TRACE_FUNC_ARG1(func_name, argfmt, arg) \
cm::Trace __CM_TRACE__(func_name, argfmt, arg)
#define CM_TRACE_FUNC_ARG2(func_name, argfmt, arg1, arg2) \
cm::Trace __CM_TRACE__(func_name, argfmt, arg1, arg2)
// more macros define....
namespace cm
{
class Trace
{
public:
explicit Trace(char *func_name, const char* argsfmt, ...)
{
char fmt[256] ={0};
sprintf(fmt, "%s%s", func_name, argsfmt);
va_list arglist;
va_start(arglist, argsfmt);
LogMsg(depth_, depth_ * 2, fmt, arglist);
va_end(arglist);
++depth_;
}
~Trace()
{
--depth_;
}
/// special the global log file.
void static LogToFile(const char *trace_file)
{
trace_file_ = trace_file;
}
private:
void LogMsg(int depth, int align, const char *fmt, va_list args)
{
FILE *fp = fopen(trace_file_.c_str(), "a+");
if (fp == NULL)
{
return;
}
time_t curTime;
time(&curTime);
char timeStamp[32] = { 0 };
strftime(timeStamp, sizeof(timeStamp),
"%Y%m%d.%H%M%S", localtime(&curTime));
// only log the timestamp when the time changes
unsigned int len = fprintf( fp, "%s %*.*s> (%d)",
(last_invoke_time_ != curTime) ?
timeStamp : " ",
2 * depth,
2 * depth,
nest_,
depth);
last_invoke_time_ = curTime;
len += vfprintf(fp, fmt, args);
len += fwrite("\n", 1, 1, fp);
fflush(fp);
fclose(fp);
}
private:
// the debug trace filename
static std::string trace_file_;
// function call stack depth
static int depth_;
static const char* nest_;
static time_t last_invoke_time_;
};
std::string Trace::trace_file_ = "";
int Trace::depth_ = 0;
// arbitrarily support nesting 34 deep for no particular reason
const char* Trace::nest_ =
"| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | ";
time_t Trace::last_invoke_time_ = 0;
} // end namespace cm
#endif // CM_TRACE_20060209_HPP
Using the Code
首先你需要调用 CM_TRACE_FILE()
来指定调试日志文件名,然后在函数入口处调用 CM_TRACE()
。这里有一个例子。
#include "cm/trace.hpp"
void foo()
{
CM_TRACE_FUNC("foo");
}
void bar(int x)
{
CM_TRACE_FUNC_ARG1("bar", "(%d)", x);
foo();
}
void foobar(int x, const char* s)
{
CM_TRACE_FUNC_ARG2("foobar", "(%d, %s)", x, s);
bar(789);
}
void foobar3(int x, int y, double z)
{
CM_TRACE_FUNC_ARG3("foobar3", "(%d, %d, %f)", x, y, z);
foobar(123, "4546");
}
int main()
{
CM_TRACE_FILE("./trace.log");
CM_TRACE_FUNC("main");
foo();
bar(23);
foobar(33, "char");
foobar3(12, 23, 34.45);
return 0;
}
然后打开 `trace.log',使用你喜欢的编辑器。在文件末尾,你将看到以下内容
20060211.132431 > (0)main()
| > (1)foo()
| > (1)bar(23)
| | > (2)foo()
| > (1)foobar(33, char)
| | > (2)bar(789)
| | | > (3)foo()
| > (1)foobar3(12, 23, 34.450000)
| | > (2)foobar(123, 4546)
| | | > (3)bar(789)
| | | | > (4)foo()
结论
本文的原理很简单,但我们可以编写简单的代码来做有用的事情。
修订历史
- v1.00 2005年2月4日。Mockey
- 原文
- v1.01 2005年2月11日。Mockey
- 将
fstream
更改为FILE*
- 将
string
更改为sprintf
以提高效率。感谢 Jon Wold 的评论 - 将
CM_TRACE
重命名为CM_TRACE_FUN
并添加CM_TRACE_FUN_ARGx
宏。 - 添加记录函数调用参数的功能。
- 将