From 77f8809c5ab4a1f98f731df3ac6e427d4ccc0a96 Mon Sep 17 00:00:00 2001 From: Dan Phung Date: Fri, 31 Jul 2026 23:42:45 -0700 Subject: [PATCH 1/6] 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 | 210 +++++++++++++++++++++++++++++++ 1 file changed, 210 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..837fda2d --- /dev/null +++ b/chb/astinterface/ASTILiveness.py @@ -0,0 +1,210 @@ +# ------------------------------------------------------------------------------ +# CodeHawk Binary Analyzer +# Author: Dan Phung +# ------------------------------------------------------------------------------ +# 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, Mapping, 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. + + get_facts is called once per instruction and returns that instruction's + reaching-def facts: instr.xdata.flag_reachingdefs for flags, + instr.xdata.reachingdefs for registers. Each fact names the variable + used at that instruction and the addresses that defined the value used + there, so the fact contributes a use at the instruction and a kill at + each of those def addresses. + + When names is given, only variables in that set are considered. "PC" is + always excluded regardless of names. It is not a value a consumer can + treat as live or dead. + """ + + def is_real_def_site(defloc: str) -> bool: + """True if defloc is an instruction address that can carry a kill. + + "init" is the analysis's marker for a value defined on function + entry rather than by an instruction. An "F"-prefixed address (e.g. + "F:0x...._0x....") belongs to an instruction the analysis inlined + from another function, so it is not a site in this function's CFG. + Neither one can kill anything here. + """ + return not (defloc == "init" or defloc.startswith("F")) + + 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 not is_real_def_site(da): + continue + kill[da].add(name) + return (use, kill) + + def _blocks(self) -> Dict[str, List[str]]: + """Map block address to its instruction addresses in execution order. + + Sorted lexicographically, the same ordering BasicBlock.lastaddr uses. + Sorting the addresses as numbers instead would raise an exception + because not every address is plain hex: the analysis writes an inlined + instruction's address as "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 _liveness( + self, + use: Dict[str, Set[str]], + kill: Dict[str, Set[str]]) -> Dict[str, Dict[str, List[str]]]: + blocks = self._blocks() + # Read successors 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. + edges = self.fn.cfg.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 _visit_order(self, blocks: Dict[str, List[str]]) -> List[str]: + """Block addresses in the order the fixpoint should visit them. + + A backward analysis converges fastest visiting blocks in reverse of + reverse-postorder, so successors are settled before their predecessors. + cfg.rpo_sorted_nodes supplies the reverse-postorder. Blocks it omits + (it is derived from the graph reachable from the entry) are appended, so + every block is still visited; order only affects how many rounds the + fixpoint takes, never the result. + """ + try: + rpo = list(self.fn.cfg.rpo_sorted_nodes) + except Exception: + return list(blocks) + ordered = [b for b in reversed(rpo) if b in blocks] + seen = set(ordered) + return ordered + [b for b in blocks if b not in seen] + + def _backward( + self, + blocks: Dict[str, List[str]], + edges: Mapping[str, Sequence[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. CodeHawk has no dataflow framework to reuse for the fixpoint + itself; the CFG traversal it does provide is used via _visit_order. + """ + block_in: Dict[str, Set[str]] = {b: set() for b in blocks} + live_in: Dict[str, Set[str]] = {} + live_out: Dict[str, Set[str]] = {} + order = self._visit_order(blocks) + + changed = True + while changed: + changed = False + for b in order: + iaddrs = blocks[b] + # 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 4468a4d19cee09c982f934e54b8621b9fa2ce2a4 Mon Sep 17 00:00:00 2001 From: Dan Phung Date: Fri, 31 Jul 2026 23:42:45 -0700 Subject: [PATCH 2/6] 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 | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/chb/ast/ASTProvenance.py b/chb/ast/ASTProvenance.py index 576e927e..620d0032 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,15 @@ 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 + + @flag_liveness.setter + def 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 +117,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 +141,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 3246b43f98249316963ab42c7ea5c45c806dcdb0 Mon Sep 17 00:00:00 2001 From: Dan Phung Date: Fri, 31 Jul 2026 23:42:45 -0700 Subject: [PATCH 3/6] 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 | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/chb/astinterface/ASTInterfaceFunction.py b/chb/astinterface/ASTInterfaceFunction.py index f2d974b3..90fa019f 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,17 @@ 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.flag_liveness = liveness + except Exception: + chklogger.logger.exception( + "flag-liveness computation failed for %s", self.function.faddr) + def set_invariants(self) -> None: invariants = self.function.invariants aexprs: Dict[str, Dict[str, Tuple[int, int, str]]] = {} From d12711d8cc0f2d6755309153c91586cf04293d78 Mon Sep 17 00:00:00 2001 From: Dan Phung Date: Fri, 21 Aug 2026 13:12:58 -0700 Subject: [PATCH 4/6] ASTI: count inlined and predicated def-sites as kills is_real_def_site excluded any "F"-prefixed def-site as "not a site in this function's CFG", which was wrong. Those instructions execute and define what they define, and their addresses are ordinary keys in fn.blocks and fn.instructions. _use_kill was therefore recording uses at those addresses while discarding their kills. --- chb/astinterface/ASTILiveness.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/chb/astinterface/ASTILiveness.py b/chb/astinterface/ASTILiveness.py index 837fda2d..05d69d60 100644 --- a/chb/astinterface/ASTILiveness.py +++ b/chb/astinterface/ASTILiveness.py @@ -92,13 +92,11 @@ def _use_kill( def is_real_def_site(defloc: str) -> bool: """True if defloc is an instruction address that can carry a kill. - "init" is the analysis's marker for a value defined on function - entry rather than by an instruction. An "F"-prefixed address (e.g. - "F:0x...._0x....") belongs to an instruction the analysis inlined - from another function, so it is not a site in this function's CFG. - Neither one can kill anything here. + Only "init" is excluded since it is the analysis's marker for a + value defined on function entry rather than by an instruction, so + there is no instruction there to do the killing. """ - return not (defloc == "init" or defloc.startswith("F")) + return defloc != "init" use: Dict[str, Set[str]] = defaultdict(set) kill: Dict[str, Set[str]] = defaultdict(set) From 0c1a707b0bbc69aa57ca762aa355817032d7cb98 Mon Sep 17 00:00:00 2001 From: Dan Phung Date: Fri, 21 Aug 2026 13:41:34 -0700 Subject: [PATCH 5/6] ASTI: warn when a flag use is reached by an 'init' definition An "init" def-site means the value was defined on function entry rather than by an instruction. For a registers that is ordinary and an incoming parameter is defined exactly there. This is not normal for a flag because the ABI leaves NZCV undefined on entry to a function. --- chb/astinterface/ASTILiveness.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/chb/astinterface/ASTILiveness.py b/chb/astinterface/ASTILiveness.py index 05d69d60..623e06d1 100644 --- a/chb/astinterface/ASTILiveness.py +++ b/chb/astinterface/ASTILiveness.py @@ -40,10 +40,13 @@ """ from collections import defaultdict + from typing import ( Callable, Dict, List, Mapping, Optional, Sequence, Set, TYPE_CHECKING, Tuple, Union) +from chb.util.loggingutil import chklogger + if TYPE_CHECKING: from chb.app.Function import Function from chb.app.Instruction import Instruction @@ -64,7 +67,7 @@ def fn(self) -> "Function": 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) + lambda instr: instr.xdata.flag_reachingdefs, warn_on_init=True) return self._liveness(use, kill) def _use_kill( @@ -73,7 +76,8 @@ def _use_kill( ["Instruction"], Sequence[Optional[Union["FlagReachingDefFact", "ReachingDefFact"]]]], - names: Optional[Set[str]] = None + names: Optional[Set[str]] = None, + warn_on_init: bool = False ) -> Tuple[Dict[str, Set[str]], Dict[str, Set[str]]]: """Build per-address use and kill (def) sets from reaching-def facts. @@ -87,6 +91,9 @@ def _use_kill( When names is given, only variables in that set are considered. "PC" is always excluded regardless of names. It is not a value a consumer can treat as live or dead. + + warn_on_init reports uses whose value is defined on function entry + rather than by an instruction. """ def is_real_def_site(defloc: str) -> bool: @@ -100,6 +107,7 @@ def is_real_def_site(defloc: str) -> bool: use: Dict[str, Set[str]] = defaultdict(set) kill: Dict[str, Set[str]] = defaultdict(set) + warned_init: Set[Tuple[str, str]] = set() for (iaddr, instr) in self.fn.instructions.items(): for fact in get_facts(instr): if fact is None: @@ -113,6 +121,13 @@ def is_real_def_site(defloc: str) -> bool: for d in fact.deflocations: da = str(d) if not is_real_def_site(da): + if warn_on_init and (iaddr, name) not in warned_init: + warned_init.add((iaddr, name)) + chklogger.logger.warning( + "flag %s used at %s in function %s is reached by " + "an '%s' definition: its value predates function " + "entry.", + name, iaddr, self.fn.faddr, da) continue kill[da].add(name) return (use, kill) From 6c904cf9202e32a7d634561cc2a77c41d5bb721e Mon Sep 17 00:00:00 2001 From: Dan Phung Date: Fri, 21 Aug 2026 20:39:55 -0700 Subject: [PATCH 6/6] change message from warning to info for flag reached by an 'init' def --- chb/astinterface/ASTILiveness.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chb/astinterface/ASTILiveness.py b/chb/astinterface/ASTILiveness.py index 623e06d1..c37b28b8 100644 --- a/chb/astinterface/ASTILiveness.py +++ b/chb/astinterface/ASTILiveness.py @@ -123,7 +123,7 @@ def is_real_def_site(defloc: str) -> bool: if not is_real_def_site(da): if warn_on_init and (iaddr, name) not in warned_init: warned_init.add((iaddr, name)) - chklogger.logger.warning( + chklogger.logger.info( "flag %s used at %s in function %s is reached by " "an '%s' definition: its value predates function " "entry.",