Document number: N3441=12-0131
Date: 2012-09-20
Project: Programming Language C++, Library Working Group
Reply-to: Aurelian Melinte <ame01 at gmx dot net>

Call Stack Utilities and std::exception Extension Proposal

I. Table of Contents



II. Introduction

This is a two part proposal to: 

III. Motivation and Scope

To diagnose software defects, it is often necessary to have the call stack information and to be able to read it in a human-readable form. Rare-to-occur software conditions and transitory anomalies do happen mostly on production machines where only release versions of the software are installed and where no compiler or debugging facilities are installed. Race conditions are exposed only in specific environments that are sometimes inaccessible to a programmer. Sometimes even no core dump is available for post-mortem analysis, either because of an administrative policy, either because the platform does not support it. Most of the time it is not even acceptable to dump core and terminate a server program.

Thus, being able to capture the call stack at any given point in time and being able to store the information for later analysis wouldbe of valuable help and would ease error diagnostic with the above situations.  One alternative available to the C++ programmer willing to get to this call stack and symbol information is to write its own tools using platform-specific APIs.  Another alternative could be to resort to third-party vendors' libraries. In both cases, the work is to be redone each time the software is ported to a different platform.

Furthermore, there are situations hard to address without support from within the library. For instance, the call stack of the exact place where an std::exception has been thrown is not currently available at the place where exception is caught. 

IV. Impact On the Standard

Additions

The proposal adds four new classes to standardize call stack information, for part a) of the proposal:

  1. A raw call stack container. 
  2. Two tools to resolve call stack addresses to symbol information. Implementations could provide more such resolvers.
  3. A tool to combine a call stack container with an address resolver for "cooked" call stack information.

Dependencies

The proposed functionality uses existing standard containers to keep the raw call stack and the symbol information. The choice of the containers is platform-dependent (in practice std::array proved to be a good choice for GNU/Linux).  It makes usage of platform specific APIs to extract the call stack and to resolve symbols but these APIs  should not add any new link dependencies.

Changes to current standard components

  1. A one method extension to std::exception. Only if part b) of the proposal is adopted.

V. Design Decisions

The proposed set of utilities does:


VI. Technical Specifications

call_stack

This class contains the raw call stack information and it offers an interface to access this information in a platform-independent way.  

call_stack:

call_stack exposes a standard container interface [1] to the call stack. However, part of the standard interface is missing, as it makes little sense to allow mutator operations to the call stack. Only read-only access is provided to the call stack information, with the exception of the swap() method.

The call stack information is accessed through a bidirectional const_iterator, as well as through a const subscript operator.

typedef platform-specific   raw_frame_type;       // void*
typedef platform-specific   const_raw_frame_type; // const void*

template < std::size_t MaxDepth> class call_stack
class call_stack
{
public:

    typedef raw_frame_type            value_type;
    typedef platform-specific         size_type;
    typedef platform-specific         difference_type;
    typedef const_raw_frame_type&     const_reference;         // no reference
    typedef const_raw_frame_type*     const_pointer;           // no pointer
    typedef platform-specific         const_iterator;          // no iterator
    typedef platform-specific         const_reverse_iterator;  // no reverse_iterator

    /**
     *  Get the call stack information upon instantiation
     *  if @param capture is true.
     */
    call_stack(bool capture = false)          noexcept;

    call_stack(call_stack&& other)            noexcept;
    call_stack(const call_stack& other)       noexcept;
    call_stack& operator=(call_stack&& other) noexcept;
    call_stack& operator=(call_stack other)   noexcept;
   
~call_stack()                             noexcept;

    size_type depth()      const noexcept;
    size_type max_depth()  const noexcept;

    size_type size()       const noexcept { return depth(); }
    size_type max_size()   const noexcept { return max_depth(); }

    bool empty()           const noexcept { return depth() == 0; }

    /*
     * const_iterator allows access to the const_raw_frame_type
     * constituting the call stack information.
     */
    const_iterator begin()  const;
    const_iterator end()    const;
    const_iterator cbegin() const;
    const_iterator cend()   const;

    const_reverse_iterator crbegin() const;
    const_reverse_iterator crend()   const;
    const_reverse_iterator rbegin()  const;
    const_reverse_iterator rend()    const;

    const_reference operator [] (size_type idx) const;
    const_reference at(size_type idx) const;

    bool operator ==(const call_stack& other) const; // no <, <=, >, >=
    bool operator !=(const call_stack& other) const;

    void swap(call_stack& other) noexcept;
}; //call_stack

symbol_info

symbol_info resolves a given address to human-readable symbol information and offers facilities for outputting the information to streams. 

The resolution is using the API offered by default by the platform that does not add link dependencies. For instance, for gcc-based platforms, symbol_info would use the glibc API. However, libraries such as libbfd (The Binary File Descriptor Library) [2] do offer better resolution (source file, line number) than glibc, but such libraries are independent of gcc and might have not been installed on a given machine. On GNU/Linux a libbfd_symbol_info can be written and used whenever libbfd is available, at the price of adding a dependency on a library that might not be installed by default. The choice to use a different symbol resolver is left to the programmer.

In the process of resolving, symbol_info can allocate dynamic storage as needed. This is undesirable in the context of an exception such as std::bad_alloc being caught, so a simpler version, symbol_info_base, with the same interface should exists. symbol_info_base is not allowed to allocate dynamic storage and consequently would not be able to resolve much symbol information, but it would allow a very basic call stack information to be printed. symbol_info_base can only use platform-specific symbol resolution APIs that do not allocate dynamic storage.  At a minimum,  symbol_info_base will only offer the call stack frame address. 

Again, only read-only access is provided to the frame information, with the exception of the swap() and resolve() methods. No method is allowed to throw.

The resolve() method allows for reuse of a constructed symbol_info object, for example by an iterator that iterates over a call_stack and is the only mutator aside swap().

class symbol_info //symbol_info_base
{
public:

    symbol_info(const_raw_frame_type addr)      noexcept;

    symbol_info(symbol_info const& other)       noexcept;
    symbol_info& operator=(symbol_info other)   noexcept;

    symbol_info(symbol_info&& other)            noexcept;
    symbol_info& operator=(symbol_info&& other) noexcept;

    void resolve(const_raw_frame_type addr)     noexcept;
    void swap(symbol_info& other)               noexcept;


    // The address the symbol info is for.
    const_raw_frame_type   addr()                    const
noexcept;
    const char*            binary_file()             const noexcept;
    const char*            raw_function_name()       const noexcept;
    const char*            demangled_function_name() const noexcept;
    char                   delta_sign()              const noexcept;
    long                   delta()                   const noexcept;
    const char*            source_file()             const noexcept;
    unsigned int           line_number()             const noexcept;


    friend inline std::ostream& operator<<(std::ostream& os,
                                           const symbol_info& frm)
    {
        os << "[" << std::hex << frm.addr() << "] "
           << frm.demangled_function_name()
           << " (" << frm.binary_file() << frm.delta_sign() << "0x" << std::hex << frm.delta() << ")"
           << " in " << frm.source_file() << ":" << std::dec << frm.line_number()
           ;
        return os;
    }
}; //symbol_info


call_stack_info

call_stack_info binds together a call_stack with a symbol resolver.  call_stack_info offers human-readable information to the call stack and offers facilities for outputting the information to streams. 

Only read-only access is offered by call_stack_info.  A bidirectional const_iterator  provides read-only frame-by-frame access to the stack information, as well as a const subscript operator. Both allow to resolve call stack frames to symbol information using the AddrResolver template parameter.

template < typename CallStack                      //= call_stack<40u>
         , typename AddrResolver                   = symbol_info
         >
class call_stack_info
{
public:

    typedef CallStack              stack_type;
    typedef AddrResolver           symbol_info_type;

    call_stack_info()                                   noexcept
    call_stack_info(const stack_type& stack)            noexcept;

    call_stack_info(call_stack_info&& other)            noexcept;
    call_stack_info& operator=(call_stack_info&& other) noexcept;
    call_stack_info(const call_stack_info& other)       noexcept;
    call_stack_info& operator=(call_stack_info other)   noexcept;

    void swap(call_stack_info& other) noexcept;

    friend inline std::ostream& operator<<(std::ostream& os,
                                           const call_stack_info& stk)
    {
        for (const auto& frm : stk._stack)
        {
            AddrResolver frmInfo(frm);
            os << frmInfo << "\n";
        }
        os << std::flush;
        return os;
    }

    std::string as_string() const;

    class const_iterator
            : public std::iterator< std::bidirectional_iterator_tag
                                  , ptrdiff_t
                                  >
    {
    public:

        const_iterator(const typename stack_type::const_iterator& it);

        bool operator==(const const_iterator& other) const;
        bool operator!=(const const_iterator& x)     const;

        const symbol_info_type& operator*()  const;
        const symbol_info_type* operator->() const;

        const_iterator& operator++();
        const_iterator operator++(int);

        const_iterator& operator--();
        const_iterator operator--(int);
    }; //const_iterator

    const_iterator begin()  const;
    const_iterator cbegin() const;
    const_iterator end()    const;
    const_iterator cend()   const;

private:

    stack_type         _stack;
};

std::exception

std::exception is extended to capture the call stack when it is instantiated.  It offers access to the call stack through its where() method: 

class std::exception
{
public:

    typedef std::call_stack< default_stack_depth/*40*/ > stack_type;

   
exception() noexcept : _where(true) {...}

    const stack_type& where() const noexcept
    {
        return _where;
    }

private:

    stack_type  _where;
};

Example Usage

Example 1:

    typedef call_stack<40> stack_type;

    stack_type; here(true);                   // Get the call stack
    std::cout << here.depth() << " frames:\n"
              << call_stack_info< stack_type, symbol_info >(here) << std::endl;

Example 2: usage within the context of an exception:

    try
    {
    ...
    }
    catch (std::exception& ex)
    {
        std::cerr << ex.what() << std::endl;
        std::cerr << "\nAt: \n"
                  << lpt::stack::call_stack_info< stack_type, symbol_info_base >(ex.where()) << std::endl;
        std::cerr << "\nAt: \n"
                  << lpt::stack::call_stack_info< stack_type, symbol_info >(ex.where()) << std::endl;
    }


VII. Existing Implementation

VIII. References


IX. Revision History

2012-09-20. Incorporated feedback from Alisdair Meredith.