C/C++ 需要一种聪明的方法来跟踪函数调用
Posted
技术标签:
【中文标题】C/C++ 需要一种聪明的方法来跟踪函数调用【英文标题】:C/C++ need a clever way to track function calls 【发布时间】:2010-07-23 03:47:06 【问题描述】:我正在寻找一种聪明的方法来跟踪函数调用和返回。 我知道我可以使用调试器,但我想要一种方法,让它在调用函数时将某些内容打印到终端,而不是单步执行代码。 我在想我也许可以使用预处理器,但我不确定最好的方法是什么。 或者有没有办法使用 gdb 打印出有用的信息,而不必单步执行代码。
【问题讨论】:
【参考方案1】:大多数编译器允许您在函数调用之前和之后注入检测函数。
在 MSVC 中,它们是 _penter
和 _pexit
。一篇不错的文章:http://www.drdobbs.com/184403601。
在 GCC 中,您将使用 -finstrument-functions
选项,请参阅 the docs。
您可以使用调试库或映射文件来获取更多信息。
【讨论】:
你不会知道如何在 gcc 的符号表中查找函数 您是要查找名称,还是要在调试对象中查找参数等内容? 只要名字就好了。 #延迟 link Dl_info this_fn_info; dladdr(this_fn, &this_fn_info); @DanielMoodie 有一个“调用站点”参数,我可以用它来查找调用的文件和行号,例如_FILE_ 和 _LINE_?【参考方案2】:一个相当侵入性的解决方案是使用 RAII 来控制函数的范围。这将对性能产生很大影响,但在日志中会非常明确,而无需用户在所有可能离开函数的代码路径中添加检测:
class ScopeLogger
public:
ScopeLogger( std::string const & msg ) : msg(msg)
std::cout << "Enter: " << msg << std::endl;
~ScopeLogger()
std::cout << "Exit: " << msg << std::endl;
std::string msg;
;
#if DEBUG
#define FUNCTION(x) ScopeLogger l_##x##_scope(x);
#endif
void foo( int value )
FUNCTION( __FUNCTION__ );
if ( value > 10 ) throw std::exception;
std::cout << "." << std::endl;
int main()
foo(0); // Enter: foo\n.\nExit: foo
foo(100); // Enter: foo\nExit: foo
如果代码是单线程的,您甚至可能希望在ScopedLogger
中添加一个具有一定缩进级别的静态变量,而不会对已经很严重的性能影响增加太多:
class ScopeLogger
public:
ScopeLogger( std::string const & msg ) : msg(msg)
std::cout << std::string(indent++,' ') << "Enter: " << msg << std::endl;
~ScopeLogger()
std::cout << std::string(--indent,' ') << "Exit: " << msg << std::endl;
std::string msg;
static int indent;
;
int ScopeLogger::indent = 0;
【讨论】:
相当麻烦,但比 Praveen 的要少。 你的代码能编译吗?std::string
的构造函数在哪里使 std::string(indent++," ")
有效?
@bruce.banner:很好,当您输入网页而不是编辑器并编译时会发生这种情况。第二个参数必须是char
,而不是char*
(即我错误地在需要单引号的地方使用了双引号)。无论如何,仅将其用作一个想法,代码还有其他问题,包括但可能不限于线程安全(或缺乏线程安全)。【参考方案3】:
由于您使用的是 GCC,因此您还可以使用链接器函数包装。
Link-Time Replacement / Wrapping
– GCC option: -Wl,--wrap,function_name
基本上,您可以使用名为“function_name()”的函数并将其包装为名为“__wrap_function_name()”的函数。您可以通过调用“__real_function_name()”来访问原始函数。
【讨论】:
【参考方案4】:#define BEGIN_FUNC(X) printf("Function %s Entered",X)
#define END_FUNC(X) printf("Function %s End",X)
foo()
BEGIN_FUNC(__func__);
//Your code here
END_FUNC(__func__);
我认为,如果您编写一个像上面这样的宏并将其用于所描述的每个功能,那么您可以在终端上获取日志。
【讨论】:
这很脆弱。如果您正在编程 c++,最好编写一个小型范围的记录器,该记录器将记录构造和对象破坏。否则,如果抛出异常或用户忘记在任何返回代码路径中编写END_FUC
宏,您可能会看到已输入但从未返回的函数。
看日志对调试没有帮助吗? OP 声明了无需使用调试器即可知道调用堆栈的任何方法。我们可以在可执行文件的每个 API 上使用它,因此可以随时获取堆栈。
@Nyan- 我认为当我们说函数调用的开始和结束时,它暗示必须在每次返回之前添加它。【参考方案5】:
您可能想查看Valgrind's Callgrind,它可以将函数调用跟踪到漂亮的图表中。它将显示函数调用,但不显示参数或返回值。
【讨论】:
【参考方案6】:或者有没有办法使用 gdb 打印出有用的信息,而不必单步执行代码
是的。仅在您真正关心的函数处设置断点。使用“继续”,直到您使用这些功能或直到您的程序崩溃。然后使用“backtrace”(或“bt”)获取堆栈跟踪。
【讨论】:
【参考方案7】:如果您需要自动化,您可以查看TARGET_ASM_FUNCTION_END_PROLOGUE
和TARGET_ASM_FUNCTION_BEGIN_EPILOGUE
。这些是编译器钩子,可让您指定要与正常函数序言/结尾一起发出的程序集片段——在您的情况下,您可以使用它们发出一个小程序集来记录相关函数的进入/退出.您还可以查看FUNCTION_PROFILE
和/或PROFILE_HOOK
(例如,http://gcc.gnu.org/onlinedocs/gccint/Function-Entry.html)。
【讨论】:
【参考方案8】:以下是说明the answer by Jonathan Fischoff 的GCC 端的示例。
这里我们调用外部工具addr2line
将位置打印为functionName at /path/to/file.cpp:line
而不是简单的地址。我已经尝试为此使用dladdr
(如对上面链接答案的评论中所建议的那样),但它只为我返回了dli_sname
中的空指针。
这种解析地址的方法有一些缺点:
由于fork
/execve
/文件读取,速度很慢。
它需要包含地址的二进制文件的确切文件路径,因此下面的简单代码无法打印共享库中的符号。
// Instrumentation
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
static void __attribute__((no_instrument_function))
log_func(const void* funcAddr, const char* action, const void* callSite)
char cmd[50];
snprintf(cmd, sizeof cmd, "addr2line -Cpfe /proc/%d/exe %p", getpid(), funcAddr);
fprintf(stderr, "%p %s %p ", callSite, action, funcAddr);
system(cmd);
extern "C" void __attribute__((no_instrument_function))
__cyg_profile_func_enter(void* this_fn, void* call_site)
log_func(this_fn, "->", call_site);
extern "C" void __attribute__((no_instrument_function))
__cyg_profile_func_exit(void* this_fn, void* call_site)
log_func(this_fn, "<-", call_site);
// Actual code we're tracing
#include <iostream>
struct Test
Test() std::cout << "Hi, I'm Test constructor\n";
void method() const std::cout << "And I'm Test method\n";
;
int main()
std::cout << "Hello, my name is main\n";
Test test;
test.method();
编译运行:
$ g++ test.cpp -o test -g -finstrument-functions && time ./test
0x8048b0b -> 0x804899b _GLOBAL__sub_I___cyg_profile_func_enter at /tmp/test.cpp:41
0x80489c4 -> 0x804890b __static_initialization_and_destruction_0(int, int) at /tmp/test.cpp:41
0x80489c4 <- 0x804890b __static_initialization_and_destruction_0(int, int) at /tmp/test.cpp:41
0x8048b0b <- 0x804899b _GLOBAL__sub_I___cyg_profile_func_enter at /tmp/test.cpp:41
0xf7a0de71 -> 0x804886a main at /tmp/test.cpp:37
Hello, my name is main
0x80488b1 -> 0x80489de Test::Test() at /tmp/test.cpp:32
Hi, I'm Test constructor
0x80488b1 <- 0x80489de Test::Test() at /tmp/test.cpp:32
0x80488c0 -> 0x8048a4a Test::method() const at /tmp/test.cpp:33
And I'm Test method
0x80488c0 <- 0x8048a4a Test::method() const at /tmp/test.cpp:33
0xf7a0de71 <- 0x804886a main at /tmp/test.cpp:37
real 0m0.062s
user 0m0.054s
sys 0m0.008s
【讨论】:
【参考方案9】:有一个__FUNCTION__
(Reference) 宏用于确定您使用的方法(格式为Class::Method
),但这更像是一个手动过程。
但是,当我最近需要相同的“跟踪”信息时,我找不到自动方法。
【讨论】:
以上是关于C/C++ 需要一种聪明的方法来跟踪函数调用的主要内容,如果未能解决你的问题,请参考以下文章