在C语言中追踪函数调用

9
我正在为一个用C语言编写的自动化系统开发一些模块,需要与硬件进行大量的工作。我发现没有简单的方法(像传统的方式)来调试事物,而不是跟踪日志。因此,我正在寻找记录函数调用的良好实践方法。至少要记录调用序列和返回值。
在应用程序中执行的方式非常直接,实际上会用无关的结构污染代码。
int function (int param){
   if(trace_level & LOG_FCALLS){
      writelog("Entering function()");
   }

   /* something useful */

   if(trace_level & LOG_FCALLS){
      writelog("Exit from function()=%d", ret);
   }
}

我决定使用一个宏来完成所有的脏活。现在看起来是这样的:
#define LOG_E(fn) const char *__fname=fn;  printf("LOG: Entry to %s\n",__fname)
#define return(ret)     printf("LOG: Exit from %s()=%d\n",__fname,ret)

int testFunc(){
   LOG_E("testFunc");

   /*do useful things */

   return(ret);
}

我看到了这段代码的问题。
  1. 我重写了return语句,需要每次都写return(ret)而不是return ret。很容易忘记这个问题。

  2. 我在我的宏中定义了字符串变量。我知道C99中存在__func__宏,但我的编译器不支持此宏或任何其他相关的宏。

  3. 如何记录函数参数的值?

我相信这不是一个新问题,我也不是第一个遇到它的人。我也知道AOP这件事,但代码插桩不是我的系统可接受的解决方案,并且我没有找到任何可能使用我的编译器实现它的方法。
因此,我正在寻找如何以最优雅的方式实现跟踪的好方法。
我的环境: 传统代码,C,Watcom 10.x,实时操作系统

1
你并没有真正地返回任何东西,这个事实怎么办?也就是说,你的 return (ret) 被宏吃掉并转换为 printf,然后函数没有返回语句可以执行。你没有在宏中返回。 - ArjunShankar
1
Watcom C 支持 __FUNCTION__ 宏。 - Matt
1
以防万一,如果你通过 #ifdef 测试了 __func__ 的存在:它不是一个宏,而是一个静态数组(就好像在函数开头声明了 static const char __func__[] = "...";)。 - mafso
@ArhujShankar 是的,你说得对。应该有返回语句。 - evrdrkgrn0
@user4419802 是的,至少谷歌是这么说的。但在版本11中,而我只有10.6。 - evrdrkgrn0
4个回答

5

这个问题的解决办法是创建一个单独的调试/测试项目,完全独立于生产代码。具体步骤如下:

  • Make sure to have a backup/commit on the production code.
  • Make a hard-copy of the production code on the hard drive. This will become your test project.
  • Create a .txt log file where you write the full signature of each function you want to log, for example:

    int function (int param)
    float function2 (void)
    ...
    
  • Create a little PC program/script that takes the above .txt file as input, then searches through the source code for matching lines of function definitions. The PC program will then generate a new .c file based on the original code, where it inserts the debug logging code inside the desired functions, after { and before }. It will take a few hours of your time to make such a program.
  • Link your test project by using the modified source code created by your script.
上述方法是我在关键任务软件中使用的,其中您需要满足安全标准(MISRA、代码覆盖率等)的要求,这些要求指出不允许在最终产品中执行未被执行的代码。
此方法确保了生产代码的完整性,并确保测试/调试代码不会意外地添加到程序中。它还将编译开关等杂乱无章的内容排除在生产代码之外。您也不会在项目中留下任何旧的调试代码,因为您可能会忘记删除它(否则我总是会在我的程序中遗漏一些调试代码片段)。

生产代码的硬拷贝。哇!你试过像SVN、Git、Mercurial这样的版本控制工具吗? - myaut
@myaut 这就是为什么我说“一定要备份/提交(commit)”。提交(commit)就是一个硬拷贝。 - Lundin
这是自动化的一个很好的例子,我会记下来的。有时候我们需要从生产系统中获取跟踪日志,例如对于可以在真实环境中重现的错误。 - evrdrkgrn0
这就是我在硬件项目中所做的,使用非常有限的调试器。我用C#编写了我的“调试代码插入器”程序,它不到100行代码。 - Hooch
@Hooch 确实,对于一个程序员来说,编写这样的程序应该不是什么难题,而且你可以使用任何你想要的编程语言。程序员有时会忘记他们可以通过编程来解决自己的编程问题 :) - Lundin

1
#if defined(DEBUG_BUILD)
#  define START_FUNCTION if(trace_level & LOG_FCALLS){writelog("+++ %s()", __func__)
   }
#  define END_FUNCTION if(trace_level & LOG_FCALLS){writelog("--- %s()", __func__)
#elif defined (TIMING_BUILD)
#  define START_FUNCTION  WRITE_TIMED_LOG("+++")
#  define END_FUNCTION WRITE_TIMED_LOG("---")
#else
#  define START_FUNCTION
#  define END_FUNCTION
#endif
int function (int param){
   START_FUNCTION;
   ...
   if(error_occurred) {
     END_FUNCTION;
     return errror_code;
   }
   ...
   END_FUNCTION;
   return 42;
}

对我来说,似乎这是一个选项。因为我不想破坏返回关键字。但令人困扰的是,我们需要在每个返回之前编写“END_FUNCTION”。 - evrdrkgrn0

0

这在MS Visual C中起作用。对于不同的数据类型(或者没有),您将需要不同版本的return宏。

#include <stdio.h>

#define TRACING

#ifdef TRACING
#define LOG_E printf("Func: %s\n", __FUNCTION__);
#define LOG_R printf("Exit: %s\n", __FUNCTION__);
#define LOG_I(ival) printf("Exit: %s %d\n", __FUNCTION__, ival);

#else
#define LOG_E
#define LOG_R
#define LOG_I(ival)
#endif

int main(void){
    int retval = 0;
    LOG_E
    printf("Hello world!\n");
    LOG_I(retval)
    return retval;

}

输出:

Func: main
Hello world!
Exit: main 0

0

你可以自定义编译器来处理这个问题。如果你正在使用GCC编译器,你可以使用MELT(来自定义你的gcc编译器)。

或者你可以自定义openwatcom(或者请一些OpenWatcom专家来完成)...


似乎 MELT 的 URL 不再有效,而是指向垃圾站点。 - Andy J

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接