diff --git a/chb/app/AppAccess.py b/chb/app/AppAccess.py index ea519394..68581eef 100644 --- a/chb/app/AppAccess.py +++ b/chb/app/AppAccess.py @@ -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 @@ -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/_metrics.xml + - disaseembly only: + s/_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 diff --git a/chb/app/AppResultMetrics.py b/chb/app/AppResultMetrics.py index f907fa16..4093e6ef 100644 --- a/chb/app/AppResultMetrics.py +++ b/chb/app/AppResultMetrics.py @@ -27,6 +27,7 @@ # SOFTWARE. # ------------------------------------------------------------------------------ +from dataclasses import dataclass, field import xml.etree.ElementTree as ET from typing import Any, Callable, Dict, List, Optional @@ -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__( @@ -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 @@ -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") @@ -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.""" @@ -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 diff --git a/chb/app/CHVersion.py b/chb/app/CHVersion.py index b2cfed15..503f0487 100644 --- a/chb/app/CHVersion.py +++ b/chb/app/CHVersion.py @@ -1,3 +1,3 @@ -chbversion: str = "0.3.20261020" +chbversion: str = "0.3.20261102" minimum_required_chb_version = "0.6.0_20260802" diff --git a/chb/cmdline/chkx b/chb/cmdline/chkx index bce4bf71..a5861706 100755 --- a/chb/cmdline/chkx +++ b/chb/cmdline/chkx @@ -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") diff --git a/chb/cmdline/commandutil.py b/chb/cmdline/commandutil.py index 3844579e..99d3ed73 100644 --- a/chb/cmdline/commandutil.py +++ b/chb/cmdline/commandutil.py @@ -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 diff --git a/chb/util/fileutil.py b/chb/util/fileutil.py index bf09b9fb..0796a5b7 100644 --- a/chb/util/fileutil.py +++ b/chb/util/fileutil.py @@ -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--> + """ 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: