Click here to Skip to main content
15,886,026 members
Articles / Programming Languages / C++

A Simple C++ Function Call Stack Trace Utility

Rate me:
Please Sign up or sign in to vote.
3.02/5 (23 votes)
10 Feb 2006CPOL1 min read 233.4K   1.9K   39   21
How to record your function call stack for debug purposes

Introduction

Under UNIX/Linux platform, program coredump generates a core file, we can analyze the core file and determine what's wrong in our program. The core file records the function call stack (e.g. under gdb you can use bt command to see backtrace), that is mostly information for us to troubleshooting.

When I debug my program, I try to record the function call stack in program debug log to help me diagnose the program's defect. Fortunately, C++ gives us a simple way to implement this.

Rational

Just use C++ constructor/destructor semantic, we can make our own trace class. At the function entrance, we declare a local variable, it will invoke the constructor immediately, when leaving the function, the local variable will be destroyed and call destructor. That's all. what we need to do is implement a class's constructor and destructor. This rational is simple enough, but this way sometimes can help us a lot.

The complete code is as follows:

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

First you need to call CM_TRACE_FILE() to special debug log filename, then at function entrance, call CM_TRACE(). Here is an example.

C++
#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;
}

Then open `trace.log', use your favourite editor. At the end of file, you will see the following:

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()

Conclusion

The rational in this article is simple, but we can write simple code do useful things.

Revision History

  • v1.00 Feb 4, 2005. Mockey
    • Original article
  • v1.01 Feb 11 2005. Mockey
    • Change fstream to FILE*
    • Change string to sprintf for more efficiency. Thanks Jon Wold's comment
    • Rename CM_TRACE to CM_TRACE_FUN and add CM_TRACE_FUN_ARGx macros.
    • Add feature to log the function call arguments.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
China China
Mockey is a C++ programmer, experienced with C/C++ program under UNIX platform, his primarily research field is communication program for telecom. He works for one of china's biggest telecom equipment supplier until Dec 2006.

He's current interest is Linux kernel programming, especial in networking hack.

Mockey lives in Chengdu, China. To contact Mockey, email at mockey.chen@gmail.com

Comments and Discussions

 
Questioncan any one explain me what is that __CM_TRACE__ Pin
online@ilovec.in22-Jan-13 19:51
online@ilovec.in22-Jan-13 19:51 
AnswerRe: can any one explain me what is that __CM_TRACE__ Pin
Member 1112462910-Mar-15 9:00
Member 1112462910-Mar-15 9:00 
Generalany idea for win32 C dll trace log. Pin
fredguo1-Mar-09 6:33
fredguo1-Mar-09 6:33 
Generalbug.. Pin
dheerajkhajuria8-Jul-07 6:17
dheerajkhajuria8-Jul-07 6:17 
GeneralRe: bug.. Pin
Mockey Chen8-Jul-07 15:29
Mockey Chen8-Jul-07 15:29 
GeneralCM_TRACE_FUNC_ARG3("foobar3", "(%d, %d, %s)", x, y, 100); Pin
Pham Thanh Toan3-Jan-07 6:57
Pham Thanh Toan3-Jan-07 6:57 
GeneralRe: CM_TRACE_FUNC_ARG3("foobar3", "(%d, %d, %s)", x, y, 100); Pin
Mockey Chen6-Jan-07 0:48
Mockey Chen6-Jan-07 0:48 
Generalsprintf is declared as deprecated Pin
DevianT (rus)21-Feb-06 13:27
DevianT (rus)21-Feb-06 13:27 
GeneralRe: sprintf is declared as deprecated Pin
Nemanja Trifunovic21-Feb-06 13:46
Nemanja Trifunovic21-Feb-06 13:46 
GeneralRe: sprintf is declared as deprecated Pin
DevianT (rus)21-Feb-06 20:21
DevianT (rus)21-Feb-06 20:21 
Generalsome advice Pin
thog14-Feb-06 16:27
thog14-Feb-06 16:27 
Hi,

I commend you for considering the need to be able to analyse what happened in the event of a defect.

I have written quite a few debugging aids over the years, especially for post-mortem debugging, so hopefully I can offer some constructive advice.

Ultimately I have found methods such as you decribe to be of use only in very limited cases such as platforms where more automated forms of stack and memory examination are not available. In fact most often just the current call stack(s) are all that are needed rather than a history of all calls made.

Facilities such as you describe can sometimes be useful for manually instrumenting small sections of code that have already been narrowed down as suspect by other means.

The main issues I owuld have with your solution are:

- Intrusive/Error prone. T
- Not thread safe/aware.
- Speed.

These can be improved but please bear in mind that a call history is only rarely useful and we shoudl avoid trying to capture information that is more conveniently and reliably available by other means.


Intrusive/Error prone.
=====================

Each function must be explicitly instrumented. Instrumenting all functions can be tedious and requires strict enforcement.
The need to explicitly record function name and argument formats is error prone.

Various levels of failure are possible.
- fail to instrument function. Log output is deceptive.
- fail to name function correctly. Log output is deceptive.
- fail to specify function argument formats correctly. Program may crash.

This last one is particularly insidious, particularly as the function may be on an infrequently used code path or may not crash all the time.
This means the attempt to improve program quality via instrumentation has in fact added a potentially hard to detect bug.

Threading
=========

Most real world applications are multi-threaded.

Your code currently does not take into account the fact that multiple threads may be running the code in question, each thread of course having it's own stack.
To remedy this...

- the depth_ member would need to be moved to thread local storage, so that it stores an independent depth count for each thread.
- I would recommend adding a thread identifier to the logged messages.
- Technically the last_invoke_time_ would need to be mutex protected, although in practice the race condition on the inequality test and assignment should only cause a subsequent spurious timestamp output.
- Similarly the trace_file_ member should be mutex protected.

This list may not be exhaustive.


Speed
=====

Logging a message on every function scope entrance (or exit) can have a large speed impact.
This can affect program behaviour and debugging by altering thread races and/or changing the code path e.g timers fire, remote servers timeout etc.

It is best that if such logging is used it is as speedy as possible.
Some issues with the code are:
- the mutexes described cost time and force calls to open the file and get the last invoke time to be serialised. Rewriting code to avoid cross thread dependencies can eliminate these.
- The logging file is opened, flushed and closed each time a message is logged. This is very costly. Definitely Consider opening once prior to all logging and closing once after all logging. Consider not flushing after every message although this will mean you may lose last few messages if a crash occurs. This dilemma is basically inescapable as file based logging of function calls is incommensurate with speed.
- The timestamp is formatted even if (last_invoke_time_ == curTime). Consider rewriting code to initialise timeStamp to the required blank string, get curTime, then test (last_invoke_time_ != curTime) and only format timeStamp and update last_invoke_time_ inside this if clause.
- the len variable is assigned and added, but is never used. It can be removed.
- The actual string output and formatting is very expensive.

These costs can reduce the speed of the instrumented code by tens, hundreds or even thousands of times.

One way to avoid many of these costs is consider logging in a pure binary format direct to memory. While this means it is difficult to keep a complete trace it is possible to keep a trace of the last 1,000 or 10,000 etc. calls. The description

A brief description of an implementation that ignores the issue of logging function arguments is,

- consider a struct that describes a function call. It may contain the address of the function called, the time called (time_t), the thread id, and bool indicating entry or exit. Such a struct would be at most say 16 bytes.
- allocate a static array of such structs. The (fixed) size of the array governs the how many calls can be stored for post mortem debugging.
- also declare a pointer to struct initialise to point to the start of the array.

The idea is that this array forms a ring buffer in memory.

Logging a message now requires the following operation; increment the pointer mod the array length, as an _atomic_ (thread safe) operation, and use this value as pointer to the struct for logging.

Note...while thread safety may typically require a mutex to protect this operation, with a little bit of planning this can be done using very fast interlocked operations on most architectures. Alternatively create per thread arrays in thread local storage if it is not required to have a global record of thread interleaving. Please note that doing this and allowing both increment and decrement will effectively be duplicating each threads current call stack manually, information that can be obtained without any manual logging at all !!

Having (safely) obtained a pointer, now fill in the struct that is pointed to with the necessary details, eg. curTime, thread id, entry/exit, address of function. Note that we are not formatting any of these as strings.
Log an entry value from the Trace ctor, store the thread id and function address as memebers so these can be reusued in dtor, ... and log an exit value from the dtor.

Note that this logging is very fast compared to file based logging. It defers any string formatting until after runtime and it has the bare minimum of thread synchronisation required.

So how is this used in practice ???

If the program crashes the contents of the ring buffer are available in the core dump (or mini dump under windows). This can be examined or dumped to file using the debugger. While in the debugger the function addresses can easily be used to look at the functions called.

Similarly while the program is running the debugger can attach and suspend the process and be used to examine the ring buffer. Note that this is often pointless as the debugger has access to the current live call stacks and a history of calls is rarely needed.

One way of thinking of this is that the debugger is being used to translate the contents of the ring buffer into something human readable i.e. this formatting has been deferred until _after_ runtime.

Equally a custom viewer could be written. This program can take the dump from the debugger, load the ring buffer contents and a map file from the build, and then run through the ring buffer translating function addresses to names, tracking entry/exit to calculate depth and producing output much like your log file. Again all these costs have been deferred until after runtime.

In all this I skipped over the issue of logging argument values. This can be dealt with but is tricky. Even your solution had to hardcode an upper limit on the number of arguments (2) and can only handle argument types that have a corresponding printf format. My point is that this is issue is tricky for any "manual" implementation to solve generally.

Ultimately I hope these last few items illustrate the following point...

While it is possible to write programs that store and decode information about the runtime state of a program e.g. deduce function names from address, display values of arguments, in reality much of this work has already been done and exists inside your debugger.

In summary....

In my experience, needing a history of _all_ function calls is only very rarely needed to successfully debug an issue, most often the current call stacks are sufficient. A record of current call stacks is readily obtained post-mortem via core dumps (in Linux) or mini dumps (Windows). Such dumps are best viewed in a debugger. Equally, attaching a debugger and suspending a process can be used to examine call stacks pre-mortem and is useful to debug deadlocks. It can be very valuable, under windows, to add a code to set a top level filter to catch exceptions (caused by a crash) and perform a minidump to capture the program state for post-mortem analysis. Under linux, core dumps tend to be created automatically in this case. It is also often valuable (under windows and linux) to add a facility to provoke a dump and resume as this can be used to perform post mortem analysis on suspected dead locks or live (soft) locks that may occur in the field when a debugger is not available.

An in-memory call ring buffer may be very occasionally useful for particularly convoluted issues, if a history of calls is required.


I hope you find this feedback useful...as I said I think you should be commended for considering the need to be able to determine what has happened in a program, as this is a crucial requirement for effectively being able to fix bugs, particularly in live applications.

best regards

Andrew
GeneralRe: some advice Pin
Gavriloaie Andrei26-Oct-07 22:54
Gavriloaie Andrei26-Oct-07 22:54 
Generalscope logger Pin
Paulius Maruka14-Feb-06 14:17
Paulius Maruka14-Feb-06 14:17 
GeneralNot a stack trace Pin
Scope12-Feb-06 16:08
Scope12-Feb-06 16:08 
Generalminor comments ... Pin
Jon Wold7-Feb-06 5:44
Jon Wold7-Feb-06 5:44 
GeneralRe: minor comments ... Pin
Mockey Chen10-Feb-06 18:53
Mockey Chen10-Feb-06 18:53 
GeneralAnd throwing in a couple of VC++ macros... Pin
eFotografo7-Feb-06 0:19
professionaleFotografo7-Feb-06 0:19 
GeneralRe: And throwing in a couple of VC++ macros... Pin
Mockey Chen7-Feb-06 4:32
Mockey Chen7-Feb-06 4:32 
GeneralRe: And throwing in a couple of VC++ macros... Pin
Pavel Vozenilek13-Feb-06 18:54
Pavel Vozenilek13-Feb-06 18:54 
GeneralCool Pin
FocusedWolf6-Feb-06 12:46
FocusedWolf6-Feb-06 12:46 
GeneralRe: Cool Pin
Mockey Chen7-Feb-06 12:18
Mockey Chen7-Feb-06 12:18 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.