From 6442dafe81202ee2562db8c612f031e1efe3c9d3 Mon Sep 17 00:00:00 2001 From: Dan Phung Date: Fri, 31 Jul 2026 23:42:45 -0700 Subject: [PATCH 1/3] ASTI: derive address-keyed flag-liveness from reaching-def facts Add ASTILiveness, which computes NZCV flag live-in/live-out per instruction address for a function by building per-address use/kill sets from the per-instruction flag-reaching-def facts and running a backward live-variable fixpoint over the CFG. Block instructions are ordered with the same lexicographic sort BasicBlock uses, which also tolerates the analysis's inlined-instruction addresses. --- chb/astinterface/ASTILiveness.py | 174 +++++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 chb/astinterface/ASTILiveness.py diff --git a/chb/astinterface/ASTILiveness.py b/chb/astinterface/ASTILiveness.py new file mode 100644 index 00000000..804e67bd --- /dev/null +++ b/chb/astinterface/ASTILiveness.py @@ -0,0 +1,174 @@ +# ------------------------------------------------------------------------------ +# CodeHawk Binary Analyzer +# Author: Henny Sipma +# ------------------------------------------------------------------------------ +# The MIT License (MIT) +# +# Copyright (c) 2024-2025 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. +# ------------------------------------------------------------------------------ +"""Address-keyed liveness derived from per-instruction reaching-def facts. + +The flag-reaching-definition facts that CodeHawk attaches to each instruction +record, at each USE site, the addresses that DEFINE the flag value used there. +From those facts this class builds per-address use/kill sets and runs a standard +backward live-variable fixpoint over the function CFG, producing live-in/live-out +sets keyed by instruction address. + +This is intentionally sound-by-over-approximation: every use recorded in the +facts is honored (never dropped), while a def that reaches no use may be absent +from the kill set, which can only make a flag appear live longer -- never +shorter. Consumers that use liveness to gate a transformation therefore never +get a false "dead". +""" + +from collections import defaultdict +from typing import ( + Callable, Dict, List, Optional, Sequence, Set, TYPE_CHECKING, Tuple, Union) + +if TYPE_CHECKING: + from chb.app.Function import Function + from chb.app.Instruction import Instruction + from chb.invariants.VarInvariantFact import ( + FlagReachingDefFact, ReachingDefFact) + + +class ASTILiveness: + """Derives address-keyed liveness for one CodeHawk function.""" + + def __init__(self, fn: "Function") -> None: + self._fn = fn + + @property + def fn(self) -> "Function": + return self._fn + + def flag_liveness(self) -> Dict[str, Dict[str, List[str]]]: + """NZCV flag live-in/live-out per instruction address.""" + (use, kill) = self._use_kill( + lambda instr: instr.xdata.flag_reachingdefs) + return self._liveness(use, kill) + + def _use_kill( + self, + get_facts: Callable[ + ["Instruction"], + Sequence[Optional[Union["FlagReachingDefFact", + "ReachingDefFact"]]]], + names: Optional[Set[str]] = None + ) -> Tuple[Dict[str, Set[str]], Dict[str, Set[str]]]: + """Build per-address use and kill (def) sets from reaching-def facts. + + When names is given only variables in that set are considered. Marker + deflocations/variables that do not denote a real instruction def site + are skipped, matching ASTIProvenance.resolve_reaching_defs. + """ + use: Dict[str, Set[str]] = defaultdict(set) + kill: Dict[str, Set[str]] = defaultdict(set) + for (iaddr, instr) in self.fn.instructions.items(): + for fact in get_facts(instr): + if fact is None: + continue + name = str(fact.variable) + if name == "PC": + continue + if names is not None and name not in names: + continue + use[iaddr].add(name) + for d in fact.deflocations: + da = str(d) + if da == "init" or da.startswith("F"): + continue + kill[da].add(name) + return (use, kill) + + def _blocks(self) -> Dict[str, List[str]]: + """Block address -> its instruction addresses in execution order. + + Sorted lexicographically, matching how BasicBlock orders its own + instructions. A numeric (int, 16) key would raise on the analysis's + inlined-instruction addresses (e.g. "F:0x...._0x...."). + """ + result: Dict[str, List[str]] = {} + for (baddr, block) in self.fn.blocks.items(): + result[baddr] = sorted(block.instructions.keys()) + return result + + def _edges(self) -> Dict[str, List[str]]: + """Block address -> successor block addresses. + + Read through the cfg.edges property (not cfg.successors, which reads the + backing map directly and returns nothing until the property has lazily + loaded it from XML). + """ + return {b: list(succs) for (b, succs) in self.fn.cfg.edges.items()} + + def _liveness( + self, + use: Dict[str, Set[str]], + kill: Dict[str, Set[str]]) -> Dict[str, Dict[str, List[str]]]: + blocks = self._blocks() + edges = self._edges() + (live_in, live_out) = self._backward(blocks, edges, use, kill) + result: Dict[str, Dict[str, List[str]]] = {} + for iaddrs in blocks.values(): + for ia in iaddrs: + lin = sorted(live_in.get(ia, set())) + lout = sorted(live_out.get(ia, set())) + if lin or lout: + result[ia] = {"live-in": lin, "live-out": lout} + return result + + def _backward( + self, + blocks: Dict[str, List[str]], + edges: Dict[str, List[str]], + use: Dict[str, Set[str]], + kill: Dict[str, Set[str]] + ) -> Tuple[Dict[str, Set[str]], Dict[str, Set[str]]]: + """Standard iterative backward live-variable analysis. + + Returns (live_in, live_out), each instruction address -> set of live + names. + """ + block_in: Dict[str, Set[str]] = {b: set() for b in blocks} + live_in: Dict[str, Set[str]] = {} + live_out: Dict[str, Set[str]] = {} + + changed = True + while changed: + changed = False + for (b, iaddrs) in blocks.items(): + # live-out of the block = union of successors' block-entry sets + cur_out: Set[str] = set() + for s in edges.get(b, []): + cur_out |= block_in.get(s, set()) + # walk the block backwards, threading live-out -> live-in + for ia in reversed(iaddrs): + live_out[ia] = set(cur_out) + lin = use.get(ia, set()) | (cur_out - kill.get(ia, set())) + live_in[ia] = lin + cur_out = lin + # cur_out is now the live-in at the block's first instruction + if cur_out != block_in[b]: + block_in[b] = cur_out + changed = True + + return (live_in, live_out) From 0941a82ae096079a9410e33ea2ae53ae69686415 Mon Sep 17 00:00:00 2001 From: Dan Phung Date: Fri, 31 Jul 2026 23:42:45 -0700 Subject: [PATCH 2/3] AST: carry and serialize address-keyed flag-liveness in provenance Add a flag-liveness map (keyed by instruction address) to ASTProvenance with the standard getter/setter, and round-trip it through serialize/deserialize alongside the other provenance facts. --- chb/ast/ASTProvenance.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/chb/ast/ASTProvenance.py b/chb/ast/ASTProvenance.py index 576e927e..29711a49 100644 --- a/chb/ast/ASTProvenance.py +++ b/chb/ast/ASTProvenance.py @@ -38,6 +38,9 @@ def __init__(self) -> None: self._reaching_definitions: Dict[int, List[int]] = {} self._flag_reaching_definitions: Dict[int, List[int]] = {} self._definitions_used: Dict[int, List[int]] = {} + # NZCV flag liveness, keyed by instruction address: + # {"0xNNNN": {"live-in": [...], "live-out": [...]}} + self._flag_liveness: Dict[str, Dict[str, List[str]]] = {} @property def instruction_mapping(self) -> Mapping[int, List[int]]: @@ -63,6 +66,14 @@ def flag_reaching_definitions(self) -> Mapping[int, List[int]]: def definitions_used(self) -> Mapping[int, List[int]]: return self._definitions_used + @property + def flag_liveness(self) -> Mapping[str, Dict[str, List[str]]]: + return self._flag_liveness + + def set_flag_liveness( + self, liveness: Dict[str, Dict[str, List[str]]]) -> None: + self._flag_liveness = liveness + def has_expression_mapping(self, exprid: int) -> bool: return exprid in self.expression_mapping @@ -105,14 +116,15 @@ def add_definitions_used(self, lvalid: int, instrids: List[int]) -> None: if instrid not in self.definitions_used[lvalid]: self._definitions_used[lvalid].append(instrid) - def serialize(self) -> Mapping[str, Mapping[int, Union[int, List[int]]]]: - result: Dict[str, Mapping[int, Union[int, List[int]]]] = {} + def serialize(self) -> Mapping[str, Any]: + result: Dict[str, Any] = {} result["instruction-mapping"] = self.instruction_mapping result["expression-mapping"] = self.expression_mapping result["lval-mapping"] = self.lval_mapping result["reaching-definitions"] = self.reaching_definitions result["flag-reaching-definitions"] = self.flag_reaching_definitions result["definitions-used"] = self.definitions_used + result["flag-liveness"] = self.flag_liveness return result def deserialize(self, d: Dict[str, Any]) -> None: @@ -128,3 +140,4 @@ def deserialize(self, d: Dict[str, Any]) -> None: int(i): v for (i, v) in d["flag-reaching-definitions"].items()} self._definitions_used = { int(i): v for (i, v) in d["definitions-used"].items()} + self._flag_liveness = d.get("flag-liveness", {}) From c6c3bb12b398a329b310f6516c395cdf2457be9d Mon Sep 17 00:00:00 2001 From: Dan Phung Date: Fri, 31 Jul 2026 23:42:45 -0700 Subject: [PATCH 3/3] ASTI: attach derived flag-liveness during AST construction Compute flag-liveness in mk_asts, alongside set_ast_provenance, so it flows through the same builder path as the other provenance facts (rather than only on the results-ast command path). The computation is auxiliary and guarded so a failure cannot abort AST generation. --- chb/astinterface/ASTInterfaceFunction.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/chb/astinterface/ASTInterfaceFunction.py b/chb/astinterface/ASTInterfaceFunction.py index f2d974b3..aacfc179 100644 --- a/chb/astinterface/ASTInterfaceFunction.py +++ b/chb/astinterface/ASTInterfaceFunction.py @@ -40,6 +40,7 @@ from chb.astinterface.ASTICodeTransformer import ASTICodeTransformer from chb.astinterface.ASTICPrettyPrinter import ASTICPrettyPrinter +from chb.astinterface.ASTILiveness import ASTILiveness from chb.astinterface.ASTInterface import ASTInterface from chb.astinterface.ASTInterfaceBasicBlock import ASTInterfaceBasicBlock from chb.astinterface.ASTInterfaceInstruction import ASTInterfaceInstruction @@ -162,6 +163,7 @@ def mk_asts(self, support: CustomASTSupport) -> List[ASTStmt]: # transfer provenance data to the AST abstract syntaxtree self.astinterface.set_ast_provenance() + self.set_flag_liveness() self.set_invariants() self.set_return_sequences() @@ -249,6 +251,18 @@ def complete_instruction_connections(self) -> None: for ll_instr in instr.ll_ast_instructions: self.astinterface.add_instr_mapping(hl_instr, ll_instr) + def set_flag_liveness(self) -> None: + # Derive address-keyed NZCV flag liveness from the reaching-def facts + # and attach it to the provenance. This is auxiliary, so a failure here + # must not abort AST generation. + try: + liveness = ASTILiveness(self.function).flag_liveness() + self.astinterface.astree.provenance.set_flag_liveness(liveness) + except Exception as e: + chklogger.logger.warning( + "flag-liveness computation failed for %s: %s", + self.function.faddr, str(e)) + def set_invariants(self) -> None: invariants = self.function.invariants aexprs: Dict[str, Dict[str, Tuple[int, int, str]]] = {}