backtrace.h — Platform-Adaptive Stack Backtrace

Introduction

backtrace.h captures the current call stack and formats it into a human-readable multi-line string. The unwinding backend is selected at build time with the following priority: libunwind > execinfo (macOS/glibc) > stub (unsupported platforms). It is used internally by xLog() to provide stack traces on fatal errors.

Design Philosophy

  1. Build-Time Backend Selection — The backend is chosen via CMake-detected macros (XK_HAS_LIBUNWIND, XK_HAS_EXECINFO). This avoids runtime overhead and ensures the best available unwinder is used on each platform.

  2. Graceful Degradation — On platforms without libunwind or execinfo, a stub backend returns a "not supported" message rather than crashing. This ensures xBacktrace() is always safe to call.

  3. Automatic Frame Skipping — Internal frames (xBacktracexBacktraceSkipbt_capture) are automatically skipped so the output starts from the caller's perspective. The skip parameter allows additional frames to be skipped (useful when called through wrapper functions like xLog).

  4. Buffer-Based Output — The caller provides a buffer; no heap allocation occurs. This makes it safe to call from signal handlers, fatal error paths, and low-memory situations.

Architecture

graph TD
    API["xBacktrace() / xBacktraceSkip()"]
    SELECT{"Build-time selection"}
    LIBUNWIND["libunwind<br/>unw_step() loop"]
    EXECINFO["execinfo<br/>backtrace() + backtrace_symbols()"]
    STUB["stub<br/>'not supported' message"]
    BUF["User buffer<br/>(formatted output)"]

    API --> SELECT
    SELECT -->|XK_HAS_LIBUNWIND| LIBUNWIND
    SELECT -->|XK_HAS_EXECINFO| EXECINFO
    SELECT -->|fallback| STUB
    LIBUNWIND --> BUF
    EXECINFO --> BUF
    STUB --> BUF

    style LIBUNWIND fill:#50b86c,color:#fff
    style EXECINFO fill:#4a90d9,color:#fff
    style STUB fill:#f5a623,color:#fff

Implementation Details

Backend Selection

BackendMacroPlatformQuality
libunwindXK_HAS_LIBUNWINDLinux (with libunwind installed)Best — accurate unwinding, symbol + offset
execinfoXK_HAS_EXECINFOmacOS, Linux (glibc)Good — requires -rdynamic on Linux for symbols
stub(fallback)AnyMinimal — returns "not supported" message

Output Format

Each frame is formatted as:

#0 0x7fff8a1b2c3d symbol_name+0x1a
#1 0x7fff8a1b2c3d another_function+0x42
#2 0x7fff8a1b2c3d <unknown>
  • #N — Frame number (0 = most recent)
  • 0xADDR — Instruction pointer address
  • symbol+offset — Function name and offset (if available)
  • <unknown> — When symbol resolution fails

Frame Skipping

Call stack:
  bt_capture()         ← INTERNAL_SKIP (2 frames)
  xBacktraceSkip()     ← INTERNAL_SKIP
  xLog()               ← user skip = 2 (from xLog)
  user_function()      ← first visible frame
  main()

xBacktrace() calls xBacktraceSkip(0, ...), which adds INTERNAL_SKIP = 2 to skip its own frames. xLog() calls xBacktraceSkip(2, ...) to also skip xLog and xLogSetCallback frames.

libunwind Backend

Uses unw_getcontext()unw_init_local()unw_step() loop. For each frame:

  • unw_get_reg(UNW_REG_IP) — Get instruction pointer
  • unw_get_proc_name() — Get symbol name and offset

execinfo Backend

Uses backtrace() to capture frame addresses, then backtrace_symbols() to resolve names. On Linux, link with -rdynamic to export symbols for resolution.

API Reference

Functions

FunctionSignatureDescriptionThread Safety
xBacktraceint xBacktrace(char *buf, size_t size)Capture the call stack into buf. Equivalent to xBacktraceSkip(0, buf, size).Thread-safe (uses only local/stack state)
xBacktraceSkipint xBacktraceSkip(int skip, char *buf, size_t size)Capture the call stack, skipping skip additional frames beyond internal frames.Thread-safe

Parameters

ParameterDescription
skipNumber of additional frames to skip (0 = no extra skipping)
bufDestination buffer. May be NULL (returns 0).
sizeSize of buf in bytes.

Return Value

Number of bytes written (excluding trailing \0), or 0 if buf is NULL or size is 0.

Usage Examples

Capture and Print Stack Trace

#include <stdio.h>
#include <xbase/backtrace.h>

void foo(void) {
    char buf[4096];
    int n = xBacktrace(buf, sizeof(buf));
    if (n > 0) {
        printf("Stack trace:\n%s", buf);
    }
}

void bar(void) { foo(); }

int main(void) {
    bar();
    return 0;
}

Output (with execinfo on macOS):

Stack trace:
#0 0x100003f20 foo+0x20
#1 0x100003f80 bar+0x10
#2 0x100003fa0 main+0x10

Skip Wrapper Frames

#include <xbase/backtrace.h>

// Custom error reporter that skips its own frame
void report_error(const char *msg) {
    char bt[2048];
    xBacktraceSkip(1, bt, sizeof(bt)); // Skip report_error itself
    fprintf(stderr, "Error: %s\nBacktrace:\n%s", msg, bt);
}

Use Cases

  1. Fatal Error DiagnosticsxLog() captures a backtrace on fatal errors, providing immediate context for debugging crashes.

  2. Debug Assertions — Custom assertion macros can include xBacktrace() to show where the assertion failed.

  3. Memory Leak Detection — Record allocation backtraces to identify where leaked objects were created.

Best Practices

  • Provide a large enough buffer. 4096 bytes is usually sufficient for 20-30 frames. The output is truncated (not corrupted) if the buffer is too small.
  • Link with -rdynamic on Linux. Without it, the execinfo backend shows only addresses, not symbol names.
  • Install libunwind for best results on Linux. It provides more accurate unwinding than execinfo, especially through optimized code and signal handlers.
  • Don't call from signal handlers with execinfo. backtrace_symbols() calls malloc(), which is not async-signal-safe. libunwind is safer in this context.

Comparison with Other Libraries

Featurexbase backtrace.hglibc backtrace()libunwindBoost.StacktraceWindows CaptureStackBackTrace
PlatformmacOS + Linux + stubLinux (glibc)Linux + macOSCross-platformWindows
AccuracyBackend-dependentGood (glibc)ExcellentBackend-dependentGood
Symbol ResolutionBuilt-inbacktrace_symbols()unw_get_proc_name()Backend-dependentSymFromAddr()
AllocationNone (user buffer)malloc() for symbolsNoneHeapNone
Signal Safetylibunwind: yes, execinfo: noNo (malloc)YesNoYes
Frame SkippingBuilt-in (skip param)ManualManualManualFramesToSkip param

Key Differentiator: xbase's backtrace provides a simple, buffer-based API with automatic frame skipping and graceful degradation across platforms. It's designed for integration into error reporting paths where heap allocation is undesirable.