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:
- a) standardize call stack utilities to retrieve, interpret
and format such retrieved information. This is a pure extension to the
existing standard.
- b) extend std::exception
with such call stack information collected at the point where the
exception is instantiated.
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:
- A raw call stack container.
- Two tools to resolve call stack addresses to
symbol information.
Implementations could provide more such resolvers.
- 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
- 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:
- Abstract away the platform-specific information on
how the call stack information is retrieved.
- Minimize side effects of the call stack retrieval process
- In
particular, there are dynamic storage allocation restrictions
when
retrieving/walking the call
stack. This because of the intended usage within an std::exception.
Retrieving the call stack could be in the context of an std::bad_alloc,
hence the restriction.
- Abstract away the platform-specific information on how
addresses are resolved to symbol information.
- Offers functionality to process call stack and symbol
information in a platform-independent way.
- Have minimal impact on the existing standard. Only std::exception
needs a method added to the interface and only if part b) of the
proposal is adopted.
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:
- Is not allowed to allocate from the dynamic
storage to either retrieve or store raw
call stack information. The amount of memory is limited through a
template parameter; this also limits the depth of the retrieved stack.
Thus, all the memory needed is available when the class is
instantiated. This limitation stems from the intended usage within an std::exception
as explained above.
- The information about the call stack
is retrieved when the call_stack
object is instantiated. The platform-specific API used to retrieve the
call stack cannot allocate dynamic storage.
- Retrieving the call stack is an operation that is costly.
Consequently,
neither the default constructor nor the copy/move constructors do
retrieve this information. The constructor has been parameterized with
a bool to explicitly request retrieval of the stack information from
the OS.
- None of the constructors nor the destructor can throw. This
limitation stems from the intended usage within an std::exception.
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
- GNU/Linux
- http://freeshell.de/~amelinte/lpt.tgz
- http://freeshell.de/~amelinte//lpt/lpt/include/lpt/call_stack.hpp
VIII.
References
IX.
Revision History
2012-09-20. Incorporated feedback from Alisdair Meredith.