"""Find loops whose induction-variable width differs from the reference. Detector for the measured loop-index rule (docs/house_rules.md §6): with a narrow (``short``/``word``) index MSVC keeps a separate ``dec reg / jne`` downcounter alongside the strength-reduced offset; with a wide (``long``) index it drops the downcounter and compares an offset against an end constant. Corpus support (research/codegen_dictionary.json, ``loop-index-width``): 29 exact functions pair a downcounter with a narrow index against 2 wide counterexamples, so a *downcounter count* difference between reference and candidate ranks an index-width recovery first. The inverse signature (end-compare implies wide) measured non-discriminative — 28 narrow counterexamples — and is deliberately not reported. Usage: python tools/loop_index_width_scan.py [symbol ...] With no symbols it scans every .text owner the two objects share. Validate on a known positive before trusting a zero -- a broken scan and a clean object look identical. """ import os import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import coff_compare as cc import capstone _md = capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_32) def _owners(obj): """Map owner symbol name -> section number for every .text COMDAT.""" preferred = {} for sym in obj["symbols"]: if sym["section"] <= 0 or sym["value"] != 0: continue if sym["name"].startswith(("$", ".")): continue section = obj["sections"][sym["section"] - 1] if not section["name"].startswith(".text"): continue is_external = sym["storage"] == cc.IMAGE_SYM_CLASS_EXTERNAL current = preferred.get(sym["section"]) if current is None or (is_external and not current[1]): preferred[sym["section"]] = (sym["name"], is_external) owners = {} for section_number, (name, _) in preferred.items(): owners.setdefault(name, section_number) return owners def _section_bytes(obj, index): """Section bytes with relocation slots zeroed on both sides. csplit targets store linked addresses where rebuilt candidates store addends; masking both sides identically keeps the instruction streams comparable and stops data tails from decoding as spurious loops. """ section = obj["sections"][index - 1] raw = bytearray(cc._section_bytes(obj, section))[:section["size"]] for entry in range(section["reloc_count"]): offset = section["reloc"] + entry * cc.RELOC_ENTRY_SIZE address = int.from_bytes(obj["data"][offset:offset + 4], "little") if address + 4 <= len(raw): raw[address:address + 4] = b"\0\0\0\0" return bytes(raw) def downcounters(code): """Return [(dec_address, register)] for every dec reg / jne-backward pair.""" found = [] previous = None for insn in _md.disasm(code, 0): if previous is not None and insn.mnemonic == "jne" \ and previous.mnemonic == "dec" and "[" not in previous.op_str: try: if int(insn.op_str, 16) <= previous.address: found.append((previous.address, previous.op_str)) except ValueError: pass previous = insn return found def scan_function(reference, candidate, name, ref_index, cand_index): ref_loops = downcounters(_section_bytes(reference, ref_index)) cand_loops = downcounters(_section_bytes(candidate, cand_index)) if len(ref_loops) == len(cand_loops): return None return (name, ref_loops, cand_loops) def main(argv): if len(argv) < 3: print(__doc__) return 2 reference = cc.load(argv[1]) candidate = cc.load(argv[2]) wanted = set(argv[3:]) ref_owners = _owners(reference) cand_owners = _owners(candidate) scanned = 0 hits = [] for name in sorted(set(ref_owners) & set(cand_owners)): if wanted and name not in wanted: continue scanned += 1 hit = scan_function(reference, candidate, name, ref_owners[name], cand_owners[name]) if hit: hits.append(hit) hits.sort(key=lambda item: -abs(len(item[1]) - len(item[2]))) for name, ref_loops, cand_loops in hits: if len(ref_loops) > len(cand_loops): advice = "reference keeps a downcounter we lost: suspect an index that should be short/word" else: advice = "candidate keeps a downcounter the reference lacks: suspect an index that should be long" print("%-34s reference %d downcounter(s) %s candidate %d %s" % (name, len(ref_loops), [hex(address) for address, _ in ref_loops], len(cand_loops), [hex(address) for address, _ in cand_loops])) print(" %s" % advice) print("scanned %d shared functions, %d downcounter-count differences" % (scanned, len(hits))) return 0 if __name__ == "__main__": sys.exit(main(sys.argv))