From a4cd5661f82b2de039d94ad949d875b91ed88af4 Mon Sep 17 00:00:00 2001 From: agent Date: Tue, 27 Oct 2026 23:12:59 +0300 Subject: [PATCH 1/2] Callstack: add stack capture and thread sampling StackTrace captures the calling thread with _Unwind_Backtrace, symbolizes addresses with dladdr and __cxa_demangle, and samples other threads through a real-time signal. Each target writes into its own slot; the sampler also reads /proc task name, scheduler state and wait channel, and keeps unanswered threads in the result. Windows captures stacks and module offsets but reports that remote sampling is unsupported. Debugger detection is exposed separately. --- Callstack/private/StackTrace.cpp | 268 +++++++++++++++++++++++++++++++ Callstack/public/StackTrace.hpp | 41 +++++ 2 files changed, 309 insertions(+) create mode 100644 Callstack/private/StackTrace.cpp create mode 100644 Callstack/public/StackTrace.hpp diff --git a/Callstack/private/StackTrace.cpp b/Callstack/private/StackTrace.cpp new file mode 100644 index 00000000..86a55731 --- /dev/null +++ b/Callstack/private/StackTrace.cpp @@ -0,0 +1,268 @@ +#include "StackTrace.hpp" + +#include +#include + +#if defined(__linux__) || defined(__ANDROID__) + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace tp { + + namespace { + struct UnwindState { + uintptr_t* frames; + size_t count; + size_t capacity; + }; + + _Unwind_Reason_Code collectFrame(_Unwind_Context* context, void* argument) { + auto* state = static_cast(argument); + const uintptr_t address = (uintptr_t) _Unwind_GetIP(context); + if (address == 0) return _URC_NO_REASON; + if (state->count >= state->capacity) return _URC_END_OF_STACK; + state->frames[state->count++] = address; + return _URC_NO_REASON; + } + + struct SampleSlot { + std::atomic tid{ 0 }; + std::atomic depth{ 0 }; + std::atomic answered{ false }; + uintptr_t frames[StackTrace::kMaxFrames] = {}; + }; + + constexpr size_t kMaxSampledThreads = 256; + + struct SampleBoard { + SampleSlot slots[kMaxSampledThreads]; + std::atomic used{ 0 }; + }; + + SampleBoard& sampleBoard() { + static SampleBoard shared; + return shared; + } + + std::mutex& samplingMutex() { + static std::mutex shared; + return shared; + } + + int sampleSignal() { return SIGRTMIN + 3; } + + int currentTid() { return (int) syscall(SYS_gettid); } + + SampleSlot* slotOf(int tid) { + SampleBoard& board = sampleBoard(); + const size_t used = std::min(board.used.load(std::memory_order_acquire), kMaxSampledThreads); + for (size_t index = 0; index < used; index++) + if (board.slots[index].tid.load(std::memory_order_relaxed) == tid) return &board.slots[index]; + return nullptr; + } + + void answerSample(int, siginfo_t*, void*) { + const int savedErrno = errno; + if (SampleSlot* slot = slotOf(currentTid())) { + slot->depth.store( + StackTrace::captureCallingThread(slot->frames, StackTrace::kMaxFrames), std::memory_order_relaxed + ); + slot->answered.store(true, std::memory_order_release); + } + errno = savedErrno; + } + + std::string firstLine(const std::filesystem::path& path) { + std::ifstream file(path); + std::string line; + std::getline(file, line); + return line; + } + + std::string schedulerState(const std::filesystem::path& task) { + std::ifstream file(task / "status"); + std::string line; + while (std::getline(file, line)) + if (line.rfind("State:", 0) == 0) return line.substr(line.find_first_not_of(" \t", 6)); + return "unknown"; + } + + std::vector processThreadIds() { + std::vector tids; + std::error_code error; + for (const auto& entry : std::filesystem::directory_iterator("/proc/self/task", error)) { + const std::string name = entry.path().filename().string(); + if (!name.empty() && std::all_of(name.begin(), name.end(), [](char c) { + return std::isdigit((unsigned char) c); + })) + tids.push_back(std::stoi(name)); + } + std::sort(tids.begin(), tids.end()); + return tids; + } + + void armSlots(const std::vector& tids) { + SampleBoard& board = sampleBoard(); + for (size_t index = 0; index < tids.size(); index++) { + SampleSlot& slot = board.slots[index]; + slot.answered.store(false, std::memory_order_relaxed); + slot.depth.store(0, std::memory_order_relaxed); + slot.tid.store(tids[index], std::memory_order_relaxed); + } + board.used.store(tids.size(), std::memory_order_release); + } + + void waitForAnswers(size_t count, double timeoutMs) { + SampleBoard& board = sampleBoard(); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::duration(timeoutMs); + const auto allAnswered = [&]() { + for (size_t index = 0; index < count; index++) + if (!board.slots[index].answered.load(std::memory_order_acquire)) return false; + return true; + }; + while (!allAnswered() && std::chrono::steady_clock::now() < deadline) + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + + ThreadSample collect(const SampleSlot& slot, int tid) { + const std::filesystem::path task = "/proc/self/task/" + std::to_string(tid); + ThreadSample sample; + sample.tid = tid; + sample.name = firstLine(task / "comm"); + sample.state = schedulerState(task); + sample.waitChannel = firstLine(task / "wchan"); + sample.answered = slot.answered.load(std::memory_order_acquire); + if (sample.answered) + sample.frames.assign(slot.frames, slot.frames + std::min(slot.depth.load(), StackTrace::kMaxFrames)); + return sample; + } + } + + size_t StackTrace::captureCallingThread(uintptr_t* frames, size_t capacity) { + UnwindState state{ frames, 0, capacity }; + _Unwind_Backtrace(collectFrame, &state); + return state.count; + } + + FrameSymbol StackTrace::symbolize(uintptr_t address) { + FrameSymbol symbol; + Dl_info info{}; + if (!dladdr((void*) (address - 1), &info)) return symbol; + if (info.dli_fname) symbol.module = std::filesystem::path(info.dli_fname).filename().string(); + symbol.moduleOffset = address - (uintptr_t) info.dli_fbase; + if (!info.dli_sname) return symbol; + int status = 0; + char* demangled = abi::__cxa_demangle(info.dli_sname, nullptr, nullptr, &status); + symbol.function = status == 0 && demangled ? demangled : info.dli_sname; + std::free(demangled); + return symbol; + } + + bool StackTrace::canSampleThreads() { return true; } + + bool StackTrace::isDebuggerAttached() { + std::ifstream status("/proc/self/status"); + std::string line; + while (std::getline(status, line)) + if (line.rfind("TracerPid:", 0) == 0) return std::strtol(line.c_str() + 10, nullptr, 10) != 0; + return false; + } + + void StackTrace::prepareThreadSampling() { + static std::once_flag prepared; + std::call_once(prepared, []() { + struct sigaction action{}; + action.sa_sigaction = answerSample; + action.sa_flags = SA_SIGINFO | SA_RESTART; + sigemptyset(&action.sa_mask); + sigaction(sampleSignal(), &action, nullptr); + uintptr_t warmup[4]; + captureCallingThread(warmup, 4); + }); + } + + std::vector StackTrace::sampleAllThreads(double answerTimeoutMs) { + prepareThreadSampling(); + std::lock_guard lock(samplingMutex()); + std::vector tids = processThreadIds(); + tids.erase(std::remove(tids.begin(), tids.end(), currentTid()), tids.end()); + if (tids.size() > kMaxSampledThreads) tids.resize(kMaxSampledThreads); + + armSlots(tids); + for (int tid : tids) + syscall(SYS_tgkill, getpid(), tid, sampleSignal()); + waitForAnswers(tids.size(), answerTimeoutMs); + + std::vector samples; + for (size_t index = 0; index < tids.size(); index++) + samples.push_back(collect(sampleBoard().slots[index], tids[index])); + sampleBoard().used.store(0, std::memory_order_release); + return samples; + } + + int StackTrace::callingThreadId() { return currentTid(); } +} + +#elif defined(_WIN32) + +#include + +namespace tp { + + size_t StackTrace::captureCallingThread(uintptr_t* frames, size_t capacity) { + return (size_t) CaptureStackBackTrace(1, (DWORD) std::min(capacity, 62), (void**) frames, nullptr); + } + + FrameSymbol StackTrace::symbolize(uintptr_t address) { + FrameSymbol symbol; + HMODULE module = nullptr; + if (!GetModuleHandleExA( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + (LPCSTR) address, + &module + )) + return symbol; + char path[MAX_PATH] = {}; + GetModuleFileNameA(module, path, MAX_PATH); + const std::string full = path; + symbol.module = full.substr(full.find_last_of("\\/") + 1); + symbol.moduleOffset = address - (uintptr_t) module; + return symbol; + } + + bool StackTrace::canSampleThreads() { return false; } + bool StackTrace::isDebuggerAttached() { return IsDebuggerPresent() != 0; } + void StackTrace::prepareThreadSampling() {} + std::vector StackTrace::sampleAllThreads(double) { return {}; } + int StackTrace::callingThreadId() { return (int) GetCurrentThreadId(); } +} + +#endif + +namespace tp { + + std::string StackTrace::describe(uintptr_t address) { + const FrameSymbol symbol = symbolize(address); + std::ostringstream text; + text << "0x" << std::hex << address; + if (!symbol.module.empty()) text << " " << symbol.module << "+0x" << symbol.moduleOffset; + text << std::dec; + if (!symbol.function.empty()) text << " " << symbol.function; + if (!symbol.file.empty()) text << " " << symbol.file << ":" << symbol.line; + return text.str(); + } +} diff --git a/Callstack/public/StackTrace.hpp b/Callstack/public/StackTrace.hpp new file mode 100644 index 00000000..802ee790 --- /dev/null +++ b/Callstack/public/StackTrace.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include +#include + +namespace tp { + + struct FrameSymbol { + std::string module; + uintptr_t moduleOffset = 0; + std::string function; + std::string file; + size_t line = 0; + }; + + struct ThreadSample { + int tid = 0; + std::string name; + std::string state; + std::string waitChannel; + bool answered = false; + std::vector frames; + }; + + class StackTrace { + public: + static constexpr size_t kMaxFrames = 64; + + static size_t captureCallingThread(uintptr_t* frames, size_t capacity); + [[nodiscard]] static FrameSymbol symbolize(uintptr_t address); + [[nodiscard]] static std::string describe(uintptr_t address); + + [[nodiscard]] static bool canSampleThreads(); + [[nodiscard]] static bool isDebuggerAttached(); + static void prepareThreadSampling(); + [[nodiscard]] static std::vector sampleAllThreads(double answerTimeoutMs = 200); + [[nodiscard]] static int callingThreadId(); + }; +} -- 2.49.1 From e05fb9c7142c746bb7e0a742a286c791086e9256 Mon Sep 17 00:00:00 2001 From: agent Date: Tue, 27 Oct 2026 23:12:59 +0300 Subject: [PATCH 2/2] Callstack: build CallStackCapture on StackTrace CallStackCapture now delegates capture and symbolization to StackTrace instead of execinfo plus an addr2line subprocess. The same implementation is enabled on Linux and Android. Reports contain the module, module-relative offset and demangled function; dladdr supplies no source line. --- Callstack/private/Callstack.cpp | 116 +++++++------------------------- 1 file changed, 25 insertions(+), 91 deletions(-) diff --git a/Callstack/private/Callstack.cpp b/Callstack/private/Callstack.cpp index 48977489..b34ae3e6 100644 --- a/Callstack/private/Callstack.cpp +++ b/Callstack/private/Callstack.cpp @@ -54,9 +54,8 @@ CallStackCapture::CallStackCapture() { mBuffLoad = 0; mBuffLen = STACKS_LENGTH; mBuff = (CallStack*) malloc(mBuffLen * sizeof(CallStack)); - - mErrorSnapshot.frames[0] = (alni) & errorNoSpaceLeft; + mErrorSnapshot.frames[0] = (alni) &errorNoSpaceLeft; platformInit(); @@ -103,110 +102,45 @@ void CallStackCapture::clear() { mSymbols.removeAll(); } -CallStackCapture::~CallStackCapture() { - free(mBuff); +CallStackCapture::~CallStackCapture() { + free(mBuff); initialized = false; } // ---------------------------------- Platform Depended ---------------------------------- // -#if defined(ENV_OS_LINUX) +#if defined(ENV_OS_LINUX) || defined(ENV_OS_ANDROID) + +#include "StackTrace.hpp" #include -#include -#include -#include -void CallStackCapture::platformInit() { +namespace { + void copyTail(char* out, const std::string& text) { + const size_t length = text.size(); + const size_t start = + length > CallStackCapture::MAX_DEBUG_INFO_LEN ? length - CallStackCapture::MAX_DEBUG_INFO_LEN : 0; + std::strcpy(out, text.c_str() + start); + } } -void CallStackCapture::platformDeinit() { -} +void CallStackCapture::platformInit() {} + +void CallStackCapture::platformDeinit() {} void CallStackCapture::platformWriteStackTrace(CallStack* stack) { - auto depth = backtrace((void**) stack->frames, (int) MAX_CALL_DEPTH_CAPTURE - 1); + uintptr_t frames[MAX_CALL_DEPTH_CAPTURE]; + const size_t depth = StackTrace::captureCallingThread(frames, MAX_CALL_DEPTH_CAPTURE - 1); + for (size_t index = 0; index < depth; index++) + stack->frames[index] = (FramePointer) frames[index]; stack->frames[depth] = 0; } -static void getGetSourceFromBinaryAddress(const char* binary, const char* address, char* file, ualni* line) { - static char buff[1024]; - - snprintf(buff, sizeof(buff), "addr2line -e %s -a %s", binary, address); - FILE* pipe = popen(buff, "r"); - if (pipe) { - fgets(buff, sizeof(buff), pipe); - fgets(buff, sizeof(buff), pipe); - pclose(pipe); - char* linePtr = strchr(buff, ':'); - - printf("%s\n", buff); - - if (linePtr != nullptr && buff[0] != '?' && buff[1] != '?' && buff[2] != ':') { - *linePtr = '\0'; - auto sourceLen = std::strlen(buff); - std::strcpy(file, buff + ((sourceLen > CallStackCapture::MAX_DEBUG_INFO_LEN) ? (sourceLen - CallStackCapture::MAX_DEBUG_INFO_LEN) : 0)); - *line = strtoul(linePtr + 1, nullptr, 10); - return; - } - } - - std::strcpy(file, "unresolved"); - *line = 0; -} - -static void getDemangledName(const char* func, char* out) { - int status; - size_t funcDemangledSize = CallStackCapture::MAX_DEBUG_INFO_LEN; - char* funcDemangled = (char*) malloc(funcDemangledSize); - char* ret = abi::__cxa_demangle(func, funcDemangled, &funcDemangledSize, &status); - if (status == 0) { - funcDemangled = ret; - auto funcLen = std::strlen(funcDemangled); - std::strcpy(out, funcDemangled + ((funcLen > CallStackCapture::MAX_DEBUG_INFO_LEN) ? (funcLen - CallStackCapture::MAX_DEBUG_INFO_LEN) : 0)); - free(ret); - return; - } - auto funcLen = std::strlen(func); - std::strcpy(out, func + ((funcLen > CallStackCapture::MAX_DEBUG_INFO_LEN) ? (funcLen - CallStackCapture::MAX_DEBUG_INFO_LEN) : 0)); - free(funcDemangled); -} - void CallStackCapture::platformWriteDebugSymbols(FramePointer frame, DebugSymbols* out) { - void* addrList[1] = { (void*) frame }; - auto symbolsArray = backtrace_symbols(addrList, 1); - - // 'bin(fun+addr)' - char* bin = *symbolsArray; - char* func = nullptr; - char* offset = nullptr; - - // 'bin fun+addr' - for (char* p = bin; *p; ++p) { - if (*p == '(') { - *p = 0; - func = p + 1; - } else if (*p == '+') { - offset = p; - } else if (*p == ')' && offset) { - *p = 0; - } - } - - if (func && offset) { - getGetSourceFromBinaryAddress(bin, func, out->file, &out->line); - - if (offset != func) { - *offset = 0; - getDemangledName(func, out->function); - } else { - std::strcpy(out->function, "unresolved"); - } - } else { - std::strcpy(out->file, "unresolved"); - std::strcpy(out->function, "unresolved"); - } - - free(symbolsArray); + const FrameSymbol symbol = StackTrace::symbolize((uintptr_t) frame); + copyTail(out->file, symbol.module.empty() ? std::string("unresolved") : symbol.module); + copyTail(out->function, symbol.function.empty() ? std::string("unresolved") : symbol.function); + out->line = symbol.line; } void CallStackCapture::printSnapshot(const CallStack* snapshot) { @@ -238,7 +172,7 @@ void CallStackCapture::platformInit() { SymInitialize(process, NULL, TRUE); } -void CallStackCapture::platformDeinit() { +void CallStackCapture::platformDeinit() { HANDLE process = GetCurrentProcess(); SymCleanup(process); } -- 2.49.1