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); } 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(); + }; +}