scientific-method-engine 0.6.0__tar.gz → 0.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/PKG-INFO +1 -1
  2. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/pyproject.toml +1 -1
  3. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/x86/pcode.py +5 -2
  4. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/x86/pcode_backend.py +90 -8
  5. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/tests/test_oracle.py +70 -3
  6. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/.gitignore +0 -0
  7. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/LICENSE +0 -0
  8. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/README.md +0 -0
  9. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/__init__.py +0 -0
  10. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/__main__.py +0 -0
  11. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/cli.py +0 -0
  12. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/ghidra/ClearNoReturnFunctions.java +0 -0
  13. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/ghidra/CreateFunctions.java +0 -0
  14. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/ghidra/ExportBoundedFlow.java +0 -0
  15. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/ghidra/ExportFunctionFingerprints.java +0 -0
  16. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/ghidra/ExportFunctionInventory.java +0 -0
  17. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/ghidra/MergeFallThroughFragment.java +0 -0
  18. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/ghidra/RecoverCitedFunctions.java +0 -0
  19. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/ghidra/RepairReturningCallers.java +0 -0
  20. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/ghidra/ReportCallArguments.java +0 -0
  21. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/ghidra/ReportCallPaths.java +0 -0
  22. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/ghidra/ReportCallSitesWithScalars.java +0 -0
  23. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/ghidra/ReportCallsToRange.java +0 -0
  24. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/ghidra/ReportConstantFirstArgumentCalls.java +0 -0
  25. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/ghidra/ReportDataBytes.java +0 -0
  26. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/ghidra/ReportDecompileMatches.java +0 -0
  27. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/ghidra/ReportDecompileWindow.java +0 -0
  28. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/ghidra/ReportFilePatternInMemory.java +0 -0
  29. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/ghidra/ReportFirstArgumentCallSummary.java +0 -0
  30. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/ghidra/ReportFunctionScalarConstants.java +0 -0
  31. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/ghidra/ReportFunctionSummary.java +0 -0
  32. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/ghidra/ReportInstructionContext.java +0 -0
  33. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/ghidra/ReportInstructionWindow.java +0 -0
  34. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/ghidra/ReportMemoryBlockForFileOffset.java +0 -0
  35. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/ghidra/ReportMemoryBlocks.java +0 -0
  36. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/ghidra/ReportRandomnessCandidates.java +0 -0
  37. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/ghidra/ReportReferences.java +0 -0
  38. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/ghidra/ReportScalarConstants.java +0 -0
  39. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/ghidra/ReportStringReferences.java +0 -0
  40. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/ghidra/ReportSymbolReferences.java +0 -0
  41. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/x86/__init__.py +0 -0
  42. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/x86/dispatch.py +0 -0
  43. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/x86/effect_order.py +0 -0
  44. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/x86/handwritten.py +0 -0
  45. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/x86/image.py +0 -0
  46. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/x86/machine.py +0 -0
  47. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/x86/pe.py +0 -0
  48. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/x86/reports.py +0 -0
  49. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/x86/result_flow.py +0 -0
  50. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/x86/semantics.py +0 -0
  51. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/x86/trace.py +0 -0
  52. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/src/scientific_method_engine/x86/values.py +0 -0
  53. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/tests/differential.py +0 -0
  54. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/tests/oracle.py +0 -0
  55. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/tests/test_differential.py +0 -0
  56. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/tests/test_dispatch.py +0 -0
  57. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/tests/test_effect_order.py +0 -0
  58. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/tests/test_nested_frame_request.py +0 -0
  59. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/tests/test_pe.py +0 -0
  60. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/tests/test_table_continuations.py +0 -0
  61. {scientific_method_engine-0.6.0 → scientific_method_engine-0.7.0}/tests/test_x86.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: scientific-method-engine
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: Bounded instruction-derived x86 evidence reports for segmented MZ/FBOV and PE32/i386 code.
5
5
  Project-URL: Source, https://github.com/kibertoad/refurbished-dinosaurs-toolkit/tree/main/packages/scientific-method-engine
6
6
  Author: kibertoad
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
5
5
  [project]
6
6
  name = "scientific-method-engine"
7
7
  # The release workflow writes the published version from the package's release tag.
8
- version = "0.6.0"
8
+ version = "0.7.0"
9
9
  description = "Bounded instruction-derived x86 evidence reports for segmented MZ/FBOV and PE32/i386 code."
10
10
  readme = "README.md"
11
11
  requires-python = ">=3.12"
@@ -300,8 +300,11 @@ def evaluate(code, args, bits, site):
300
300
  # Identities that keep flag expressions small; constants fold in values.op.
301
301
  mask = (1 << bits) - 1
302
302
  for x, y in ((a, b), (b, a)):
303
- if x.number == 0:
304
- return Value(bits, ("constant", 0), origin) if name == "and" else Value(bits, y.term, origin)
303
+ if x.number == 0 and name == "and":
304
+ return Value(bits, ("constant", 0), origin)
305
+ if x.number == 0 and boolean(y):
306
+ # Flag selections OR a zero arm in; a value operand keeps its OR, as reports write it.
307
+ return Value(bits, y.term, origin)
305
308
  if name == "and" and (x.number == mask or (x.number == 1 and boolean(y))):
306
309
  return Value(bits, y.term, origin)
307
310
  if name == "or" and (x.number == mask):
@@ -77,7 +77,7 @@ def bit(value, site):
77
77
  # SLESS(x, 0) is the sign bit of x; a SLEIGH shift writes it as SLESS(x << k, 0).
78
78
  if term[0] == "sless" and term[2] == ("constant", 0):
79
79
  inner, shift = term[1], 0
80
- if inner[0] == "shl" and inner[2][0] == "constant":
80
+ if inner[0] == "shl" and inner[2][0] == "constant" and inner not in LEAVES:
81
81
  inner, shift = inner[1], inner[2][1]
82
82
  width = width_of(inner, value)
83
83
  if width is not None and shift < width:
@@ -90,7 +90,7 @@ def bit(value, site):
90
90
  inner, mask = term[1][1], term[1][2]
91
91
  if mask[0] == "constant" and mask[1] and mask[1] & (mask[1] - 1) == 0:
92
92
  position = mask[1].bit_length() - 1
93
- if inner[0] in ("shr", "sar") and inner[2][0] == "constant":
93
+ if inner[0] in ("shr", "sar") and inner[2][0] == "constant" and inner not in LEAVES:
94
94
  position += inner[2][1]
95
95
  inner = inner[1]
96
96
  width = width_of(inner, value)
@@ -121,6 +121,9 @@ def width_of(term, value):
121
121
 
122
122
  # Widths of the terms the current instruction read or computed; set per frame.
123
123
  WIDTHS = {}
124
+ # The current instruction's operand values. Field decomposition stops at them, because reports
125
+ # treat each operand as one value even when an earlier instruction built it from fields.
126
+ LEAVES = set()
124
127
 
125
128
 
126
129
  def fields(term, width):
@@ -130,6 +133,8 @@ def fields(term, width):
130
133
  (right when negative). Rotates and shifts of joined values take this form in p-code.
131
134
  """
132
135
  head = term[0]
136
+ if term in LEAVES:
137
+ return [(term, width, 0)]
133
138
  top = top_bit(term)
134
139
  if top is not None:
135
140
  return [top]
@@ -252,6 +257,7 @@ class Frame:
252
257
 
253
258
  def __init__(self, state, ins, image, widths=None, roles=None, present=None):
254
259
  self.state, self.ins, self.image = state, ins, image
260
+ # present(value, frame) may rewrite a written value into the equal term reports use.
255
261
  self.present = present
256
262
  self.widths = widths or {}
257
263
  self.roles = roles or {}
@@ -325,6 +331,7 @@ class Frame:
325
331
  addressing_register=register)
326
332
  self.loaded[index] = self.cache[index]
327
333
  WIDTHS.setdefault(self.cache[index].term, self.cache[index].bits)
334
+ LEAVES.add(self.cache[index].term)
328
335
  return extract(self.cache[index], d * 8, size * 8)
329
336
  location = evidence if d == 0 else op("add", evidence, const(d, evidence.bits), state.at)
330
337
  state.access(segment, location, size, value, addressing_register=register)
@@ -345,9 +352,12 @@ class Frame:
345
352
 
346
353
  def execute(self):
347
354
  WIDTHS.clear()
355
+ LEAVES.clear()
348
356
  for index, value in self.before.items():
349
357
  WIDTHS[value.term] = value.bits
350
- self.run = Run(self.state, self.ops, self.memory, self.constant, present=self.present)
358
+ LEAVES.add(value.term)
359
+ present = (lambda value: self.present(value, self)) if self.present else None
360
+ self.run = Run(self.state, self.ops, self.memory, self.constant, present=present)
351
361
  self.run.execute()
352
362
  for value in [*self.loaded.values(), *self.run.temps.values(), *self.run.registers.values()]:
353
363
  if isinstance(value, Value): # Real-mode temporaries may hold a segmented Address.
@@ -442,7 +452,13 @@ class Pypcode:
442
452
  # A handwritten instruction produced some of these flags; its predicate decides.
443
453
  return answer, info
444
454
  condition = Run(state, ops, None, flags=flags).execute(stop_at_branch=True)
445
- return (None if condition.number is None else bool(condition.number)), info
455
+ if condition.number is None:
456
+ return None, info
457
+ if answer is None:
458
+ # The handwritten record says why it could not decide; p-code flags decided it.
459
+ info = {key: value for key, value in info.items() if key != "reason"}
460
+ info["decidedBy"] = "p-code flags"
461
+ return bool(condition.number), info
446
462
 
447
463
  def string_iteration(self, state, ins, operation, width, source_segment, delta_step):
448
464
  if operation not in self.mnemonics:
@@ -605,9 +621,25 @@ def compare(state, ins, image):
605
621
  state.event("compare", operation=m, left=a.report(), right=b.report())
606
622
 
607
623
 
624
+ def unfolded(name, value, f, site):
625
+ """A logic result p-code folded to one operand, written as the operation reports use.
626
+
627
+ ``x | 0``, ``x ^ 0`` and ``x & ~0`` equal ``x``; the interpreter folds them, and reports
628
+ keep the instruction's operation in the result's expression.
629
+ """
630
+ if name not in ("and", "or", "xor") or value.number is not None:
631
+ return value
632
+ a = f.value(0)
633
+ b = resize(f.value(1), a.bits)
634
+ neutral = (1 << a.bits) - 1 if name == "and" else 0
635
+ if value.bits == a.bits and (b.number == neutral and value.term == a.term or a.number == neutral and value.term == b.term):
636
+ return Value(value.bits, op(name, a, b, site).term, value.sources)
637
+ return value
638
+
639
+
608
640
  def arithmetic(state, ins, image):
609
641
  m = ins.mnemonic
610
- f = run_plain(state, ins, image)
642
+ f = run_plain(state, ins, image, present=lambda v, frame: unfolded(m, v, frame, state.at))
611
643
  a = f.value(0)
612
644
  b = resize(f.value(1), a.bits)
613
645
  result = f.result(0)
@@ -705,7 +737,7 @@ def rotate(state, ins, image):
705
737
  state.forget_flags(keep_carry=True)
706
738
  return
707
739
  # Reports write a rotate as its shifted copies, from the leftmost copy for ROL and RCL.
708
- f = run_plain(state, ins, image, present=lambda v: arranged(v, m in ("rol", "rcl"), state.at))
740
+ f = run_plain(state, ins, image, present=lambda v, _: arranged(v, m in ("rol", "rcl"), state.at))
709
741
  a = f.value(0)
710
742
  value = f.result(0)
711
743
  carry = bit(f.run.flags["CF"], state.at)
@@ -733,10 +765,33 @@ def temporary(f, code):
733
765
  return f.run.temps[o.output[1]]
734
766
 
735
767
 
768
+ def low_product(value, site):
769
+ """The low half of a product of two extended values, as the product at the operands' width.
770
+
771
+ SLEIGH writes a two- or three-operand IMUL as the double-width product of the sign-extended
772
+ operands, truncated; the low half does not depend on the extension, and reports write it as
773
+ the operand-width product. An operand that already holds a sign extension (after CBW or MOVSX)
774
+ reaches the product as one extension of the narrower value, which is extended back to the
775
+ operand's width here.
776
+ """
777
+ term = value.term
778
+ if term[0] != "extract" or term[2] != 0 or term[1][0] != "mul":
779
+ return value
780
+ factors = []
781
+ for factor in term[1][1:]:
782
+ if factor[0] in ("signExtend", "zeroExtend") and factor[2] <= value.bits:
783
+ factors.append(resize(Value(factor[2], factor[1]), value.bits, signed=factor[0] == "signExtend"))
784
+ elif factor[0] == "constant":
785
+ factors.append(const(factor[1], value.bits))
786
+ else:
787
+ return value
788
+ return Value(value.bits, op("mul", *factors, site).term, value.sources)
789
+
790
+
736
791
  def multiply(state, ins, image):
737
792
  m = ins.mnemonic
738
793
  if m == "imul" and len(ins.operands) in (2, 3):
739
- f = run_plain(state, ins, image)
794
+ f = run_plain(state, ins, image, present=lambda v, _: low_product(v, state.at))
740
795
  left = resize(f.value(0 if len(ins.operands) == 2 else 1), ins.operands[0].size * 8)
741
796
  right = resize(f.value(1 if len(ins.operands) == 2 else 2), left.bits, signed=True)
742
797
  result = f.result(0)
@@ -786,10 +841,37 @@ def divide(state, ins, image):
786
841
  quotient=quotient.report(), remainder=remainder.report(), fault=fault)
787
842
 
788
843
 
844
+ def through_extension(value, source, form):
845
+ """``value`` as ``form(source)`` when p-code wrote ``form`` of the value ``source`` sign-extends.
846
+
847
+ When the source register holds a sign extension (after CBW, CWDE or MOVSX), p-code reads
848
+ through it to the narrower value it extended, because ``pcode.evaluate`` folds an extension
849
+ of an extension. Both name the same bits; reports name them in the register the instruction
850
+ reads.
851
+ """
852
+ if value.number is not None or source.term[0] != "signExtend":
853
+ return value
854
+ inner = Value(source.term[2], source.term[1])
855
+ if value.term == form(inner).term:
856
+ return Value(value.bits, form(source).term, value.sources)
857
+ return value
858
+
859
+
789
860
  def conversion(state, ins, image):
790
861
  m = ins.mnemonic
791
862
  before = {name: state.reg(name) for name in ("al", "ax", "eax")}
792
- f = run_plain(state, ins, image)
863
+
864
+ def present(value, _):
865
+ if value.bits not in (16, 32):
866
+ return value
867
+ if m in ("cwd", "cdq"):
868
+ # The high half is the sign bit of AX or EAX.
869
+ return through_extension(value, before["ax" if value.bits == 16 else "eax"],
870
+ lambda v: resize(extract(v, v.bits - 1, 1), value.bits, signed=True))
871
+ # CBW and CWDE write the sign extension of AL or AX.
872
+ return through_extension(value, before["al" if value.bits == 16 else "ax"],
873
+ lambda v: resize(v, value.bits, signed=True))
874
+ f = run_plain(state, ins, image, present=present)
793
875
  (destination, value), = f.run.registers.items()
794
876
  wide = value.bits == 32
795
877
  if m in ("cbw", "cwde"):
@@ -1,7 +1,7 @@
1
1
  """Unicorn oracle cases per instruction group (ADR 0003). Synthetic machine code only."""
2
2
  import unittest
3
3
 
4
- from oracle import check
4
+ from oracle import STACK, check, configuration, run_report
5
5
 
6
6
  DATA = {"ds": 0x3000, "es": 0x4000}
7
7
  SAME = {"ds": 0x4000, "es": 0x4000}
@@ -70,8 +70,31 @@ class ArithmeticAndLogic(unittest.TestCase):
70
70
 
71
71
  def test_counted_loop_exits_on_decrement_flags(self):
72
72
  # inc ax; dec cx; jnz back: the handwritten backend forgets DEC's flags and splits.
73
- check(self, "b90300 b80000 40 49 75fc c3", resolved=("ax", "cx"),
74
- extended="DEC flags from p-code resolve the loop exit")
73
+ result = check(self, "b90300 b80000 40 49 75fc c3", resolved=("ax", "cx"),
74
+ extended="DEC flags from p-code resolve the loop exit")
75
+ branches = [e for e in result["paths"][0]["events"] if e["kind"] == "branch"]
76
+ self.assertEqual([e["taken"] for e in branches], [True, True, False])
77
+ # A branch p-code decided does not keep the handwritten backend's reason for not deciding it.
78
+ for e in branches:
79
+ self.assertNotIn("reason", e)
80
+ self.assertEqual((e["decidedBy"], e["flagProducer"]), ("p-code flags", 7))
81
+
82
+ def test_undecided_branch_keeps_the_reason(self):
83
+ # inc ax; jnz: AX is unknown, so neither backend decides the branch and both arms run.
84
+ data = bytes.fromhex("40 7501 90 c3".replace(" ", ""))
85
+ result = run_report(data, configuration(data, dict(STACK)), "trace")
86
+ branches = [e for path in result["paths"] for e in path["events"] if e["kind"] == "branch"]
87
+ self.assertEqual(sorted(e["taken"] for e in branches), [False, True])
88
+ for e in branches:
89
+ self.assertEqual(e["reason"], "flag producer unresolved")
90
+ self.assertNotIn("decidedBy", e)
91
+
92
+ def test_neutral_operands_keep_the_operation(self):
93
+ # BX is unknown to the engine, so each result stays an expression over it.
94
+ result = check(self, "b80100 80cf00 83f300 81e3ffff 09c3 c3", resolved=("ax",))
95
+ bx = result["paths"][0]["registers"]["bx"]["expression"]
96
+ # or(and(xor(..., 0), 0xffff), ax): neither the XOR with 0 nor the AND with ~0 folds away.
97
+ self.assertEqual((bx[0], bx[1][0], bx[1][1][0]), ("or", "and", "xor"))
75
98
 
76
99
  def test_byte_forms_and_memory(self):
77
100
  check(self, "bb1000 c707ff00 8007 01 fe07 8b07 b4ff 00e0 c3", registers=DATA, resolved=("ax",))
@@ -93,6 +116,23 @@ class ShiftsAndRotates(unittest.TestCase):
93
116
  def test_rotates_through_carry_and_full_counts(self):
94
117
  check(self, "b83412 f9 c1d00c f8 c1d805 c1c010 c1c808 83d000 c3", resolved=("ax",))
95
118
 
119
+ def test_chained_double_word_shifts(self):
120
+ # DX:AX shifted right twice through CF; the engine leaves both unknown, so each RCR's
121
+ # result and carry stay expressions over the previous one.
122
+ result = check(self, "b90100 d1ea d1d8 d1ea d1d8 c3", resolved=("cx",))
123
+ ax = result["paths"][0]["registers"]["ax"]["expression"]
124
+ self.assertEqual(ax[0], "or")
125
+ # The second RCR shifts out bit 0 of the first RCR's result, not bit 1 of the initial AX.
126
+ rotates = [e for e in result["paths"][0]["events"] if e.get("operation") == "rcr"]
127
+ self.assertEqual(rotates[1]["carryOut"]["expression"][1][0], "or")
128
+
129
+ def test_double_word_shift_from_a_zero_high_half(self):
130
+ # DX starts at zero; RCL carries AX's unknown top bits into it. The second RCL shifts out
131
+ # DX's bit 15, which is still zero; ADC moves that carry into BX for Unicorn to check.
132
+ result = check(self, "b90100 31d2 d1e0 d1d2 d1e0 d1d2 bb0000 11db c3", resolved=("cx", "bx"),
133
+ extended="a rotate's carry out folded from the operand's known bits")
134
+ self.assertEqual(result["paths"][0]["registers"]["dx"]["expression"][0], "or")
135
+
96
136
  def test_memory_operands(self):
97
137
  # RCL by one on memory: Capstone reports its implicit count with a size of 0.
98
138
  check(self, "bb1000 c7070180 d107 c12f04 f9 d117 8b07 c7070180 f8 d017 8b07 c3", registers=DATA, resolved=("ax",))
@@ -102,6 +142,20 @@ class MultiplyAndDivide(unittest.TestCase):
102
142
  def test_products(self):
103
143
  check(self, "b80300 bb0500 f7e3 b0f0 b304 f6eb 6bc3fd 0fafc3 c3", resolved=("ax", "dx"))
104
144
 
145
+ def test_low_products_of_unknown_operands(self):
146
+ # The engine leaves CX unknown; the low products keep the operand-width form.
147
+ result = check(self, "b80300 0fafc8 6bd1fd 69d90500 c3")
148
+ registers = result["paths"][0]["registers"]
149
+ self.assertEqual(registers["cx"]["expression"][0], "mul")
150
+ self.assertEqual(registers["dx"]["expression"][0], "mul")
151
+
152
+ def test_low_products_of_extended_operands(self):
153
+ # AX holds the sign extension of an unknown byte; the products name AX, not the byte.
154
+ result = check(self, "a00000 98 0fafc8 6bd0fd b80100 c3", registers=DATA, resolved=("ax",))
155
+ registers = result["paths"][0]["registers"]
156
+ self.assertEqual(registers["cx"]["expression"][0], "mul")
157
+ self.assertEqual(registers["dx"]["expression"][0], "mul")
158
+
105
159
  def test_quotients(self):
106
160
  check(self, "ba0000 b86400 bb0700 f7f3 89c1 b8f6ff 99 f7fb c3", resolved=("ax", "cx", "dx"))
107
161
 
@@ -114,6 +168,19 @@ class Conversions(unittest.TestCase):
114
168
  def test_sign_extensions(self):
115
169
  check(self, "b080 98 89c3 99 b8ff7f 6698 6699 c3", resolved=("ax", "bx", "dx", "eax", "edx"))
116
170
 
171
+ def test_sign_fill_of_an_extended_byte(self):
172
+ # The byte at DS:0 is unknown to the engine; CWD names the sign bit of AX.
173
+ result = check(self, "a00000 98 99 b80100 c3", registers=DATA, resolved=("ax",))
174
+ dx = result["paths"][0]["registers"]["dx"]["expression"]
175
+ self.assertEqual(tuple(dx[1][2:4]), (15, 1))
176
+
177
+ def test_sign_extension_of_an_extended_byte(self):
178
+ # The byte at DS:0 is unknown to the engine; CWDE extends AX and CDQ names the sign bit of EAX.
179
+ result = check(self, "a00000 98 6698 6699 b80100 c3", registers=DATA, resolved=("ax",))
180
+ cwde = next(e for e in result["paths"][0]["events"] if e["kind"] == "conversion" and e["destinationBits"] == 32)
181
+ self.assertEqual(cwde["result"]["expression"][1][2:4], (8, 16))
182
+ self.assertEqual(tuple(result["paths"][0]["registers"]["edx"]["expression"][1][2:4]), (31, 1))
183
+
117
184
 
118
185
  class FlagsAndDirection(unittest.TestCase):
119
186
  def test_direction_controls_string_steps(self):