Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion chb/app/AppAccess.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@

from chb.app.AppCfgInfo import AppCfgInfo
from chb.app.AppResultData import AppResultData
from chb.app.AppResultMetrics import AppResultMetrics
from chb.app.AppResultMetrics import AppResultMetrics, DisassemblyMetrics
from chb.app.ASMCallgraph import ASMCallgraph
from chb.app.BDictionary import BDictionary
from chb.app.CallbackTables import CallbackTables
Expand Down Expand Up @@ -712,6 +712,28 @@ def result_metrics(self) -> AppResultMetrics:
h = UF.get_resultmetrics_xheader(self.path, self.filename)
return AppResultMetrics(self.filename, x, h)

# Disassembly Metrics -----------------------------------------------------------

@property
def disassembly_metrics(self) -> DisassemblyMetrics:
"""Returns metrics for the disassembly process

Depending on whether full analysis was done or only disassembly
(chkx analyze -d) was done they will be found in two different files:
- full analysis:
r/<binary>_metrics.xml
- disaseembly only:
s/<binary>_disassembly.xml
"""
try:
result_metrics = self.result_metrics
return result_metrics.disassembly
except UF.CHBFileNotFoundError:
# If only disassembly was done and not full analysis, the disassembly
# stats are in a different file.
x = UF.get_disassembly_statistics_xnode(self.path, self.filename)
return DisassemblyMetrics(x)

# User data -----------------------------------------------------------

@property
Expand Down
78 changes: 65 additions & 13 deletions chb/app/AppResultMetrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
# SOFTWARE.
# ------------------------------------------------------------------------------

from dataclasses import dataclass, field
import xml.etree.ElementTree as ET

from typing import Any, Callable, Dict, List, Optional
Expand Down Expand Up @@ -61,6 +62,56 @@
]


@dataclass
class DisassemblyMetrics:
xnode: ET.Element
all_overlap: int = field(init=False)
coverage: int = field(init=False)
datablocks: int = field(init=False)
function_count: int = field(init=False)
instruction_count: int = field(init=False)
jumptables: int = field(init=False)
overlap: int = field(init=False)
pcoverage: float = field(init=False)
unknown_instrs: int = field(init=False)

def __post_init__(self) -> None:
self.function_count = self._int_field("functions")
self.instruction_count = self._int_field("instrs")
self.unknown_instrs = self._int_field("unknown-instrs")

self.all_overlap = self._int_field("alloverlap")
self.overlap = self._int_field("overlap")

self.coverage = self._int_field("coverage")
self.pcoverage = float(self.xnode.get('pcoverage', "0.0"))

self.datablocks = self._int_field("datablocks")
self.jumptables = self._int_field("jumptables")


def _int_field(self, field_name: str) -> int:
return int(self.xnode.get(field_name, "0"))


def to_json_result(self) -> JSONResult:
content: Dict[str, Any] = {}
content["function_count"] = self.function_count
content["instruction_count"] = self.instruction_count
content["unknown-instructions"] = self.unknown_instrs

content["all-overlap"] = self.all_overlap
content["overlap"] = self.overlap

content["coverage"] = self.coverage
content["pcoverage"] = self.pcoverage

content["datablocks"] = self.datablocks
content["jumptables"] = self.jumptables

return JSONResult("disassembly-stats", content, "ok")


class AppResultMetrics:

def __init__(
Expand All @@ -75,7 +126,7 @@ def __init__(
self._calls: Optional[ET.Element] = None
self._jumps: Optional[ET.Element] = None
self._precision: Optional[ET.Element] = None
self._disassembly: Optional[ET.Element] = None
self._disassembly: Optional[DisassemblyMetrics] = None
self._functions: Dict[str, AppResultFunctionMetrics] = {}
self._imports: Optional[ET.Element] = None
self._ccmetrics: Optional[ET.Element] = None
Expand Down Expand Up @@ -173,17 +224,18 @@ def cc_metrics(self) -> ET.Element:
return self._ccmetrics

@property
def disassembly(self) -> ET.Element:
def disassembly(self) -> DisassemblyMetrics:
if self._disassembly is None:
self._disassembly = self.xnode.find("disassembly")
if self._disassembly is None:
disassembly_node = self.xnode.find("disassembly")
if disassembly_node is None:
raise UF.CHBError("Disassembly is missing from resultmetrics")
self._disassembly = DisassemblyMetrics(disassembly_node)
return self._disassembly

@property
def imports(self) -> ET.Element:
if self._imports is None:
self._imports = self.disassembly.find("imports")
self._imports = self.disassembly.xnode.find("imports")
if self._imports is None:
raise UF.CHBError(
"Imports is missing from disassembly in resultmetrics")
Expand Down Expand Up @@ -323,19 +375,19 @@ def no_summary(self) -> int:

@property
def instruction_count(self) -> int:
return int(self.disassembly.get('instrs', "0"))
return self.disassembly.instruction_count

@property
def function_count(self) -> int:
return int(self.disassembly.get('functions', "0"))
return self.disassembly.function_count

@property
def unknown_instructions(self) -> int:
return int(self.disassembly.get('unknown-instrs', '0'))
return self.disassembly.unknown_instrs

@property
def pcoverage(self) -> float:
return float(self.disassembly.get('pcoverage', "0.0"))
return self.disassembly.pcoverage

def time_share(self, count: int) -> Dict[str, float]:
"""Return a list of the most time-intensive functions with %time."""
Expand Down Expand Up @@ -410,10 +462,10 @@ def f(fn: AppResultFunctionMetrics) -> None:

def to_json_result(self, includefns: bool = False) -> JSONResult:
content: Dict[str, Any] = {}
content["instructions"] = int(self.instruction_count)
content["unknowninstrs"] = int(self.unknown_instructions)
content["functions"] = int(self.function_count)
content["functioncoverage"] = float(self.pcoverage)
content["instructions"] = self.instruction_count
content["unknowninstrs"] = self.unknown_instructions
content["functions"] = self.function_count
content["functioncoverage"] = self.pcoverage
content["espprecision"] = self.esp_precision
content["readsprecision"] = self.reads_precision
content["writesprecision"] = self.writes_precision
Expand Down
2 changes: 1 addition & 1 deletion chb/app/CHVersion.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
chbversion: str = "0.3.20261020"
chbversion: str = "0.3.20261102"

minimum_required_chb_version = "0.6.0_20260802"
8 changes: 8 additions & 0 deletions chb/cmdline/chkx
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,14 @@ def parse() -> argparse.Namespace:
ddatacmd.set_defaults(func=ddatacommand)
ddataparsers = ddatacmd.add_subparsers(title="show options")

# -- disassembly stats --
ddatastats = ddataparsers.add_parser("stats")
ddatastats.add_argument("xname", help="name of executable")
ddatastats.add_argument("-o", "--output",
help="Optional output file where stats should "
"be saved as json")
ddatastats.set_defaults(func=UCC.ddata_stats)

# -- ddata globalvars --
ddatagvars = ddataparsers.add_parser("globalvars")
ddatagvars.add_argument("xname", help="name of executable")
Expand Down
32 changes: 32 additions & 0 deletions chb/cmdline/commandutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -2810,6 +2810,38 @@ def show_function_semantics_table(args: argparse.Namespace) -> NoReturn:
exit(0)


def ddata_stats(args: argparse.Namespace) -> NoReturn:
"""Prints disassembly statistics"""
# arguments
xname: str = str(args.xname)

try:
(path, xfile) = get_path_filename(xname)
except UF.CHBError as e:
print(str(e.wrap()))
exit(1)

xinfo = XI.XInfo()
xinfo.load(path, xfile)

app = get_app(path, xfile, xinfo)
metrics = app.disassembly_metrics

if args.output:
jsonok = JU.jsonok("disassembly-stats", metrics.to_json_result().content)
with open(args.output, "w") as fp:
json.dump(jsonok, fp, indent=4)
else:
print("Instructions : %d" % metrics.instruction_count)
print("Unknown instructions : %d" % metrics.unknown_instrs)
print("Functions : %d (coverage: %.2f%%)" % (metrics.function_count, metrics.pcoverage))
print("Function overlap : %d (counting multiples: %d)" % (metrics.overlap, metrics.all_overlap))
print("Jumptables : %d" % (metrics.jumptables))
print("Data blocks : %d" % (metrics.datablocks))

exit(0)


def ddata_gvars(args: argparse.Namespace) -> NoReturn:

# arguments
Expand Down
10 changes: 9 additions & 1 deletion chb/util/fileutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -1056,8 +1056,16 @@ def get_disassembly_statistics_filename(path: str, xfile: str) -> str:


def get_disassembly_statistics_xnode(path: str, xfile: str) -> ET.Element:
"""This file/node is only found when we run disassembly only (chkx analyze -d)

The node will be at <--root--><disassembly-status><disassembly>
"""
filename = get_disassembly_statistics_filename(path, xfile)
return get_chb_xnode(filename, "disassembly")
disassembly_status_node = get_chb_xnode(filename, "disassembly-status")
disassembly_node = disassembly_status_node.find("disassembly")
if disassembly_node is None:
raise CHBXmlRootElementNotFoundError(filename, "disassembly")
return disassembly_node


def get_features_filename(path: str, xfile: str) -> str:
Expand Down