calltrace-rs 1.1.4

High-performance function call tracing library for C/C++ applications using GCC instrumentation with Rust safety guarantees
Documentation

CallTrace - High-Performance Function Call Tracing Library

Crates.io Documentation License Build Status Coverage Rust

A Rust-based library for tracing function calls with argument capture and return value tracking, built using GCC's -finstrument-functions feature. Provides memory-safe, high-performance function tracing for C/C++ applications with comprehensive crash analysis.

Features

  • ๐Ÿฆ€ Memory Safe: Written in Rust with guaranteed memory safety
  • โšก High Performance: Zero-cost abstractions with minimal overhead
  • ๐Ÿ” Argument Capture: Captures function arguments using DWARF debug info
  • ๐Ÿ”„ Return Value Tracing: Captures function return values in RAX/XMM0 registers
  • ๐Ÿท๏ธ Symbol Resolution: Resolves function names using dladdr() for readable output
  • ๐Ÿงต Thread Safe: Complete multi-threading support with thread relationship tracking
  • ๐Ÿ“Š Structured Output: JSON output with hierarchical call trees and precise timing
  • ๐Ÿ›ก๏ธ Robust Error Handling: Graceful degradation with error tracking
  • ๐Ÿ’ฅ Crash Analysis: Comprehensive crash reporting with thread identification and stack traces
  • ๐Ÿงจ Signal Handling: Captures crashes (SIGSEGV, SIGABRT, etc.) with full context

Installation

Using the Prebuilt Library

Add CallTrace to your project dependencies:

[dependencies]
calltrace = "1.0"

Using as Dynamic Library

Download the latest release from GitHub Releases or build from source:

git clone https://github.com/signal-slot/calltrace.git
cd calltrace
cargo build --release

The compiled library will be available at target/release/libcalltrace.so.

Quick Start

1. Build the Library

cargo build --release

2. Compile Your Program with Instrumentation

โš ๏ธ Important: Use -rdynamic flag for proper symbol resolution:

gcc -rdynamic -finstrument-functions -g your_program.c -o your_program

Without -rdynamic, function names will appear as addresses like 0x401234 instead of readable names like main.

CMake Integration

For CMake-based projects, there are several approaches to set the required compilation flags:

Method 1: Command Line Configuration (Recommended for entire projects)

cmake -B build \
  -DCMAKE_C_FLAGS="-finstrument-functions -rdynamic -g" \
  -DCMAKE_CXX_FLAGS="-finstrument-functions -rdynamic -g" \
  -DCMAKE_EXE_LINKER_FLAGS="-rdynamic"
cmake --build build

Method 2: CMakeLists.txt Target-Specific

# Add to your CMakeLists.txt for specific targets
target_compile_options(your_target PRIVATE
    -finstrument-functions
    -rdynamic
    -g
)
target_link_options(your_target PRIVATE -rdynamic)

# Or for all targets in the project
add_compile_options(-finstrument-functions -rdynamic -g)
add_link_options(-rdynamic)

Method 3: CMake Toolchain File

# Create a toolchain file (calltrace-toolchain.cmake)
set(CMAKE_C_FLAGS_INIT "-finstrument-functions -rdynamic -g")
set(CMAKE_CXX_FLAGS_INIT "-finstrument-functions -rdynamic -g")
set(CMAKE_EXE_LINKER_FLAGS_INIT "-rdynamic")

# Use with: cmake -DCMAKE_TOOLCHAIN_FILE=calltrace-toolchain.cmake

โš ๏ธ Performance Note: For large applications (like Qt GUI apps), CallTrace may significantly increase startup time due to extensive function call tracing. Consider using with smaller, targeted applications or implementing function filtering.

3. Run with CallTrace

CALLTRACE_OUTPUT=trace.json LD_PRELOAD=./target/release/libcalltrace.so ./your_program

4. Analyze the Results

The trace will be saved to trace.json with a complete hierarchical call tree including:

  • Function names and addresses (resolved using symbol table)
  • Call timing and duration (microsecond precision)
  • Thread relationships and creation info
  • Function arguments (when CALLTRACE_CAPTURE_ARGS=1)
  • Return values (captured from RAX/XMM0 registers)
  • Crash information with thread identification (if program crashes)

API Overview

CallTrace provides a simple C-compatible API for integration:

use calltrace::{calltrace_init, calltrace_cleanup, CallTraceConfig};

// Initialize tracing with custom configuration
let config = CallTraceConfig {
    output_file: Some("trace.json".to_string()),
    capture_args: true,
    max_depth: 1000,
    pretty_json: true,
    debug_mode: false,
};

unsafe {
    calltrace_init(&config);
    // Your instrumented code runs here
    calltrace_cleanup();
}

Environment Variables Configuration

For applications that can't modify CallTrace initialization, use environment variables:

Variable Description Default
CALLTRACE_OUTPUT Output file path Required for file output
CALLTRACE_CAPTURE_ARGS Enable argument capture false
CALLTRACE_MAX_DEPTH Maximum call depth 100
CALLTRACE_PRETTY_JSON Pretty-print JSON true
CALLTRACE_DEBUG Enable debug output false

Example with argument capture:

CALLTRACE_CAPTURE_ARGS=1 CALLTRACE_OUTPUT=trace.json LD_PRELOAD=./target/debug/libcalltrace.so ./your_program

Example Output

{
  "metadata": {
    "start_time": "1755336097.437215",
    "end_time": "1755336097.437274",
    "duration_ms": 0.059,
    "process_info": {
      "pid": 144031,
      "architecture": "x86_64",
      "executable_path": null
    },
    "calltrace_version": "1.0.0"
  },
  "threads": [
    {
      "thread_id": 144031,
      "thread_name": "Thread-144031",
      "is_main_thread": false,
      "start_time": "1755336097.437220",
      "call_tree": {
        "id": 1,
        "function": "main",
        "address": "0x55b0242fb234",
        "call_site": "0x7f1d550c23bb",
        "start_time": "1755336097.437226",
        "end_time": "1755336097.437265",
        "duration_us": 39,
        "call_depth": 0,
        "arguments": [],
        "return_value": null,
        "children": [
          {
            "id": 2,
            "function": "simple_return",
            "address": "0x55b0242fb1d9",
            "call_site": "0x55b0242fb26e",
            "start_time": "1755336097.437249",
            "end_time": "1755336097.437253",
            "duration_us": 4,
            "call_depth": 1,
            "arguments": [],
            "return_value": null,
            "children": []
          }
        ]
      }
    }
  ],
  "statistics": {
    "total_threads": 1,
    "total_function_calls": 0,
    "total_nodes": 2,
    "session_duration_us": 59
  }
}

Crash Handling

CallTrace automatically installs signal handlers for common crash signals and provides comprehensive crash analysis:

Supported Crash Signals

  • SIGSEGV - Segmentation fault
  • SIGABRT - Abort signal
  • SIGILL - Illegal instruction
  • SIGFPE - Floating point exception
  • SIGBUS - Bus error
  • SIGTRAP - Trace/breakpoint trap

Crash Information

When a crash occurs, CallTrace captures:

  • Thread Identification: Which thread crashed (using gettid())
  • Signal Details: Signal number and human-readable name
  • Register Context: Complete x86_64 register dump at crash time
  • Stack Backtrace: Function call stack with symbol resolution
  • Call Tree State: Current function call hierarchy when crash occurred
  • Timing Information: Precise crash timestamp

Example Crash Output

{
  "metadata": { /* normal session metadata */ },
  "threads": [ /* normal thread traces */ ],
  "statistics": { /* normal session statistics */ },
  "crash": {
    "signal": 11,
    "signal_name": "SIGSEGV (Segmentation fault)",
    "thread_id": 12345,
    "register_context": {
      "rdi": 140737080113288,
      "rsi": 140737080115432,
      "rsp": 140737080112384,
      "rip": 140324016650424,
      /* ... complete register dump ... */
    },
    "backtrace": [
      {
        "address": "0x0000558a4b2a1250",
        "function_name": "cause_crash",
        "library_name": "./my_program",
        "offset": "0x15"
      }
    ],
    "crash_time": "2025-08-16 22:07:52 JST",
    "crash_timestamp": 1755349672.89132
  }
}

Integration with Normal Tracing

Crash information is seamlessly integrated with normal function tracing:

  1. Thread Correlation: The thread_id in crash info matches thread IDs in the threads array
  2. Call Tree Preservation: Function call hierarchy is preserved up to the crash point
  3. Unified Output: Single JSON file contains both normal trace and crash analysis
  4. Timing Continuity: Crash timestamp aligns with session timing information

Examples and Documentation

CallTrace includes comprehensive examples demonstrating various use cases:

Additional Resources

Architecture

CallTrace uses:

  • GCC Instrumentation: Hooks __cyg_profile_func_enter/exit for function tracing
  • Symbol Resolution: Uses dladdr() for simple and reliable function name resolution
  • DWARF Analysis: Parses debug information for function signatures (using gimli crate, when argument capture is enabled)
  • Register Reading: Captures x86_64 registers (RAX, XMM0) for argument and return value extraction
  • Signal Handling: Installs crash handlers for comprehensive crash analysis with thread identification
  • Thread-Safe Storage: Uses atomic operations and Arc<RwLock<T>> for concurrent access
  • Structured Serialization: Uses serde for type-safe JSON output with precise timing

Performance Optimizations

  • Atomic Fast-Path: Uses atomic flags to minimize overhead when features are disabled
  • Function Info Caching: LRU cache for DWARF information to avoid repeated parsing
  • Zero-Copy Operations: Minimal allocations in the hot path
  • Inline Assembly: Direct register capture using inline assembly for maximum performance

Development

Running Tests

# Build the library
cargo build

# Compile test program with proper flags
gcc -rdynamic -finstrument-functions -g test_simple_return.c -o test_simple_return

# Run with tracing
CALLTRACE_OUTPUT=test_output.json LD_PRELOAD=./.target/debug/libcalltrace.so ./test_simple_return

# Run with argument capture enabled
CALLTRACE_CAPTURE_ARGS=1 CALLTRACE_OUTPUT=test_args.json LD_PRELOAD=./.target/debug/libcalltrace.so ./test_simple_return

Creating Test Programs

Example test program (test_simple_return.c):

#include <stdio.h>

int simple_return() {
    printf("Inside simple_return, about to return 42\n");
    return 42;
}

int main() {
    printf("Starting simple return test\n");
    int result = simple_return();
    printf("Got result: %d\n", result);
    return 0;
}

Cargo Features

  • dwarf_support: Enable DWARF debug information parsing (default)

Troubleshooting

Function names show as addresses (e.g., 0x401234)

Problem: Function names appear as hexadecimal addresses instead of readable names like main.

Solution: Compile your program with the -rdynamic flag:

gcc -rdynamic -finstrument-functions -g your_program.c -o your_program

The -rdynamic flag exports symbols to the dynamic symbol table, which allows dladdr() to resolve function names.

No argument capture

Problem: Arguments show as empty arrays even with CALLTRACE_CAPTURE_ARGS=1.

Solutions:

  1. Ensure DWARF debug information is available (use -g flag)
  2. Verify function signatures are available in debug info
  3. Check that the target architecture is x86_64

Performance Impact

Problem: Tracing causes significant slowdown.

Solutions:

  1. Disable argument capture if not needed (default behavior)
  2. Use release build: LD_PRELOAD=./target/release/libcalltrace.so
  3. Set CALLTRACE_MAX_DEPTH to limit deep recursion
  4. Consider filtering functions at compile time

Crash handling conflicts

Problem: CallTrace crash handlers not capturing crashes or conflicts with existing handlers.

Solutions:

  1. CallTrace installs signal handlers early during library initialization
  2. Existing handlers are preserved and restored after crash analysis
  3. Use CALLTRACE_DEBUG=1 to see handler installation messages
  4. For applications with custom signal handlers, ensure CallTrace loads first

Missing thread identification in crashes

Problem: Thread ID shows as 0 or incorrect value in crash reports.

Solutions:

  1. Ensure running on Linux (uses gettid() system call)
  2. Verify CallTrace is properly initialized before crash occurs
  3. Check that crash occurs in instrumented code (compiled with -finstrument-functions)

Technical Notes

  • Return Value Limitations: Due to GCC's instrumentation timing, return values are captured after function cleanup, which may result in overwritten registers for some functions.
  • Thread Safety: All operations are thread-safe and support multi-threaded applications.
  • Memory Usage: Function info is cached with LRU eviction to prevent memory bloat.
  • Crash Handler Behavior: Signal handlers are installed once and preserve existing handlers. Thread identification uses gettid() for precise thread correlation.
  • Platform Support: Currently supports x86_64 Linux. Other platforms may require register capture adjustments.

License

This project is licensed under either of

at your option.

Contributing

Contributions are welcome! To contribute to CallTrace:

  1. Fork the repository and create a feature branch
  2. Follow conventional commits - see Commit Convention
  3. Write tests for your changes and ensure all tests pass: cargo test
  4. Update documentation if you're adding new features
  5. Submit a pull request with a clear description of your changes

Development Setup

# Clone and build
git clone https://github.com/signal-slot/calltrace.git
cd calltrace
cargo build

# Run tests
cargo test

# Run examples
chmod +x examples/run_all_examples.sh
./examples/run_all_examples.sh

Code Quality

The project maintains high code quality standards:

  • All code is formatted with cargo fmt
  • Lints are checked with cargo clippy
  • Tests must pass on multiple Rust versions
  • Security audits run automatically via CI/CD

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.