diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7b38a8389..7ae456796 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,15 +60,15 @@ jobs: steps: - uses: actions/checkout@v6 - name: Install Dependencies - run: sudo apt-get install --no-install-recommends libncursesw5-dev libhwloc-dev libnl-3-dev libnl-genl-3-dev libsensors-dev libcap-dev + run: sudo apt-get install --no-install-recommends libncursesw5-dev libhwloc-dev libnl-3-dev libnl-genl-3-dev libsensors-dev libcap-dev libiberty-dev libunwind-dev - name: Bootstrap run: ./autogen.sh - name: Configure - run: ./configure --enable-werror --enable-openvz --enable-vserver --enable-ancient-vserver --enable-unicode --enable-hwloc --enable-delayacct --enable-sensors --enable-capabilities || ( cat config.log; exit 1; ) + run: ./configure --enable-werror --enable-openvz --enable-vserver --enable-ancient-vserver --enable-unicode --enable-hwloc --enable-delayacct --enable-sensors --enable-capabilities --enable-backtrace --enable-demangling || ( cat config.log; exit 1; ) - name: Build run: make -k - name: Distcheck - run: make distcheck DISTCHECK_CONFIGURE_FLAGS='--enable-werror --enable-openvz --enable-vserver --enable-ancient-vserver --enable-unicode --enable-hwloc --enable-delayacct --enable-sensors --enable-capabilities' + run: make distcheck DISTCHECK_CONFIGURE_FLAGS='--enable-werror --enable-openvz --enable-vserver --enable-ancient-vserver --enable-unicode --enable-hwloc --enable-delayacct --enable-sensors --enable-capabilities --enable-backtrace --enable-demangling' build-ubuntu-latest-full-featured-clang: runs-on: ubuntu-latest @@ -83,15 +83,15 @@ jobs: sudo add-apt-repository "deb http://apt.llvm.org/${ubuntu_codename}/ llvm-toolchain-${ubuntu_codename}-22 main" -y sudo apt-get update -q - name: Install Dependencies - run: sudo apt-get install --no-install-recommends clang-22 libncursesw5-dev libhwloc-dev libnl-3-dev libnl-genl-3-dev libsensors-dev libcap-dev + run: sudo apt-get install --no-install-recommends clang-22 libncursesw5-dev libhwloc-dev libnl-3-dev libnl-genl-3-dev libsensors-dev libcap-dev libiberty-dev libunwind-dev - name: Bootstrap run: ./autogen.sh - name: Configure - run: ./configure --enable-werror --enable-openvz --enable-vserver --enable-ancient-vserver --enable-unicode --enable-hwloc --enable-delayacct --enable-sensors --enable-capabilities || ( cat config.log; exit 1; ) + run: ./configure --enable-werror --enable-openvz --enable-vserver --enable-ancient-vserver --enable-unicode --enable-hwloc --enable-delayacct --enable-sensors --enable-capabilities --enable-backtrace --enable-demangling || ( cat config.log; exit 1; ) - name: Build run: make -k - name: Distcheck - run: make distcheck DISTCHECK_CONFIGURE_FLAGS='--enable-werror --enable-openvz --enable-vserver --enable-ancient-vserver --enable-unicode --enable-hwloc --enable-delayacct --enable-sensors --enable-capabilities' + run: make distcheck DISTCHECK_CONFIGURE_FLAGS='--enable-werror --enable-openvz --enable-vserver --enable-ancient-vserver --enable-unicode --enable-hwloc --enable-delayacct --enable-sensors --enable-capabilities --enable-backtrace --enable-demangling' build-ubuntu-latest-gcc-static: runs-on: ubuntu-latest @@ -153,11 +153,11 @@ jobs: sudo add-apt-repository "deb http://apt.llvm.org/${ubuntu_codename}/ llvm-toolchain-${ubuntu_codename}-22 main" -y sudo apt-get update -q - name: Install Dependencies - run: sudo apt-get install --no-install-recommends clang-22 clang-tools-22 libncursesw5-dev libnl-3-dev libnl-genl-3-dev libsensors-dev libcap-dev + run: sudo apt-get install --no-install-recommends clang-22 clang-tools-22 libncursesw5-dev libnl-3-dev libnl-genl-3-dev libsensors-dev libcap-dev libiberty-dev libunwind-dev - name: Bootstrap run: ./autogen.sh - name: Configure - run: scan-build-22 -analyze-headers --status-bugs ./configure --enable-debug --enable-werror --enable-openvz --enable-vserver --enable-ancient-vserver --enable-unicode --enable-delayacct --enable-sensors --enable-capabilities || ( cat config.log; exit 1; ) + run: scan-build-22 -analyze-headers --status-bugs ./configure --enable-debug --enable-werror --enable-openvz --enable-vserver --enable-ancient-vserver --enable-unicode --enable-delayacct --enable-sensors --enable-capabilities --enable-backtrace --enable-demangling || ( cat config.log; exit 1; ) - name: Build run: scan-build-22 -analyze-headers --status-bugs make -j"$(nproc)" @@ -182,11 +182,11 @@ jobs: - name: Install LLVM Toolchain run: sudo apt-get install --no-install-recommends clang-22 libclang-rt-22-dev llvm-22 - name: Install Dependencies - run: sudo apt-get install --no-install-recommends libncursesw5-dev libhwloc-dev libnl-3-dev libnl-genl-3-dev libsensors-dev libcap-dev + run: sudo apt-get install --no-install-recommends libncursesw5-dev libhwloc-dev libnl-3-dev libnl-genl-3-dev libsensors-dev libcap-dev libiberty-dev libunwind-dev - name: Bootstrap run: ./autogen.sh - name: Configure - run: ./configure --enable-werror --enable-openvz --enable-vserver --enable-ancient-vserver --enable-unicode --enable-hwloc --enable-delayacct --enable-sensors --enable-capabilities || ( cat config.log; exit 1; ) + run: ./configure --enable-werror --enable-openvz --enable-vserver --enable-ancient-vserver --enable-unicode --enable-hwloc --enable-delayacct --enable-sensors --enable-capabilities --enable-backtrace --enable-demangling || ( cat config.log; exit 1; ) - name: Build run: make -k - name: Run sanitized htop (1) @@ -273,12 +273,12 @@ jobs: release: '15.0' usesh: true prepare: | - pkg install -y gmake autoconf automake pkgconf git + pkg install -y gmake autoconf automake pkgconf git libunwind gnulibiberty git config --global --add safe.directory /home/runner/work/htop/htop run: | set -e ./autogen.sh - ./configure --enable-unicode --enable-werror + ./configure --enable-unicode --enable-werror --enable-backtrace --enable-demangling gmake -k build-netbsd-latest-gcc: diff --git a/Action.c b/Action.c index 8123c4c8d..a6094ce9c 100644 --- a/Action.c +++ b/Action.c @@ -28,7 +28,9 @@ in the source distribution for its full text. #include "Macros.h" #include "MainPanel.h" #include "MemoryMeter.h" +#include "Object.h" #include "OpenFilesScreen.h" +#include "Panel.h" #include "Platform.h" #include "Process.h" #include "ProcessLocksScreen.h" @@ -49,6 +51,10 @@ in the source distribution for its full text. #include "AffinityPanel.h" #endif +#if defined(HAVE_BACKTRACE_SCREEN) +#include "BacktraceScreen.h" +#endif + Object* Action_pickFromVector(State* st, Panel* list, int x, bool follow) { MainPanel* mainPanel = st->mainPanel; @@ -606,6 +612,35 @@ static Htop_Reaction actionShowLocks(State* st) { return HTOP_REFRESH | HTOP_REDRAW_BAR; } +#if defined(HAVE_BACKTRACE_SCREEN) +static Htop_Reaction actionBacktrace(State *st) { + Process* selectedProcess = (Process *) Panel_getSelected((Panel *)st->mainPanel); + const Vector* allProcesses = st->mainPanel->super.items; + + Vector* processes = Vector_new(Class(Process), false, VECTOR_DEFAULT_SIZE); + if (!Process_isUserlandThread(selectedProcess)) { + for (int i = 0; i < Vector_size(allProcesses); i++) { + Process* process = (Process *)Vector_get(allProcesses, i); + if (Process_getThreadGroup(process) == Process_getThreadGroup(selectedProcess)) { + Vector_add(processes, process); + } + } + } else { + Vector_add(processes, selectedProcess); + } + + BacktracePanel* panel = BacktracePanel_new(processes, st->host->settings); + ScreenManager* screenManager = ScreenManager_new(NULL, st->host, st, false); + ScreenManager_add(screenManager, (Panel *)panel, 0); + + ScreenManager_run(screenManager, NULL, NULL, NULL); + BacktracePanel_delete((Object *)panel); + ScreenManager_delete(screenManager); + + return HTOP_REFRESH | HTOP_REDRAW_BAR | HTOP_UPDATE_PANELHDR; +} +#endif + static Htop_Reaction actionStrace(State* st) { if (!Action_writeableProcess(st)) return HTOP_OK; @@ -689,6 +724,9 @@ static const struct { { .key = " F8 [: ", .roInactive = true, .info = "lower priority (+ nice)" }, #if (defined(HAVE_LIBHWLOC) || defined(HAVE_AFFINITY)) { .key = " a: ", .roInactive = true, .info = "set CPU affinity" }, +#endif +#if defined(HAVE_BACKTRACE_SCREEN) + { .key = " b: ", .roInactive = false, .info = "show process backtrace" }, #endif { .key = " e: ", .roInactive = false, .info = "show process environment" }, { .key = " i: ", .roInactive = true, .info = "set IO priority" }, @@ -941,6 +979,9 @@ void Action_setBindings(Htop_Action* keys) { keys['\\'] = actionIncFilter; keys[']'] = actionHigherPriority; keys['a'] = actionSetAffinity; +#if defined(HAVE_BACKTRACE_SCREEN) + keys['b'] = actionBacktrace; +#endif keys['c'] = actionTagAllChildren; keys['e'] = actionShowEnvScreen; keys['h'] = actionHelp; diff --git a/BacktraceScreen.c b/BacktraceScreen.c new file mode 100644 index 000000000..e75c66a64 --- /dev/null +++ b/BacktraceScreen.c @@ -0,0 +1,482 @@ +/* +htop - BacktraceScreen.c +(C) 2025 htop dev team +Released under the GNU GPLv2+, see the COPYING file +in the source distribution for its full text. +*/ + +#include "config.h" // IWYU pragma: keep + +#include "BacktraceScreen.h" + +#include +#include // IWYU pragma: keep +#include +#include +#include +#include +#include + +#include "CRT.h" +#include "FunctionBar.h" +#include "Macros.h" +#include "Object.h" +#include "Panel.h" +#include "Process.h" +#include "ProvideCurses.h" +#include "RichString.h" +#include "Settings.h" +#include "Vector.h" +#include "XUtils.h" +#include "generic/UnwindPtrace.h" + + +#if defined(HAVE_BACKTRACE_SCREEN) + +static const char* const BacktraceScreenFunctions[] = { +#if defined(HAVE_DEMANGLING) + "Mangle ", +#endif + "Full Path", + "Refresh", + "Done ", + NULL +}; + +static const char* const BacktraceScreenKeys[] = { +#if defined(HAVE_DEMANGLING) + "F2", +#endif + "F3", + "F5", + "Esc", + NULL +}; + +static const int BacktraceScreenEvents[] = { +#if defined(HAVE_DEMANGLING) + KEY_F(2), +#endif + KEY_F(3), + KEY_F(5), + 27, +}; + +typedef enum BacktraceScreenDisplayOptions_ { + DEMANGLE_NAME_FUNCTION = 1 << 0, + SHOW_FULL_PATH_OBJECT = 1 << 1, +} BacktraceScreenDisplayOptions; + +BacktraceFrameData* BacktraceFrameData_new(void) { + BacktraceFrameData* this = AllocThis(BacktraceFrameData); + this->address = 0; + this->offset = 0; + this->functionName = NULL; + this->demangleFunctionName = NULL; + this->objectPath = NULL; + this->index = 0; + this->isSignalFrame = false; + return this; +} + +void BacktraceFrameData_delete(Object* object) { + BacktraceFrameData* this = (BacktraceFrameData*)object; + free(this->functionName); + free(this->demangleFunctionName); + free(this->objectPath); + free(this); +} + +static void BacktracePanel_displayHeader(BacktracePanel* this) { + const BacktracePanelPrintingHelper* printingHelper = &this->printingHelper; + const int displayOptions = this->displayOptions; + + bool showDemangledNames = (displayOptions & DEMANGLE_NAME_FUNCTION) && printingHelper->hasDemangledNames; + + bool showFullPathObject = !!(displayOptions & SHOW_FULL_PATH_OBJECT); + size_t maxObjLen = showFullPathObject ? printingHelper->maxObjPathLen : printingHelper->maxObjNameLen; + + /* + * The parameters for printf are of type int. + * A check is needed to prevent integer overflow. + */ + assert(printingHelper->maxFrameNumLen <= INT_MAX); + assert(printingHelper->maxAddrLen <= INT_MAX - strlen("0x")); + assert(maxObjLen <= INT_MAX); + + char* line = NULL; + xAsprintf(&line, "%*s %-*s %-*s %s", + (int)printingHelper->maxFrameNumLen, "#", + (int)(printingHelper->maxAddrLen + strlen("0x")), "ADDRESS", + (int)maxObjLen, "FILE", + (showDemangledNames ? "NAME (demangled)" : "NAME") + ); + + Panel_setHeader((Panel*)this, line); + free(line); +} + +static const char* getBasename(const char* path) { + char *lastSlash = strrchr(path, '/'); + return lastSlash ? lastSlash + 1 : path; +} + +static void BacktracePanel_makePrintingHelper(const BacktracePanel* this, BacktracePanelPrintingHelper* printingHelper) { + Vector* lines = this->super.items; + unsigned int maxFrameNum = 0; + size_t longestAddress = 0; + + printingHelper->hasDemangledNames = false; + + for (int i = 0; i < Vector_size(lines); i++) { + const BacktracePanelRow* row = (const BacktracePanelRow*)Vector_get(lines, i); + if (row->type != BACKTRACE_PANEL_ROW_DATA_FRAME) { + continue; + } + + if (row->data.frame->demangleFunctionName) + printingHelper->hasDemangledNames = true; + + if (row->data.frame->objectPath) { + const char* objectName = getBasename(row->data.frame->objectPath); + size_t objectNameLength = strlen(objectName); + size_t objectPathLength = (size_t)(objectName - row->data.frame->objectPath) + objectNameLength; + + printingHelper->maxObjNameLen = MAXIMUM(objectNameLength, printingHelper->maxObjNameLen); + printingHelper->maxObjPathLen = MAXIMUM(objectPathLength, printingHelper->maxObjPathLen); + } + + maxFrameNum = MAXIMUM(row->data.frame->index, maxFrameNum); + + longestAddress = MAXIMUM(row->data.frame->address, longestAddress); + } + + printingHelper->maxFrameNumLen = MAXIMUM(countDigits(maxFrameNum, 10), printingHelper->maxFrameNumLen); + printingHelper->maxAddrLen = MAXIMUM(countDigits(longestAddress, 16), printingHelper->maxAddrLen); +} + +static void BacktracePanel_makeBacktrace(Vector* frames, pid_t pid, char** error) { +#ifdef HAVE_LIBUNWIND_PTRACE + UnwindPtrace_makeBacktrace(frames, pid, error); +#else + (void)frames; + (void)pid; + xAsprintf(error, "The backtrace screen is not implemented"); +#endif +} + +static void BacktracePanel_populateFrames(BacktracePanel* this) { + char* error = NULL; + + Vector* data = Vector_new(Class(BacktraceFrameData), false, VECTOR_DEFAULT_SIZE); + for (int i = 0; i < Vector_size(this->processes); i++) { + const Process* process = (Process*)Vector_get(this->processes, i); + BacktracePanel_makeBacktrace(data, Process_getPid(process), &error); + + BacktracePanelRow* header = BacktracePanelRow_new(this); + header->process = process; + header->type = BACKTRACE_PANEL_ROW_PROCESS_INFORMATION; + Panel_add((Panel*)this, (Object*)header); + + if (!error) { + for (int j = 0; j < Vector_size(data); j++) { + BacktracePanelRow* row = BacktracePanelRow_new(this); + row->process = process; + row->type = BACKTRACE_PANEL_ROW_DATA_FRAME; + row->data.frame = (BacktraceFrameData*)Vector_get(data, j); + + Panel_add((Panel*)this, (Object*)row); + } + } else { + BacktracePanelRow* errorRow = BacktracePanelRow_new(this); + errorRow->process = process; + errorRow->type = BACKTRACE_PANEL_ROW_ERROR; + errorRow->data.error = error; + error = NULL; + Panel_add((Panel*)this, (Object*)errorRow); + } + + Vector_prune(data); + } + Vector_delete(data); + + BacktracePanelPrintingHelper* printingHelper = &this->printingHelper; + BacktracePanel_makePrintingHelper(this, printingHelper); + BacktracePanel_displayHeader(this); +} + +static HandlerResult BacktracePanel_eventHandler(Panel* super, int ch) { + BacktracePanel* this = (BacktracePanel*)super; + int* const displayOptions = &this->displayOptions; + + HandlerResult result = IGNORED; + + switch (ch) { +#if defined(HAVE_DEMANGLING) + case KEY_F(2): + *displayOptions ^= DEMANGLE_NAME_FUNCTION; + + bool showDemangledNames = !!(*displayOptions & DEMANGLE_NAME_FUNCTION); + FunctionBar_setLabel(super->defaultBar, KEY_F(2), showDemangledNames ? "Mangle " : "Demangle"); + + this->super.needsRedraw = true; + BacktracePanel_displayHeader(this); + break; +#endif + + case 'p': + case KEY_F(3): + *displayOptions ^= SHOW_FULL_PATH_OBJECT; + + bool showFullPathObject = !!(*displayOptions & SHOW_FULL_PATH_OBJECT); + FunctionBar_setLabel(super->defaultBar, KEY_F(3), showFullPathObject ? "Basename " : "Full Path"); + + this->super.needsRedraw = true; + BacktracePanel_displayHeader(this); + break; + + case KEY_CTRL('L'): + case KEY_F(5): + Panel_prune(super); + BacktracePanel_populateFrames(this); + break; + } + + return result; +} + +BacktracePanel* BacktracePanel_new(Vector* processes, const Settings* settings) { + BacktracePanel* this = AllocThis(BacktracePanel); + this->processes = processes; + + this->printingHelper.maxAddrLen = strlen("ADDRESS") - strlen("0x"); + this->printingHelper.maxFrameNumLen = strlen("#"); + this->printingHelper.maxObjNameLen = strlen("FILE"); + this->printingHelper.maxObjPathLen = strlen("FILE"); + this->printingHelper.hasDemangledNames = false; + + this->settings = settings; + this->displayOptions = + DEMANGLE_NAME_FUNCTION | + (settings->showProgramPath ? SHOW_FULL_PATH_OBJECT : 0) | + 0; + + Panel* super = (Panel*) this; + Panel_init(super, 1, 1, 0, 1, Class(BacktracePanelRow), true, + FunctionBar_new(BacktraceScreenFunctions, BacktraceScreenKeys, BacktraceScreenEvents) + ); + + bool showFullPathObject = !!(this->displayOptions & SHOW_FULL_PATH_OBJECT); + FunctionBar_setLabel(super->defaultBar, KEY_F(3), showFullPathObject ? "Basename " : "Full Path"); + + BacktracePanel_populateFrames(this); + + return this; +} + +void BacktracePanel_delete(Object* object) { + BacktracePanel* this = (BacktracePanel*)object; + Vector_delete(this->processes); + Panel_delete(object); +} + +static void BacktracePanelRow_highlightBasename(const BacktracePanelRow* row, RichString* out, char* line, int objectPathStart) { + assert(row); + assert(row->type == BACKTRACE_PANEL_ROW_DATA_FRAME); + assert(objectPathStart >= 0); + + const Process* process = row->process; + + char* procExe = process->procExe ? process->procExe + process->procExeBasenameOffset : NULL; + if (!procExe) + return; + + size_t endBasenameIndex = objectPathStart; + size_t lastSlashBasenameIndex = objectPathStart; + for (; line[endBasenameIndex] != 0 && line[endBasenameIndex] != ' '; endBasenameIndex++) { + if (line[endBasenameIndex] == '/') { + lastSlashBasenameIndex = endBasenameIndex + 1; + } + } + + size_t sizeBasename = endBasenameIndex - lastSlashBasenameIndex; + if (strncmp(line + lastSlashBasenameIndex, procExe, sizeBasename) == 0) { + RichString_setAttrn(out, CRT_colors[PROCESS_BASENAME], lastSlashBasenameIndex, sizeBasename); + } +} + +static void BacktracePanelRow_displayInformation(const Object* super, RichString* out) { + const BacktracePanelRow* row = (const BacktracePanelRow*)super; + assert(row); + assert(row->type == BACKTRACE_PANEL_ROW_PROCESS_INFORMATION); + + const Process* process = row->process; + + char* informations = NULL; + int colorBasename = DEFAULT_COLOR; + int indexProcessComm = -1; + int len = -1; + size_t highlightLen = 0; + size_t highlightOffset = 0; + + const char* processName = ""; + if (process->mergedCommand.str) { + processName = process->mergedCommand.str; + for (size_t i = 0; i < process->mergedCommand.highlightCount; i++) { + const ProcessCmdlineHighlight* highlight = process->mergedCommand.highlights; + if (highlight->flags & CMDLINE_HIGHLIGHT_FLAG_BASENAME) { + highlightLen = highlight->length; + highlightOffset = highlight->offset; + break; + } + } + } else if (process->cmdline) { + processName = process->cmdline; + } + if (highlightLen == 0) { + highlightLen = strlen(processName); + } + + if (Process_isThread(process)) { + colorBasename = PROCESS_THREAD_BASENAME; + len = xAsprintf(&informations, "Thread %d: %n%s", Process_getPid(process), &indexProcessComm, processName); + } else { + colorBasename = PROCESS_BASENAME; + len = xAsprintf(&informations, "Process %d: %n%s",Process_getPid(process), &indexProcessComm, processName); + } + + RichString_appendnWide(out, CRT_colors[DEFAULT_COLOR] | A_BOLD, informations, len); + if (indexProcessComm != -1) { + RichString_setAttrn(out, CRT_colors[colorBasename] | A_BOLD, indexProcessComm + highlightOffset, highlightLen); + } + + free(informations); +} + +static void BacktracePanelRow_displayFrame(const Object* super, RichString* out) { + const BacktracePanelRow* row = (const BacktracePanelRow*)super; + assert(row); + assert(row->type == BACKTRACE_PANEL_ROW_DATA_FRAME); + + const BacktracePanel* panel = row->panel; + const BacktracePanelPrintingHelper* printingHelper = &panel->printingHelper; + const int displayOptions = panel->displayOptions; + + const BacktraceFrameData* frame = row->data.frame; + + const char* functionName = "???"; + if ((displayOptions & DEMANGLE_NAME_FUNCTION) && frame->demangleFunctionName) { + functionName = frame->demangleFunctionName; + } else if (frame->functionName) { + functionName = frame->functionName; + } + + char* completeFunctionName = NULL; + xAsprintf(&completeFunctionName, "%s+0x%zx", functionName, frame->offset); + + size_t objectLength = printingHelper->maxObjPathLen; + const char* objectDisplayed = frame->objectPath; + if (!(displayOptions & SHOW_FULL_PATH_OBJECT)) { + objectLength = printingHelper->maxObjNameLen; + if (frame->objectPath) + objectDisplayed = getBasename(frame->objectPath); + } + + size_t maxAddrLen = printingHelper->maxAddrLen; + char* line = NULL; + int objectPathStart = -1; + + /* + * The parameters for printf are of type int. + * A check is needed to prevent integer overflow. + */ + assert(printingHelper->maxFrameNumLen <= INT_MAX); + assert(maxAddrLen <= INT_MAX); + assert(objectLength <= INT_MAX); + + int len = xAsprintf(&line, "%*u 0x%0*zx %n%-*s %s", + (int)printingHelper->maxFrameNumLen, frame->index, + (int)maxAddrLen, frame->address, + &objectPathStart, + (int)objectLength, objectDisplayed ? objectDisplayed : "-", + completeFunctionName + ); + + int colors = frame->functionName ? CRT_colors[DEFAULT_COLOR] : CRT_colors[PROCESS_SHADOW]; + + RichString_appendnAscii(out, colors, line, len); + + if (panel->settings->highlightBaseName) + BacktracePanelRow_highlightBasename(row, out, line, objectPathStart); + + free(completeFunctionName); + free(line); +} + +static void BacktracePanelRow_displayError(const Object* super, RichString* out) { + const BacktracePanelRow* row = (const BacktracePanelRow*)super; + assert(row); + assert(row->type == BACKTRACE_PANEL_ROW_ERROR); + assert(row->data.error); + + RichString_appendAscii(out, CRT_colors[DEFAULT_COLOR], row->data.error); +} + +static void BacktracePanelRow_display(const Object* super, RichString* out) { + const BacktracePanelRow* row = (const BacktracePanelRow*)super; + assert(row); + + switch (row->type) { + case BACKTRACE_PANEL_ROW_DATA_FRAME: + BacktracePanelRow_displayFrame(super, out); + break; + + case BACKTRACE_PANEL_ROW_PROCESS_INFORMATION: + BacktracePanelRow_displayInformation(super, out); + break; + + case BACKTRACE_PANEL_ROW_ERROR: + BacktracePanelRow_displayError(super, out); + break; + } +} + +BacktracePanelRow* BacktracePanelRow_new(const BacktracePanel* panel) { + BacktracePanelRow* this = AllocThis(BacktracePanelRow); + this->panel = panel; + return this; +} + +void BacktracePanelRow_delete(Object* object) { + BacktracePanelRow* this = (BacktracePanelRow*)object; + switch (this->type) { + case BACKTRACE_PANEL_ROW_DATA_FRAME: + BacktraceFrameData_delete((Object *)this->data.frame); + break; + + case BACKTRACE_PANEL_ROW_ERROR: + free(this->data.error); + break; + } + free(this); +} + +const ObjectClass BacktraceFrameData_class = { + .extends = Class(Object), + .delete = BacktraceFrameData_delete, +}; + +const PanelClass BacktracePanel_class = { + .super = { + .extends = Class(Panel), + .delete = BacktracePanel_delete, + }, + .eventHandler = BacktracePanel_eventHandler, +}; + +const ObjectClass BacktracePanelRow_class = { + .extends = Class(Object), + .delete = BacktracePanelRow_delete, + .display = BacktracePanelRow_display, +}; +#endif diff --git a/BacktraceScreen.h b/BacktraceScreen.h new file mode 100644 index 000000000..8ee68e146 --- /dev/null +++ b/BacktraceScreen.h @@ -0,0 +1,82 @@ +#ifndef HEADER_BacktraceScreen +#define HEADER_BacktraceScreen +/* +htop - BacktraceScreen.h +(C) 2025 htop dev team +Released under the GNU GPLv2+, see the COPYING file +in the source distribution for its full text. +*/ + +#include +#include + +#include "Object.h" +#include "Panel.h" +#include "Process.h" +#include "Settings.h" +#include "Vector.h" + + +typedef struct BacktraceFrameData_ { + Object super; + + size_t address; + size_t offset; + char* functionName; + char* demangleFunctionName; + char* objectPath; + unsigned int index; + bool isSignalFrame; +} BacktraceFrameData; + +typedef struct BacktracePanelPrintingHelper_ { + size_t maxAddrLen; + size_t maxFrameNumLen; + size_t maxObjPathLen; + size_t maxObjNameLen; + bool hasDemangledNames; +} BacktracePanelPrintingHelper; + +typedef struct BacktracePanel_ { + Panel super; + + Vector* processes; + BacktracePanelPrintingHelper printingHelper; + const Settings* settings; + int displayOptions; +} BacktracePanel; + +typedef enum BacktracePanelRowType_ { + BACKTRACE_PANEL_ROW_DATA_FRAME, + BACKTRACE_PANEL_ROW_ERROR, + BACKTRACE_PANEL_ROW_PROCESS_INFORMATION +} BacktracePanelRowType; + +typedef struct BacktracePanelRow_ { + Object super; + + int type; + union { + BacktraceFrameData* frame; + char* error; + } data; + + const BacktracePanel* panel; + const Process* process; +} BacktracePanelRow; + +BacktraceFrameData* BacktraceFrameData_new(void); +void BacktraceFrameData_delete(Object* object); + +BacktracePanel* BacktracePanel_new(Vector* processes, const Settings* settings); +void BacktracePanel_delete(Object* object); + +BacktracePanelRow* BacktracePanelRow_new(const BacktracePanel* panel); +void BacktracePanelRow_delete(Object *object); + +extern const ObjectClass BacktraceFrameData_class; + +extern const PanelClass BacktracePanel_class; +extern const ObjectClass BacktracePanelRow_class; + +#endif diff --git a/Makefile.am b/Makefile.am index 8c6b845c5..ef1c44533 100644 --- a/Makefile.am +++ b/Makefile.am @@ -161,7 +161,23 @@ myhtopheaders = \ UptimeMeter.h \ UsersTable.h \ Vector.h \ - XUtils.h + XUtils.h \ + generic/ProvideDemangle.h + +if HAVE_BACKTRACE_SCREEN +myhtopheaders += \ + BacktraceScreen.h \ + generic/UnwindPtrace.h + +myhtopsources += \ + BacktraceScreen.c \ + generic/UnwindPtrace.c + +if HAVE_DEMANGLING +myhtopheaders += generic/Demangle.h +myhtopsources += generic/Demangle.c +endif +endif # Linux # ----- diff --git a/README.md b/README.md index 1194c0157..8627436df 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,17 @@ To install on the local system run `make install`. By default `make install` ins enable hwloc support for CPU affinity; disables affinity support - dependency: *libhwloc* - default: *no* + * `--enable-backtrace`: + enable showing backtraces of a process + - default: *no* + - possible values: + - unwind-ptrace: use **libunwind-ptrace** to get backtraces + * `--enable-demangling`: + enable demangling support for backtraces + - default: *check* + - possible values: + - libiberty: use **libiberty** (GNU) to demangle function names + - libdemangle: use **libdemangle** (Solaris) to demangle function names * `--enable-static`: build a static htop binary; hwloc and delay accounting are not supported - default: *no* diff --git a/configure.ac b/configure.ac index 768cd4748..7482aef1b 100644 --- a/configure.ac +++ b/configure.ac @@ -985,47 +985,127 @@ $3 } # htop_search_header_dir AC_ARG_ENABLE( - [unwind], + [backtrace], [AS_HELP_STRING( - [--enable-unwind], - [enable unwind support for printing backtraces; requires libunwind @<:@default=check@:>@] + [--enable-backtrace=BACKEND], + [enable showing backtraces of a processes; available backends: unwind-ptrace @<:@default=no@:>@] )], [], - [enable_unwind=check] + [enable_backtrace=no] ) -case "$enable_unwind" in + +backtrace_backend_found=no +have_libunwind_ptrace=no +case $enable_backtrace in + check|yes) + have_libunwind_ptrace=check + ;; + unwind-ptrace|libunwind-ptrace) + have_libunwind_ptrace=check + enable_backtrace=unwind-ptrace + ;; +esac + +AC_ARG_WITH( + [libunwind], + [AS_HELP_STRING( + [--with-libunwind], + [use libunwind for printing local backtrace @<:@default=check@:>@] + )], + [], + [ + with_libunwind=${enable_unwind-'check'} + if test "x${enable_unwind+y}" = xy; then + AC_MSG_WARN([--enable-unwind is deprecated; use --with-libunwind instead]) + fi + ] +) + +default_libunwind_libs='-lunwind' +case $with_libunwind in no) ;; check|yes) - HTOP_PKG_CHECK_MODULES(LIBUNWIND, libunwind, - [], - [ - if test "$enable_static" = yes; then - AC_CHECK_LIB([lzma], [lzma_index_buffer_decode]) - fi - : "${LIBUNWIND_LIBS='-lunwind'}" - ] - ) + libunwind_ptrace_name='' + if test "$have_libunwind_ptrace" != no; then + libunwind_ptrace_name='libunwind-ptrace ' + fi + + while :; do + HTOP_PKG_CHECK_MODULES( + [LIBUNWIND], + [${libunwind_ptrace_name}libunwind], + [ + if test "$have_libunwind_ptrace" != no; then + enable_backtrace=unwind-ptrace + fi + break + ], [ + test "x$libunwind_ptrace_name" != x || break + libunwind_ptrace_name='' + ] + ) + done + + if test "x${LIBUNWIND_LIBS+y}" = x; then + if test "$enable_static" = yes; then + AC_CHECK_LIB( + [lzma], + [lzma_index_buffer_decode], + [default_libunwind_libs='-lunwind -llzma'] + ) + fi + + if test "$have_libunwind_ptrace" != no; then + AC_CHECK_LIB( + [unwind-ptrace], + [_UPT_create], + [:], + [have_libunwind_ptrace=no], + [-lunwind-generic $default_libunwind_libs] + ) + fi + fi htop_save_CFLAGS=$CFLAGS CFLAGS="$AM_CFLAGS $LIBUNWIND_CFLAGS $CFLAGS" + # Debian installs LLVM libunwind headers in the non-standard + # "/usr/include/libunwind" directory. + # (Package name: "libunwind-${llvm_version}-dev") + # libunwind (HP) headers can be found in the default include + # directories in Debian. (Package name: "libunwind-dev") if htop_search_header_dir libunwind.h "/usr/include/libunwind"; then AC_DEFINE([HAVE_LIBUNWIND_H], 1, [Define to 1 if you have the header file.]) - elif test "$enable_unwind" = yes; then + if test "$have_libunwind_ptrace" != no; then + AC_CHECK_HEADER( + [libunwind-ptrace.h], + [:], + [have_libunwind_ptrace=no] + ) + fi + elif test "$with_libunwind" = yes; then AC_MSG_ERROR([cannot find required header file libunwind.h]) else - enable_unwind=no + with_libunwind=no + have_libunwind_ptrace=no fi CFLAGS=$htop_save_CFLAGS ;; *) - AC_MSG_ERROR([bad value '$enable_unwind' for --enable-unwind]) + AC_MSG_ERROR([bad value '$with_libunwind' for --with-libunwind]) ;; esac -if test "$enable_unwind" != no; then +if test "$with_libunwind" != no; then + if test "x${LIBUNWIND_LIBS+y}" = x; then + if test "$have_libunwind_ptrace" != no; then + default_libunwind_libs="-lunwind-ptrace -lunwind-generic $default_libunwind_libs" + fi + LIBUNWIND_LIBS=$default_libunwind_libs + fi + htop_save_CPPFLAGS=$CPPFLAGS htop_save_CFLAGS=$CFLAGS htop_save_LIBS=$LIBS @@ -1033,8 +1113,10 @@ if test "$enable_unwind" != no; then CFLAGS="$AM_CFLAGS $LIBUNWIND_CFLAGS $CFLAGS" LIBS="$LIBUNWIND_LIBS $LIBS" - # unw_init_local() is a macro in HP's implementation of libunwind - # (the most popular one, Mosberger et al.) + # Almost all unw_*() functions are defined as macros in HP's + # implementation of libunwind (Mosberger et al.). + # Define UNW_LOCAL_ONLY to avoid unnecessary dependency on + # "-lunwind-generic" when linking. AC_MSG_CHECKING([whether $LIBUNWIND_LIBS supports local unwinding]) AC_LINK_IFELSE( [AC_LANG_PROGRAM( @@ -1075,34 +1157,72 @@ if test "$enable_unwind" != no; then fi if test "$libunwind_local_support" = no; then AC_MSG_RESULT([no]) - if test "$enable_unwind" = yes; then + if test "$with_libunwind" = yes; then AC_MSG_WARN([this build of libunwind might not support local unwinding]) else - LIBS=$htop_save_LIBS - enable_unwind=no + with_libunwind=no + have_libunwind_ptrace=no fi fi ], [ AC_MSG_RESULT([no]) - if test "$enable_unwind" = yes; then + if test "$with_libunwind" = yes; then AC_MSG_FAILURE([cannot link with libunwind]) - else - LIBS=$htop_save_LIBS - enable_unwind=no fi + with_libunwind=no + have_libunwind_ptrace=no + ] + ) + + AC_MSG_CHECKING([for unw_get_elf_filename in $LIBUNWIND_LIBS]) + AC_LINK_IFELSE( + [AC_LANG_PROGRAM( + [[ +#define UNW_LOCAL_ONLY +#include + ]], [[ + static unw_cursor_t cursor; + /* Requires libunwind (HP implementation) 1.8 or later. + First argument must be non-null. */ + unw_get_elf_filename(&cursor, (char*)0, (size_t)0, (unw_word_t*)0); + ]] + )], [ + AC_DEFINE([HAVE_LIBUNWIND_ELF_FILENAME], [1], [Define if libunwind has unw_get_elf_filename]) + AC_MSG_RESULT(yes) + ], [ + AC_MSG_RESULT(no) + AC_MSG_WARN([showing filenames in backtrace screen requires libunwind 1.8 or later]) ] ) + if test "$have_libunwind_ptrace" != no && test "x$ac_cv_lib_unwind_ptrace__UPT_create" != xyes; then + dnl Expect LIBS to contain "-lunwind-ptrace" at this point. + dnl Therefore call AC_CHECK_FUNC and not AC_CHECK_LIB. + AC_CHECK_FUNC([_UPT_create], [], [have_libunwind_ptrace=no]) + fi + CPPFLAGS=$htop_save_CPPFLAGS CFLAGS=$htop_save_CFLAGS + LIBS=$htop_save_LIBS fi -if test "$enable_unwind" != no; then +if test "$with_libunwind" != no; then AM_CFLAGS="$AM_CFLAGS $LIBUNWIND_CFLAGS" - enable_unwind=yes + LIBS="$LIBUNWIND_LIBS $LIBS" + with_libunwind=yes AC_DEFINE([HAVE_LOCAL_UNWIND], 1, [Define to 1 if local unwinding is enabled.]) + if test "$have_libunwind_ptrace" != no; then + have_libunwind_ptrace=yes + backtrace_backend_found=yes + enable_backtrace=unwind-ptrace + AC_DEFINE([HAVE_LIBUNWIND_PTRACE], 1, [Define to 1 if libunwind-ptrace is present]) + fi else + if test "x$enable_backtrace" = xunwind-ptrace; then + AC_MSG_ERROR([requires --with-libunwind for --enable-backtrace=unwind-ptrace]) + fi + # Fall back to backtrace(3) and add -lexecinfo if needed AC_SEARCH_LIBS([backtrace], [execinfo]) @@ -1168,6 +1288,154 @@ char** (*fn)(void* const*, BACKTRACE_RETURN_TYPE) = backtrace_symbols; ) fi +case $enable_backtrace in + no|check) + enable_backtrace=no + ;; + yes) + AC_MSG_ERROR([did not find any backend for --enable-backtrace]) + ;; + *) + if test "$backtrace_backend_found" = no; then + AC_MSG_ERROR([backend '$enable_backtrace' for --enable-backtrace is not supported or not found]) + fi + AC_DEFINE([HAVE_BACKTRACE_SCREEN], [1], [Define if the backtrace feature is enabled]) + ;; +esac +AM_CONDITIONAL([HAVE_BACKTRACE_SCREEN], [test "$enable_backtrace" != no]) + +AC_ARG_ENABLE( + [demangling], + [AS_HELP_STRING( + [--enable-demangling=BACKEND], + [enable demangling support for backtraces; available backends: libiberty libdemangle @<:@default=check@:>@] + )], + [], + [enable_demangling=check] +) + +demangling_backend_found=no +have_libiberty=no +have_libdemangle=no +case $enable_demangling in + check|yes) + have_libiberty=check + have_libdemangle=check + ;; + libiberty|iberty|liberty) + have_libiberty=check + enable_demangling=libiberty + ;; + libdemangle) + have_libdemangle=check + ;; +esac + +demangle_header_paths='' + +if test "$have_libiberty" != no; then + AC_CHECK_LIB( + [iberty], + [cplus_demangle], + [demangle_header_paths="/usr/local/include/libiberty /usr/include/libiberty"], + [have_libiberty=no] + ) +fi +if test "$have_libdemangle" != no; then + AC_CHECK_LIB([demangle], [cplus_demangle], [:], [have_libdemangle=no]) +fi + +if test "$have_libiberty$have_libdemangle" != nono; then + if htop_search_header_dir demangle.h "$demangle_header_paths"; then + AC_DEFINE([HAVE_DEMANGLE_H], 1, [Define to 1 if you have the header file.]) + else + have_libdemangle=no + if test "$have_libiberty" != no; then + AC_CHECK_HEADERS([libiberty/demangle.h], [], [have_libiberty=no]) + fi + fi +fi + +htop_save_CPPFLAGS=$CPPFLAGS +htop_save_LIBS=$LIBS +CPPFLAGS="$AM_CPPFLAGS -I$srcdir $CPPFLAGS" + +if test "$demangling_backend_found" = no && test "$have_libiberty" != no; then + LIBS="-liberty $htop_save_LIBS" + + AC_MSG_CHECKING([whether libiberty cplus_demangle works]) + AC_LINK_IFELSE( + [AC_LANG_PROGRAM( + [[ +#include +#include "generic/ProvideDemangle.h" +/* char* cplus_demangle(const char* mangled, int options); */ + ]], [[ + char* demangled = cplus_demangle("", DMGL_AUTO); + free(demangled); + ]] + )], [ + AC_MSG_RESULT([yes]) + have_libiberty=yes + demangling_backend_found=yes + enable_demangling=libiberty + AC_DEFINE([HAVE_LIBIBERTY_CPLUS_DEMANGLE], [1], [Define to 1 if libiberty supports cplus_demangle function.]) + ], [ + AC_MSG_RESULT([no]) + have_libiberty=no + LIBS=$htop_save_LIBS + ] + ) +fi + +if test "$demangling_backend_found" = no && test "$have_libdemangle" != no; then + LIBS="-ldemangle $htop_save_LIBS" + + AC_MSG_CHECKING([whether libdemangle cplus_demangle works]) + AC_LINK_IFELSE( + [AC_LANG_PROGRAM( + [[ +#include "generic/ProvideDemangle.h" +/* int cplus_demangle(const char* symbol, char* interpretation, size_t size); */ + ]], [[ + static char demangled[1]; + int ret = cplus_demangle("", demangled, sizeof(demangled)); + if (ret == DEMANGLE_ENAME) { + ret = 0; + } + return (ret <= 0 && ret > -126 ? ret : 1); + ]] + )], [ + AC_MSG_RESULT([yes]) + have_libdemangle=yes + demangling_backend_found=yes + enable_demangling=libdemangle + AC_DEFINE([HAVE_LIBDEMANGLE_CPLUS_DEMANGLE], [1], [Define to 1 if libdemangle supports cplus_demangle function.]) + ], [ + AC_MSG_RESULT([no]) + have_libdemangle=no + LIBS=$htop_save_LIBS + ] + ) +fi + +CPPFLAGS=$htop_save_CPPFLAGS + +case $enable_demangling in + no|check) + enable_demangling=no + ;; + yes) + AC_MSG_ERROR([did not find any backend for --enable-demangling]) + ;; + *) + if test "$demangling_backend_found" = no; then + AC_MSG_ERROR([backend '$enable_demangling' for --enable-demangling is not supported or not found]) + fi + AC_DEFINE([HAVE_DEMANGLING], [1], [Define to 1 if the demangling is supported.]) + ;; +esac +AM_CONDITIONAL([HAVE_DEMANGLING], [test "$have_demangling" != no]) AC_ARG_ENABLE( [hwloc], @@ -1604,7 +1872,8 @@ AC_MSG_RESULT([ (Linux) capabilities: $enable_capabilities unicode: $enable_unicode affinity: $enable_affinity - unwind: $enable_unwind + backtrace: $enable_backtrace + demangling: $enable_demangling hwloc: $enable_hwloc debug: $enable_debug static: $enable_static diff --git a/generic/Demangle.c b/generic/Demangle.c new file mode 100644 index 000000000..fc5a0420b --- /dev/null +++ b/generic/Demangle.c @@ -0,0 +1,67 @@ +/* +htop - generic/Demangle.c +(C) 2025 htop dev team +Released under the GNU GPLv2+, see the COPYING file +in the source distribution for its full text. +*/ + +#include "config.h" // IWYU pragma: keep + +#include "generic/Demangle.h" + +#include // IWYU pragma: keep +#include // IWYU pragma: keep +#include // IWYU pragma: keep +#include // IWYU pragma: keep +#include // IWYU pragma: keep + +#include "generic/ProvideDemangle.h" + + +#ifdef HAVE_DEMANGLING +char* Demangle_demangle(const char* mangled) { +# if defined(HAVE_LIBIBERTY_CPLUS_DEMANGLE) + // The default in htop is to show as many details from the mangled + // name as possible. + int options = DMGL_AUTO | + DMGL_TYPES | + DMGL_VERBOSE /* Includes "disambiguators" of Rust crates */ | + DMGL_PARAMS; + + return cplus_demangle(mangled, options); +# elif defined(HAVE_LIBDEMANGLE_CPLUS_DEMANGLE) + // The cplus_demangle() API from libdemangle is flawed. It does not + // provide us the required size of the buffer to store the demangled + // name, and it leaves the buffer content undefined if the specified + // buffer size is not big enough. + + // No crash on allocation failure. This is for safety against + // incredibly long demangled names in untrustable programs. + static size_t bufSize = 128; + static char* buf; + + while (true) { + if (!buf) { + buf = malloc(bufSize); + if (!buf) + return NULL; + } + + int res = cplus_demangle(mangled, buf, bufSize); + if (res == 0) + break; + if (res != DEMANGLE_ESPACE || bufSize > SIZE_MAX / 2) + return NULL; + + bufSize *= 2; + free(buf); + buf = NULL; + } + + return strdup(buf); +# else + (void)mangled; + return NULL; +# endif +} +#endif /* HAVE_DEMANGLING */ diff --git a/generic/Demangle.h b/generic/Demangle.h new file mode 100644 index 000000000..0fa862ad0 --- /dev/null +++ b/generic/Demangle.h @@ -0,0 +1,24 @@ +#ifndef HEADER_Demangle +#define HEADER_Demangle +/* +htop - generic/Demangle.h +(C) 2025 htop dev team +Released under the GNU GPLv2+, see the COPYING file +in the source distribution for its full text. +*/ + +#include "Macros.h" + + +#ifdef HAVE_DEMANGLING +ATTR_NONNULL ATTR_MALLOC +char* Demangle_demangle(const char* mangled); +#else +ATTR_NONNULL +static inline char* Demangle_demangle(const char* mangled) { + (void)mangled; + return NULL; +} +#endif /* HAVE_DEMANGLING */ + +#endif diff --git a/generic/ProvideDemangle.h b/generic/ProvideDemangle.h new file mode 100644 index 000000000..926a2fdf1 --- /dev/null +++ b/generic/ProvideDemangle.h @@ -0,0 +1,35 @@ +#ifndef HEADER_ProvideDemangle +#define HEADER_ProvideDemangle +/* +htop - generic/ProvideDemangle.h +(C) 2026 htop dev team +Released under the GNU GPLv2+, see the COPYING file +in the source distribution for its full text. +*/ + +// IWYU pragma: no_include "config.h" + +// IWYU pragma: begin_exports + +#if !defined(HAVE_DECL_BASENAME) +// Suppress libiberty's own declaration of basename(3) +// +// It's a pity that we need this workaround as libiberty developers +// refuse fix their headers and export an unwanted interface to us. +// htop doesn't use basename(3) API: The POSIX version is flawed by +// design; libiberty ships with the GNU version of basename(3) that +// is incompatible with the interface specified by POSIX). +// +// +#define HAVE_DECL_BASENAME 1 +#endif + +#if defined(HAVE_DEMANGLE_H) +#include +#elif defined(HAVE_LIBIBERTY_DEMANGLE_H) +#include +#endif + +// IWYU pragma: end_exports + +#endif /* HEADER_ProvideDemangle */ diff --git a/generic/UnwindPtrace.c b/generic/UnwindPtrace.c new file mode 100644 index 000000000..7f93aaafa --- /dev/null +++ b/generic/UnwindPtrace.c @@ -0,0 +1,146 @@ +/* +htop - generic/UnwindPtrace.c +(C) 2025 htop dev team +Released under the GNU GPLv2+, see the COPYING file +in the source distribution for its full text. +*/ + +#include "config.h" // IWYU pragma: keep + +#include "generic/UnwindPtrace.h" + +#include +#include +#include +#include +#include + +#include "BacktraceScreen.h" +#include "XUtils.h" + +#ifdef HAVE_LIBUNWIND_PTRACE +#include +#include +#endif + +#ifdef HAVE_DEMANGLING +#include "generic/Demangle.h" +#endif + + +#ifdef HAVE_LIBUNWIND_PTRACE +static int ptraceAttach(pid_t pid) { +# if defined(HTOP_LINUX) + return !ptrace(PTRACE_ATTACH, pid, (void*)0, (void*)0) ? 0 : errno; +# elif defined(HTOP_FREEBSD) || defined(HTOP_NETBSD) || defined(HTOP_OPENBSD) || defined(HTOP_DARWIN) + return !ptrace(PT_ATTACH, pid, (caddr_t)0, 0) ? 0 : errno; +# else + (void)pid; + return ENOSYS; +# endif +} + +static int ptraceDetach(pid_t pid) { +# if defined(HTOP_LINUX) + return !ptrace(PTRACE_DETACH, pid, (void*)0, (void*)0) ? 0 : errno; +# elif defined(HTOP_FREEBSD) || defined(HTOP_NETBSD) || defined(HTOP_OPENBSD) || defined(HTOP_DARWIN) + return !ptrace(PT_DETACH, pid, (caddr_t)0, 0) ? 0 : errno; +# else + (void)pid; + return ENOSYS; +# endif +} + +void UnwindPtrace_makeBacktrace(Vector* frames, pid_t pid, char** error) { + *error = NULL; + + if (pid <= 0) { + *error = xStrdup("Invalid PID"); + return; + } + + unw_addr_space_t addrSpace = unw_create_addr_space(&_UPT_accessors, 0); + if (!addrSpace) { + *error = xStrdup("Cannot initialize libunwind"); + return; + } + + int ptraceErrno = ptraceAttach(pid); + if (ptraceErrno) { + xAsprintf(error, "ptrace: %s (%d)", strerror(ptraceErrno), ptraceErrno); + goto addr_space_error; + } + + int waitStatus = 0; + if (wait(&waitStatus) == -1) { + int waitErrno = errno; + xAsprintf(error, "wait: %s (%d)", strerror(waitErrno), waitErrno); + goto ptrace_error; + } + + if (WIFSTOPPED(waitStatus) == 0) { + *error = xStrdup("The process chosen is not stopped correctly"); + goto ptrace_error; + } + + struct UPT_info* context = _UPT_create(pid); + if (!context) { + *error = xStrdup("Cannot create the context of libunwind-ptrace"); + goto ptrace_error; + } + + unw_cursor_t cursor; + int ret = unw_init_remote(&cursor, addrSpace, context); + if (ret < 0) { + xAsprintf(error, "libunwind cursor: ret=%d", ret); + goto context_error; + } + + unsigned int index = 0; + do { + char buffer[2048] = {0}; + + BacktraceFrameData* frame = BacktraceFrameData_new(); + frame->index = index; + + unw_word_t pc; + ret = unw_get_reg(&cursor, UNW_REG_IP, &pc); + if (ret != 0) { + xAsprintf(error, "Cannot get program counter register: error %d", -ret); + BacktraceFrameData_delete((Object *)frame); + break; + } + frame->address = pc; + + frame->isSignalFrame = unw_is_signal_frame(&cursor) > 0; + +# if defined(HAVE_LIBUNWIND_ELF_FILENAME) + unw_word_t offsetElfFileName; + if (unw_get_elf_filename(&cursor, buffer, sizeof(buffer), &offsetElfFileName) == 0) + frame->objectPath = xStrndup(buffer, sizeof(buffer)); +# endif + + unw_word_t offset; + if (unw_get_proc_name(&cursor, buffer, sizeof(buffer), &offset) == 0) { + frame->offset = offset; + frame->functionName = xStrndup(buffer, sizeof(buffer)); + +# if defined(HAVE_DEMANGLING) + char* demangledName = Demangle_demangle(frame->functionName); + frame->demangleFunctionName = demangledName; +# endif + } + Vector_add(frames, (Object *)frame); + index++; + } while (unw_step(&cursor) > 0 && index < INT_MAX); + +context_error: + _UPT_destroy(context); + +ptrace_error: + ptraceDetach(pid); + +addr_space_error: + unw_destroy_addr_space(addrSpace); +} +#endif diff --git a/generic/UnwindPtrace.h b/generic/UnwindPtrace.h new file mode 100644 index 000000000..dcfb42ba9 --- /dev/null +++ b/generic/UnwindPtrace.h @@ -0,0 +1,19 @@ +#ifndef HEADER_UnwindPtrace +#define HEADER_UnwindPtrace +/* +htop - generic/UnwindPtrace.h +(C) 2025 htop dev team +Released under the GNU GPLv2+, see the COPYING file +in the source distribution for its full text. +*/ + +#include + +#include "Vector.h" + + +#ifdef HAVE_LIBUNWIND_PTRACE +void UnwindPtrace_makeBacktrace(Vector* frames, pid_t pid, char** error); +#endif + +#endif diff --git a/htop.1.in b/htop.1.in index e812fe032..09aba1f81 100644 --- a/htop.1.in +++ b/htop.1.in @@ -279,6 +279,10 @@ Pause/resume process updates. .B m Merge exe, comm and cmdline, where applicable. (This is a toggle key.) .TP +.B b +Show the backtrace of a process. (This feature requires enabling +at compile time.) +.TP .B Ctrl-L Refresh: redraw screen and recalculate values. .TP