Skip to content

Commit

Permalink
Fixup with refactor full
Browse files Browse the repository at this point in the history
  • Loading branch information
Christoph Weiss committed Apr 5, 2023
1 parent 6da3dd9 commit 1b76c50
Show file tree
Hide file tree
Showing 5 changed files with 139 additions and 56 deletions.
15 changes: 10 additions & 5 deletions src/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from .interface.analyzer import Analyzer
from .interface.drawer import Drawer
from .interface.coverage_reader import CoverageReader


def print_assembly(basic_blocks):
Expand Down Expand Up @@ -38,15 +39,19 @@ def main():
default="x86",
help="Specify target platform for assembly",
)
parser.add_argument(
"-v", "--view", action="store_true", help="View as a dot graph instead of saving to a file"
)
parser.add_argument("-o", "--output", help="View as a dot graph instead of saving to a file")
parser.add_argument("-v", "--view", action="store_true", help="View as a dot graph")
parser.add_argument("-o", "--output", help="Target output filename")
parser.add_argument("-c", "--coverage", help="Coverage file for printing coverage")

args = parser.parse_args()
lines = read_lines(args.assembly_file)
analyser = Analyzer(target_name=args.target, disassembler="OBJDUMP")
analyser.parse_lines(lines=lines)
Drawer().draw_cfg(analyser.function_name, analyser.basic_blocks, args.output)
if args.coverage:
CoverageReader(instructions=analyser.instructions).update_by_csv(args.coverage)
Drawer(analyser.configuration).draw_cfg(
name=analyser.function_name, basic_blocks=analyser.basic_blocks, output=args.output
)


if __name__ == "__main__":
Expand Down
28 changes: 0 additions & 28 deletions src/data/basic_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,34 +33,6 @@ def add_no_jump_edge(self, basic_block_key) -> None:
else:
self.no_jump_edge = basic_block_key

def get_label(self):
"""Return content of the block for dot graph."""

def escape(instruction: str) -> str:
"""
Escape used dot graph characters in given instruction so they will be
displayed correctly.
"""
instruction = instruction.replace("<", r"\<")
instruction = instruction.replace(">", r"\>")
instruction = instruction.replace("|", r"\|")
instruction = instruction.replace("{", r"\{")
instruction = instruction.replace("}", r"\}")
instruction = instruction.replace(" ", " ")
return instruction

# Left align in dot.
label = r"\l".join([str(i.address) + ": " + escape(i.text) for i in self.instructions])

# Left justify the last line too.
label += r"\l"
if self.jump_edge:
if self.no_jump_edge:
label += "|{<s0>No Jump|<s1>Jump}"
else:
label += "|{<s1>Jump}"
return "{" + label + "}"

def __str__(self) -> str:
return "\n".join([i.text for i in self.instructions])

Expand Down
41 changes: 41 additions & 0 deletions src/data/instruction.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
#!/usr/bin/env python3
"""Represents a single assembly instruction with it operands, location and optional branch target"""
from enum import Enum
from typing import List

from .address import Address


class Coverage(Enum):
"""Enumeration for coverage records"""

MISS = "missed"
"""Indicates instruction is not executed"""
LINE_TAKEN = "taken"
"""Indicates instruction is executed"""
JUMP_PASS = "skipped"
"""Indicates, if branch is just passed without jump"""
JUMP_TAKEN = "jumped"
"""Indicates, if branch is just jumped without passed"""
JUMP_BOTH = "both taken"
"""Indicates, if branch is just jumped without passed"""


class Instruction:
"""Instruction Class"""

Expand All @@ -29,6 +45,11 @@ class Instruction:
target: Address = None
"""Optional target of the instruction (branch)"""

coverage: Coverage = Coverage.MISS
"""If line is executed on test"""

branch_taken: bool = None

def __init__(self, body, text, lineno, address, opcode, ops, target):
self.body = body
self.text = text
Expand All @@ -37,9 +58,29 @@ def __init__(self, body, text, lineno, address, opcode, ops, target):
self.opcode = opcode
self.ops = ops
self.target = target
self.coverage = Coverage.MISS

def update_coverage(self, addresses: set[int]) -> None:
"""Update the coverage information of the instruction."""
if len(addresses) == 0:
self.coverage = Coverage.LINE_TAKEN
elif len(addresses) == 2 and self.target.abs in addresses:
self.coverage = Coverage.JUMP_BOTH
elif len(addresses) == 1 and self.target.abs in addresses:
self.coverage = Coverage.JUMP_TAKEN
elif len(addresses) == 1:
self.coverage = Coverage.JUMP_PASS
else:
raise AssertionError("Invalid Coverage Information")

def __str__(self):
result = f"{self.address}: {self.opcode}"
if self.ops:
result += f" {self.ops}"
return result

def __repr__(self) -> str:
result = f"{self.address}: {self.opcode}"
if self.ops:
result += f" {self.ops}"
return result
4 changes: 2 additions & 2 deletions src/gdb_asm2cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def invoke(self, _arg, _from_tty): # pylint: disable=bad-option-value,no-self-u
assembly_lines = gdb.execute("disassemble", from_tty=False, to_string=True).split("\n")
analyzer = Analyzer(target_name=target_name, disassembler="GDB")
analyzer.parse_lines(assembly_lines)
Drawer().view_cfg(analyzer.function_name, analyzer.basic_blocks)
Drawer(analyzer.configuration).view_cfg(analyzer.function_name, analyzer.basic_blocks)
# Catch error coming from GDB side before other errors.
except gdb.error as ex:
raise gdb.GdbError(ex)
Expand All @@ -92,7 +92,7 @@ def invoke(self, _arg, _from_tty): # pylint: disable=no-self-use
assembly_lines = gdb.execute("disassemble", from_tty=False, to_string=True).split("\n")
analyzer = Analyzer(target_name="x86", disassembler="GDB")
analyzer.parse_lines(assembly_lines)
Drawer().view_cfg(analyzer.function_name, analyzer.basic_blocks)
Drawer(analyzer.configuration).view_cfg(analyzer.function_name, analyzer.basic_blocks)
# Catch error coming from GDB side before other errors.
except gdb.error as ex:
raise gdb.GdbError(ex)
Expand Down
107 changes: 86 additions & 21 deletions src/interface/drawer.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,45 +6,110 @@

from graphviz import Digraph

from ..configuration.configuration import Asm2CfgConfiguration
from ..data.basic_block import BasicBlock
from ..data.instruction import Coverage

coverage_color = {
Coverage.MISS: "red",
Coverage.LINE_TAKEN: "green",
Coverage.JUMP_PASS: "yellow",
Coverage.JUMP_TAKEN: "yellow",
Coverage.JUMP_BOTH: "green",
}


class Drawer:
"""Drawer Class"""

def _create_cfg(self, diagram_name: str, basic_blocks: Dict[int, BasicBlock]):
def __init__(self, config: Asm2CfgConfiguration, graph_options: dict = None) -> None:
self.config = config
self.graph_option = graph_options if graph_options else {}

def set_graph_option(self, graph_options: dict = None) -> None:
"""Set new graph options"""
self.graph_option = graph_options

@staticmethod
def _escape(text: str) -> str:
"""
Escape used dot graph characters in given instruction so they will be
displayed correctly.
"""
text = text.replace("<", r"(")
text = text.replace(">", r")")
text = text.replace("|", r"\|")
text = text.replace("[", r"\[")
text = text.replace("]", r"\]")
text = text.replace("{", r"\{")
text = text.replace("}", r"\}")
text = text.replace("\t", " ")
return text

def _create_label(self, basic_block: BasicBlock, line_coverage=False):
"""Create annotated graph label"""
label = ""

# start label
label += '< <TABLE BORDER="1" CELLBORDER="0" CELLSPACING="0" CELLPADDING="0"> \n'
# for each instruction in block
for instr in basic_block.instructions:
bg_color = coverage_color[instr.coverage] if line_coverage else "white"
label += (
"<TR>"
f'<TD ALIGN="LEFT" COLSPAN="2" BGCOLOR="{bg_color}" >'
f"0x{instr.address.abs:x}: {Drawer._escape(text=instr.text)}"
"</TD></TR>\n"
)

# add JUMP/NO JUMP cells with dot PORT navigation
cells = [basic_block.jump_edge, basic_block.no_jump_edge]
span = 3 - len(set(x for x in cells if x is not None)) # 3 - count of trues in cells
label += "<TR>"
if basic_block.no_jump_edge:
label += f'<TD BORDER="1" COLSPAN="{span}" PORT="pass">NO JUMP</TD>'
if basic_block.jump_edge:
label += f'<TD BORDER="1" COLSPAN="{span}" PORT="jump">JUMP</TD>'
if not basic_block.jump_edge and not basic_block.no_jump_edge:
label += '<TD BORDER="1" COLSPAN="2">FIN</TD>'
label += "</TR> \n</TABLE> >"
return label

def _create_cfg(self, name: str, basic_blocks: Dict[int, BasicBlock], coverage=False):
"""Create a cgf"""
dot = Digraph(name=diagram_name, comment=diagram_name, engine="dot")
dot.attr("graph", label=diagram_name)
dot = Digraph(name=name, comment=name, engine="dot")
dot.attr("graph", label=name)

# Create nodes in graph
for address, basic_block in basic_blocks.items():
key = str(address)
dot.node(key, shape="record", label=basic_block.get_label())
label = self._create_label(basic_block, coverage)
dot.node(name=key, label=label, shape="plaintext", **self.graph_option)

# Create edges in graph
for basic_block in basic_blocks.values():
if basic_block.jump_edge:
if basic_block.no_jump_edge is not None:
dot.edge(f"{basic_block.key}:s0", str(basic_block.no_jump_edge))
dot.edge(f"{basic_block.key}:s1", str(basic_block.jump_edge))
elif basic_block.no_jump_edge:
dot.edge(str(basic_block.key), str(basic_block.no_jump_edge))
dot.edge(f"{basic_block.key}:jump", str(basic_block.jump_edge))
if basic_block.no_jump_edge:
dot.edge(f"{basic_block.key}:pass", str(basic_block.no_jump_edge))
return dot

def view_cfg(self, function_name: str, basic_blocks: Dict[int, BasicBlock]):
def view_cfg(self, name: str, basic_blocks: Dict[int, BasicBlock]):
"""view a function graph"""
dot = self._create_cfg(function_name, basic_blocks)
dot = self._create_cfg(name, basic_blocks)
dot.format = "gv"
with tempfile.NamedTemporaryFile(mode="w+b", prefix=function_name) as filename:
with tempfile.NamedTemporaryFile(mode="w+b", prefix=name) as filename:
dot.view(filename.name)
print(
f"Opening a file {filename.name}.{dot.format} with default viewer. Don't forget to delete it later."
)
print(f"Opening a file {filename.name}.{dot.format} with default viewer.")

def draw_cfg(
self, function_name: str, basic_blocks: Dict[int, BasicBlock], output: str = None
):
def draw_cfg(self, name: str, basic_blocks: Dict[int, BasicBlock], output: str = None):
"""Draw a function graph"""
dot = self._create_cfg(function_name, basic_blocks)
dot = self._create_cfg(name, basic_blocks, coverage=True)

filename = output if output else function_name
filename = output if output else name
dot.format = "pdf"
dot.render(filename=filename, cleanup=True)
print(f"Saved CFG to a file {function_name}.{dot.format}")
print(f"Saved CFG to a file {name}.{dot.format}")
if self.config.verbose:
dot.format = "gv"
dot.render(filename=filename, cleanup=True)

0 comments on commit 1b76c50

Please sign in to comment.