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
95 changes: 95 additions & 0 deletions chb/app/ASMCallgraph.py
Original file line number Diff line number Diff line change
@@ -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)
9 changes: 9 additions & 0 deletions chb/app/AppAccess.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
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.0-20260901"
chbversion: str = "0.3.0-20260930"

minimum_required_chb_version = "0.6.0_20260802"
81 changes: 64 additions & 17 deletions chb/arm/opcodes/ARMSignedDivide.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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
3 changes: 3 additions & 0 deletions chb/cmdline/AnalysisManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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:
Expand Down
10 changes: 10 additions & 0 deletions chb/cmdline/chkx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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")
Expand Down
33 changes: 33 additions & 0 deletions chb/cmdline/commandutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions chb/util/fileutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down