Add addr2line back-end (#5)

This commit is contained in:
Jeremy Rifkin 2023-07-09 21:10:53 -04:00 committed by GitHub
parent b5af425d09
commit 6d9d2a9747
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 286 additions and 5 deletions

View File

@ -20,6 +20,7 @@ jobs:
symbols: [
CPPTRACE_GET_SYMBOLS_WITH_LIBBACKTRACE,
CPPTRACE_GET_SYMBOLS_WITH_LIBDL,
CPPTRACE_GET_SYMBOLS_WITH_ADDR2LINE,
CPPTRACE_GET_SYMBOLS_WITH_NOTHING,
]
demangle: [

View File

@ -22,6 +22,7 @@ jobs:
symbols: [
CPPTRACE_GET_SYMBOLS_WITH_LIBBACKTRACE,
CPPTRACE_GET_SYMBOLS_WITH_LIBDL,
CPPTRACE_GET_SYMBOLS_WITH_ADDR2LINE,
#CPPTRACE_GET_SYMBOLS_WITH_NOTHING,
]
demangle: [
@ -66,6 +67,7 @@ jobs:
]
symbols: [
CPPTRACE_GET_SYMBOLS_WITH_DBGHELP,
# CPPTRACE_GET_SYMBOLS_WITH_ADDR2LINE, # TODO
# CPPTRACE_GET_SYMBOLS_WITH_NOTHING,
]
demangle: [

View File

@ -16,6 +16,7 @@ set(CMAKE_POSITION_INDEPENDENT_CODE ON)
include(GNUInstallDirs)
include(CheckCXXSourceCompiles)
include(CheckCXXCompilerFlag)
file(GLOB_RECURSE sources src/*.cpp)
add_library(cpptrace ${sources} include/cpptrace/cpptrace.hpp)
@ -359,6 +360,11 @@ endif()
if(CPPTRACE_BUILD_TEST)
add_executable(test test/test.cpp)
target_link_libraries(test PRIVATE cpptrace)
# Clang has been fast to adopt dwarf 5, other tools (e.g. addr2line from binutils) have not
check_cxx_compiler_flag("-gdwarf-4" HAS_DWARF4)
if(HAS_DWARF4)
target_compile_options(test PRIVATE "$<$<CONFIG:Debug>:-gdwarf-4>")
endif()
if(CPPTRACE_BUILD_TEST_RDYNAMIC)
set_property(TARGET test PROPERTY ENABLE_EXPORTS ON)
endif()

View File

@ -58,23 +58,23 @@ also manually set which back-end you want used.
| winapi | `CPPTRACE_UNWIND_WITH_WINAPI` | ✔️ | ❌ | Frames are captured with `CaptureStackBackTrace`. |
| N/A | `CPPTRACE_UNWIND_WITH_NOTHING` | ✔️ | ✔️ | Unwinding is not done, stack traces will be empty. |
Some back-ends require a fixed buffer has to be created to read addresses into while unwinding. By default the buffer
can hold addresses for 100 frames. This is configurable with `CPPTRACE_HARD_MAX_FRAMES`.
These back-ends require a fixed buffer has to be created to read addresses into while unwinding. By default the buffer
can hold addresses for 100 frames (beyond the `skip` frames). This is configurable with `CPPTRACE_HARD_MAX_FRAMES`.
**Symbol resolution**
| Library | CMake config | Windows | Linux | Info |
|---------|--------------|---------|-------|------|
| libbacktrace | `CPPTRACE_GET_SYMBOLS_WITH_LIBBACKTRACE` | ❌ | ✔️ | Libbacktrace is already installed on most systems, or available through the compiler directly. If it is installed but backtrace.h is not already in the include path (this can happen when using clang when backtrace lives in gcc's include folder), `CPPTRACE_BACKTRACE_PATH` can be used to specify where the library should be looked for. |
| libdl | `CPPTRACE_GET_SYMBOLS_WITH_LIBDL` | ❌ | ✔️ | Libdl uses dynamic export information. Compiling with `-rdynamic` is often needed. |
| libdl | `CPPTRACE_GET_SYMBOLS_WITH_LIBDL` | ❌ | ✔️ | Libdl uses dynamic export information. Compiling with `-rdynamic` is needed for symbol information to be retrievable. |
| addr2line | `CPPTRACE_GET_SYMBOLS_WITH_ADDR2LINE` | ❌ | ✔️ | Symbols are resolved by invoking `addr2line` via `fork()`. |
| dbghelp | `CPPTRACE_GET_SYMBOLS_WITH_DBGHELP` | ✔️ | ❌ | Dbghelp.h allows access to symbols via debug info. |
| N/A | `CPPTRACE_GET_SYMBOLS_WITH_NOTHING` | ✔️ | ✔️ | No attempt is made to resolve symbols. |
**Demangling**
Lastly, on unix systems symbol demangling is done with `<cxxabi.h>`. On windows symbols extracted with dbghelp.h aren't
mangled.
Lastly, depending on other back-ends used a demangler back-end may be needed. A demangler back-end is not needed when
doing full traces with libbacktrace, getting symbols with addr2line, or getting symbols with dbghelp.
| Library | CMake config | Windows | Linux | Info |
|---------|--------------|---------|-------|------|

View File

@ -3,8 +3,119 @@
#ifdef _MSC_VER
#define CPPTRACE_FORCE_NO_INLINE __declspec(noinline)
#define CPPTRACE_PFUNC __FUNCSIG__
#define CPPTRACE_MAYBE_UNUSED
#pragma warning(push)
#pragma warning(disable: 4505) // Unused local function
#else
#define CPPTRACE_FORCE_NO_INLINE __attribute__((noinline))
#define CPPTRACE_PFUNC __extension__ __PRETTY_FUNCTION__
#define CPPTRACE_MAYBE_UNUSED __attribute__((unused))
#endif
#include <stdlib.h>
#include <stdio.h>
#include <string>
#include <vector>
#include <sstream>
// Lightweight std::source_location.
struct source_location {
const char* const file;
//const char* const function; // disabled for now due to static constexpr restrictions
const int line;
constexpr source_location(
//const char* _function /*= __builtin_FUNCTION()*/,
const char* _file = __builtin_FILE(),
int _line = __builtin_LINE()
) : file(_file), /*function(_function),*/ line(_line) {}
};
CPPTRACE_MAYBE_UNUSED
static void primitive_assert_impl(
bool condition,
bool verify,
const char* expression,
const char* signature,
source_location location,
const char* message = nullptr
) {
if(!condition) {
const char* action = verify ? "verification" : "assertion";
const char* name = verify ? "verify" : "assert";
if(message == nullptr) {
fprintf(stderr, "Cpptrace %s failed at %s:%d: %s\n",
action, location.file, location.line, signature);
} else {
fprintf(stderr, "Cpptrace %s failed at %s:%d: %s: %s\n",
action, location.file, location.line, signature, message);
}
fprintf(stderr, " primitive_%s(%s);\n", name, expression);
abort();
}
}
template<typename T>
void nothing() {}
#define PHONY_USE(E) (nothing<decltype(E)>())
// Still present in release mode, nonfatal
#define internal_verify(c, ...) primitive_assert_impl(c, true, #c, CPPTRACE_PFUNC, {}, ##__VA_ARGS__)
#ifndef NDEBUG
#define CPPTRACE_PRIMITIVE_ASSERT(c, ...) \
primitive_assert_impl(c, false, #c, CPPTRACE_PFUNC, {}, ##__VA_ARGS__)
#else
#define CPPTRACE_PRIMITIVE_ASSERT(c, ...) PHONY_USE(c)
#endif
CPPTRACE_MAYBE_UNUSED
static std::vector<std::string> split(const std::string& s, const std::string& delims) {
std::vector<std::string> vec;
size_t old_pos = 0;
size_t pos = 0;
while((pos = s.find_first_of(delims, old_pos)) != std::string::npos) {
vec.emplace_back(s.substr(old_pos, pos - old_pos));
old_pos = pos + 1;
}
vec.emplace_back(std::string(s.substr(old_pos)));
return vec;
}
template<typename C>
CPPTRACE_MAYBE_UNUSED
static std::string join(const C& container, const std::string& delim) {
auto iter = std::begin(container);
auto end = std::end(container);
std::string str;
if(std::distance(iter, end) > 0) {
str += *iter;
while(++iter != end) {
str += delim;
str += *iter;
}
}
return str;
}
constexpr const char * const ws = " \t\n\r\f\v";
CPPTRACE_MAYBE_UNUSED
static std::string trim(const std::string& s) {
size_t l = s.find_first_not_of(ws);
size_t r = s.find_last_not_of(ws) + 1;
return s.substr(l, r - l);
}
CPPTRACE_MAYBE_UNUSED
static std::string to_hex(uintptr_t addr) {
std::stringstream sstream;
sstream<<std::hex<<uintptr_t(addr);
return std::move(sstream).str();
}
#ifdef _MSC_VER
#pragma warning(pop)
#endif
#endif

View File

@ -0,0 +1,161 @@
#ifdef CPPTRACE_GET_SYMBOLS_WITH_ADDR2LINE
#include <cpptrace/cpptrace.hpp>
#include "cpptrace_symbols.hpp"
#include "../platform/cpptrace_common.hpp"
#include <stdio.h>
#include <signal.h>
#include <vector>
#include <unordered_map>
#include <unistd.h>
#include <dlfcn.h>
#include <sys/types.h>
#include <sys/wait.h>
namespace cpptrace {
namespace detail {
struct dlframe {
std::string obj_path;
std::string symbol;
uintptr_t obj_base = 0;
uintptr_t raw_address = 0;
};
// aladdr queries are needed to get pre-ASLR addresses and targets to run addr2line on
std::vector<dlframe> backtrace_frames(const std::vector<void*>& addrs) {
// reference: https://github.com/bminor/glibc/blob/master/debug/backtracesyms.c
std::vector<dlframe> frames;
frames.reserve(addrs.size());
for(const auto addr : addrs) {
Dl_info info;
dlframe frame;
frame.raw_address = reinterpret_cast<uintptr_t>(addr);
if(dladdr(addr, &info)) {
// dli_sname and dli_saddr are only present with -rdynamic, sname will be included
// but we don't really need dli_saddr
frame.obj_path = info.dli_fname;
frame.obj_base = reinterpret_cast<uintptr_t>(info.dli_fbase);
frame.symbol = info.dli_sname ?: "?";
}
frames.push_back(frame);
}
return frames;
}
struct pipe_t {
union {
struct {
int read_end;
int write_end;
};
int data[2];
};
};
static_assert(sizeof(pipe_t) == 2 * sizeof(int), "Unexpected struct packing");
static std::string resolve_addresses(const std::string& addresses, const std::string& executable) {
pipe_t output_pipe;
pipe_t input_pipe;
internal_verify(pipe(output_pipe.data) == 0);
internal_verify(pipe(input_pipe.data) == 0);
pid_t pid = fork();
if(pid == -1) { return ""; } // error? TODO: Diagnostic
if(pid == 0) { // child
dup2(output_pipe.write_end, STDOUT_FILENO);
dup2(input_pipe.read_end, STDIN_FILENO);
close(output_pipe.read_end);
close(output_pipe.write_end);
close(input_pipe.read_end);
close(input_pipe.write_end);
close(STDERR_FILENO); // TODO: Might be worth conditionally enabling or piping
// TODO: Prevent against path injection?
execlp("addr2line", "addr2line", "-e", executable.c_str(), "-f", "-C", "-p", nullptr);
exit(1); // TODO: Diagnostic?
}
internal_verify(write(input_pipe.write_end, addresses.data(), addresses.size()) != -1);
close(input_pipe.read_end);
close(input_pipe.write_end);
close(output_pipe.write_end);
std::string output;
constexpr int buffer_size = 4096;
char buffer[buffer_size];
size_t count = 0;
while((count = read(output_pipe.read_end, buffer, buffer_size)) > 0) {
output.insert(output.end(), buffer, buffer + count);
}
// TODO: check status from addr2line?
waitpid(pid, nullptr, 0);
return output;
}
struct symbolizer::impl {
std::vector<stacktrace_frame> resolve_frames(const std::vector<void*>& frames) {
std::vector<stacktrace_frame> trace(frames.size(), stacktrace_frame { 0, 0, 0, "", "" });
std::vector<dlframe> dlframes = backtrace_frames(frames);
std::unordered_map<
std::string,
std::vector<std::pair<std::string, std::reference_wrapper<stacktrace_frame>>>
> entries;
for(size_t i = 0; i < dlframes.size(); i++) {
const auto& entry = dlframes[i];
entries[entry.obj_path].push_back({
to_hex(entry.raw_address - entry.obj_base),
trace[i]
});
// Set what is known for now, and resolutions from addr2line should overwrite
trace[i].filename = entry.obj_path;
trace[i].symbol = entry.symbol;
}
for(const auto& entry : entries) {
const auto& object_name = entry.first;
const auto& entries_vec = entry.second;
std::string address_input;
for(const auto& pair : entries_vec) {
address_input += pair.first;
address_input += '\n';
}
auto output = split(trim(resolve_addresses(address_input, object_name)), "\n");
internal_verify(output.size() == entries_vec.size());
for(size_t i = 0; i < output.size(); i++) {
// result will be of the form <identifier> " at " path:line
// path may be ?? if addr2line cannot resolve, line may be ?
const auto& line = output[i];
auto at_location = line.find(" at ");
internal_verify(at_location != std::string::npos);
auto symbol = line.substr(0, at_location);
auto colon = line.rfind(":");
internal_verify(colon != std::string::npos);
internal_verify(colon > at_location);
auto filename = line.substr(at_location + 4, colon - at_location - 4);
auto line_number = line.substr(colon + 1);
if(line_number != "?") {
entries_vec[i].second.get().line = std::stoi(line_number);
}
if(filename != "??") {
entries_vec[i].second.get().filename = filename;
}
if(symbol != "") {
entries_vec[i].second.get().symbol = symbol;
}
}
}
return trace;
}
};
symbolizer::symbolizer() : pimpl{new impl} {}
symbolizer::~symbolizer() = default;
//stacktrace_frame symbolizer::resolve_frame(void* addr) {
// return pimpl->resolve_frame(addr);
//}
std::vector<stacktrace_frame> symbolizer::resolve_frames(const std::vector<void*>& frames) {
return pimpl->resolve_frames(frames);
}
}
}
#endif