From ef97c84351e79989e94221e2e44fa70c44c3fbb8 Mon Sep 17 00:00:00 2001 From: yossizap Date: Mon, 9 Dec 2019 18:43:42 +0000 Subject: [PATCH] Add processes widget for kernel and child debugging (#1894) * Update r2 for dp fixes and general debug fixes * Added ProcessesWidget for kernel debugging and switching between children * Update r2 for dpl fixes * Update r2 for linux and gdbserver dp/dp= fixes * Added switchedThread and switchedProcess events to refresh their widgets Seek doesn't always change after switching if the other process is at the same offset in the same binary so it's better to have another event for it. * Disable threads/processes widget during a debugtask/when not debugging and clear it's history after a session * Improve Processes Widget's UI - Remove vertical numbers - Highlight the current process with bold instead of using the "current" column * Updated r2 for fork fixes --- radare2 | 2 +- src/Cutter.pro | 3 + src/core/Cutter.cpp | 34 ++++++ src/core/Cutter.h | 13 ++ src/core/MainWindow.cpp | 3 + src/core/MainWindow.h | 1 + src/core/MainWindow.ui | 9 ++ src/widgets/ProcessesWidget.cpp | 202 ++++++++++++++++++++++++++++++++ src/widgets/ProcessesWidget.h | 49 ++++++++ src/widgets/ProcessesWidget.ui | 72 ++++++++++++ src/widgets/ThreadsWidget.cpp | 15 ++- 11 files changed, 400 insertions(+), 3 deletions(-) create mode 100644 src/widgets/ProcessesWidget.cpp create mode 100644 src/widgets/ProcessesWidget.h create mode 100644 src/widgets/ProcessesWidget.ui diff --git a/radare2 b/radare2 index cb60b5e8..9fca7b1f 160000 --- a/radare2 +++ b/radare2 @@ -1 +1 @@ -Subproject commit cb60b5e8fd8dc76d847d9935d2ded4df2e05b63e +Subproject commit 9fca7b1f582a744426e9f0bf650ed3206804510c diff --git a/src/Cutter.pro b/src/Cutter.pro index 6ac0329f..7772f4e5 100644 --- a/src/Cutter.pro +++ b/src/Cutter.pro @@ -332,6 +332,7 @@ SOURCES += \ widgets/StackWidget.cpp \ widgets/RegistersWidget.cpp \ widgets/ThreadsWidget.cpp \ + widgets/ProcessesWidget.cpp \ widgets/BacktraceWidget.cpp \ dialogs/OpenFileDialog.cpp \ common/CommandTask.cpp \ @@ -466,6 +467,7 @@ HEADERS += \ widgets/StackWidget.h \ widgets/RegistersWidget.h \ widgets/ThreadsWidget.h \ + widgets/ProcessesWidget.h \ widgets/BacktraceWidget.h \ dialogs/OpenFileDialog.h \ common/StringsTask.h \ @@ -563,6 +565,7 @@ FORMS += \ widgets/StackWidget.ui \ widgets/RegistersWidget.ui \ widgets/ThreadsWidget.ui \ + widgets/ProcessesWidget.ui \ widgets/BacktraceWidget.ui \ dialogs/OpenFileDialog.ui \ dialogs/preferences/DebugOptionsWidget.ui \ diff --git a/src/core/Cutter.cpp b/src/core/Cutter.cpp index 60a3c241..f0db880c 100644 --- a/src/core/Cutter.cpp +++ b/src/core/Cutter.cpp @@ -1098,6 +1098,16 @@ QJsonDocument CutterCore::getProcessThreads(int pid) } } +QJsonDocument CutterCore::getChildProcesses(int pid) +{ + // Return the currently debugged process and it's children + if (-1 == pid) { + return cmdj("dpj"); + } + // Return the given pid and it's child processes + return cmdj("dpj " + QString::number(pid)); +} + QJsonDocument CutterCore::getRegisterValues() { return cmdj("drj"); @@ -1199,6 +1209,30 @@ void CutterCore::setCurrentDebugThread(int tid) emit refreshCodeViews(); emit stackChanged(); syncAndSeekProgramCounter(); + emit switchedThread(); + emit debugTaskStateChanged(); + }); + } +} + +void CutterCore::setCurrentDebugProcess(int pid) +{ + if (!currentlyDebugging) { + return; + } + + emit debugTaskStateChanged(); + asyncCmd("dp=" + QString::number(pid), debugTask); + if (!debugTask.isNull()) { + emit debugTaskStateChanged(); + connect(debugTask.data(), &R2Task::finished, this, [this] () { + debugTask.clear(); + emit registersChanged(); + emit refreshCodeViews(); + emit stackChanged(); + emit flagsChanged(); + syncAndSeekProgramCounter(); + emit switchedProcess(); emit debugTaskStateChanged(); }); } diff --git a/src/core/Cutter.h b/src/core/Cutter.h index c00fc256..b0168919 100644 --- a/src/core/Cutter.h +++ b/src/core/Cutter.h @@ -259,6 +259,10 @@ public: RVA getProgramCounterValue(); void setRegister(QString regName, QString regValue); void setCurrentDebugThread(int tid); + /** + * @brief Attach to a given pid from a debug session + */ + void setCurrentDebugProcess(int pid); QJsonDocument getStack(int size = 0x100); /** * @brief Get a list of a given process's threads @@ -266,6 +270,12 @@ public: * @return JSON object result of dptj */ QJsonDocument getProcessThreads(int pid); + /** + * @brief Get a list of a given process's child processes + * @param pid The pid of the process, -1 for the currently debugged process + * @return JSON object result of dptj + */ + QJsonDocument getChildProcesses(int pid); QJsonDocument getBacktrace(); void startDebug(); void startEmulation(); @@ -480,6 +490,9 @@ signals: void refreshCodeViews(); void stackChanged(); + void switchedThread(); + void switchedProcess(); + void classNew(const QString &cls); void classDeleted(const QString &cls); void classRenamed(const QString &oldName, const QString &newName); diff --git a/src/core/MainWindow.cpp b/src/core/MainWindow.cpp index f92b8c5d..736670fe 100644 --- a/src/core/MainWindow.cpp +++ b/src/core/MainWindow.cpp @@ -62,6 +62,7 @@ #include "widgets/DisassemblyWidget.h" #include "widgets/StackWidget.h" #include "widgets/ThreadsWidget.h" +#include "widgets/ProcessesWidget.h" #include "widgets/RegistersWidget.h" #include "widgets/BacktraceWidget.h" #include "widgets/HexdumpWidget.h" @@ -317,6 +318,7 @@ void MainWindow::initDocks() flagsDock = new FlagsWidget(this, ui->actionFlags); stackDock = new StackWidget(this, ui->actionStack); threadsDock = new ThreadsWidget(this, ui->actionThreads); + processesDock = new ProcessesWidget(this, ui->actionProcesses); backtraceDock = new BacktraceWidget(this, ui->actionBacktrace); registersDock = new RegistersWidget(this, ui->actionRegisters); memoryMapDock = new MemoryMapWidget(this, ui->actionMemoryMap); @@ -840,6 +842,7 @@ void MainWindow::restoreDocks() splitDockWidget(stackDock, registersDock, Qt::Vertical); tabifyDockWidget(stackDock, backtraceDock); tabifyDockWidget(backtraceDock, threadsDock); + tabifyDockWidget(threadsDock, processesDock); updateDockActionsChecked(); } diff --git a/src/core/MainWindow.h b/src/core/MainWindow.h index 29111b3a..f6350ffb 100644 --- a/src/core/MainWindow.h +++ b/src/core/MainWindow.h @@ -250,6 +250,7 @@ private: QDockWidget *calcDock = nullptr; QDockWidget *stackDock = nullptr; QDockWidget *threadsDock = nullptr; + QDockWidget *processesDock = nullptr; QDockWidget *registersDock = nullptr; QDockWidget *backtraceDock = nullptr; QDockWidget *memoryMapDock = nullptr; diff --git a/src/core/MainWindow.ui b/src/core/MainWindow.ui index bf48b6e1..9b181c0c 100644 --- a/src/core/MainWindow.ui +++ b/src/core/MainWindow.ui @@ -153,6 +153,7 @@ + @@ -941,6 +942,14 @@ Threads + + + true + + + Processes + + true diff --git a/src/widgets/ProcessesWidget.cpp b/src/widgets/ProcessesWidget.cpp new file mode 100644 index 00000000..b4e4b2d4 --- /dev/null +++ b/src/widgets/ProcessesWidget.cpp @@ -0,0 +1,202 @@ +#include +#include "ProcessesWidget.h" +#include "ui_ProcessesWidget.h" +#include "common/JsonModel.h" +#include "QuickFilterView.h" +#include + +#include "core/MainWindow.h" + +#define DEBUGGED_PID (-1) + +enum ColumnIndex { + COLUMN_PID = 0, + COLUMN_UID, + COLUMN_STATUS, + COLUMN_PATH, +}; + +ProcessesWidget::ProcessesWidget(MainWindow *main, QAction *action) : + CutterDockWidget(main, action), + ui(new Ui::ProcessesWidget) +{ + ui->setupUi(this); + + // Setup processes model + modelProcesses = new QStandardItemModel(1, 4, this); + modelProcesses->setHorizontalHeaderItem(COLUMN_PID, new QStandardItem(tr("PID"))); + modelProcesses->setHorizontalHeaderItem(COLUMN_UID, new QStandardItem(tr("UID"))); + modelProcesses->setHorizontalHeaderItem(COLUMN_STATUS, new QStandardItem(tr("Status"))); + modelProcesses->setHorizontalHeaderItem(COLUMN_PATH, new QStandardItem(tr("Path"))); + ui->viewProcesses->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter); + ui->viewProcesses->verticalHeader()->setVisible(false); + ui->viewProcesses->setFont(Config()->getFont()); + + modelFilter = new ProcessesFilterModel(this); + modelFilter->setSourceModel(modelProcesses); + ui->viewProcesses->setModel(modelFilter); + + // CTRL+F switches to the filter view and opens it in case it's hidden + QShortcut *searchShortcut = new QShortcut(QKeySequence::Find, this); + connect(searchShortcut, &QShortcut::activated, ui->quickFilterView, &QuickFilterView::showFilter); + searchShortcut->setContext(Qt::WidgetWithChildrenShortcut); + + // ESC switches back to the processes table and clears the buffer + QShortcut *clearShortcut = new QShortcut(QKeySequence(Qt::Key_Escape), this); + connect(clearShortcut, &QShortcut::activated, this, [this]() { + ui->quickFilterView->clearFilter(); + ui->viewProcesses->setFocus(); + }); + clearShortcut->setContext(Qt::WidgetWithChildrenShortcut); + + refreshDeferrer = createRefreshDeferrer([this]() { + updateContents(); + }); + + connect(ui->quickFilterView, &QuickFilterView::filterTextChanged, modelFilter, + &ProcessesFilterModel::setFilterWildcard); + connect(Core(), &CutterCore::refreshAll, this, &ProcessesWidget::updateContents); + connect(Core(), &CutterCore::seekChanged, this, &ProcessesWidget::updateContents); + connect(Core(), &CutterCore::debugTaskStateChanged, this, &ProcessesWidget::updateContents); + // Seek doesn't necessarily change when switching processes + connect(Core(), &CutterCore::switchedProcess, this, &ProcessesWidget::updateContents); + connect(Config(), &Configuration::fontsUpdated, this, &ProcessesWidget::fontsUpdatedSlot); + connect(ui->viewProcesses, &QTableView::activated, this, &ProcessesWidget::onActivated); +} + +ProcessesWidget::~ProcessesWidget() {} + +void ProcessesWidget::updateContents() +{ + if (!refreshDeferrer->attemptRefresh(nullptr)) { + return; + } + + if (Core()->currentlyDebugging) { + setProcessesGrid(); + } else { + // Remove rows from the previous debugging session + modelProcesses->removeRows(0, modelProcesses->rowCount()); + } + + if (Core()->isDebugTaskInProgress() || !Core()->currentlyDebugging) { + ui->viewProcesses->setDisabled(true); + } else { + ui->viewProcesses->setDisabled(false); + } +} + +QString ProcessesWidget::translateStatus(QString status) +{ + switch (status.toStdString().c_str()[0]) { + case R_DBG_PROC_STOP: + return "Stopped"; + case R_DBG_PROC_RUN: + return "Running"; + case R_DBG_PROC_SLEEP: + return "Sleeping"; + case R_DBG_PROC_ZOMBIE: + return "Zombie"; + case R_DBG_PROC_DEAD: + return "Dead"; + case R_DBG_PROC_RAISED: + return "Raised event"; + default: + return "Unknown status"; + } +} + +void ProcessesWidget::setProcessesGrid() +{ + QJsonArray processesValues = Core()->getChildProcesses(DEBUGGED_PID).array(); + int i = 0; + QFont font; + + for (const QJsonValue &value : processesValues) { + QJsonObject processesItem = value.toObject(); + int pid = processesItem["pid"].toVariant().toInt(); + int uid = processesItem["uid"].toVariant().toInt(); + QString status = translateStatus(processesItem["status"].toString()); + QString path = processesItem["path"].toString(); + bool current = processesItem["current"].toBool(); + + // Use bold font to highlight active thread + font.setBold(current); + + QStandardItem *rowPid = new QStandardItem(QString::number(pid)); + QStandardItem *rowUid = new QStandardItem(QString::number(uid)); + QStandardItem *rowStatus = new QStandardItem(status); + QStandardItem *rowPath = new QStandardItem(path); + + rowPid->setFont(font); + rowUid->setFont(font); + rowStatus->setFont(font); + rowPath->setFont(font); + + modelProcesses->setItem(i, COLUMN_PID, rowPid); + modelProcesses->setItem(i, COLUMN_UID, rowUid); + modelProcesses->setItem(i, COLUMN_STATUS, rowStatus); + modelProcesses->setItem(i, COLUMN_PATH, rowPath); + i++; + } + + // Remove irrelevant old rows + if (modelProcesses->rowCount() > i) { + modelProcesses->removeRows(i, modelProcesses->rowCount() - i); + } + + modelFilter->setSourceModel(modelProcesses); + ui->viewProcesses->resizeColumnsToContents();; +} + +void ProcessesWidget::fontsUpdatedSlot() +{ + ui->viewProcesses->setFont(Config()->getFont()); +} + +void ProcessesWidget::onActivated(const QModelIndex &index) +{ + if (!index.isValid()) + return; + + int pid = modelFilter->data(index.sibling(index.row(), COLUMN_PID)).toInt(); + + // Verify that the selected pid is still in the processes list since dp= will + // attach to any given id. If it isn't found simply update the UI. + QJsonArray processesValues = Core()->getChildProcesses(DEBUGGED_PID).array(); + for (QJsonValue value : processesValues) { + QString status = value.toObject()["status"].toString(); + if (pid == value.toObject()["pid"].toInt()) { + if (QString(R_DBG_PROC_ZOMBIE) == status || QString(R_DBG_PROC_DEAD) == status) { + QMessageBox msgBox; + msgBox.setText(tr("Unable to switch to the requested process.")); + msgBox.exec(); + } else { + Core()->setCurrentDebugProcess(pid); + } + break; + } + } + + updateContents(); +} + +ProcessesFilterModel::ProcessesFilterModel(QObject *parent) + : QSortFilterProxyModel(parent) +{ + setFilterCaseSensitivity(Qt::CaseInsensitive); + setSortCaseSensitivity(Qt::CaseInsensitive); +} + +bool ProcessesFilterModel::filterAcceptsRow(int row, const QModelIndex &parent) const +{ + // All columns are checked for a match + for (int i = COLUMN_PID; i <= COLUMN_PATH; ++i) { + QModelIndex index = sourceModel()->index(row, i, parent); + if (sourceModel()->data(index).toString().contains(filterRegExp())) { + return true; + } + } + + return false; +} diff --git a/src/widgets/ProcessesWidget.h b/src/widgets/ProcessesWidget.h new file mode 100644 index 00000000..2552fca6 --- /dev/null +++ b/src/widgets/ProcessesWidget.h @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "core/Cutter.h" +#include "CutterDockWidget.h" + +class MainWindow; + +namespace Ui { +class ProcessesWidget; +} + +class ProcessesFilterModel : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + ProcessesFilterModel(QObject *parent = nullptr); + +protected: + bool filterAcceptsRow(int row, const QModelIndex &parent) const override; +}; + +class ProcessesWidget : public CutterDockWidget +{ + Q_OBJECT + +public: + explicit ProcessesWidget(MainWindow *main, QAction *action = nullptr); + ~ProcessesWidget(); + +private slots: + void updateContents(); + void setProcessesGrid(); + void fontsUpdatedSlot(); + void onActivated(const QModelIndex &index); + +private: + QString translateStatus(QString status); + std::unique_ptr ui; + QStandardItemModel *modelProcesses; + ProcessesFilterModel *modelFilter; + RefreshDeferrer *refreshDeferrer; +}; diff --git a/src/widgets/ProcessesWidget.ui b/src/widgets/ProcessesWidget.ui new file mode 100644 index 00000000..5027fbc6 --- /dev/null +++ b/src/widgets/ProcessesWidget.ui @@ -0,0 +1,72 @@ + + + ProcessesWidget + + + + 0 + 0 + 463 + 300 + + + + Processes + + + + + 10 + + + 0 + + + 0 + + + 0 + + + + + true + + + false + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::ScrollPerPixel + + + + + + + + 0 + 0 + + + + + + + + + + QuickFilterView + QWidget +
widgets/QuickFilterView.h
+ 1 +
+
+ + +
diff --git a/src/widgets/ThreadsWidget.cpp b/src/widgets/ThreadsWidget.cpp index 941bb4d1..a3ad4c20 100644 --- a/src/widgets/ThreadsWidget.cpp +++ b/src/widgets/ThreadsWidget.cpp @@ -55,6 +55,10 @@ ThreadsWidget::ThreadsWidget(MainWindow *main, QAction *action) : &ThreadsFilterModel::setFilterWildcard); connect(Core(), &CutterCore::refreshAll, this, &ThreadsWidget::updateContents); connect(Core(), &CutterCore::seekChanged, this, &ThreadsWidget::updateContents); + connect(Core(), &CutterCore::debugTaskStateChanged, this, &ThreadsWidget::updateContents); + // Seek doesn't necessarily change when switching threads/processes + connect(Core(), &CutterCore::switchedThread, this, &ThreadsWidget::updateContents); + connect(Core(), &CutterCore::switchedProcess, this, &ThreadsWidget::updateContents); connect(Config(), &Configuration::fontsUpdated, this, &ThreadsWidget::fontsUpdatedSlot); connect(ui->viewThreads, &QTableView::activated, this, &ThreadsWidget::onActivated); } @@ -67,10 +71,17 @@ void ThreadsWidget::updateContents() return; } - setThreadsGrid(); + if (Core()->currentlyDebugging) { + setThreadsGrid(); + } else { + // Remove rows from the previous debugging session + modelThreads->removeRows(0, modelThreads->rowCount()); + } if (Core()->isDebugTaskInProgress() || !Core()->currentlyDebugging) { - ui->viewThreads->setSelectionMode(QAbstractItemView::NoSelection); + ui->viewThreads->setDisabled(true); + } else { + ui->viewThreads->setDisabled(false); } }