smda 4.2.0__tar.gz → 4.2.2__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 (135) hide show
  1. {smda-4.2.0/src/smda.egg-info → smda-4.2.2}/PKG-INFO +3 -1
  2. {smda-4.2.0 → smda-4.2.2}/README.md +2 -0
  3. {smda-4.2.0 → smda-4.2.2}/pyproject.toml +1 -1
  4. {smda-4.2.0 → smda-4.2.2}/src/smda/SmdaConfig.py +1 -1
  5. smda-4.2.2/src/smda/__init__.py +1 -0
  6. {smda-4.2.0 → smda-4.2.2}/src/smda/aarch64/AArch64Backend.py +7 -0
  7. {smda-4.2.0 → smda-4.2.2}/src/smda/common/RecursiveDisassembler.py +1 -0
  8. {smda-4.2.0 → smda-4.2.2}/src/smda/common/SmdaFunction.py +8 -3
  9. {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/PeSymbolProvider.py +4 -2
  10. {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/WinApiResolver.py +2 -1
  11. {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/import_parsers.py +22 -0
  12. smda-4.2.2/src/smda/ida/IdaExporter.py +123 -0
  13. {smda-4.2.0 → smda-4.2.2}/src/smda/ida/IdaInterface.py +13 -1
  14. {smda-4.2.0 → smda-4.2.2}/src/smda/intel/FunctionAnalysisState.py +6 -4
  15. {smda-4.2.0 → smda-4.2.2}/src/smda/intel/FunctionCandidateManager.py +46 -6
  16. {smda-4.2.0 → smda-4.2.2}/src/smda/intel/IntelInstructionEscaper.py +4 -3
  17. {smda-4.2.0 → smda-4.2.2}/src/smda/intel/X86Backend.py +10 -3
  18. {smda-4.2.0 → smda-4.2.2/src/smda.egg-info}/PKG-INFO +3 -1
  19. {smda-4.2.0 → smda-4.2.2}/tests/testAArch64Disassembler.py +70 -3
  20. {smda-4.2.0 → smda-4.2.2}/tests/testCommonModels.py +25 -0
  21. {smda-4.2.0 → smda-4.2.2}/tests/testEscaper.py +13 -0
  22. {smda-4.2.0 → smda-4.2.2}/tests/testIntegration.py +4 -2
  23. {smda-4.2.0 → smda-4.2.2}/tests/testIntelDisassembler.py +129 -0
  24. {smda-4.2.0 → smda-4.2.2}/tests/testPeSymbolProvider.py +67 -0
  25. smda-4.2.0/src/smda/__init__.py +0 -1
  26. smda-4.2.0/src/smda/ida/IdaExporter.py +0 -92
  27. {smda-4.2.0 → smda-4.2.2}/LICENSE +0 -0
  28. {smda-4.2.0 → smda-4.2.2}/setup.cfg +0 -0
  29. {smda-4.2.0 → smda-4.2.2}/src/smda/Disassembler.py +0 -0
  30. {smda-4.2.0 → smda-4.2.2}/src/smda/DisassemblyResult.py +0 -0
  31. {smda-4.2.0 → smda-4.2.2}/src/smda/DisassemblyStatistics.py +0 -0
  32. {smda-4.2.0 → smda-4.2.2}/src/smda/aarch64/AArch64CapstoneVerification.py +0 -0
  33. {smda-4.2.0 → smda-4.2.2}/src/smda/aarch64/AArch64Disassembler.py +0 -0
  34. {smda-4.2.0 → smda-4.2.2}/src/smda/aarch64/AArch64InstructionEscaper.py +0 -0
  35. {smda-4.2.0 → smda-4.2.2}/src/smda/aarch64/FunctionAnalysisState.py +0 -0
  36. {smda-4.2.0 → smda-4.2.2}/src/smda/aarch64/FunctionCandidateManager.py +0 -0
  37. {smda-4.2.0 → smda-4.2.2}/src/smda/aarch64/__init__.py +0 -0
  38. {smda-4.2.0 → smda-4.2.2}/src/smda/aarch64/analyzers.py +0 -0
  39. {smda-4.2.0 → smda-4.2.2}/src/smda/aarch64/definitions.py +0 -0
  40. {smda-4.2.0 → smda-4.2.2}/src/smda/cil/CilDisassembler.py +0 -0
  41. {smda-4.2.0 → smda-4.2.2}/src/smda/cil/CilInstructionEscaper.py +0 -0
  42. {smda-4.2.0 → smda-4.2.2}/src/smda/cil/FunctionAnalysisState.py +0 -0
  43. {smda-4.2.0 → smda-4.2.2}/src/smda/cil/__init__.py +0 -0
  44. {smda-4.2.0 → smda-4.2.2}/src/smda/common/BasicBlock.py +0 -0
  45. {smda-4.2.0 → smda-4.2.2}/src/smda/common/BinaryInfo.py +0 -0
  46. {smda-4.2.0 → smda-4.2.2}/src/smda/common/BlockLocator.py +0 -0
  47. {smda-4.2.0 → smda-4.2.2}/src/smda/common/CodeXref.py +0 -0
  48. {smda-4.2.0 → smda-4.2.2}/src/smda/common/DominatorTree.py +0 -0
  49. {smda-4.2.0 → smda-4.2.2}/src/smda/common/ExceptionHandling.py +0 -0
  50. {smda-4.2.0 → smda-4.2.2}/src/smda/common/SmdaBasicBlock.py +0 -0
  51. {smda-4.2.0 → smda-4.2.2}/src/smda/common/SmdaInstruction.py +0 -0
  52. {smda-4.2.0 → smda-4.2.2}/src/smda/common/SmdaReport.py +0 -0
  53. {smda-4.2.0 → smda-4.2.2}/src/smda/common/TailcallAnalyzer.py +0 -0
  54. {smda-4.2.0 → smda-4.2.2}/src/smda/common/Tarjan.py +0 -0
  55. {smda-4.2.0 → smda-4.2.2}/src/smda/common/__init__.py +0 -0
  56. {smda-4.2.0 → smda-4.2.2}/src/smda/common/arch/ArchBackend.py +0 -0
  57. {smda-4.2.0 → smda-4.2.2}/src/smda/common/arch/__init__.py +0 -0
  58. {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/AbstractLabelProvider.py +0 -0
  59. {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/CilSymbolProvider.py +0 -0
  60. {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/DelphiKbSymbolProvider.py +0 -0
  61. {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/DelphiPythiaProvider.py +0 -0
  62. {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/DelphiReSymProvider.py +0 -0
  63. {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/ElfApiResolver.py +0 -0
  64. {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/ElfSymbolProvider.py +0 -0
  65. {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/GoLabelProvider.py +0 -0
  66. {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/MachoDemangler.py +0 -0
  67. {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/MachoSymbolProvider.py +0 -0
  68. {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/OrdinalHelper.py +0 -0
  69. {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/PdbSymbolProvider.py +0 -0
  70. {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/RustSymbolProvider.py +0 -0
  71. {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/__init__.py +0 -0
  72. {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/rust_demangler/__init__.py +0 -0
  73. {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/rust_demangler/main.py +0 -0
  74. {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/rust_demangler/rust.py +0 -0
  75. {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/rust_demangler/rust_legacy.py +0 -0
  76. {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/rust_demangler/rust_v0.py +0 -0
  77. {smda-4.2.0 → smda-4.2.2}/src/smda/common/labelprovider/rust_demangler/utils.py +0 -0
  78. {smda-4.2.0 → smda-4.2.2}/src/smda/dalvik/DalvikDisassembler.py +0 -0
  79. {smda-4.2.0 → smda-4.2.2}/src/smda/dalvik/DalvikFunctionAnalysisState.py +0 -0
  80. {smda-4.2.0 → smda-4.2.2}/src/smda/dalvik/DalvikOpcodeDecoder.py +0 -0
  81. {smda-4.2.0 → smda-4.2.2}/src/smda/dalvik/__init__.py +0 -0
  82. {smda-4.2.0 → smda-4.2.2}/src/smda/ida/BackendInterface.py +0 -0
  83. {smda-4.2.0 → smda-4.2.2}/src/smda/ida/__init__.py +0 -0
  84. {smda-4.2.0 → smda-4.2.2}/src/smda/intel/BitnessAnalyzer.py +0 -0
  85. {smda-4.2.0 → smda-4.2.2}/src/smda/intel/FunctionCandidate.py +0 -0
  86. {smda-4.2.0 → smda-4.2.2}/src/smda/intel/IndirectCallAnalyzer.py +0 -0
  87. {smda-4.2.0 → smda-4.2.2}/src/smda/intel/IntelDisassembler.py +0 -0
  88. {smda-4.2.0 → smda-4.2.2}/src/smda/intel/JumpTableAnalyzer.py +0 -0
  89. {smda-4.2.0 → smda-4.2.2}/src/smda/intel/LanguageAnalyzer.py +0 -0
  90. {smda-4.2.0 → smda-4.2.2}/src/smda/intel/MnemonicTfIdf.py +0 -0
  91. {smda-4.2.0 → smda-4.2.2}/src/smda/intel/__init__.py +0 -0
  92. {smda-4.2.0 → smda-4.2.2}/src/smda/intel/definitions.py +0 -0
  93. {smda-4.2.0 → smda-4.2.2}/src/smda/utility/BracketQueue.py +0 -0
  94. {smda-4.2.0 → smda-4.2.2}/src/smda/utility/DelphiKbFileLoader.py +0 -0
  95. {smda-4.2.0 → smda-4.2.2}/src/smda/utility/DexFileLoader.py +0 -0
  96. {smda-4.2.0 → smda-4.2.2}/src/smda/utility/ElfFileLoader.py +0 -0
  97. {smda-4.2.0 → smda-4.2.2}/src/smda/utility/FileLoader.py +0 -0
  98. {smda-4.2.0 → smda-4.2.2}/src/smda/utility/MachoBinary.py +0 -0
  99. {smda-4.2.0 → smda-4.2.2}/src/smda/utility/MachoFileLoader.py +0 -0
  100. {smda-4.2.0 → smda-4.2.2}/src/smda/utility/MemoryFileLoader.py +0 -0
  101. {smda-4.2.0 → smda-4.2.2}/src/smda/utility/PeFileLoader.py +0 -0
  102. {smda-4.2.0 → smda-4.2.2}/src/smda/utility/PriorityQueue.py +0 -0
  103. {smda-4.2.0 → smda-4.2.2}/src/smda/utility/StringExtractor.py +0 -0
  104. {smda-4.2.0 → smda-4.2.2}/src/smda/utility/__init__.py +0 -0
  105. {smda-4.2.0 → smda-4.2.2}/src/smda/utility/common.py +0 -0
  106. {smda-4.2.0 → smda-4.2.2}/src/smda.egg-info/SOURCES.txt +0 -0
  107. {smda-4.2.0 → smda-4.2.2}/src/smda.egg-info/dependency_links.txt +0 -0
  108. {smda-4.2.0 → smda-4.2.2}/src/smda.egg-info/requires.txt +0 -0
  109. {smda-4.2.0 → smda-4.2.2}/src/smda.egg-info/top_level.txt +0 -0
  110. {smda-4.2.0 → smda-4.2.2}/tests/testAArch64EscaperCapstoneVerification.py +0 -0
  111. {smda-4.2.0 → smda-4.2.2}/tests/testAArch64EscaperCoverage.py +0 -0
  112. {smda-4.2.0 → smda-4.2.2}/tests/testAArch64EscaperImproved.py +0 -0
  113. {smda-4.2.0 → smda-4.2.2}/tests/testAArch64MachoCorpus.py +0 -0
  114. {smda-4.2.0 → smda-4.2.2}/tests/testBenchmarkEval.py +0 -0
  115. {smda-4.2.0 → smda-4.2.2}/tests/testBitnessAnalyzer.py +0 -0
  116. {smda-4.2.0 → smda-4.2.2}/tests/testBracketQueue.py +0 -0
  117. {smda-4.2.0 → smda-4.2.2}/tests/testCandidateSafeguards.py +0 -0
  118. {smda-4.2.0 → smda-4.2.2}/tests/testDalvikDisassembler.py +0 -0
  119. {smda-4.2.0 → smda-4.2.2}/tests/testDefinitionsExpansion.py +0 -0
  120. {smda-4.2.0 → smda-4.2.2}/tests/testDelphiPythiaProvider.py +0 -0
  121. {smda-4.2.0 → smda-4.2.2}/tests/testElfSymbolProvider.py +0 -0
  122. {smda-4.2.0 → smda-4.2.2}/tests/testExceptionHandling.py +0 -0
  123. {smda-4.2.0 → smda-4.2.2}/tests/testFileFormatParsers.py +0 -0
  124. {smda-4.2.0 → smda-4.2.2}/tests/testIndirectCallAnalyzer.py +0 -0
  125. {smda-4.2.0 → smda-4.2.2}/tests/testLanguageAnalyzer.py +0 -0
  126. {smda-4.2.0 → smda-4.2.2}/tests/testLateCandidateProduction.py +0 -0
  127. {smda-4.2.0 → smda-4.2.2}/tests/testMachoSymbolProvider.py +0 -0
  128. {smda-4.2.0 → smda-4.2.2}/tests/testPdataExtraction.py +0 -0
  129. {smda-4.2.0 → smda-4.2.2}/tests/testPeFileLoader.py +0 -0
  130. {smda-4.2.0 → smda-4.2.2}/tests/testRustSymbolProvider.py +0 -0
  131. {smda-4.2.0 → smda-4.2.2}/tests/testSmdaInstructionDetail.py +0 -0
  132. {smda-4.2.0 → smda-4.2.2}/tests/testSmdaInstructionDetailAArch64.py +0 -0
  133. {smda-4.2.0 → smda-4.2.2}/tests/testSmdaReportBuffer.py +0 -0
  134. {smda-4.2.0 → smda-4.2.2}/tests/testTarjan.py +0 -0
  135. {smda-4.2.0 → smda-4.2.2}/tests/testXmetadataNormalization.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: smda
3
- Version: 4.2.0
3
+ Version: 4.2.2
4
4
  Summary: A recursive disassmbler optimized for CFG recovery from memory dumps. Based on capstone.
5
5
  Author-email: Daniel Plohmann <daniel.plohmann@mailbox.org>
6
6
  License: BSD 2-Clause
@@ -146,6 +146,8 @@ make test
146
146
 
147
147
  ## Version History
148
148
 
149
+ * 2026-07-09: v4.2.2 - Now also parsing delay import tables from Windows PEs.
150
+ * 2026-07-09: v4.2.1 - Better detection of CFG instructions with prefixes, improved accuracy of gap search. (THX: @r0ny123) IDA ARM64 export.
149
151
  * 2026-06-24: v4.2.0 - Further improvements for inter-procedural operand escaping (closer to x86_x64 and traditional PIC hashing).
150
152
  * 2026-06-23: v4.1.0 - Significantly extended Aarch64 mnemonic escapes and improved PIC/OPC hashing. (THX: @r0ny123)
151
153
  * 2026-06-23: v4.0.2 - Improvements to Aarch64 function recovery, adressing tailcalls and gap function cornercases.
@@ -105,6 +105,8 @@ make test
105
105
 
106
106
  ## Version History
107
107
 
108
+ * 2026-07-09: v4.2.2 - Now also parsing delay import tables from Windows PEs.
109
+ * 2026-07-09: v4.2.1 - Better detection of CFG instructions with prefixes, improved accuracy of gap search. (THX: @r0ny123) IDA ARM64 export.
108
110
  * 2026-06-24: v4.2.0 - Further improvements for inter-procedural operand escaping (closer to x86_x64 and traditional PIC hashing).
109
111
  * 2026-06-23: v4.1.0 - Significantly extended Aarch64 mnemonic escapes and improved PIC/OPC hashing. (THX: @r0ny123)
110
112
  * 2026-06-23: v4.0.2 - Improvements to Aarch64 function recovery, adressing tailcalls and gap function cornercases.
@@ -1,5 +1,5 @@
1
1
  [build-system]
2
- requires = ["setuptools>=64.0.0,<82.1.0", "wheel>=0.47.0"]
2
+ requires = ["setuptools>=64.0.0,<83.1.0", "wheel>=0.47.0"]
3
3
  build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
@@ -4,7 +4,7 @@ import os
4
4
 
5
5
  class SmdaConfig:
6
6
  # keep this in sync with smda.__version__
7
- VERSION = "4.2.0"
7
+ VERSION = "4.2.2"
8
8
  ESCAPER_DOWNWARD_COMPATIBILITY = "1.13.16"
9
9
  CONFIG_FILE_PATH = str(os.path.abspath(__file__))
10
10
  PROJECT_ROOT = str(os.path.abspath(os.sep.join([CONFIG_FILE_PATH, "..", "..", ".."])))
@@ -0,0 +1 @@
1
+ __version__ = "4.2.2"
@@ -130,6 +130,7 @@ class AArch64Backend(ArchBackend):
130
130
  @classmethod
131
131
  def _callFallthroughFunctionStart(cls, d, addr):
132
132
  if addr in d.fc_manager.getFunctionStartCandidates():
133
+ print(f"found candidate at call fallthrough 0x{addr:08x}")
133
134
  return addr
134
135
 
135
136
  cursor = addr
@@ -138,7 +139,13 @@ class AArch64Backend(ArchBackend):
138
139
  skipped_nop = True
139
140
  cursor += INSTRUCTION_SIZE
140
141
  if skipped_nop and cursor in d.fc_manager.getFunctionStartCandidates():
142
+ print(f"skipped NOPs, found candidate at 0x{cursor:08x}")
141
143
  return cursor
144
+ if skipped_nop and cursor % 16 == 0:
145
+ word = cls._wordAt(d, cursor)
146
+ if word not in (None, 0, NOP):
147
+ print(f"skipped NOPs, found non-NOP at 0x{cursor:08x}: 0x{word:08x}")
148
+ return cursor
142
149
  return None
143
150
 
144
151
  def _analyzeCondBranch(self, d, instruction, state):
@@ -275,6 +275,7 @@ class RecursiveDisassembler:
275
275
  while True:
276
276
  for i in cache:
277
277
  i_address, i_size, i_mnemonic, i_op_str = i
278
+
278
279
  i_op_str = i_op_str.strip()
279
280
  i_relative_address = i_address - self.disassembly.binary_info.base_addr
280
281
  i_bytes = self.disassembly.binary_info.binary[i_relative_address : i_relative_address + i_size]
@@ -199,20 +199,25 @@ class SmdaFunction:
199
199
  architecture = self.smda_report.architecture if self.smda_report else ""
200
200
  if architecture == "dalvik":
201
201
  return sum(1 for block in self.blocks.values() for ins in block if ins.mnemonic.startswith("invoke-"))
202
- return sum(1 for block in self.blocks.values() for ins in block if ins.mnemonic == "call")
202
+ # capstone prepends mandatory prefixes (bnd/rep/lock/...) to the mnemonic string
203
+ return sum(1 for block in self.blocks.values() for ins in block if ins.mnemonic.split(" ")[-1] == "call")
203
204
 
204
205
  @property
205
206
  def num_returns(self):
206
207
  architecture = self.smda_report.architecture if self.smda_report else ""
207
208
  if architecture == "dalvik":
208
209
  return sum(1 for block in self.blocks.values() for ins in block if ins.mnemonic.startswith("return"))
209
- return sum(1 for block in self.blocks.values() for ins in block if ins.mnemonic in ("ret", "retn"))
210
+ # capstone prepends mandatory prefixes (bnd/rep/lock/...) to the mnemonic string
211
+ return sum(
212
+ 1 for block in self.blocks.values() for ins in block if ins.mnemonic.split(" ")[-1] in ("ret", "retn")
213
+ )
210
214
 
211
215
  def isApiThunk(self):
212
216
  if self.num_instructions != 1:
213
217
  return False
214
218
  first_ins = self.blocks[self.offset][0]
215
- if first_ins.mnemonic not in ["jmp", "call"]:
219
+ # capstone prepends mandatory prefixes (bnd/rep/lock/...) to the mnemonic string
220
+ if first_ins.mnemonic.split(" ")[-1] not in ["jmp", "call"]:
216
221
  return False
217
222
  return len(self.apirefs) != 0
218
223
 
@@ -6,7 +6,7 @@ import logging
6
6
  import lief
7
7
 
8
8
  from .AbstractLabelProvider import AbstractLabelProvider
9
- from .import_parsers import parse_pe_imports, resolve_pe_base_addr
9
+ from .import_parsers import parse_pe_delay_imports, parse_pe_imports, resolve_pe_base_addr
10
10
 
11
11
  lief.logging.disable()
12
12
  LOGGER = logging.getLogger(__name__)
@@ -89,7 +89,9 @@ class PeSymbolProvider(AbstractLabelProvider):
89
89
  return function_symbols
90
90
 
91
91
  def parseImports(self, lief_binary, base_addr=None):
92
- return parse_pe_imports(lief_binary, base_addr)
92
+ import_symbols = parse_pe_imports(lief_binary, base_addr)
93
+ import_symbols.update(parse_pe_delay_imports(lief_binary, base_addr))
94
+ return import_symbols
93
95
 
94
96
  def collectSymbols(self, lief_binary, base_addr=None):
95
97
  symbols = {}
@@ -9,7 +9,7 @@ import lief
9
9
  lief.logging.disable()
10
10
 
11
11
  from .AbstractLabelProvider import AbstractLabelProvider # noqa: E402
12
- from .import_parsers import parse_pe_imports # noqa: E402
12
+ from .import_parsers import parse_pe_delay_imports, parse_pe_imports # noqa: E402
13
13
 
14
14
  LOGGER = logging.getLogger(__name__)
15
15
 
@@ -41,6 +41,7 @@ class WinApiResolver(AbstractLabelProvider):
41
41
  if not isinstance(lief_binary, lief.PE.Binary):
42
42
  return
43
43
  self._api_map["lief"] = parse_pe_imports(lief_binary, binary_info.base_addr)
44
+ self._api_map["lief"].update(parse_pe_delay_imports(lief_binary, binary_info.base_addr))
44
45
 
45
46
  def setOsName(self, os_name):
46
47
  self._os_name = os_name
@@ -34,6 +34,28 @@ def parse_pe_imports(lief_binary, base_addr=None):
34
34
  return import_symbols
35
35
 
36
36
 
37
+ def parse_pe_delay_imports(lief_binary, base_addr=None):
38
+ if not hasattr(lief_binary, "delay_imports"):
39
+ return {}
40
+ if not lief_binary.has_delay_imports:
41
+ return {}
42
+ active_base = resolve_pe_base_addr(lief_binary, base_addr)
43
+ magic = getattr(lief_binary.optional_header, "magic", None)
44
+ ptr_size = 8 if magic is not None and magic == lief.PE.PE_TYPE.PE32_PLUS else 4
45
+ import_symbols = {}
46
+ for delay_import in lief_binary.delay_imports:
47
+ lib_name_lower = (getattr(delay_import, "name", "") or "").lower()
48
+ for index, func in enumerate(delay_import.entries):
49
+ slot_addr = active_base + delay_import.iat + index * ptr_size
50
+ if func.name:
51
+ import_symbols[slot_addr] = (lib_name_lower, func.name)
52
+ elif func.is_ordinal:
53
+ resolved_ordinal = OrdinalHelper.resolveOrdinal(lib_name_lower, func.ordinal)
54
+ ordinal_name = resolved_ordinal if resolved_ordinal else f"#{func.ordinal}"
55
+ import_symbols[slot_addr] = (lib_name_lower, ordinal_name)
56
+ return import_symbols
57
+
58
+
37
59
  def parse_elf_relocation_imports(lief_binary):
38
60
  """Build import map keyed by relocation slot address: addr -> (lib, name)."""
39
61
  if not isinstance(lief_binary, lief.ELF.Binary):
@@ -0,0 +1,123 @@
1
+ import datetime
2
+ import logging
3
+
4
+ from capstone import CS_ARCH_ARM64, CS_ARCH_X86, CS_MODE_32, CS_MODE_64, CS_MODE_LITTLE_ENDIAN, Cs
5
+
6
+ from smda.DisassemblyResult import DisassemblyResult
7
+
8
+ from .IdaInterface import IdaInterface
9
+
10
+ LOGGER = logging.getLogger(__name__)
11
+
12
+
13
+ class IdaExporter:
14
+ def __init__(self, config, bitness=None):
15
+ self.config = config
16
+ self.ida_interface = IdaInterface()
17
+ self.bitness = bitness if bitness else self.ida_interface.getBitness()
18
+ self.architecture = self.ida_interface.getArchitecture()
19
+ self.capstone = None
20
+ self.disassembly = DisassemblyResult()
21
+ self.disassembly.smda_version = config.VERSION
22
+ self._initCapstone()
23
+
24
+ def _initCapstone(self):
25
+ if self.architecture == "aarch64":
26
+ self.capstone = Cs(CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN)
27
+ elif self.bitness == 64:
28
+ self.capstone = Cs(CS_ARCH_X86, CS_MODE_64)
29
+ else:
30
+ self.capstone = Cs(CS_ARCH_X86, CS_MODE_32)
31
+
32
+ @staticmethod
33
+ def _splitInstructionBytes(capstone, offset, instruction_bytes, architecture, errors=None):
34
+ """Split *instruction_bytes* (which IDA may report as N×4 bytes for an AArch64
35
+ MOV-macro head) into one ``(addr, size, mnemonic, op_str, bytes)`` tuple per
36
+ decoded 4-byte instruction.
37
+
38
+ Background
39
+ ----------
40
+ IDA's MOV-macro feature collapses a ``MOVZ+MOVK`` pair (materialising a 32-bit
41
+ constant in a Wn register across two 4-byte AArch64 instructions) into one
42
+ logical "head" whose reported size is 8 bytes. Without splitting,
43
+ :meth:`analyzeBuffer` would pass the whole 8-byte buffer to
44
+ :meth:`capstone.disasm_lite` and then take only ``cache[0]`` (the MOVZ),
45
+ silently dropping the MOVK and producing an exported report that drifted from
46
+ SMDA's own per-instruction AArch64 output.
47
+
48
+ This static helper iterates every instruction capstone decoded from the buffer
49
+ and emits one tuple per sub-instruction with correctly sliced 4-byte payloads,
50
+ so both the MOVZ (head) and the MOVK (head+4) appear as their own records.
51
+ """
52
+ cache = list(capstone.disasm_lite(instruction_bytes, offset))
53
+ out = []
54
+ consumed = 0
55
+ for i_address, i_size, i_mnemonic, i_op_str in cache:
56
+ sub_bytes = bytes(instruction_bytes[consumed : consumed + i_size])
57
+ out.append((i_address, i_size, i_mnemonic, i_op_str, sub_bytes))
58
+ consumed += i_size
59
+ while consumed < len(instruction_bytes):
60
+ chunk = 4 if architecture == "aarch64" else (len(instruction_bytes) - consumed)
61
+ chunk = min(chunk, len(instruction_bytes) - consumed)
62
+ sub_bytes = bytes(instruction_bytes[consumed : consumed + chunk])
63
+ bytes_hex = sub_bytes.hex()
64
+ LOGGER.warning("missing capstone disassembly output at 0x%x (%s)", offset + consumed, bytes_hex)
65
+ if errors is not None:
66
+ errors[offset + consumed] = {"type": "capstone disassembly failure", "instruction_bytes": bytes_hex}
67
+ out.append((offset + consumed, chunk, "error", "error", sub_bytes))
68
+ consumed += chunk
69
+ return out
70
+
71
+ def _convertIdaInsToSmda(self, offset, instruction_bytes):
72
+ return self._splitInstructionBytes(
73
+ self.capstone, offset, instruction_bytes, self.architecture, self.disassembly.errors
74
+ )
75
+
76
+ def analyzeBuffer(self, binary_info, cb_analysis_timeout=None):
77
+ """instead of performing a full analysis, simply collect all data from IDA and convert it into a report"""
78
+ self.disassembly.analysis_start_ts = datetime.datetime.now(datetime.timezone.utc)
79
+ self.disassembly.binary_info = binary_info
80
+ self.disassembly.binary_info.architecture = self.ida_interface.getArchitecture()
81
+ if not self.disassembly.binary_info.base_addr:
82
+ self.disassembly.binary_info.base_addr = self.ida_interface.getBaseAddr()
83
+ if not self.disassembly.binary_info.binary:
84
+ self.disassembly.binary_info.binary = self.ida_interface.getBinary()
85
+ if not self.disassembly.binary_info.bitness:
86
+ self.disassembly.binary_info.bitness = self.bitness
87
+ self.disassembly.function_symbols = self.ida_interface.getFunctionSymbols()
88
+ api_map = self.ida_interface.getApiMap()
89
+ for function_offset in self.ida_interface.getFunctions():
90
+ if self.ida_interface.isExternalFunction(function_offset):
91
+ continue
92
+ converted_function = []
93
+ for block in self.ida_interface.getBlocks(function_offset):
94
+ converted_block = []
95
+ for instruction_offset in block:
96
+ instruction_bytes = self.ida_interface.getInstructionBytes(instruction_offset)
97
+ smda_instructions = self._convertIdaInsToSmda(instruction_offset, instruction_bytes)
98
+ num_subs = len(smda_instructions)
99
+ for idx, smda_instruction in enumerate(smda_instructions):
100
+ converted_block.append(smda_instruction)
101
+ self.disassembly.instructions[smda_instruction[0]] = (
102
+ smda_instruction[2],
103
+ smda_instruction[1],
104
+ )
105
+ # IDA tracks code refs at the macro-head level only; for split
106
+ # macro heads, attach in-refs to the first sub-instruction (entry)
107
+ # and out-refs to the last sub-instruction (exit).
108
+ if idx == 0:
109
+ for in_ref in self.ida_interface.getCodeInRefs(instruction_offset):
110
+ self.disassembly.addCodeRefs(in_ref[0], in_ref[1])
111
+ if idx == num_subs - 1:
112
+ for out_ref in self.ida_interface.getCodeOutRefs(instruction_offset):
113
+ self.disassembly.addCodeRefs(out_ref[0], out_ref[1])
114
+ if out_ref[1] in api_map:
115
+ self.disassembly.addr_to_api[smda_instruction[0]] = api_map[out_ref[1]]
116
+ converted_function.append(converted_block)
117
+ self.disassembly.functions[function_offset] = converted_function
118
+ if self.disassembly.isRecursiveFunction(function_offset):
119
+ self.disassembly.recursive_functions.add(function_offset)
120
+ if self.disassembly.isLeafFunction(function_offset):
121
+ self.disassembly.leaf_functions.add(function_offset)
122
+ self.disassembly.analysis_end_ts = datetime.datetime.now(datetime.timezone.utc)
123
+ return self.disassembly
@@ -52,7 +52,7 @@ class IdaInterface:
52
52
  class Ida74Interface(BackendInterface):
53
53
  def __init__(self):
54
54
  self.version = "IDA Pro 7.4 - 8.4"
55
- self._processor_map = {"metapc": "intel"}
55
+ self._processor_map = {"metapc": "intel", "ARM": "aarch64"}
56
56
  self._api_map = {}
57
57
  self._import_module_name = ""
58
58
 
@@ -60,6 +60,10 @@ class Ida74Interface(BackendInterface):
60
60
  # https://reverseengineering.stackexchange.com/a/11398
61
61
  info = ida_idaapi.get_inf_structure()
62
62
  procname = info.procname if idaapi.IDA_SDK_VERSION >= 800 else info.procName
63
+ if procname == "ARM":
64
+ if self.getBitness() != 64:
65
+ raise ValueError(f"Unsupported Architecture: {procname} ({self.getBitness()}bit)")
66
+ return "aarch64"
63
67
  if procname in self._processor_map:
64
68
  return self._processor_map[procname]
65
69
  else:
@@ -177,6 +181,10 @@ class Ida73Interface(BackendInterface):
177
181
  # https://reverseengineering.stackexchange.com/a/11398
178
182
  info = idaapi.get_inf_structure()
179
183
  procname = info.procName
184
+ if procname == "ARM":
185
+ if self.getBitness() != 64:
186
+ raise ValueError(f"Unsupported Architecture: {procname} ({self.getBitness()}bit)")
187
+ return "aarch64"
180
188
  if procname in self._processor_map:
181
189
  return self._processor_map[procname]
182
190
  else:
@@ -290,6 +298,10 @@ class Ida85Interface(BackendInterface):
290
298
 
291
299
  def getArchitecture(self):
292
300
  procname = idaapi.inf_get_procname()
301
+ if procname == "ARM":
302
+ if self.getBitness() != 64:
303
+ raise ValueError(f"Unsupported Architecture: {procname} ({self.getBitness()}bit)")
304
+ return "aarch64"
293
305
  if procname in self._processor_map:
294
306
  return self._processor_map[procname]
295
307
  else:
@@ -167,14 +167,15 @@ class FunctionAnalysisState:
167
167
  # for instruction in sorted(self.instructions):
168
168
  # print("0x%08x: %s %s" % (instruction[0], instruction[2], instruction[3]))
169
169
  if as_gap and not self.is_sanely_ending:
170
- if len(self.instructions) == 1 and self.instructions[0][2] == "jmp":
170
+ # capstone prepends mandatory prefixes (bnd/rep/lock/...) to the mnemonic string
171
+ if len(self.instructions) == 1 and self.instructions[0][2].split(" ")[-1] == "jmp":
171
172
  byte = self.disassembly.getByte(self.instructions[0][0])
172
173
  if isinstance(byte, int):
173
174
  byte = chr(byte)
174
175
  if byte == "\xeb":
175
176
  return False
176
177
  # sane case, stub found that just jumps to a referenced function
177
- elif self.num_blocks_analyzed == 1 and self.instructions[-1][2] in [
178
+ elif self.num_blocks_analyzed == 1 and self.instructions[-1][2].split(" ")[-1] in [
178
179
  "jmp",
179
180
  "call",
180
181
  ]:
@@ -226,11 +227,12 @@ class FunctionAnalysisState:
226
227
  block = []
227
228
  for i in range(ins[start], len(self.instructions)):
228
229
  current = self.instructions[i]
230
+ current_mnemonic = current[2].split(" ")[-1]
229
231
  block.append(current)
230
232
  # if one code reference is to another address than the next
231
233
  if (
232
234
  current[0] in self.code_refs_from
233
- and current[2] not in self.CALL_MNEMONICS
235
+ and current_mnemonic not in self.CALL_MNEMONICS
234
236
  and i != len(self.instructions) - 1
235
237
  and any(r != self.instructions[i + 1][0] for r in self.code_refs_from[current[0]])
236
238
  ):
@@ -252,7 +254,7 @@ class FunctionAnalysisState:
252
254
  )
253
255
  ):
254
256
  break
255
- if current[2] in self.END_MNEMONICS:
257
+ if current_mnemonic in self.END_MNEMONICS:
256
258
  break
257
259
  if block:
258
260
  blocks.append(block)
@@ -8,12 +8,16 @@ from smda.common.ExceptionHandling import reraise_non_operational_exception
8
8
  from smda.utility.BracketQueue import BracketQueue
9
9
  from smda.utility.PriorityQueue import PriorityQueue
10
10
 
11
- from .definitions import DEFAULT_PROLOGUES, GAP_SEQUENCES
11
+ from .definitions import COMMON_PROLOGUES, DEFAULT_PROLOGUES, GAP_SEQUENCES
12
12
  from .FunctionCandidate import FunctionCandidate
13
13
  from .LanguageAnalyzer import LanguageAnalyzer
14
14
 
15
15
  LOGGER = logging.getLogger(__name__)
16
16
 
17
+ # bytes.lstrip() can skip long runs of one-byte padding in C instead of
18
+ # crawling them byte-by-byte in the gap scanner.
19
+ _PADDING_STRIP_BYTES = bytes(sorted(seq[0] for seq in GAP_SEQUENCES[1]))
20
+
17
21
 
18
22
  class FunctionCandidateManager:
19
23
  def __init__(self, config):
@@ -147,11 +151,31 @@ class FunctionCandidateManager:
147
151
  prev_ins = 0
148
152
  min_code = min(self.disassembly.code_map) if self.disassembly.code_map else self.getBitMask()
149
153
  max_code = max(self.disassembly.code_map) if self.disassembly.code_map else 0
150
- for code_area in self._code_areas:
154
+ # Raw memory dumps are loaded without section info, so self._code_areas is empty; fall back to
155
+ # the full mapped image so the head (before the first instruction) and tail (after the last
156
+ # instruction) still get gap-scanned. Without this, functions that lie entirely before the
157
+ # first or after the last already-discovered instruction (e.g. trailing jmp/thunk tables) are
158
+ # never reached by the gap pass.
159
+ using_synthetic_area = not self._code_areas
160
+ code_areas = self._code_areas or [
161
+ [
162
+ self.disassembly.binary_info.base_addr,
163
+ self.disassembly.binary_info.base_addr + self.disassembly.binary_info.binary_size,
164
+ ]
165
+ ]
166
+ for code_area in code_areas:
151
167
  if code_area[0] < min_code < code_area[1] and min_code != code_area[0]:
152
168
  gaps.append([code_area[0], min_code, min_code - code_area[0]])
153
169
  if code_area[0] < max_code < code_area[1] and max_code != code_area[1]:
154
- gaps.append([max_code, code_area[1], code_area[1] - max_code])
170
+ # For the synthetic full-image fallback (raw memory dumps with no section info),
171
+ # the tail gap must start AFTER the last instruction's address, mirroring the
172
+ # interior-hole branch's prev_ins + 1 below. max_code is itself in code_map, so a
173
+ # gap starting at max_code leaves the gap-pointer on a code_map address;
174
+ # getNextGap()'s strict "gap[0] > gap_pointer" test then finds no further gap,
175
+ # returns the bitmask sentinel, and the scan terminates -- abandoning the whole tail
176
+ # region unscanned. Section-derived code areas keep the legacy start (behavior-neutral).
177
+ tail_start = max_code + 1 if using_synthetic_area else max_code
178
+ gaps.append([tail_start, code_area[1], code_area[1] - tail_start])
155
179
  for ins in sorted(self.disassembly.code_map.keys()):
156
180
  if prev_ins != 0 and ins - prev_ins > 1:
157
181
  gaps.append([prev_ins + 1, ins, ins - prev_ins])
@@ -197,6 +221,13 @@ class FunctionCandidateManager:
197
221
  def isEffectiveNop(self, byte_sequence):
198
222
  return byte_sequence in GAP_SEQUENCES[len(byte_sequence)]
199
223
 
224
+ def isHotpatchPrologue(self, byte_window):
225
+ # An MSVC hotpatch stub (`mov edi, edi; push ebp; mov ebp, esp`) opens with a
226
+ # `mov edi, edi` that is byte-identical to a 2-byte effective NOP, yet it IS the
227
+ # function's true entry. Recognizing the 5-byte prologue lets callers avoid
228
+ # skipping/rounding past that leading NOP and mislocating the start two bytes late.
229
+ return byte_window in COMMON_PROLOGUES["5"].get(self.bitness, {})
230
+
200
231
  def isAlignmentSequence(self, instruction_sequence):
201
232
  is_alignment_sequence = False
202
233
  instructions_analyzed = 0
@@ -213,7 +244,7 @@ class FunctionCandidateManager:
213
244
  break
214
245
  if len(instruction_sequence) > instructions_analyzed and instruction_sequence[
215
246
  instructions_analyzed
216
- ].mnemonic in [
247
+ ].mnemonic.split(" ")[-1] in [
217
248
  "leave",
218
249
  "ret",
219
250
  "retn",
@@ -260,11 +291,15 @@ class FunctionCandidateManager:
260
291
  LOGGER.warning("could not fetch raw byte for gap pointer.")
261
292
  # try to find padding symbols and skip them
262
293
  if byte in GAP_SEQUENCES[1]:
294
+ window = get_window_slice(gap_offset, 256)
295
+ run = len(window) - len(window.lstrip(_PADDING_STRIP_BYTES))
263
296
  LOGGER.debug(
264
- "nextGapCandidate() found 0xCC / 0x00 - gap_ptr += 1: 0x%08x",
297
+ "nextGapCandidate() found %d-byte padding run - gap_ptr += %d: 0x%08x",
298
+ run,
299
+ run,
265
300
  self.gap_pointer,
266
301
  )
267
- self.gap_pointer += 1
302
+ self.gap_pointer += run if run else 1
268
303
  continue
269
304
  # try to find instructions that directly encode as NOP and skip them
270
305
  ins_buf = list(self.capstone.disasm_lite(get_window_slice(gap_offset, 15), gap_offset))
@@ -285,6 +320,11 @@ class FunctionCandidateManager:
285
320
  found_multi_byte_nop = False
286
321
  for gap_length in range(max(GAP_SEQUENCES.keys()), 1, -1):
287
322
  if get_window_slice(gap_offset, gap_length) in GAP_SEQUENCES[gap_length]:
323
+ # Do not skip a `mov edi, edi` effective NOP when it is the landing pad of
324
+ # a hotpatch stub: that byte pair is the function's true start, and skipping
325
+ # it would mislocate the function two bytes late (at the `push ebp`).
326
+ if self.isHotpatchPrologue(get_window_slice(gap_offset, 5)):
327
+ break
288
328
  LOGGER.debug(
289
329
  "nextGapCandidate() found %d byte effective nop - gap_ptr += %d: 0x%08x",
290
330
  gap_length,
@@ -2114,7 +2114,7 @@ class IntelInstructionEscaper:
2114
2114
  esc_regs = True
2115
2115
  esc_consts = True
2116
2116
  if offsets_only:
2117
- if ins.mnemonic in [
2117
+ if ins.mnemonic.split(" ")[-1] in [
2118
2118
  "call",
2119
2119
  "lcall",
2120
2120
  "jmp",
@@ -2191,11 +2191,12 @@ class IntelInstructionEscaper:
2191
2191
  @staticmethod
2192
2192
  def escapeBinary(ins, escape_intraprocedural_jumps=False, lower_addr=None, upper_addr=None):
2193
2193
  escaped_sequence = ins.bytes
2194
+ mnemonic = ins.mnemonic.split(" ")[-1]
2194
2195
  # remove segment, operand, address, repeat override prefixes
2195
- if ins.mnemonic in ["call", "lcall", "jmp", "ljmp", "loop", "loopne", "loope"]:
2196
+ if mnemonic in ["call", "lcall", "jmp", "ljmp", "loop", "loopne", "loope"]:
2196
2197
  escaped_sequence = IntelInstructionEscaper.escapeBinaryJumpCall(ins, escape_intraprocedural_jumps)
2197
2198
  return escaped_sequence
2198
- if ins.mnemonic in [
2199
+ if mnemonic in [
2199
2200
  "je",
2200
2201
  "jne",
2201
2202
  "js",
@@ -367,12 +367,19 @@ class X86Backend(ArchBackend):
367
367
  state.endBlock()
368
368
  state.setSanelyEnding(True)
369
369
  if d.fc_manager.isAlignmentSequence(instruction_sequence):
370
- next_aligned_address = previous_address + (16 - previous_address % 16)
370
+ # A hotpatch stub right after a call can look like alignment padding
371
+ # (its leading `mov edi, edi` is an effective NOP), but that byte pair
372
+ # is the next function's true entry. Seed it directly rather than the
373
+ # 16-byte-rounded address, which would land two bytes late.
374
+ if d.fc_manager.isHotpatchPrologue(d._getDisasmWindowBuffer(i_address)[:5]):
375
+ next_candidate_address = i_address
376
+ else:
377
+ next_candidate_address = previous_address + (16 - previous_address % 16)
371
378
  LOGGER.debug(
372
379
  " Adding: 0x%x as candidate.",
373
- next_aligned_address,
380
+ next_candidate_address,
374
381
  )
375
- d.fc_manager.addCandidate(next_aligned_address, is_gap=True)
382
+ d.fc_manager.addCandidate(next_candidate_address, is_gap=True)
376
383
  return True
377
384
  else:
378
385
  LOGGER.debug(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: smda
3
- Version: 4.2.0
3
+ Version: 4.2.2
4
4
  Summary: A recursive disassmbler optimized for CFG recovery from memory dumps. Based on capstone.
5
5
  Author-email: Daniel Plohmann <daniel.plohmann@mailbox.org>
6
6
  License: BSD 2-Clause
@@ -146,6 +146,8 @@ make test
146
146
 
147
147
  ## Version History
148
148
 
149
+ * 2026-07-09: v4.2.2 - Now also parsing delay import tables from Windows PEs.
150
+ * 2026-07-09: v4.2.1 - Better detection of CFG instructions with prefixes, improved accuracy of gap search. (THX: @r0ny123) IDA ARM64 export.
149
151
  * 2026-06-24: v4.2.0 - Further improvements for inter-procedural operand escaping (closer to x86_x64 and traditional PIC hashing).
150
152
  * 2026-06-23: v4.1.0 - Significantly extended Aarch64 mnemonic escapes and improved PIC/OPC hashing. (THX: @r0ny123)
151
153
  * 2026-06-23: v4.0.2 - Improvements to Aarch64 function recovery, adressing tailcalls and gap function cornercases.
@@ -927,8 +927,8 @@ class TestAArch64StaticFixture(unittest.TestCase):
927
927
  self.assertEqual(self.report.bitness, 64)
928
928
  self.assertEqual(self.report.base_addr, 0x400000)
929
929
  self.assertEqual(self.report.oep, 0x534)
930
- self.assertEqual(len(self.report.xcfg), 277)
931
- self.assertEqual(sum(1 for f in self.report.getFunctions() for _ in f.getInstructions()), 19882)
930
+ self.assertEqual(len(self.report.xcfg), 278)
931
+ self.assertEqual(sum(1 for f in self.report.getFunctions() for _ in f.getInstructions()), 19881)
932
932
  self.assertEqual(sum(1 for f in self.report.getFunctions() for _ in f.getBlocks()), 3525)
933
933
  self.assertIsNotNone(self.report.getFunction(0x400534))
934
934
 
@@ -979,7 +979,7 @@ class TestAArch64StaticFixture(unittest.TestCase):
979
979
  self.assertEqual(roundtrip.architecture, "aarch64")
980
980
  self.assertEqual(roundtrip.bitness, 64)
981
981
  self.assertEqual(roundtrip.oep, 0x534)
982
- self.assertEqual(len(roundtrip.xcfg), 277)
982
+ self.assertEqual(len(roundtrip.xcfg), 278)
983
983
 
984
984
 
985
985
  class TestAArch64Analyzers(unittest.TestCase):
@@ -1273,5 +1273,72 @@ class TestAArch64Analyzers(unittest.TestCase):
1273
1273
  self.assertEqual(targets[:3], [0x411120, 0x411140, 0x4110E0])
1274
1274
 
1275
1275
 
1276
+ class TestAArch64MovMacroSplit(unittest.TestCase):
1277
+ """IDA's MOV-macro feature collapses MOVZ+MOVK pairs (materialising a 32-bit
1278
+ constant in a Wn register across two 4-byte AArch64 instructions) into one
1279
+ logical "head" whose reported size is 8 bytes. Without splitting,
1280
+ IdaExporter._convertIdaInsToSmda would feed all 8 bytes to capstone and
1281
+ take only ``cache[0]`` (the MOVZ), silently dropping the MOVK and producing
1282
+ an exported report that drifted from SMDA's own per-instruction AArch64 output.
1283
+
1284
+ These tests exercise IdaExporter._splitInstructionBytes directly (no IDA
1285
+ mocking required), verifying that an 8-byte macro head from IDA is correctly
1286
+ unpacked into two 4-byte sub-instructions.
1287
+ """
1288
+
1289
+ def _split(self, offset, instruction_bytes):
1290
+ from capstone import CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN, Cs
1291
+
1292
+ from smda.ida.IdaExporter import IdaExporter
1293
+
1294
+ capstone = Cs(CS_ARCH_ARM64, CS_MODE_LITTLE_ENDIAN)
1295
+ errors = {}
1296
+ return (
1297
+ IdaExporter._splitInstructionBytes(capstone, offset, instruction_bytes, "aarch64", errors),
1298
+ errors,
1299
+ )
1300
+
1301
+ def test_movz_movk_macro_splits_into_two_4_byte_instructions(self):
1302
+ # The IDA export example:
1303
+ # 03 20 80 52 MOVZ W3, #0x100 -> capstone: 'mov' w3, #0x100
1304
+ # 03 00 A1 72 MOVK W3, #0x800, LSL #16 -> capstone: 'movk' w3, #0x800, lsl #16
1305
+ # originally shown by IDA as one 8-byte "MOV W3, #0x8000100" macro at 0x100004838.
1306
+ macro_bytes = bytes.fromhex("032080520300A172")
1307
+ insns, errors = self._split(0x100004838, macro_bytes)
1308
+
1309
+ self.assertEqual(errors, {})
1310
+ self.assertEqual(len(insns), 2)
1311
+
1312
+ addr, size, mnem, ops, raw = insns[0]
1313
+ self.assertEqual(addr, 0x100004838)
1314
+ self.assertEqual(size, 4)
1315
+ self.assertEqual(mnem, "mov")
1316
+ self.assertEqual(ops, "w3, #0x100")
1317
+ self.assertEqual(raw, bytes.fromhex("03208052"))
1318
+
1319
+ addr, size, mnem, ops, raw = insns[1]
1320
+ self.assertEqual(addr, 0x10000483C)
1321
+ self.assertEqual(size, 4)
1322
+ self.assertEqual(mnem, "movk")
1323
+ self.assertEqual(ops, "w3, #0x800, lsl #16")
1324
+ self.assertEqual(raw, bytes.fromhex("0300A172"))
1325
+
1326
+ def test_single_4_byte_instruction_still_returns_one_tuple(self):
1327
+ # Non-macro AArch64 instructions return exactly 4 bytes from
1328
+ # IdaInterface.getInstructionBytes; the split helper must preserve
1329
+ # the single-instruction output shape (a one-element list).
1330
+ single = bytes.fromhex("200080D2") # mov x0, #1
1331
+ insns, errors = self._split(0x401000, single)
1332
+
1333
+ self.assertEqual(errors, {})
1334
+ self.assertEqual(len(insns), 1)
1335
+ addr, size, mnem, ops, raw = insns[0]
1336
+ self.assertEqual(addr, 0x401000)
1337
+ self.assertEqual(size, 4)
1338
+ self.assertEqual(mnem, "mov")
1339
+ self.assertEqual(ops, "x0, #1")
1340
+ self.assertEqual(raw, single)
1341
+
1342
+
1276
1343
  if __name__ == "__main__":
1277
1344
  unittest.main()