# Copyright (c) 2011-2015 Rusty Wagner
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
import threading
from Analysis import *
from Fonts import *
from View import *
from FindDialog import *
from ArchitectureDialog import *
class DisassemblerBlock:
def __init__(self, block):
self.block = block
self.edges = []
self.incoming = []
self.new_exits = []
class DisassemblerEdge:
def __init__(self, color, dest):
self.color = color
self.dest = dest
self.points = []
self.start_index = 0
def addPoint(self, row, col, index = 0):
self.points += [[row, col, 0]]
if len(self.points) > 1:
self.points[len(self.points) - 2][2] = index
class DisassemblerHistoryEntry:
def __init__(self, view):
self.function = view.function
self.scroll_x = view.horizontalScrollBar().value()
self.scroll_y = view.verticalScrollBar().value()
self.cur_instr = view.cur_instr
self.highlight_token = view.highlight_token
class DisassemblerView(QAbstractScrollArea):
statusUpdated = Signal(QWidget, name="statusUpdated")
def __init__(self, data, filename, view, parent):
super(DisassemblerView, self).__init__(parent)
self.status = ""
self.view = view
self.data = data
for type in ExeFormats:
exe = type(data)
if exe.valid:
self.data = exe
self.view.exe = exe
break
# Create analysis and start it in another thread
self.analysis = Analysis(self.data)
self.analysis_thread = threading.Thread(None, self.analysis_thread_proc)
self.analysis_thread.daemon = True
self.analysis_thread.start()
# Start disassembly view at the entry point of the binary
if hasattr(self.data, "entry"):
self.function = self.data.entry()
else:
self.function = None
self.update_id = None
self.ready = False
self.desired_pos = None
self.highlight_token = None
self.cur_instr = None
self.scroll_mode = False
self.blocks = {}
self.show_il = False
self.simulation = None
# Create timer to automatically refresh view when it needs to be updated
self.updateTimer = QTimer()
self.updateTimer.setInterval(100)
self.updateTimer.setSingleShot(False)
self.updateTimer.timeout.connect(self.updateTimerEvent)
self.updateTimer.start()
self.initFont()
# Initialize scroll bars
self.width = 0
self.height = 0
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.horizontalScrollBar().setSingleStep(self.charWidth)
self.verticalScrollBar().setSingleStep(self.charHeight)
areaSize = self.viewport().size()
self.adjustSize(areaSize.width(), areaSize.height())
# Setup navigation
self.view.register_navigate("disassembler", self, self.navigate)
self.view.register_navigate("make_proc", self, self.make_proc)
self.search_regex = None
self.last_search_type = FindDialog.SEARCH_HEX
def initFont(self):
# Get font and compute character sizes
self.font = getMonospaceFont()
self.baseline = int(QFontMetricsF(self.font).ascent())
self.charWidth = QFontMetricsF(self.font).width('X')
self.charHeight = int(QFontMetricsF(self.font).height()) + getExtraFontSpacing()
self.charOffset = getFontVerticalOffset()
def adjustSize(self, width, height):
# Recompute size information
self.renderWidth = self.width
self.renderHeight = self.height
self.renderXOfs = 0
self.renderYOfs = 0
if self.renderWidth < width:
self.renderXOfs = int((width - self.renderWidth) / 2)
self.renderWidth = width
if self.renderHeight < height:
self.renderYOfs = int((height - self.renderHeight) / 2)
self.renderHeight = height
# Update scroll bar information
self.horizontalScrollBar().setPageStep(width)
self.horizontalScrollBar().setRange(0, self.renderWidth - width)
self.verticalScrollBar().setPageStep(height)
self.verticalScrollBar().setRange(0, self.renderHeight - height)
def resizeEvent(self, event):
# Window was resized, adjust scroll bar
self.adjustSize(event.size().width(), event.size().height())
def get_cursor_pos(self):
if self.cur_instr is None:
return self.function
return self.cur_instr
def set_cursor_pos(self, addr):
if not self.view.navigate("disassembler", addr):
self.view_in_hex_editor(addr)
def get_selection_range(self):
return (self.get_cursor_pos(), self.get_cursor_pos())
def set_selection_range(self, begin, end):
self.set_cursor_pos(begin)
def write(self, data):
pos = self.get_cursor_pos()
if pos is None:
return False
return self.data.write(pos, data) == len(data)
def copy_address(self):
clipboard = QApplication.clipboard()
clipboard.clear()
mime = QMimeData()
mime.setText("0x%x" % self.get_cursor_pos())
clipboard.setMimeData(mime)
def analysis_thread_proc(self):
self.analysis.analyze()
def closeRequest(self):
# Stop analysis when closing tab
self.analysis.stop()
return True
def paintEvent(self, event):
# Initialize painter
p = QPainter(self.viewport())
p.setFont(self.font)
xofs = self.horizontalScrollBar().value()
yofs = self.verticalScrollBar().value()
if not self.ready:
# Analysis for the current function is not yet complete, paint loading screen
gradient = QLinearGradient(QPointF(0, 0), QPointF(self.viewport().size().width(), self.viewport().size().height()))
gradient.setColorAt(0, QColor(232, 232, 232))
gradient.setColorAt(1, QColor(192, 192, 192))
p.setPen(QColor(0, 0, 0, 0))
p.setBrush(QBrush(gradient))
p.drawRect(0, 0, self.viewport().size().width(), self.viewport().size().height())
if self.function is None:
text = "No function selected"
else:
text = "Loading..."
p.setPen(Qt.black)
p.drawText((self.viewport().size().width() / 2) - ((len(text) * self.charWidth) / 2),
(self.viewport().size().height() / 2) + self.charOffset + self.baseline - (self.charHeight / 2), text)
return
# Render background
gradient = QLinearGradient(QPointF(-xofs, -yofs), QPointF(self.renderWidth - xofs, self.renderHeight - yofs))
gradient.setColorAt(0, QColor(232, 232, 232))
gradient.setColorAt(1, QColor(192, 192, 192))
p.setPen(QColor(0, 0, 0, 0))
p.setBrush(QBrush(gradient))
p.drawRect(0, 0, self.viewport().size().width(), self.viewport().size().height())
p.translate(self.renderXOfs - xofs, self.renderYOfs - yofs)
# Render each node
for block in self.blocks.values():
# Render shadow
p.setPen(QColor(0, 0, 0, 0))
p.setBrush(QColor(0, 0, 0, 128))
p.drawRect(block.x + self.charWidth + 4, block.y + self.charWidth + 4,
block.width - (4 + 2 * self.charWidth), block.height - (4 + 2 * self.charWidth))
# Render node background
gradient = QLinearGradient(QPointF(0, block.y + self.charWidth),
QPointF(0, block.y + block.height - self.charWidth))
gradient.setColorAt(0, QColor(255, 255, 252))
gradient.setColorAt(1, QColor(255, 255, 232))
p.setPen(Qt.black)
p.setBrush(QBrush(gradient))
p.drawRect(block.x + self.charWidth, block.y + self.charWidth,
block.width - (4 + 2 * self.charWidth), block.height - (4 + 2 * self.charWidth))
if self.cur_instr != None:
y = block.y + (2 * self.charWidth) + (len(block.block.header_text.lines) * self.charHeight)
for instr in block.block.instrs:
if instr.addr == self.cur_instr:
p.setPen(QColor(0, 0, 0, 0))
p.setBrush(QColor(255, 255, 128, 128))
p.drawRect(block.x + self.charWidth + 3, y, block.width - (10 + 2 * self.charWidth),
len(instr.text.lines) * self.charHeight)
y += len(instr.text.lines) * self.charHeight
if self.highlight_token:
# Render highlighted tokens
x = block.x + (2 * self.charWidth)
y = block.y + (2 * self.charWidth)
for line in block.block.header_text.tokens:
for token in line:
if token[2:] == self.highlight_token:
p.setPen(QColor(0, 0, 0, 0))
p.setBrush(QColor(192, 0, 0, 64))
p.drawRect(x + token[0] * self.charWidth, y,
token[1] * self.charWidth, self.charHeight)
y += self.charHeight
for instr in block.block.instrs:
for line in instr.text.tokens:
for token in line:
if token[2:] == self.highlight_token:
p.setPen(QColor(0, 0, 0, 0))
p.setBrush(QColor(192, 0, 0, 64))
p.drawRect(x + token[0] * self.charWidth, y,
token[1] * self.charWidth, self.charHeight)
y += self.charHeight
# Render node text
x = block.x + (2 * self.charWidth)
y = block.y + (2 * self.charWidth)
for line in block.block.header_text.lines:
partx = x
for part in line:
p.setPen(part[1])
p.drawText(partx, y + self.charOffset + self.baseline, part[0])
partx += len(part[0]) * self.charWidth
y += self.charHeight
for instr in block.block.instrs:
for line in instr.text.lines:
partx = x
for part in line:
p.setPen(part[1])
p.drawText(partx, y + self.charOffset + self.baseline, part[0])
partx += len(part[0]) * self.charWidth
y += self.charHeight
# Render edges
for edge in block.edges:
p.setPen(edge.color)
p.setBrush(edge.color)
p.drawPolyline(edge.polyline)
p.drawConvexPolygon(edge.arrow)
def isMouseEventInBlock(self, event):
# Convert coordinates to system used in blocks
xofs = self.horizontalScrollBar().value()
yofs = self.verticalScrollBar().value()
x = event.x() + xofs - self.renderXOfs
y = event.y() + yofs - self.renderYOfs
# Check each block for hits
for block in self.blocks.values():
# Compute coordinate relative to text area in block
blockx = x - (block.x + (2 * self.charWidth))
blocky = y - (block.y + (2 * self.charWidth))
# Check to see if click is within bounds of block
if (blockx < 0) or (blockx > (block.width - 4 * self.charWidth)):
continue
if (blocky < 0) or (blocky > (block.height - 4 * self.charWidth)):
continue
return True
return False
def getInstrForMouseEvent(self, event):
# Convert coordinates to system used in blocks
xofs = self.horizontalScrollBar().value()
yofs = self.verticalScrollBar().value()
x = event.x() + xofs - self.renderXOfs
y = event.y() + yofs - self.renderYOfs
# Check each block for hits
for block in self.blocks.values():
# Compute coordinate relative to text area in block
blockx = x - (block.x + (2 * self.charWidth))
blocky = y - (block.y + (2 * self.charWidth))
# Check to see if click is within bounds of block
if (blockx < 0) or (blockx > (block.width - 4 * self.charWidth)):
continue
if (blocky < 0) or (blocky > (block.height - 4 * self.charWidth)):
continue
# Compute row within text
row = int(blocky / self.charHeight)
# Determine instruction for this row
cur_row = len(block.block.header_text.lines)
if row < cur_row:
return block.block.entry
for instr in block.block.instrs:
if row < cur_row + len(instr.text.lines):
return instr.addr
cur_row += len(instr.text.lines)
return None
def getTokenForMouseEvent(self, event):
# Convert coordinates to system used in blocks
xofs = self.horizontalScrollBar().value()
yofs = self.verticalScrollBar().value()
x = event.x() + xofs - self.renderXOfs
y = event.y() + yofs - self.renderYOfs
# Check each block for hits
for block in self.blocks.values():
# Compute coordinate relative to text area in block
blockx = x - (block.x + (2 * self.charWidth))
blocky = y - (block.y + (2 * self.charWidth))
# Check to see if click is within bounds of block
if (blockx < 0) or (blockx > (block.width - 4 * self.charWidth)):
continue
if (blocky < 0) or (blocky > (block.height - 4 * self.charWidth)):
continue
# Compute row and column within text
col = int(blockx / self.charWidth)
row = int(blocky / self.charHeight)
# Check tokens to see if one was clicked
cur_row = 0
for line in block.block.header_text.tokens:
if cur_row == row:
for token in line:
if (col >= token[0]) and (col < (token[0] + token[1])):
# Clicked on a token
return token
cur_row += 1
for instr in block.block.instrs:
for line in instr.text.tokens:
if cur_row == row:
for token in line:
if (col >= token[0]) and (col < (token[0] + token[1])):
# Clicked on a token
return token
cur_row += 1
return None
def find_instr(self, addr):
for block in self.blocks.values():
for instr in block.block.instrs:
if instr.addr == addr:
return instr
return None
def nop_out(self, addr):
instr = self.find_instr(addr)
if instr != None:
self.view.begin_undo()
instr.patch_to_nop(self.data)
self.view.commit_undo()
def always_branch(self, addr):
instr = self.find_instr(addr)
if instr != None:
self.view.begin_undo()
instr.patch_to_always_branch(self.data)
self.view.commit_undo()
def invert_branch(self, addr):
instr = self.find_instr(addr)
if instr != None:
self.view.begin_undo()
instr.patch_to_invert_branch(self.data)
self.view.commit_undo()
def skip_and_return_zero(self, addr):
instr = self.find_instr(addr)
if instr != None:
self.view.begin_undo()
instr.patch_to_zero_return(self.data)
self.view.commit_undo()
def skip_and_return_value(self, addr):
instr = self.find_instr(addr)
if instr != None:
value, ok = QInputDialog.getText(self, "Skip and Return Value", "Return value:", QLineEdit.Normal)
if ok:
try:
value = int(value, 0)
except:
QMessageBox.critical(self, "Error", "Expected numerical address")
return
self.view.begin_undo()
instr.patch_to_fixed_return_value(self.data, value)
self.view.commit_undo()
def view_in_hex_editor(self, addr):
if not self.view.navigate("exe", addr):
self.view.navigate("hex", addr)
def show_address(self):
if "address" in self.analysis.options:
addr = False
else:
addr = True
self.analysis.set_address_view(addr)
def context_menu(self, addr):
popup = QMenu()
view_in_hex = popup.addAction("View in &hex editor")
view_in_hex.triggered.connect(lambda : self.view_in_hex_editor(addr))
view_in_hex.setShortcut(QKeySequence(Qt.Key_H))
popup.addAction("Copy address", self.copy_address)
enter_name_action = popup.addAction("Re&name symbol", self.enter_name)
enter_name_action.setShortcut(QKeySequence(Qt.Key_N))
undefine_name_action = popup.addAction("&Undefine symbol", self.undefine_name)
undefine_name_action.setShortcut(QKeySequence(Qt.Key_U))
show_address_action = popup.addAction("Show &address", self.show_address)
show_address_action.setCheckable(True)
show_address_action.setChecked("address" in self.analysis.options)
popup.addSeparator()
patch = popup.addMenu("&Patch")
patch.addAction("Convert to NOP").triggered.connect(lambda : self.nop_out(addr))
instr = self.find_instr(addr)
if instr:
if instr.is_patch_branch_allowed():
patch.addAction("Never branch").triggered.connect(lambda : self.nop_out(addr))
patch.addAction("Always branch").triggered.connect(lambda : self.always_branch(addr))
patch.addAction("Invert branch").triggered.connect(lambda : self.invert_branch(addr))
if instr.is_patch_to_zero_return_allowed():
patch.addAction("Skip and return zero").triggered.connect(lambda : self.skip_and_return_zero(addr))
if instr.is_patch_to_fixed_return_value_allowed():
patch.addAction("Skip and return value...").triggered.connect(lambda : self.skip_and_return_value(addr))
popup.exec_(QCursor.pos())
def mousePressEvent(self, event):
if (event.button() != Qt.LeftButton) and (event.button() != Qt.RightButton):
return
if not self.isMouseEventInBlock(event):
# Click outside any block, enter scrolling mode
self.scroll_base_x = event.x()
self.scroll_base_y = event.y()
self.scroll_mode = True
self.viewport().grabMouse()
return
# Check for click on a token and highlight it
token = self.getTokenForMouseEvent(event)
if token:
self.highlight_token = token[2:]
else:
self.highlight_token = None
# Update current instruction
instr = self.getInstrForMouseEvent(event)
if instr != None:
self.cur_instr = instr
else:
self.cur_instr = None
self.viewport().update()
if (instr != None) and (event.button() == Qt.RightButton):
self.context_menu(instr)
def mouseMoveEvent(self, event):
if self.scroll_mode:
x_delta = self.scroll_base_x - event.x()
y_delta = self.scroll_base_y - event.y()
self.scroll_base_x = event.x()
self.scroll_base_y = event.y()
self.horizontalScrollBar().setValue(self.horizontalScrollBar().value() + x_delta)
self.verticalScrollBar().setValue(self.verticalScrollBar().value() + y_delta)
def mouseReleaseEvent(self, event):
if event.button() != Qt.LeftButton:
return
if self.scroll_mode:
self.scroll_mode = False
self.viewport().releaseMouse()
def mouseDoubleClickEvent(self, event):
token = self.getTokenForMouseEvent(event)
if token and (token[2] == "ptr"):
self.analysis.lock.acquire()
if not self.analysis.functions.has_key(token[3]):
# Not a function or not analyzed, go to address in hex editor
addr = token[3]
self.analysis.lock.release()
self.view_in_hex_editor(addr)
else:
self.view.add_history_entry()
self.function = token[3]
self.ready = False
self.desired_pos = None
self.cur_instr = None
self.highlight_token = None
self.viewport().update()
self.analysis.lock.release()
def go_to_address(self):
addr_str, ok = QInputDialog.getText(self, "Go To Address", "Address:", QLineEdit.Normal)
if ok:
try:
addr = int(addr_str, 16)
if (addr < self.data.start()) or (addr > self.data.end()):
if hasattr(self.data, "symbols_by_name") and (addr_str in self.data.symbols_by_name):
addr = self.data.symbols_by_name[addr_str]
else:
QMessageBox.critical(self, "Error", "Address out of range")
return
except:
if hasattr(self.data, "symbols_by_name") and (addr_str in self.data.symbols_by_name):
addr = self.data.symbols_by_name[addr_str]
elif (addr_str[0] == '@') and hasattr(self.data, "symbols_by_name") and (addr_str[1:] in self.data.symbols_by_name):
addr = self.data.symbols_by_name[addr_str[1:]]
else:
QMessageBox.critical(self, "Error", "Invalid address or symbol")
return
# Try navigating within disassembly, if it isn't within a function then
# navigate to the hex editor
if not self.view.navigate("disassembler", addr):
self.view_in_hex_editor(addr)
def enter_name(self):
# A symbol must be selected
if (self.highlight_token == None) or (self.highlight_token[0] != "ptr"):
QMessageBox.critical(self, "Error", "No symbol selected.")
return
addr = self.highlight_token[1]
name = self.highlight_token[2]
# Ask for new name
new_name, ok = QInputDialog.getText(self, "Rename Symbol", "Symbol name:", QLineEdit.Normal, name)
if ok:
self.analysis.create_symbol(addr, new_name)
def undefine_name(self):
# A symbol must be selected
if (self.highlight_token == None) or (self.highlight_token[0] != "ptr"):
QMessageBox.critical(self, "Error", "No symbol selected.")
return
addr = self.highlight_token[1]
name = self.highlight_token[2]
# Ask for new name
self.analysis.undefine_symbol(addr, name)
def navigate_for_find(self, addr):
func, instr = self.analysis.find_instr(addr, True)
if func != None:
self.navigate(addr)
else:
self.make_proc(addr)
self.cur_instr = addr
self.desired_pos = None
def perform_find(self, dlg):
self.search_regex = dlg.search_regex()
if self.cur_instr != None:
self.search_start = self.cur_instr
else:
if self.function is None:
return
self.search_start = self.function
found_loc = self.data.find(self.search_regex, self.search_start)
if found_loc != -1:
self.view.add_history_entry()
self.navigate_for_find(found_loc)
self.search_pos = found_loc + 1
return
found_loc = self.data.find(self.search_regex, self.data.start())
if (found_loc != -1) and (found_loc < self.search_start):
self.view.add_history_entry()
self.navigate_for_find(found_loc)
self.search_pos = found_loc + 1
return
QMessageBox.information(self, "Not Found", "Search string not found.")
def find(self):
dlg = FindDialog(self.last_search_type, self)
if dlg.exec_() == QDialog.Accepted:
self.last_search_type = dlg.search_type()
self.perform_find(dlg)
def find_next(self):
if self.search_regex == None:
QMessageBox.critical(self, "Error", "No active search")
return
found_loc = self.data.find(self.search_regex, self.search_pos)
if self.search_pos >= self.search_start:
if found_loc != -1:
self.view.add_history_entry()
self.navigate_for_find(found_loc)
self.search_pos = found_loc + 1
return
self.search_pos = 0
else:
if (found_loc != -1) and (found_loc < self.search_start):
self.view.add_history_entry()
self.navigate_for_find(found_loc)
self.search_pos = found_loc + 1
return
QMessageBox.information(self, "End of Search", "No additional matches found.")
self.search_pos = self.search_start
return
found_loc = self.data.find(self.search_regex, self.search_pos)
if found_loc < self.search_start:
self.view.add_history_entry()
self.navigate_for_find(found_loc)
self.search_pos = found_loc + 1
return
QMessageBox.information(self, "End of Search", "No additional matches found.")
self.search_pos = self.search_start
def keyPressEvent(self, event):
if event.key() == Qt.Key_H:
if self.cur_instr != None:
self.view_in_hex_editor(self.cur_instr)
else:
if self.function is not None:
self.view_in_hex_editor(self.function)
elif event.key() == Qt.Key_G:
self.go_to_address()
elif event.key() == Qt.Key_N:
self.enter_name()
elif event.key() == Qt.Key_U:
self.undefine_name()
elif event.key() == Qt.Key_Slash:
dlg = FindDialog(FindDialog.SEARCH_REGEX, self)
if dlg.exec_() == QDialog.Accepted:
self.perform_find(dlg)
else:
super(DisassemblerView, self).keyPressEvent(event)
def prepareGraphNode(self, block):
# Compute size of node in pixels
width = 0
height = 0
for line in block.block.header_text.lines:
chars = 0
for part in line:
chars += len(part[0])
if chars > width:
width = chars
height += 1
for instr in block.block.instrs:
for line in instr.text.lines:
chars = 0
for part in line:
chars += len(part[0])
if chars > width:
width = chars
height += 1
block.width = (width + 4) * self.charWidth + 4
block.height = (height * self.charHeight) + (4 * self.charWidth) + 4
def adjustGraphLayout(self, block, col, row):
block.col += col
block.row += row
for edge in block.new_exits:
self.adjustGraphLayout(self.blocks[edge], col, row)
def computeGraphLayout(self, block):
# Compute child node layouts and arrange them horizontally
col = 0
row_count = 1
for edge in block.new_exits:
self.computeGraphLayout(self.blocks[edge])
self.adjustGraphLayout(self.blocks[edge], col, 1)
col += self.blocks[edge].col_count
if (self.blocks[edge].row_count + 1) > row_count:
row_count = self.blocks[edge].row_count + 1
block.row = 0
if col >= 2:
# Place this node centered over the child nodes
block.col = (col - 2) / 2
block.col_count = col
else:
# No child nodes, set single node's width (nodes are 2 columns wide to allow
# centering over a branch)
block.col = 0
block.col_count = 2
block.row_count = row_count
def isEdgeMarked(self, edges, row, col, index):
if index >= len(edges[row][col]):
return False
return edges[row][col][index]
def markEdge(self, edges, row, col, index):
while len(edges[row][col]) <= index:
edges[row][col] += [False]
edges[row][col][index] = True
def findHorizEdgeIndex(self, edges, row, min_col, max_col):
# Find a valid index
i = 0
while True:
valid = True
for col in range(min_col, max_col + 1):
if self.isEdgeMarked(edges, row, col, i):
valid = False
break
if valid:
break
i += 1
# Mark chosen index as used
for col in range(min_col, max_col + 1):
self.markEdge(edges, row, col, i)
return i
def findVertEdgeIndex(self, edges, col, min_row, max_row):
# Find a valid index
i = 0
while True:
valid = True
for row in range(min_row, max_row + 1):
if self.isEdgeMarked(edges, row, col, i):
valid = False
break
if valid:
break
i += 1
# Mark chosen index as used
for row in range(min_row, max_row + 1):
self.markEdge(edges, row, col, i)
return i
def routeEdge(self, horiz_edges, vert_edges, edge_valid, start, end, color):
edge = DisassemblerEdge(color, end)
# Find edge index for initial outgoing line
i = 0
while True:
if not self.isEdgeMarked(vert_edges, start.row + 1, start.col + 1, i):
break
i += 1
self.markEdge(vert_edges, start.row + 1, start.col + 1, i)
edge.addPoint(start.row + 1, start.col + 1)
edge.start_index = i
horiz = False
# Find valid column for moving vertically to the target node
if end.row < (start.row + 1):
min_row = end.row
max_row = start.row + 1
else:
min_row = start.row + 1
max_row = end.row
col = start.col + 1
if min_row != max_row:
ofs = 0
while True:
col = start.col + 1 - ofs
if col >= 0:
valid = True
for row in range(min_row, max_row + 1):
if not edge_valid[row][col]:
valid = False
break
if valid:
break
col = start.col + 1 + ofs
if col < len(edge_valid[min_row]):
valid = True
for row in range(min_row, max_row + 1):
if not edge_valid[row][col]:
valid = False
break
if valid:
break
ofs += 1
if col != (start.col + 1):
# Not in same column, need to generate a line for moving to the correct column
if col < (start.col + 1):
min_col = col
max_col = start.col + 1
else:
min_col = start.col + 1
max_col = col
index = self.findHorizEdgeIndex(horiz_edges, start.row + 1, min_col, max_col)
edge.addPoint(start.row + 1, col, index)
horiz = True
if end.row != (start.row + 1):
# Not in same row, need to generate a line for moving to the correct row
index = self.findVertEdgeIndex(vert_edges, col, min_row, max_row)
edge.addPoint(end.row, col, index)
horiz = False
if col != (end.col + 1):
# Not in ending column, need to generate a line for moving to the correct column
if col < (end.col + 1):
min_col = col
max_col = end.col + 1
else:
min_col = end.col + 1
max_col = col
index = self.findHorizEdgeIndex(horiz_edges, end.row, min_col, max_col)
edge.addPoint(end.row, end.col + 1, index)
horiz = True
# If last line was horizontal, choose the ending edge index for the incoming edge
if horiz:
index = self.findVertEdgeIndex(vert_edges, end.col + 1, end.row, end.row)
edge.points[len(edge.points) - 1][2] = index
return edge
def renderFunction(self, func):
# Create render nodes
self.blocks = {}
for block in func.blocks.values():
self.blocks[block.entry] = DisassemblerBlock(block)
self.prepareGraphNode(self.blocks[block.entry])
# Populate incoming lists
for block in self.blocks.values():
for edge in block.block.exits:
self.blocks[edge].incoming += [block.block.entry]
# Construct acyclic graph where each node is used as an edge exactly once
block = func.blocks[func.entry]
visited = [func.entry]
queue = [self.blocks[func.entry]]
changed = True
while changed:
changed = False
# First pick nodes that have single entry points
while len(queue) > 0:
block = queue.pop()
for edge in block.block.exits:
if edge in visited:
continue
# If node has no more unseen incoming edges, add it to the graph layout now
if len(self.blocks[edge].incoming) == 1:
self.blocks[edge].incoming.remove(block.block.entry)
block.new_exits += [edge]
queue += [self.blocks[edge]]
visited += [edge]
changed = True
# No more nodes satisfy constraints, pick a node to continue constructing the graph
best = None
for block in self.blocks.values():
if not block.block.entry in visited:
continue
for edge in block.block.exits:
if edge in visited:
continue
if (best == None) or (len(self.blocks[edge].incoming) < best_edges) or ((len(self.blocks[edge].incoming) == best_edges) and (edge < best)):
best = edge
best_edges = len(self.blocks[edge].incoming)
best_parent = block
if best != None:
self.blocks[best].incoming.remove(best_parent.block.entry)
best_parent.new_exits += [best]
visited += [best]
changed = True
# Compute graph layout from bottom up
self.computeGraphLayout(self.blocks[func.entry])
# Prepare edge routing
horiz_edges = [None] * (self.blocks[func.entry].row_count + 1)
vert_edges = [None] * (self.blocks[func.entry].row_count + 1)
edge_valid = [None] * (self.blocks[func.entry].row_count + 1)
for row in range(0, self.blocks[func.entry].row_count + 1):
horiz_edges[row] = [None] * (self.blocks[func.entry].col_count + 1)
vert_edges[row] = [None] * (self.blocks[func.entry].col_count + 1)
edge_valid[row] = [True] * (self.blocks[func.entry].col_count + 1)
for col in range(0, self.blocks[func.entry].col_count + 1):
horiz_edges[row][col] = []
vert_edges[row][col] = []
for block in self.blocks.values():
edge_valid[block.row][block.col + 1] = False
# Perform edge routing
for block in self.blocks.values():
start = block
for edge in block.block.exits:
end = self.blocks[edge]
color = Qt.black
if edge == block.block.true_path:
color = QColor(0, 144, 0)
elif edge == block.block.false_path:
color = QColor(144, 0, 0)
start.edges += [self.routeEdge(horiz_edges, vert_edges, edge_valid, start, end, color)]
# Compute edge counts for each row and column
col_edge_count = [0] * (self.blocks[func.entry].col_count + 1)
row_edge_count = [0] * (self.blocks[func.entry].row_count + 1)
for row in range(0, self.blocks[func.entry].row_count + 1):
for col in range(0, self.blocks[func.entry].col_count + 1):
if len(horiz_edges[row][col]) > row_edge_count[row]:
row_edge_count[row] = len(horiz_edges[row][col])
if len(vert_edges[row][col]) > col_edge_count[col]:
col_edge_count[col] = len(vert_edges[row][col])
# Compute row and column sizes
col_width = [0] * (self.blocks[func.entry].col_count + 1)
row_height = [0] * (self.blocks[func.entry].row_count + 1)
for block in self.blocks.values():
if (int(block.width / 2)) > col_width[block.col]:
col_width[block.col] = int(block.width / 2)
if (int(block.width / 2)) > col_width[block.col + 1]:
col_width[block.col + 1] = int(block.width / 2)
if int(block.height) > row_height[block.row]:
row_height[block.row] = int(block.height)
# Compute row and column positions
col_x = [0] * self.blocks[func.entry].col_count
row_y = [0] * self.blocks[func.entry].row_count
self.col_edge_x = [0] * (self.blocks[func.entry].col_count + 1)
self.row_edge_y = [0] * (self.blocks[func.entry].row_count + 1)
x = 16
for i in range(0, self.blocks[func.entry].col_count):
self.col_edge_x[i] = x
x += 8 * col_edge_count[i]
col_x[i] = x
x += col_width[i]
y = 16
for i in range(0, self.blocks[func.entry].row_count):
self.row_edge_y[i] = y
y += 8 * row_edge_count[i]
row_y[i] = y
y += row_height[i]
self.col_edge_x[self.blocks[func.entry].col_count] = x
self.row_edge_y[self.blocks[func.entry].row_count] = y
self.width = x + 16 + (8 * col_edge_count[self.blocks[func.entry].col_count])
self.height = y + 16 + (8 * row_edge_count[self.blocks[func.entry].row_count])
# Compute node positions
for block in self.blocks.values():
block.x = int((col_x[block.col] + col_width[block.col] + 4 * col_edge_count[block.col + 1]) - (block.width / 2))
if (block.x + block.width) > (col_x[block.col] + col_width[block.col] + col_width[block.col + 1] + 8 * col_edge_count[block.col + 1]):
block.x = int((col_x[block.col] + col_width[block.col] + col_width[block.col + 1] + 8 * col_edge_count[block.col + 1]) - block.width)
block.y = row_y[block.row]
# Precompute coordinates for edges
for block in self.blocks.values():
for edge in block.edges:
start = edge.points[0]
start_row = start[0]
start_col = start[1]
last_index = edge.start_index
last_pt = QPoint(self.col_edge_x[start_col] + (8 * last_index) + 4,
block.y + block.height + 4 - (2 * self.charWidth))
pts = [last_pt]
for i in range(0, len(edge.points)):
end = edge.points[i]
end_row = end[0]
end_col = end[1]
last_index = end[2]
if start_col == end_col:
new_pt = QPoint(last_pt.x(), self.row_edge_y[end_row] + (8 * last_index) + 4)
else:
new_pt = QPoint(self.col_edge_x[end_col] + (8 * last_index) + 4, last_pt.y())
pts += [new_pt]
last_pt = new_pt
start_col = end_col
new_pt = QPoint(last_pt.x(), edge.dest.y + self.charWidth - 1)
pts += [new_pt]
edge.polyline = pts
pts = [QPoint(new_pt.x() - 3, new_pt.y() - 6), QPoint(new_pt.x() + 3, new_pt.y() - 6), new_pt]
edge.arrow = pts
# Adjust scroll bars for new size
areaSize = self.viewport().size()
self.adjustSize(areaSize.width(), areaSize.height())
if self.desired_pos:
# There was a position saved, navigate to it
self.horizontalScrollBar().setValue(self.desired_pos[0])
self.verticalScrollBar().setValue(self.desired_pos[1])
elif self.cur_instr != None:
self.show_cur_instr()
else:
# Ensure start node is visible
start_x = self.blocks[func.entry].x + self.renderXOfs + int(self.blocks[func.entry].width / 2)
self.horizontalScrollBar().setValue(start_x - int(areaSize.width() / 2))
self.verticalScrollBar().setValue(0)
self.update_id = func.update_id
self.ready = True
self.viewport().update(0, 0, areaSize.width(), areaSize.height())
def updateTimerEvent(self):
status = self.analysis.status
if status != self.status:
self.status = status
self.statusUpdated.emit(self)
if self.function is None:
return
if self.ready:
# Check for updated code
self.analysis.lock.acquire()
if self.update_id != self.analysis.functions[self.function].update_id:
self.renderFunction(self.analysis.functions[self.function])
self.analysis.lock.release()
return
# View not up to date, check to see if active function is ready
self.analysis.lock.acquire()
if self.analysis.functions.has_key(self.function):
if self.analysis.functions[self.function].ready:
# Active function now ready, generate graph
self.renderFunction(self.analysis.functions[self.function])
self.analysis.lock.release()
def show_cur_instr(self):
for block in self.blocks.values():
row = len(block.block.header_text.lines)
for instr in block.block.instrs:
if self.cur_instr == instr.addr:
x = block.x + int(block.width / 2)
y = block.y + (2 * self.charWidth) + int((row + 0.5) * self.charHeight)
self.horizontalScrollBar().setValue(x + self.renderXOfs -
int(self.horizontalScrollBar().pageStep() / 2))
self.verticalScrollBar().setValue(y + self.renderYOfs -
int(self.verticalScrollBar().pageStep() / 2))
return
row += len(instr.text.lines)
def navigate(self, addr):
# Check to see if address is within current function
for block in self.blocks.values():
row = len(block.block.header_text.lines)
for instr in block.block.instrs:
if (addr >= instr.addr) and (addr < (instr.addr + len(instr.opcode))):
self.cur_instr = instr.addr
self.show_cur_instr()
self.viewport().update()
return True
row += len(instr.text.lines)
# Check other functions for this address
func, instr = self.analysis.find_instr(addr)
if func != None:
self.function = func
self.cur_instr = instr
self.highlight_token = None
self.ready = False
self.desired_pos = None
self.viewport().update()
return True
return False
def make_proc(self, addr):
# Create a procedure at the requested address if one does not already exist
if self.data.architecture() is None:
# Architecture not defined yet, ask the user and set it now
arch_dlg = ArchitectureDialog(self)
if arch_dlg.exec_() == QDialog.Rejected:
return False
self.data.default_arch = arch_dlg.result
self.analysis.lock.acquire()
if addr not in self.analysis.functions:
self.analysis.queue.append(addr)
self.analysis.lock.release()
self.function = addr
self.cur_instr = None
self.highlight_token = None
self.ready = False
self.desired_pos = None
self.viewport().update()
return True
def navigate_to_history_entry(self, entry):
self.function = entry.function
self.ready = False
self.desired_pos = [entry.scroll_x, entry.scroll_y]
self.cur_instr = entry.cur_instr
self.highlight_token = entry.highlight_token
self.viewport().update()
def get_history_entry(self):
return DisassemblerHistoryEntry(self)
def fontChanged(self):
self.initFont()
if self.ready:
# Rerender function to update layout
self.analysis.lock.acquire()
self.renderFunction(self.analysis.functions[self.function])
self.analysis.lock.release()
def getPriority(data, ext):
if Analysis.isPreferredForFile(data):
return 80
return 0
getPriority = staticmethod(getPriority)
def getViewName():
return "Disassembler"
getViewName = staticmethod(getViewName)
def getShortViewName():
return "Disassembler"
getShortViewName = staticmethod(getShortViewName)
def handlesNavigationType(name):
return (name == "disassembler") or (name == "make_proc")
handlesNavigationType = staticmethod(handlesNavigationType)
ViewTypes += [DisassemblerView]