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_PROLOGUETARGET_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++ 需要一种聪明的方法来跟踪函数调用的主要内容,如果未能解决你的问题,请参考以下文章

C/C++学习笔记 关于调用约定

回调函数(callback)是啥? ,,

如何运用c++里的“__stdcall”?

Jasmine函数或变量来检查是否已调用所有声明的'expect'函数?

Linux调用可执行程序

C/C++主调函数从被调函数中获取(各种类型)数据内容方式的梳理归纳