1
0
Fork 0

Merge pull request #3149 from shocoman/graph-highlight-mode

Add Highlighting mode to the Graph view
This commit is contained in:
Duncan Ogilvie 2023-08-08 21:46:47 +02:00 committed by GitHub
commit b7347f4506
Signed by: GitHub
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 209 additions and 119 deletions

View File

@ -85,10 +85,7 @@ DisassemblerGraphView::DisassemblerGraphView(QWidget* parent)
//colorsUpdatedSlot(); <-- already called somewhere //colorsUpdatedSlot(); <-- already called somewhere
} }
DisassemblerGraphView::~DisassemblerGraphView() DisassemblerGraphView::~DisassemblerGraphView() {}
{
delete this->highlight_token;
}
void DisassemblerGraphView::resetGraph() void DisassemblerGraphView::resetGraph()
{ {
@ -96,7 +93,8 @@ void DisassemblerGraphView::resetGraph()
this->ready = false; this->ready = false;
this->viewportReady = false; this->viewportReady = false;
this->desired_pos = nullptr; this->desired_pos = nullptr;
this->highlight_token = nullptr; this->mHighlightToken = ZydisTokenizer::SingleToken();
this->mHighlightingModeEnabled = false;
this->cur_instr = 0; this->cur_instr = 0;
this->scroll_base_x = 0; this->scroll_base_x = 0;
this->scroll_base_y = 0; this->scroll_base_y = 0;
@ -851,6 +849,19 @@ void DisassemblerGraphView::paintEvent(QPaintEvent* event)
paintZoom(p, viewportRect, xofs, yofs); paintZoom(p, viewportRect, xofs, yofs);
} }
// while selecting a token to highlight, draw a thin 2px red border around the viewport
if(!saveGraph && mHighlightingModeEnabled)
{
QPainter p(this->viewport());
QPen pen(Qt::red);
pen.setWidth(2);
p.setPen(pen);
p.setBrush(Qt::NoBrush); // don't fill the background
QRect viewportRect = this->viewport()->rect();
viewportRect.adjust(1, 1, -1, -1);
p.drawRect(viewportRect);
}
if(saveGraph) if(saveGraph)
{ {
//TODO: speed up large graph saving or show gif loader so it won't look like it has crashed //TODO: speed up large graph saving or show gif loader so it won't look like it has crashed
@ -990,16 +1001,13 @@ duint DisassemblerGraphView::getInstrForMouseEvent(QMouseEvent* event)
return 0; return 0;
} }
bool DisassemblerGraphView::getTokenForMouseEvent(QMouseEvent* event, Token & tokenOut) bool DisassemblerGraphView::getTokenForMouseEvent(QMouseEvent* event, ZydisTokenizer::SingleToken & tokenOut)
{ {
Q_UNUSED(event);
Q_UNUSED(tokenOut);
/* TODO
//Convert coordinates to system used in blocks //Convert coordinates to system used in blocks
int xofs = this->horizontalScrollBar()->value(); int xofs = this->horizontalScrollBar()->value();
int yofs = this->verticalScrollBar()->value(); int yofs = this->verticalScrollBar()->value();
int x = event->x() + xofs - this->renderXOfs; int x = (event->x() + xofs - this->renderXOfs) / zoomLevel;
int y = event->y() + yofs - this->renderYOfs; int y = (event->y() + yofs - this->renderYOfs) / zoomLevel;
//Check each block for hits //Check each block for hits
for(auto & blockIt : this->blocks) for(auto & blockIt : this->blocks)
@ -1016,44 +1024,46 @@ bool DisassemblerGraphView::getTokenForMouseEvent(QMouseEvent* event, Token & to
//Compute row and column within text //Compute row and column within text
int col = int(blockx / this->charWidth); int col = int(blockx / this->charWidth);
int row = int(blocky / this->charHeight); int row = int(blocky / this->charHeight);
//Check tokens to see if one was clicked //Check tokens to see if one was clicked
int cur_row = 0; int selectedCodeRow = row - block.block.header_text.lines.size();
for(auto & line : block.block.header_text.tokens) if(selectedCodeRow < 0)
return false; // skip the header
int rowIndex = 0;
for(auto & instr : block.block.instrs)
{ {
if(cur_row == row) int lineCount = instr.text.lines.size();
if(rowIndex + lineCount > selectedCodeRow)
{ {
for(Token & token : line) // instruction found, try to get the row and precise token from it
size_t instrRow = selectedCodeRow - rowIndex;
if(instrRow < instr.text.lineTokens.size())
{ {
if((col >= token.start) && (col < (token.start + token.length))) auto & instrToken = instr.text.lineTokens.at(instrRow);
int x = instrToken.x; // skip the breakpoint/CIP mark and RVA prefix
for(auto & token : instrToken.tokens)
{ {
//Clicked on a token auto tokenLength = token.text.size();
tokenOut = token; if(col >= x && col < x + tokenLength)
return true;
}
}
}
cur_row += 1;
}
for(Instr & instr : block.block.instrs)
{
for(auto & line : instr.text.tokens)
{
if(cur_row == row)
{
for(Token & token : line)
{
if((col >= token.start) && (col < (token.start + token.length)))
{ {
//Clicked on a token
tokenOut = token; tokenOut = token;
return true; return true;
} }
x += tokenLength;
} }
} }
cur_row += 1;
return false; // selected area doesn't have associated tokens
} }
rowIndex += lineCount;
} }
}*/ }
return false; return false;
} }
@ -1106,44 +1116,82 @@ void DisassemblerGraphView::mousePressEvent(QMouseEvent* event)
wMenu.exec(event->globalPos()); //execute context menu wMenu.exec(event->globalPos()); //execute context menu
} }
} }
else if((event->button() == Qt::LeftButton || event->button() == Qt::RightButton) && inBlock) else if(event->button() & (Qt::LeftButton | Qt::RightButton))
{ {
//Check for click on a token and highlight it // the highlighting behaviour mostly mimics that of CPUDisassembly
Token token; auto oldHighlightToken = mHighlightToken;
delete this->highlight_token; if((event->button() == Qt::RightButton || inBlock) && mHighlightingModeEnabled)
if(this->getTokenForMouseEvent(event, token)) mHighlightToken = ZydisTokenizer::SingleToken();
this->highlight_token = HighlightToken::fromToken(token); bool overrideCtxMenu = mHighlightingModeEnabled; // "intercept" the standard ctx menu
else
this->highlight_token = nullptr;
//Update current instruction if(inBlock)
duint instr = this->getInstrForMouseEvent(event);
if(instr != 0)
{ {
this->cur_instr = instr; //Check for click on a token and highlight it
emit selectionChanged(instr); ZydisTokenizer::SingleToken currentToken;
if(this->getTokenForMouseEvent(event, currentToken))
{
bool isHighlightable = ZydisTokenizer::IsHighlightableToken(currentToken);
if(isHighlightable && (mPermanentHighlightingMode || mHighlightingModeEnabled))
{
if(oldHighlightToken == currentToken && event->button() == Qt::LeftButton)
// on LMB, deselect an already highlighted token
mHighlightToken = ZydisTokenizer::SingleToken();
else
mHighlightToken = currentToken;
}
}
//Update current instruction when the highlighting mode is off
duint instr = this->getInstrForMouseEvent(event);
if(instr != 0 && !mHighlightingModeEnabled)
{
this->cur_instr = instr;
emit selectionChanged(instr);
}
this->viewport()->update();
} }
this->viewport()->update(); if(event->button() == Qt::LeftButton && !inBlock)
{
//Left click outside any block, enter scrolling mode
this->scroll_base_x = event->x();
this->scroll_base_y = event->y();
this->scroll_mode = true;
this->setCursor(Qt::ClosedHandCursor);
this->viewport()->grabMouse();
}
else
{
// preserve the Highlighting mode only when scrolling with LMB
mHighlightingModeEnabled = false;
}
// if the highlighting has changed, show it on the current graph
if(!ZydisTokenizer::TokenEquals(&oldHighlightToken, &mHighlightToken))
for(auto & blockIt : this->blocks)
for(auto & instr : blockIt.second.block.instrs)
instr.text.updateHighlighting(mHighlightToken,
mInstructionHighlightColor,
mInstructionHighlightBackgroundColor);
if(event->button() == Qt::RightButton) if(event->button() == Qt::RightButton)
{ {
showContextMenu(event); if(overrideCtxMenu && !mHighlightToken.text.isEmpty())
{
// show the "copy highlighted token" context menu
QMenu wMenu(this);
mHighlightMenuBuilder->build(&wMenu);
wMenu.exec(event->globalPos());
lastRightClickPosition.pos = {};
}
else if(!overrideCtxMenu)
{
showContextMenu(event);
}
} }
} }
else if(event->button() == Qt::LeftButton)
{
//Left click outside any block, enter scrolling mode
this->scroll_base_x = event->x();
this->scroll_base_y = event->y();
this->scroll_mode = true;
this->setCursor(Qt::ClosedHandCursor);
this->viewport()->grabMouse();
}
else if(event->button() == Qt::RightButton)
{
showContextMenu(event);
}
} }
void DisassemblerGraphView::mouseMoveEvent(QMouseEvent* event) void DisassemblerGraphView::mouseMoveEvent(QMouseEvent* event)
@ -1963,7 +2011,6 @@ bool DisassemblerGraphView::navigate(duint addr)
{ {
this->function = func; this->function = func;
this->cur_instr = instr; this->cur_instr = instr;
this->highlight_token = nullptr;
this->ready = false; this->ready = false;
this->desired_pos = nullptr; this->desired_pos = nullptr;
this->viewport()->update(); this->viewport()->update();
@ -1997,6 +2044,7 @@ void DisassemblerGraphView::setGraphLayout(DisassemblerGraphView::LayoutType lay
void DisassemblerGraphView::tokenizerConfigUpdatedSlot() void DisassemblerGraphView::tokenizerConfigUpdatedSlot()
{ {
disasm.UpdateConfig(); disasm.UpdateConfig();
mPermanentHighlightingMode = ConfigBool("Disassembler", "PermanentHighlightingMode");
loadCurrentGraph(); loadCurrentGraph();
} }
@ -2036,7 +2084,11 @@ void DisassemblerGraphView::loadCurrentGraph()
block.true_path = node.brtrue; block.true_path = node.brtrue;
block.terminal = node.terminal; block.terminal = node.terminal;
block.indirectcall = node.indirectcall; block.indirectcall = node.indirectcall;
block.header_text = Text(getSymbolicName(block.entry), mLabelColor, mLabelBackgroundColor);
auto headerRich = Text::makeRich(getSymbolicName(block.entry), mLabelColor, mLabelBackgroundColor);
block.header_text = Text();
block.header_text.addLine({headerRich}, {});
{ {
Instr instr; Instr instr;
for(const BridgeCFInstruction & nodeInstr : node.instrs) for(const BridgeCFInstruction & nodeInstr : node.instrs)
@ -2045,18 +2097,17 @@ void DisassemblerGraphView::loadCurrentGraph()
currentBlockMap[addr] = block.entry; currentBlockMap[addr] = block.entry;
Instruction_t instrTok = disasm.DisassembleAt((byte_t*)nodeInstr.data, sizeof(nodeInstr.data), 0, addr, false); Instruction_t instrTok = disasm.DisassembleAt((byte_t*)nodeInstr.data, sizeof(nodeInstr.data), 0, addr, false);
RichTextPainter::List richText; RichTextPainter::List richText;
ZydisTokenizer::TokenToRichText(instrTok.tokens, richText, 0); auto zydisTokens = instrTok.tokens;
ZydisTokenizer::TokenToRichText(zydisTokens, richText, nullptr);
zydisTokens.x += 1; // account for the breakpoint/CIP mark
// add rva to node instruction text // add rva to node instruction text
if(showGraphRva) if(showGraphRva)
{ {
RichTextPainter::CustomRichText_t rvaText; QString rvaText = QString().number(instrTok.rva, 16).toUpper().trimmed() + " ";
rvaText.underline = false; auto rvaRich = Text::makeRich(rvaText, mAddressColor, mAddressBackgroundColor);
rvaText.textColor = mAddressColor; richText.insert(richText.begin(), rvaRich);
rvaText.textBackground = mAddressBackgroundColor; zydisTokens.x += rvaText.length(); // pad tokens for the highlighting mode
rvaText.text = QString().number(instrTok.rva, 16).toUpper().trimmed() + " ";
rvaText.flags = rvaText.textBackground.alpha() ? RichTextPainter::FlagAll : RichTextPainter::FlagColor;
richText.insert(richText.begin(), rvaText);
} }
auto size = instrTok.length; auto size = instrTok.length;
@ -2101,7 +2152,9 @@ void DisassemblerGraphView::loadCurrentGraph()
richText.push_back(spaceText); richText.push_back(spaceText);
richText.push_back(commentText); richText.push_back(commentText);
} }
instr.text = Text(richText); instr.text = Text();
instr.text.addLine(richText, zydisTokens);
instr.text.updateHighlighting(mHighlightToken, mInstructionHighlightColor, mInstructionHighlightBackgroundColor);
//The summary contains calls, rets, user comments and string references //The summary contains calls, rets, user comments and string references
if(!onlySummary || if(!onlySummary ||
@ -2303,6 +2356,7 @@ void DisassemblerGraphView::setupContextMenu()
gotoMenu->addBuilder(childrenAndParentMenu); gotoMenu->addBuilder(childrenAndParentMenu);
mMenuBuilder->addMenu(makeMenu(DIcon("goto"), tr("Go to")), gotoMenu); mMenuBuilder->addMenu(makeMenu(DIcon("goto"), tr("Go to")), gotoMenu);
mMenuBuilder->addAction(makeShortcutAction(DIcon("helpmnemonic"), tr("Help on mnemonic"), SLOT(mnemonicHelpSlot()), "ActionHelpOnMnemonic")); mMenuBuilder->addAction(makeShortcutAction(DIcon("helpmnemonic"), tr("Help on mnemonic"), SLOT(mnemonicHelpSlot()), "ActionHelpOnMnemonic"));
mMenuBuilder->addAction(makeShortcutAction(DIcon("highlight"), tr("&Highlighting mode"), SLOT(enableHighlightingModeSlot()), "ActionHighlightingMode"));
mMenuBuilder->addSeparator(); mMenuBuilder->addSeparator();
auto ifgraphZoomMode = [this](QMenu*) auto ifgraphZoomMode = [this](QMenu*)
{ {
@ -2345,6 +2399,17 @@ void DisassemblerGraphView::setupContextMenu()
return true; return true;
})); }));
// Highlighting mode menu
mHighlightMenuBuilder = new MenuBuilder(this);
mHighlightMenuBuilder->addAction(makeAction(DIcon("copy"), tr("Copy token &text"), SLOT(copyHighlightedTokenTextSlot())));
mHighlightMenuBuilder->addAction(makeAction(DIcon("copy_address"), tr("Copy token &value"), SLOT(copyHighlightedTokenValueSlot())), [this](QMenu*)
{
QString text;
if(!getHighlightedTokenValueText(text))
return false;
return text != mHighlightToken.text;
});
mMenuBuilder->loadFromConfig(); mMenuBuilder->loadFromConfig();
} }
@ -2412,6 +2477,8 @@ void DisassemblerGraphView::colorsUpdatedSlot()
mBreakpointColor = ConfigColor("GraphBreakpointColor"); mBreakpointColor = ConfigColor("GraphBreakpointColor");
mDisabledBreakpointColor = ConfigColor("GraphDisabledBreakpointColor"); mDisabledBreakpointColor = ConfigColor("GraphDisabledBreakpointColor");
mBookmarkBackgroundColor = ConfigColor("DisassemblyBookmarkBackgroundColor"); mBookmarkBackgroundColor = ConfigColor("DisassemblyBookmarkBackgroundColor");
mInstructionHighlightColor = ConfigColor("InstructionHighlightColor");
mInstructionHighlightBackgroundColor = ConfigColor("InstructionHighlightBackgroundColor");
fontChanged(); fontChanged();
loadCurrentGraph(); loadCurrentGraph();
@ -2618,3 +2685,32 @@ void DisassemblerGraphView::dbgStateChangedSlot(DBGSTATE state)
this->viewport()->update(); this->viewport()->update();
} }
} }
void DisassemblerGraphView::copyHighlightedTokenTextSlot()
{
Bridge::CopyToClipboard(mHighlightToken.text);
}
void DisassemblerGraphView::copyHighlightedTokenValueSlot()
{
QString text;
if(getHighlightedTokenValueText(text))
Bridge::CopyToClipboard(text);
}
bool DisassemblerGraphView::getHighlightedTokenValueText(QString & text)
{
if(mHighlightToken.type <= ZydisTokenizer::TokenType::MnemonicUnusual)
return false;
duint value = mHighlightToken.value.value;
if(!mHighlightToken.value.size && !DbgFunctions()->ValFromString(mHighlightToken.text.toUtf8().constData(), &value))
return false;
text = ToHexString(value);
return true;
}
void DisassemblerGraphView::enableHighlightingModeSlot()
{
mHighlightingModeEnabled = !mHighlightingModeEnabled;
this->viewport()->update();
}

View File

@ -56,61 +56,45 @@ public:
} }
}; };
struct Token
{
int start; //token[0]
int length; //token[1]
QString type; //token[2]
duint addr; //token[3]
QString name; //token[4]
};
struct HighlightToken
{
QString type; //highlight_token[0]
duint addr; //highlight_token[1]
QString name; //highlight_token[2]
bool equalsToken(const Token & token)
{
return this->type == token.type &&
this->addr == token.addr &&
this->name == token.name;
}
static HighlightToken* fromToken(const Token & token)
{
//TODO: memory leaks
auto result = new HighlightToken();
result->type = token.type;
result->addr = token.addr;
result->name = token.name;
return result;
}
};
struct Text struct Text
{ {
// text to render; some words here may be colored (with highlighting mode)
std::vector<RichTextPainter::List> lines; std::vector<RichTextPainter::List> lines;
// original text (w/o highlighting)
std::vector<RichTextPainter::List> backupLines;
// tokens for selection in "Highlighting mode"; one "InstructionToken" per line
std::vector<ZydisTokenizer::InstructionToken> lineTokens;
Text() {} Text() {}
Text(const QString & text, QColor color, QColor background) void addLine(const RichTextPainter::List & richText, const ZydisTokenizer::InstructionToken & tokens)
{
lines.push_back(richText);
backupLines.push_back(richText);
lineTokens.push_back(tokens);
}
void updateHighlighting(const ZydisTokenizer::SingleToken & token, QColor color, QColor background)
{
// highlight the given token and restore the original colors to the rest of the text
for(size_t nLine = 0; nLine < lines.size(); nLine++)
for(size_t nRich = 0; nRich < lines[nLine].size(); nRich++)
{
auto & rt = lines[nLine][nRich], & orig = backupLines[nLine][nRich];
rt.textColor = rt.text == token.text ? color : orig.textColor;
rt.textBackground = rt.text == token.text ? background : orig.textBackground;
}
}
static RichTextPainter::CustomRichText_t makeRich(const QString & text, QColor color, QColor background)
{ {
RichTextPainter::List richText;
RichTextPainter::CustomRichText_t rt; RichTextPainter::CustomRichText_t rt;
rt.underline = false; rt.underline = false;
rt.text = text; rt.text = text;
rt.textColor = color; rt.textColor = color;
rt.textBackground = background; rt.textBackground = background;
rt.flags = rt.textBackground.alpha() ? RichTextPainter::FlagAll : RichTextPainter::FlagColor; rt.flags = rt.textBackground.alpha() ? RichTextPainter::FlagAll : RichTextPainter::FlagColor;
richText.push_back(rt); return rt;
lines.push_back(richText);
}
Text(const RichTextPainter::List & richText)
{
lines.push_back(richText);
} }
QString ToQString() const QString ToQString() const
@ -222,7 +206,7 @@ public:
void paintEvent(QPaintEvent* event); void paintEvent(QPaintEvent* event);
bool isMouseEventInBlock(QMouseEvent* event); bool isMouseEventInBlock(QMouseEvent* event);
duint getInstrForMouseEvent(QMouseEvent* event); duint getInstrForMouseEvent(QMouseEvent* event);
bool getTokenForMouseEvent(QMouseEvent* event, Token & token); bool getTokenForMouseEvent(QMouseEvent* event, ZydisTokenizer::SingleToken & token);
bool find_instr(duint addr, Instr & instr); bool find_instr(duint addr, Instr & instr);
void mousePressEvent(QMouseEvent* event); void mousePressEvent(QMouseEvent* event);
void mouseMoveEvent(QMouseEvent* event); void mouseMoveEvent(QMouseEvent* event);
@ -289,6 +273,9 @@ public slots:
void zoomToCursorSlot(); void zoomToCursorSlot();
void getCurrentGraphSlot(BridgeCFGraphList* graphList); void getCurrentGraphSlot(BridgeCFGraphList* graphList);
void dbgStateChangedSlot(DBGSTATE state); void dbgStateChangedSlot(DBGSTATE state);
void copyHighlightedTokenTextSlot();
void copyHighlightedTokenValueSlot();
void enableHighlightingModeSlot();
private: private:
bool graphZoomMode; bool graphZoomMode;
@ -324,7 +311,6 @@ private:
bool viewportReady; bool viewportReady;
int* desired_pos; int* desired_pos;
std::unordered_map<duint, DisassemblerBlock> blocks; std::unordered_map<duint, DisassemblerBlock> blocks;
HighlightToken* highlight_token;
std::vector<int> col_edge_x; std::vector<int> col_edge_x;
std::vector<int> row_edge_y; std::vector<int> row_edge_y;
CachedFontMetrics* mFontMetrics; CachedFontMetrics* mFontMetrics;
@ -343,6 +329,11 @@ private:
bool mHistoryLock; //Don't add a history while going to previous/next bool mHistoryLock; //Don't add a history while going to previous/next
LayoutType layoutType; LayoutType layoutType;
MenuBuilder* mHighlightMenuBuilder;
ZydisTokenizer::SingleToken mHighlightToken;
bool mHighlightingModeEnabled;
bool mPermanentHighlightingMode;
QAction* mToggleOverview; QAction* mToggleOverview;
QAction* mToggleSummary; QAction* mToggleSummary;
QAction* mToggleSyncOrigin; QAction* mToggleSyncOrigin;
@ -374,6 +365,8 @@ private:
QColor graphNodeColor; QColor graphNodeColor;
QColor graphNodeBackgroundColor; QColor graphNodeBackgroundColor;
QColor graphCurrentShadowColor; QColor graphCurrentShadowColor;
QColor mInstructionHighlightColor;
QColor mInstructionHighlightBackgroundColor;
BridgeCFGraph currentGraph; BridgeCFGraph currentGraph;
std::unordered_map<duint, duint> currentBlockMap; std::unordered_map<duint, duint> currentBlockMap;
@ -382,4 +375,5 @@ private:
XrefBrowseDialog* mXrefDlg; XrefBrowseDialog* mXrefDlg;
void addReferenceAction(QMenu* menu, duint addr, const QString & description); void addReferenceAction(QMenu* menu, duint addr, const QString & description);
bool getHighlightedTokenValueText(QString & text);
}; };