Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ address. The one thing it does not do is *find* code -- see
Both are exact, not approximate. See **Verification** below for what that means
and how to reproduce it.

For semantic instruction comparisons, `pydasm.ghidra.render_body(ins, address,
preserve_operand_order=True)` retains decoded destination/source order. This
correctly displays register-to-segment `MOV` (for example `8E C0` is `MOV ES, AX`)
while retaining segment prefixes and implied shift counts. The default remains
compatible with historical Ghidra exports.

## Install

No dependencies beyond the standard library. Python 3.11 or newer (the decoder
Expand Down
21 changes: 13 additions & 8 deletions src/pydasm/ghidra.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ def _render_operand(instruction: Instruction, operand: OperandType, address: int
raise DecodeError(f"cannot render operand kind {kind!r}")


def _effective_operands(instruction: Instruction) -> list[OperandType]:
def _effective_operands(instruction: Instruction, *, preserve_operand_order: bool = False) -> list[OperandType]:
"""The operands as Ghidra orders and completes them.

Four adjustments to what the decode table resolved:
Expand All @@ -392,9 +392,9 @@ def _effective_operands(instruction: Instruction) -> list[OperandType]:
if instruction.mnemonic is Mnemonic.NOP:
return []

if instruction.mnemonic in _XCHG_MNEMONICS:
if not preserve_operand_order and instruction.mnemonic in _XCHG_MNEMONICS:
operands.reverse()
elif opcode == 0x8E and len(operands) == 2 and not operands[1].is_memory():
elif not preserve_operand_order and opcode == 0x8E and len(operands) == 2 and not operands[1].is_memory():
# `MOV Sreg, r16` prints register-first, `MOV Sreg, m16` does not: Ghidra
# keeps the memory operand last in both directions of the move, so only
# the register-to-register form reads "backwards".
Expand All @@ -409,12 +409,12 @@ def _effective_operands(instruction: Instruction) -> list[OperandType]:
return operands


def render_operands(instruction: Instruction, address: int) -> str:
def render_operands(instruction: Instruction, address: int, *, preserve_operand_order: bool = False) -> str:
"""The operand column for one instruction, without the leading separator."""
if instruction.mnemonic in _STRING_OPS:
return _string_operands(instruction)

operands = _effective_operands(instruction)
operands = _effective_operands(instruction, preserve_operand_order=preserve_operand_order)
if not operands:
return ""

Expand All @@ -430,8 +430,13 @@ def render_bytes(instruction: Instruction) -> str:
return " ".join(f"{byte:02X}" for byte in instruction.instruction_bytes)


def render_body(instruction: Instruction, address: int) -> str:
"""The listing body: padded mnemonic plus operands, with no address."""
def render_body(instruction: Instruction, address: int, *, preserve_operand_order: bool = False) -> str:
"""The listing body, optionally retaining decoded destination/source order.

The default reproduces the historical exporter, including its reversed
register-to-segment MOV operands. Opt in for semantic comparisons while
keeping segment prefixes, implied shift counts and address rendering.
"""
if not instruction.is_valid or not instruction.is_complete:
return ""
# The mnemonic is padded to the column and the operands follow immediately,
Expand All @@ -441,7 +446,7 @@ def render_body(instruction: Instruction, address: int) -> str:
# A REP-suffixed mnemonic (`MOVSB.REP`, nine characters) fills the field
# exactly, so the operands need the separator back or they run into it.
mnemonic = ghidra_mnemonic(instruction)
operands = render_operands(instruction, address)
operands = render_operands(instruction, address, preserve_operand_order=preserve_operand_order)
if operands and len(mnemonic) >= MNEMONIC_COLUMN:
return f"{mnemonic} {operands}"
return f"{mnemonic:<{MNEMONIC_COLUMN}}{operands}"
Expand Down
31 changes: 31 additions & 0 deletions tests/test_decoded_operand_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Semantic comparisons can opt out of exporter operand reordering."""
import pytest
from pydasm import decode_one
from pydasm.ghidra import render_body


@pytest.mark.parametrize("encoding,expected", [
("8ec0", "MOV ES, AX"),
("8ed0", "MOV SS, AX"),
("8eda", "MOV DS, DX"),
("8cc0", "MOV AX, ES"),
("8cd0", "MOV AX, SS"),
("8cda", "MOV DX, DS"),
("8e1e3412", "MOV DS, word ptr [0x1234]"),
("8c1e3412", "MOV word ptr [0x1234], DS"),
("91", "XCHG CX, AX"),
("368a45ff", "MOV AL, byte ptr SS:[DI + -0x1]"),
("262e8b07", "MOV AX, word ptr CS:[BX]"),
("d1e0", "SHL AX, 0x1"),
("f3a4", "MOVSB.REP ES:DI, SI"),
("2eac", "LODSB CS:SI"),
])
def test_decoded_order_retains_prefix_and_implied_operands(encoding, expected):
ins = decode_one(bytes.fromhex(encoding))
assert render_body(ins, 0, preserve_operand_order=True) == expected


def test_default_exporter_compatibility_is_unchanged():
ins = decode_one(bytes.fromhex("8ec0"))
assert render_body(ins, 0) == "MOV AX, ES"
assert render_body(ins, 0, preserve_operand_order=False) == render_body(ins, 0)
Loading