diff --git a/chb/app/ASMCallgraph.py b/chb/app/ASMCallgraph.py new file mode 100644 index 00000000..5acf47d5 --- /dev/null +++ b/chb/app/ASMCallgraph.py @@ -0,0 +1,95 @@ +# ------------------------------------------------------------------------------ +# CodeHawk Binary Analyzer +# Author: Henny Sipma +# ------------------------------------------------------------------------------ +# The MIT License (MIT) +# +# Copyright (c) 2026 Aarno Labs LLC +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# ------------------------------------------------------------------------------ + +import xml.etree.ElementTree as ET + +from dataclasses import dataclass +from typing import Any, Dict, List, Optional, Sequence + +from chb.jsoninterface.JSONResult import JSONResult + + +@dataclass +class ASMCallgraphEdge: + srcfn: str + callsite: str + kind: str + target: str + + def to_json_result(self) -> JSONResult: + content: Dict[str, str] = {} + content["srcfn"] = self.srcfn + content["callsite"] = self.callsite + content["kind"] = self.kind + content["target"] = self.target + return JSONResult("asm-callgraph-edge", content, "ok") + + +class ASMCallgraph: + + def __init__(self, xnode: ET.Element) -> None: + self.xnode = xnode + self._edges: List[ASMCallgraphEdge] = [] + self._initialize() + + @property + def edges(self) -> List[ASMCallgraphEdge]: + return self._edges + + def srcfn_callees(self, src: str) -> List[ASMCallgraphEdge]: + return [e for e in self.edges if e.srcfn == src] + + def target_callers(self, tgt: str) -> List[ASMCallgraphEdge]: + return [e for e in self.edges if e.target == tgt] + + def to_json_result(self) -> JSONResult: + content: Dict[str, Any] = {} + content["edges"] = edges = [] + for e in self.edges: + eresult = e.to_json_result().content + edges.append(eresult) + return JSONResult("asm-callgraph", content, "ok") + + def _initialize(self) -> None: + xedges = self.xnode.find("edges") + if xedges is not None: + for xedge in xedges.findall("edge"): + srcfn = xedge.get("src", "?") + callsite = xedge.get("cs", "?") + xtgt = xedge.find("tgt") + if xtgt is not None: + kind = xtgt.get("kd", "?") + if kind == "app": + tgt = xtgt.get("a", "?") + elif kind == "so": + tgt = xtgt.get("fn", "?") + elif kind == "unr": + tgt = "unknown" + else: + tgt = "kind not known: " + kind + edge = ASMCallgraphEdge(srcfn, callsite, kind, tgt) + self._edges.append(edge) diff --git a/chb/app/AppAccess.py b/chb/app/AppAccess.py index 38d73aa2..74b87a3d 100644 --- a/chb/app/AppAccess.py +++ b/chb/app/AppAccess.py @@ -52,6 +52,7 @@ from chb.app.AppCfgInfo import AppCfgInfo from chb.app.AppResultData import AppResultData from chb.app.AppResultMetrics import AppResultMetrics +from chb.app.ASMCallgraph import ASMCallgraph from chb.app.BDictionary import BDictionary from chb.app.CallbackTables import CallbackTables from chb.app.Callgraph import ( @@ -113,6 +114,7 @@ def __init__( # functions self._appresultdata: Optional[AppResultData] = None self._appcfginfo: Optional[AppCfgInfo] = None + self._asm_callgraph: Optional[ASMCallgraph] = None self._functioninfos: Dict[str, FunctionInfo] = {} # callgraph @@ -241,6 +243,13 @@ def systeminfo(self) -> SystemInfo: self._systeminfo = SystemInfo(self.bdictionary, xinfo) return self._systeminfo + @property + def asm_callgraph(self) -> ASMCallgraph: + if self._asm_callgraph is None: + xcallgraph = UF.get_callgraph_xnode(self.path, self.filename) + self._asm_callgraph = ASMCallgraph(xcallgraph) + return self._asm_callgraph + @property def type_constraints(self) -> TypeConstraintStore: return self._typeconstraints diff --git a/chb/app/CHVersion.py b/chb/app/CHVersion.py index 760a53f2..f6aee678 100644 --- a/chb/app/CHVersion.py +++ b/chb/app/CHVersion.py @@ -1,3 +1,3 @@ -chbversion: str = "0.3.0-20260901" +chbversion: str = "0.3.0-20260930" minimum_required_chb_version = "0.6.0_20260802" diff --git a/chb/arm/opcodes/ARMSignedDivide.py b/chb/arm/opcodes/ARMSignedDivide.py index e2179cc8..f3493754 100644 --- a/chb/arm/opcodes/ARMSignedDivide.py +++ b/chb/arm/opcodes/ARMSignedDivide.py @@ -4,7 +4,7 @@ # ------------------------------------------------------------------------------ # The MIT License (MIT) # -# Copyright (c) 2021 Aarno Labs LLC +# Copyright (c) 2021-2026 Aarno Labs LLC # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -30,15 +30,74 @@ from chb.app.InstrXData import InstrXData from chb.arm.ARMDictionaryRecord import armregistry -from chb.arm.ARMOpcode import ARMOpcode, simplify_result +from chb.arm.ARMOpcode import ARMOpcode, ARMOpcodeXData, simplify_result from chb.arm.ARMOperand import ARMOperand import chb.util.fileutil as UF - from chb.util.IndexedTable import IndexedTableValue +from chb.util.loggingutil import chklogger if TYPE_CHECKING: from chb.arm.ARMDictionary import ARMDictionary + from chb.invariants.XVariable import XVariable + from chb.invariants.XXpr import XXpr + + +class ARMSignedDivideXData(ARMOpcodeXData): + """Data format: + - variables: + 0: vrd + + - expressions: + 0: xrn + 1: xrm + 2: result + 3: rresult (result rewritten) + """ + + def __init__(self, xdata: InstrXData) -> None: + ARMOpcodeXData.__init__(self, xdata) + + @property + def vrd(self) -> "XVariable": + return self.var(0, "vrd") + + @property + def xrn(self) -> "XXpr": + return self.xpr(0, "xrn") + + @property + def xrm(self) -> "XXpr": + return self.xpr(1, "xrm") + + @property + def result(self) -> "XXpr": + return self.xpr(2, "result") + + @property + def is_result_ok(self) -> bool: + return self.is_xpr_ok(2) + + @property + def rresult(self) -> "XXpr": + return self.xpr(3, "rresult") + + @property + def is_rresult_ok(self) -> bool: + return self.is_xpr_ok(3) + + @property + def result_simplified(self) -> str: + if self.is_result_ok and self.is_rresult_ok: + return simplify_result( + self.xdata.args[3], self.xdata.args[4], self.result, self.rresult) + else: + return str(self.xrn) + " / " + str(self.xrm) + + @property + def annotation(self) -> str: + assignment = str(self.vrd) + " := " + self.result_simplified + return self.add_instruction_condition(assignment) @armregistry.register_tag("SDIV", ARMOpcode) @@ -65,17 +124,5 @@ def operands(self) -> List[ARMOperand]: return [self.armd.arm_operand(i) for i in self.args] def annotation(self, xdata: InstrXData) -> str: - """xdata format: a:vxxxx - - vars[0]: lhs - xprs[0]: rhs1 - xprs[1]: rhs2 - xprs[2]: (rhs1 * rhs2) % e^32 (syntactic) - xprs[3]: (rhs1 * rhs2) % e^32 (simplified) - """ - - lhs = str(xdata.vars[0]) - result = xdata.xprs[2] - rresult = xdata.xprs[3] - xresult = simplify_result(xdata.args[3], xdata.args[4], result, rresult) - return lhs + " := " + xresult + xd = ARMSignedDivideXData(xdata) + return xd.annotation diff --git a/chb/cmdline/AnalysisManager.py b/chb/cmdline/AnalysisManager.py index f8784521..91db94c2 100644 --- a/chb/cmdline/AnalysisManager.py +++ b/chb/cmdline/AnalysisManager.py @@ -228,6 +228,7 @@ def disassemble( preamble_cutoff: int = 12, save_asm: str = "yes", save_asm_cfg_info: bool = False, + save_asm_callgraph: bool = False, print_datasections: List[str] = []) -> None: cwd = os.getcwd() chklogger.logger.debug("change directory to %s", self.path) @@ -243,6 +244,8 @@ def disassemble( cmd.append("-save_asm") if save_asm_cfg_info: cmd.append("-save_asm_cfg_info") + if save_asm_callgraph: + cmd.append("-save_asm_callgraph") if collectdiagnostics: cmd.append("-diagnostics") if self.mips: diff --git a/chb/cmdline/chkx b/chb/cmdline/chkx index ee0ab449..523da7b1 100755 --- a/chb/cmdline/chkx +++ b/chb/cmdline/chkx @@ -382,6 +382,10 @@ def parse() -> argparse.Namespace: "--save_asm_cfg_info", action="store_true", help="save list of functions with cfg info in xml (may be slow)") + analyzecmd.add_argument( + "--save_asm_callgraph", + action="store_true", + help="save list of callgraph edges (without argument info) in xml") analyzecmd.add_argument( "--construct_all_functions", action="store_true", @@ -600,6 +604,12 @@ def parse() -> argparse.Namespace: ddatagvars.add_argument("xname", help="name of executable") ddatagvars.set_defaults(func=UCC.ddata_gvars) + # -- ddata asm_callgraph -- + ddatacallgraph = ddataparsers.add_parser("asm_callgraph") + ddatacallgraph.add_argument("xname", help="name of executable") + ddatacallgraph.add_argument("output", help="name of json file to store results") + ddatacallgraph.set_defaults(func=UCC.ddata_asm_callgraph) + # -- ddata userdata -- ddatauserdata = ddataparsers.add_parser("userdata") ddatauserdata.add_argument("xname", help="name of executable") diff --git a/chb/cmdline/commandutil.py b/chb/cmdline/commandutil.py index 1ddebbea..af9c20e1 100644 --- a/chb/cmdline/commandutil.py +++ b/chb/cmdline/commandutil.py @@ -403,6 +403,7 @@ def analyzecmd(args: argparse.Namespace) -> NoReturn: failonfunctionfailure: bool = args.fail_on_function_failure save_asm: bool = args.save_asm save_asm_cfg_info: bool = args.save_asm_cfg_info + save_asm_callgraph: bool = args.save_asm_callgraph print_datasections: List[str] = args.print_datasections thumb: List[str] = args.thumb floatabi: Optional[str] = args.float_abi @@ -574,6 +575,7 @@ def analyzecmd(args: argparse.Namespace) -> NoReturn: collectdiagnostics=collectdiagnostics, preamble_cutoff=preamble_cutoff, save_asm_cfg_info=save_asm_cfg_info, + save_asm_callgraph=save_asm_callgraph, print_datasections=print_datasections) except subprocess.CalledProcessError as e: print_error(str(e.output)) @@ -2836,6 +2838,37 @@ def ddata_gvars(args: argparse.Namespace) -> NoReturn: exit(0) +def ddata_asm_callgraph(args: argparse.Namespace) -> NoReturn: + + # arguments + xname: str = str(args.xname) + filename: str = str(args.output) + + try: + (path, xfile) = get_path_filename(xname) + except UF.CHBError as e: + print(str(e.wrap())) + exit(1) + + if not UF.has_callgraph_file(path, xfile): + print_error("No assembly-level callgraph found. " + + "Please disassemble with option --save_asm_callgraph") + exit(1) + + xinfo = XI.XInfo() + xinfo.load(path, xfile) + + app = get_app(path, xfile, xinfo) + + callgraph = app.asm_callgraph + + jsonok = JU.jsonok("asm-callgraph", callgraph.to_json_result().content) + with open(filename, "w") as fp: + json.dump(jsonok, fp, indent=4) + + exit(0) + + def ddata_userdata(args: argparse.Namespace) -> NoReturn: # arguments diff --git a/chb/util/fileutil.py b/chb/util/fileutil.py index 4b7e71fb..c0d768dd 100644 --- a/chb/util/fileutil.py +++ b/chb/util/fileutil.py @@ -532,6 +532,24 @@ def get_systeminfo_xnode(path: str, xfile: str) -> ET.Element: return get_chb_xnode(filename, "system-info") +def get_callgraph_filename(path: str, xfile: str) -> str: + fdir = get_analysis_dir(path, xfile) + return get_chb_filename(fdir, xfile, "callgraph.xml") + + +def get_callgraph_xnode(path: str, xfile: str) -> ET.Element: + filename = get_callgraph_filename(path, xfile) + if os.path.isfile(filename): + return get_chb_xnode(filename, "callgraph") + else: + raise CHBFileNotFoundError(filename) + + +def has_callgraph_file(path: str, xfile: str) -> bool: + filename = get_callgraph_filename(path, xfile) + return os.path.isfile(filename) + + def has_global_locations_file(path:str, xfile: str) -> bool: filename = get_global_locations_filename(path, xfile) return os.path.isfile(filename)