Source code for peat.modules.rockwell.clx_relay_ladder_parser

"""
ControlLogix ladder logic parsing libraries.

Authors

- Craig Buchanan
- Christopher Goes
- Greg Walkup
"""

from collections.abc import Callable
from typing import Final

from peat import log
from peat.protocols.data_packing import *

LayoutType = list[tuple[int, str]]


[docs] def decompile_ladder_process_logic( process_logic: list[int], symbol_list: LayoutType, template_tags: dict, starting_address: int = 0, ) -> str: """ Transforms device bytecode into a string representing the ladder logic. Args: process_logic: the bytecode representation of the relay ladder routine symbol_list: template_tags: starting_address: the starting address of the relay ladder routine in the device Returns: String representing the source code of a relay ladder routine from the respective bytecode format from the device """ log.trace3( f"decompile_ladder_process_logic: len={len(process_logic)}, " f"starting_address={starting_address}" ) process_logic = instruction_buffer_to_instruction_list(process_logic) out = decompile_process_logic_segment(process_logic, starting_address) out_resolved = _resolve_all_token_names("".join(out), symbol_list, template_tags) return out_resolved
[docs] def disassemble_ladder_process_logic(process_logic: list[int], starting_address: int = 0) -> str: """ Disassembles a buffer (list of bytes) representing the process logic. """ log.trace3( f"disassemble_ladder_process_logic: len={len(process_logic)}, " f"starting_address={starting_address}" ) # TODO: disassemble_ladder_process_logic was previously commented out, # it is non-functional as-is. # instruction_byte_list = zip(*(iter(process_logic),) * 4) # # breakpoint() # # do 4 ints need to be combined? or have they already been unpacked? # instruction_list = [ # unpack_dint("".join(instruction)) for instruction in instruction_byte_list # ] instruction_list = process_logic out = [] if instruction_list: out.append("Relay Ladder") for instruction in instruction_list: out.append( f"[0x{starting_address:0>8x}] " f"{instruction & 0xFFFFFFFF:0>8x} " + disassemble_instruction(instruction) ) starting_address += 0x4 return "\n".join(out) else: log.debug( f"No instruction list for ladder_process logic starting " f"address: {starting_address}\nbytes: {process_logic}" ) return ""
def _get_line_prefix(address: int, instruction: int, indent_level: int = 0) -> str: """ Returns a standard line prefix based on an address and an instruction. If both the address and instruction are 0, prints spaces that evenly space out the columns instead. """ out = [] if address == 0x00 and instruction == 0x00: out.append(" " * 24) else: out.append(f"[0x{address:0>8x}] {instruction & 0xFFFFFFFF:0>8x} ") out.append(" " * indent_level) return "".join(out) def _decompile_OPERAND( instruction_list: list[int], starting_address: int, indent_level: int, operand: int ) -> tuple[str, int]: """ Decompiles an OPERAND instruction. This represents an argument to a operation. Args: instruction_list: A list of instructions, starting with the instruction to be decompiled starting_address: The address that this instruction starts at indent_level: The current level of indentation Returns: Tuple containing an array of strings that represents this instruction and the address at which the instruction and its sub-instructions end. """ opcode = (instruction_list[0] & 0xFF000000) >> 24 # operand_index = opcode & 0x07 # Second hex digit is the operand number operand_index = opcode & 0x0F # If second byte is "d8", it's an intermediate result if (operand & 0x00FF0000) == 0x00D80000: argument = operand & 0x0000FFFF decompiled = "{}[{}] := INTERMED RESULT [0x{:0>4x}]".format( LOGIC_OPCODE[opcode]["name"], str(operand_index), argument ) return decompiled, starting_address else: if operand & 0x00800000: operand &= 0x007FFFFF operand += MODULE_SEGMENT_ADDRESS decompiled = "{}[{}] := [@0x{:0>8x}@]".format( LOGIC_OPCODE[opcode]["name"], str(operand_index), operand ) return decompiled, starting_address def _decompile_CONST( instruction_list: list[int], starting_address: int, indent_level: int, operand: int ) -> tuple[str, int]: """ Decompiles a CONST instruction. This holds an immediate value in its lowest two bytes. Args: instruction_list: A list of instructions, starting with the instruction to be decompiled starting_address: The address that this instruction starts at indent_level: The current level of indentation operand: The lower three bytes of the instruction, as an integer Returns: Tuple containing an array of strings that represents this instruction and the address at which the instruction ends. """ opcode = (instruction_list[0] & 0xFF000000) >> 24 val = operand & 0x0000FFFF operand_index = (operand & 0x00FE0000) >> 17 # Don"t think this is right for constants # if (operand & 0x00800000): # operand = operand & 0x007FFFFF # val = operand + MODULE_SEGMENT_ADDRESS return ( "{}[{}] := [0x{:0>8x}]".format(LOGIC_OPCODE[opcode]["name"], operand_index, val), starting_address, ) def _decompile_LOADMEM( instruction_list: list[int], starting_address: int, indent_level: int, operand: int ) -> tuple[str, int]: """ Decompiles a LOAD MEM instruction. Args: instruction_list: A list of instructions, starting with the instruction to be decompiled starting_address: The address that this instruction starts at indent_level: The current level of indentation operand: The lower three bytes of the instruction, as an integer Returns: Tuple containing an array of strings that represents this instruction and the address at which the instruction and its sub-instructions end. """ opcode = (instruction_list[0] & 0xFF000000) >> 24 argument = operand >> 3 return ( "{} := 0x{:0>8x}".format(LOGIC_OPCODE[opcode]["name"], argument), starting_address, ) # Still unsure exactly what these do - they are grouped as "STR" in original code def _decompile_STR( instruction_list: list[int], starting_address: int, indent_level: int, operand: int ) -> tuple[str, int]: """ Decompiles a "STR" instruction. Not exactly sure what these are supposed to represent. Args: instruction_list: A list of instructions, starting with the instruction to be decompiled starting_address: The address that this instruction starts at indent_level: The current level of indentation operand: The lower three bytes of the instruction, as an integer Returns: Tuple containing an array of strings that represents this instruction and the address at which the instruction and its sub-instructions end. """ opcode = (instruction_list[0] & 0xFF000000) >> 24 # Special case - PUSH ARG is best guess for what this does; it postfixes # operation arguments and appears to allow operations to take a variable number # of arguments if opcode == 0xAC and operand == 0xFD1000: return "PUSH ARG", starting_address # Parse intermediate results elif (operand & 0x00E00000) == 0x00800000: # If third hex digit is 8 or 9 operand_index = (operand & 0x001E0000) >> 17 argument = operand & 0x0000FFFF return ( "{}[{}] := INTERMED RESULT [0x{:0>4x}]".format( LOGIC_OPCODE[opcode]["name"], str(operand_index), argument ), starting_address, ) else: argument = operand >> 3 return ( "{} := [0x{:0>8x}]".format(LOGIC_OPCODE[opcode]["name"], argument), starting_address, ) def _decompile_default( instruction_list: list[int], starting_address: int, indent_level: int, operand: int ) -> tuple: """ Default decompiliation function. Just returns the stored name of the opcode. Args: instruction_list: A list of instructions, starting with the instruction to be decompiled starting_address: The address that this instruction starts at indent_level: The current level of indentation operand: The lower three bytes of the instruction, as an integer Returns: Tuple containing an array of strings that represents this instruction and the address at which the instruction and its sub-instructions end. """ opcode = (instruction_list[0] & 0xFF000000) >> 24 # TODO: not sure if this function is working correctly, should just return name... return LOGIC_OPCODE[opcode]["dec"](opcode, operand), starting_address def _decompile_TIMER( instruction_list: list[int], starting_address: int, indent_level: int, operand: int ) -> tuple[str, int]: """ Decompiles a timer-related instruction. Does so by just printing out the instruction name. Args: instruction_list: A list of instructions, starting with the instruction to be decompiled starting_address: The address that this instruction starts at indent_level: The current level of indentation operand: The lower three bytes of the instruction, as an integer Returns: Tuple containing an array of strings that represents this instruction and the address at which the instruction and its sub-instructions end. """ opcode = (instruction_list[0] & 0xFF000000) >> 24 return LOGIC_OPCODE[opcode]["name"], starting_address def _decompile_PTR( instruction_list: list[int], starting_address: int, indent_level: int, operand: int, resolve_operand: bool = True, ) -> tuple[str, int]: """ Decompiles a PTR instruction. This represents a pointer to somewhere in memory. The pointer is right-shifted by two bits. Args: instruction_list: A list of instructions, starting with the instruction to be decompiled starting_address: The address that this instruction starts at indent_level: The current level of indentation operand: The lower three bytes of the instruction, as an integer resolve_operand: Boolean representing whether or not to perform name resolution on the operand of this instruction Returns: Tuple containing an array of strings that represents this instruction and the address at which the instruction and its sub-instructions end. """ opcode = (instruction_list[0] & 0xFF000000) >> 24 operand_index = opcode & 0x0F argument = operand << 2 if resolve_operand: return ( "{}[{}] := [@0x{:0>8x}@]".format( LOGIC_OPCODE[opcode]["name"], str(operand_index), argument ), starting_address, ) else: return ( "{}[{}] := [0x{:0>8x}]".format( LOGIC_OPCODE[opcode]["name"], str(operand_index), argument ), starting_address, ) def _decompile_BIT_OP( instruction_list: list[int], starting_address: int, indent_level: int, operand: int ) -> tuple[str, int]: """ Decompiles instructions that operate on individual boolean variables. Ex: XIC, OTE Args: instruction_list: A list of instructions, starting with the instruction to be decompiled starting_address: The address that this instruction starts at indent_level: The current level of indentation operand: The lower three bytes of the instruction, as an integer Returns: Tuple containing an array of strings that represents this instruction and the address at which the instruction and its sub-instructions end. """ opcode = (instruction_list[0] & 0xFF000000) >> 24 operand_bit_index = opcode & 0x07 argument_value = operand & 0x007FFFFF argument_segment = (operand & 0x00800000) >> 23 # Still don"t really know the purpose of MODULE_SEGMENT_ADDRESS if argument_segment == 1: argument_value += MODULE_SEGMENT_ADDRESS argument = f"@0x{argument_value & 0xFFFFFFFF:0>8x}@" return ( "{}({})[{}]".format(LOGIC_OPCODE[opcode]["name"], argument, str(operand_bit_index)), starting_address, ) def _decompile_RUNGEND( instruction_list: list[int], starting_address: int, indent_level: int, operand: int ) -> tuple[str, int]: """ Decompiles a rung ending instruction. Args: instruction_list: A list of instructions, starting with the instruction to be decompiled starting_address: The address that this instruction starts at indent_level: The current level of indentation operand: The lower three bytes of the instruction, as an integer Returns: Tuple containing an array of strings that represents this instruction and the address at which the instruction and its sub-instructions end. """ return "RUNG END", starting_address def _decompile_BST( instruction_list: list[int], starting_address: int, indent_level: int, operand: int ) -> tuple[list[str], int]: """ Decompiles a branch start instruction and branch sub-instructions. Includes the sub-instructions that are a part of the starting branch. Args: instruction_list: A list of instructions, starting with the instruction to be decompiled and containing all of its (potential) sub-instructions starting_address: The address that this instruction starts at indent_level: The current level of indentation operand: The lower three bytes of the instruction, as an integer Returns: Tuple containing an array of strings that represents this instruction and the address at which the instruction and its sub-instructions end. """ out = ["BRANCH START ["] branch_length = len(instruction_list) # Loop through instructions until we find a next branch or branch end instruction for idx, instruction in enumerate(instruction_list[1:]): next_opcode = (instruction & 0xFF000000) >> 24 if next_opcode in LOGIC_OPCODE and LOGIC_OPCODE[next_opcode]["name"] in [ "NXB", "BND", ]: branch_length = idx + 1 break # Decompile the branch as one indent level higher branch_out = decompile_process_logic_segment( instruction_list[1:branch_length], starting_address + 4, indent_level + 1 ) out.append("".join(branch_out)) # Offset the address by however many instructions were in the branch return out, starting_address + (4 * (branch_length - 1)) def _decompile_NXB( instruction_list: list[int], starting_address: int, indent_level: int, operand: int ) -> tuple[list[str], int]: """ Decompiles a "next branch" instruction and the branches instructions. Inclues the instructions that make up the branch it represents. Args: instruction_list: A list of instructions, starting with the instruction to be decompiled and containing all of its (potential) sub-instructions starting_address: The address that this instruction starts at indent_level: The current level of indentation operand: The lower three bytes of the instruction, as an integer Returns: Tuple containing an array of strings that represents this instruction and the address at which the instruction and its sub-instructions end. """ out = ["] NEXT BRANCH ["] branch_length = len(instruction_list) # Loop through instructions until we find a next branch or branch end instruction for idx, instruction in enumerate(instruction_list[1:]): next_opcode = (instruction & 0xFF000000) >> 24 # Use dict.get() to handle unknown opcodes if LOGIC_OPCODE.get(next_opcode, {}).get("name") in ["NXB", "BND"]: branch_length = idx + 1 break # Decompile the branch as one indent level higher branch_out = decompile_process_logic_segment( instruction_list[1:branch_length], starting_address + 4, indent_level + 1 ) out.append("".join(branch_out)) # Offset the address by however many instructions were in the branch return out, starting_address + (4 * (branch_length - 1)) def _decompile_BND( instruction_list: list[int], starting_address: int, indent_level: int, operand: int ) -> tuple: """ Decompiles a branch end instruction. Args: instruction_list: A list of instructions, starting with the instruction to be decompiled starting_address: The address that this instruction starts at indent_level: The current level of indentation operand: The lower three bytes of the instruction, as an integer Returns: Tuple containing an array of strings that represents this instruction and the address at which the instruction ends. """ return "] BRANCH END", starting_address def _decompile_RUNG( instruction_list: list[int], starting_address: int, indent_level: int, operand: int ) -> tuple[list[str], int]: """ Decompiles a ladder-logic rung. Args: instruction_list: A list of instructions, starting with the instruction to be decompiled and containing all of its (potential) sub-instructions. starting_address: The address that this instruction starts at indent_level: The current level of indentation operand: The lower three bytes of the instruction, as an integer Returns: Tuple containing an array of strings that represents this instruction and the address at which the instruction and its sub-instructions end. """ out = ["RUNG:"] current_address = starting_address rungend_address = (operand << 2) - 4 rungend_idx = (rungend_address - current_address) // 4 rung_instruction_list = instruction_list[2:rungend_idx] block_out = decompile_process_logic_segment( rung_instruction_list, current_address + 8, indent_level + 1 ) out.append("".join(block_out)) rungend_instruction = instruction_list[rungend_idx : rungend_idx + 1] rungend_out = decompile_process_logic_segment( rungend_instruction, rungend_address, indent_level ) out.append("".join(rungend_out)) return out, rungend_address def _decompile_BLOCK( instruction_list: list[int], starting_address: int, indent_level: int, operand: int ) -> tuple[list[str], int]: """ Decompile an instruction block. Args: instruction_list: A list of instructions, starting with the instruction to be decompiled and containing all of its (potential) sub-instructions starting_address: The address that this instruction starts at indent_level: The current level of indentation operand: The lower three bytes of the instruction, as an integer Returns: Tuple containing an array of strings that represents this instruction and the address at which the instruction and its sub-instructions end. """ out = ["INSTRUCTION BLOCK:"] current_address = starting_address next_instructions = instruction_list[1:3] if ( (len(next_instructions) == 2) and (next_instructions[0] & 0xFFFFFFFF == 0xACFD1FFC) and (next_instructions[1] & 0xFFFFFFFF == 0x179F1000) ): blockend_address = operand - 8 blockstart_idx = 3 inblock_address = current_address + 12 next_instruction_address = blockend_address + 4 else: blockend_address = operand - 4 blockstart_idx = 1 inblock_address = current_address + 4 next_instruction_address = blockend_address blockend_idx = 1 + ((blockend_address - current_address) // 4) block_instruction_list = instruction_list[blockstart_idx:blockend_idx] block_out = decompile_process_logic_segment( block_instruction_list, inblock_address, indent_level + 1 ) out.append("".join(block_out)) out.append("\n" + _get_line_prefix(0x00, 0x00, indent_level) + "END INSTRUCTION BLOCK") return out, next_instruction_address def _decompile_SPC_block_op_default( instruction_list: list[int], starting_address: int, indent_level: int, spc_instruction: int, ) -> tuple[str, int]: """ Decompiles normal SPC operations. These contain sub-instructions with their parameters. Args: instruction_list: A list of instructions, starting with the SPC instruction to be decompiled and containing all of its (potential) sub-instructions. starting_address: The address that this instruction starts at indent_level: The current level of indentation Returns: Tuple containing an array of strings that represents this instruction and the address at which the instruction and its sub-instructions end. """ out = [] current_address = starting_address next_instructions = instruction_list[0:2] arg_length, arg_offset = _get_spc_arg_length2(next_instructions) out.append(OPERATION_STRUCT[spc_instruction]["name"]) # Calculate which of the following instructions are the parameters # The "header" and "footer" of the operation are currently ignored, as they appear # to be mostly deterministic for each operation argsstart_idx = 1 + arg_offset + OPERATION_STRUCT[spc_instruction]["header_len"] argsstart_address = current_address + (argsstart_idx * 4) argsend_idx = argsstart_idx + arg_length instruction_args = instruction_list[argsstart_idx:argsend_idx] # Output the operation's args, enclosed by square brackets out.append("[") out.append( decompile_process_logic_segment(instruction_args, argsstart_address, indent_level + 1) ) out.append("\n" + _get_line_prefix(0x00, 0x00, indent_level) + "]") spc_instruction_length = argsend_idx + OPERATION_STRUCT[spc_instruction]["footer_len"] - 1 current_address += spc_instruction_length * 4 return "".join(out), current_address def _decompile_SPC_block_op_CPT( instruction_list: list[int], starting_address: int, indent_level: int, spc_instruction: int, ) -> tuple[str, int]: """ Special decompiliation routine for operations with sub-operations. Special routine for operations that are composed exclusively of sub-operations (e.g. CPT, CMP). Args: instruction_list: A list of instructions, starting with the SPC instruction to be decompiled and containing all of its (potential) sub-instructions. starting_address: The address that this instruction starts at indent_level: The current level of indentation spc_instruction: Returns: Tuple containing an array of strings that represents this instruction and the address at which the instruction and its sub-instructions end. """ out = [] block_out, current_address = _decompile_SPC_block_op_default( instruction_list, starting_address, indent_level, spc_instruction ) out.append("".join(block_out)) blockstart_idx = ((current_address - starting_address) // 4) + 1 # Last segment of the footer contains the end address for the operation blockend_address = 0x00FFFFFF & instruction_list[blockstart_idx - 1] blockend_idx = (blockend_address - starting_address) // 4 block_size = blockend_idx - blockstart_idx # Last two segments are another footer, ignore them block_instruction_list = instruction_list[blockstart_idx : blockend_idx - 2] block_out = decompile_process_logic_segment( block_instruction_list, current_address + 4, indent_level + 1 ) out.append("".join(block_out)) current_address += (block_size + 1) * 4 return "".join(out), current_address def _decompile_SPC_block_op( instruction_list: list[int], starting_address: int, indent_level: int, operand: int ) -> tuple[str, int]: """ Decompiles a SPC block operation. Currently, this can be done two different ways depending on the operation. """ spc_instruction = operand & 0x0000FFFF if spc_instruction in OPERATION_STRUCT: # These are the four instructions identified so far that use this # format. It might be nice to generalize this a bit? if ( OPERATION_STRUCT[spc_instruction]["name"] == "CPT" or OPERATION_STRUCT[spc_instruction]["name"] == "CMP" or OPERATION_STRUCT[spc_instruction]["name"] == "FAL" or OPERATION_STRUCT[spc_instruction]["name"] == "FSC" ): return _decompile_SPC_block_op_CPT( instruction_list, starting_address, indent_level, spc_instruction ) else: return _decompile_SPC_block_op_default( instruction_list, starting_address, indent_level, spc_instruction ) return "???", starting_address def _decompile_SPC_array_def( instruction_list: list[int], starting_address: int, indent_level: int, operand: int ) -> tuple[str, int]: """ Decompiles static array definitions. An array definition appears to include the data type size, size of dimensions 2, 1, and 0, and the number of nonzero dimensions. Currently, these arguments are not given labels. """ return "ARRAY DEFINITION:", starting_address def _decompile_SPC_function_ptr( instruction_list: list[int], starting_address: int, indent_level: int, operand: int ) -> tuple[str, int]: """ Decompiles a function pointer indicator. This also disables name resolution on the argument it modifies. """ out = [] current_address = starting_address next_instruction = instruction_list[1] next_opcode = (next_instruction & 0xFF000000) >> 24 out.append("FUNCTION PTR:") # If next operand is pointer, print it out, but without name resolution if LOGIC_OPCODE[next_opcode]["name"] == "PTR": current_address += 4 out.append("\n" + _get_line_prefix(current_address, next_instruction, indent_level)) out.append( _decompile_PTR( instruction_list[1:], current_address, indent_level, next_instruction & 0x00FFFFFF, resolve_operand=False, )[0] ) return "".join(out), current_address def _decompile_SPC_structure_type( instruction_list: list[int], starting_address: int, indent_level: int, operand: int ) -> tuple[str, int]: """ Decompiles a structure type indicator. """ structure_type = operand & 0x0000FFFF if structure_type in STRUCTURE_TYPES: return STRUCTURE_TYPES[structure_type] + ":", starting_address return "UNKNOWN TYPE:", starting_address def _decompile_SPC_default( instruction_list: list[int], starting_address: int, indent_level: int, operand: int ) -> tuple[str, int]: """ Prints out the given name of the SPC extended opcode. """ extended_opcode = (operand & 0xFFFF0000) >> 16 if extended_opcode in EXTENDED_OPCODE: return EXTENDED_OPCODE[extended_opcode]["name"], starting_address return "???", starting_address def _decompile_SPC( instruction_list: list[int], starting_address: int, indent_level: int, operand: int ) -> tuple[list[str], int]: """ Decompiles the SPC instruction. This can can mean a lot of different things, but which mainly deals with multi-instruction operations. Args: instruction_list: A list of instructions, starting with the SPC instruction to be decompiled and containing all of its (potential) sub-instructions. starting_address: The address that this instruction starts at indent_level: The current level of indentation Returns: Tuple containing an array of strings that represents this instruction and the address at which the instruction and its sub-instructions end. """ out = [] # Choose which sub-function to call based on the extended opcode extended_opcode = (operand & 0xFFFF0000) >> 16 if extended_opcode in EXTENDED_OPCODE: spc_dec = EXTENDED_OPCODE[extended_opcode]["dec"] # Select function to call else: spc_dec = _decompile_SPC_default spc_out, current_address = spc_dec(instruction_list, starting_address, indent_level, operand) out.append(spc_out) return out, current_address def _get_spc_arg_length2(next_instructions: list) -> tuple[int, int]: """ Gets the argument list length of an operation. The argument length is either the fourth-highest hex digit, or contained in a separate block length instruction immediately following the operation if the argument list is longer than 15 elements. Args: next_instructions: The next instructions to evaluate (starting with the operation instruction itself). Returns: Tuple containing the argument length and how much to offset the argument length by. """ arg_count = (next_instructions[0] & 0x000F0000) >> 16 arg_offset = 0 if len(next_instructions) == 2: next_opcode = (next_instructions[1] & 0xFFFF0000) >> 16 # If the next instruction is a block length instruction, # use its operand as the length and offset the argument list by 1 if (next_opcode in EXTENDED_OPCODE) and ( EXTENDED_OPCODE[next_opcode]["name"] == "BLOCK LENGTH" ): arg_count += next_instructions[1] & 0x0000FFFF arg_offset = 1 return arg_count, arg_offset MODULE_SEGMENT_ADDRESS: Final[int] = 0x0C000000 # Type identifiers, used in certain SPC instructions STRUCTURE_TYPES: Final[dict[int, str]] = { 0x00C2: "ARRAY (SINT)", 0x00C4: "ARRAY (DINT)", 0x00CA: "ARRAY (REAL)", 0x0FFA: "ALMA STRUCT", 0x0FFB: "ALMD STRUCT", } # Operation codes # These appear as the lower two bytes of certain SPC instructions, and denote # multi-instruction operations OPERATION_STRUCT: Final[dict[int, dict[str, str | int]]] = { 0x8000: { "name": "ADD (DINT)", "header_len": 0, "footer_len": 2, }, 0x8001: { "name": "SUB (DINT)", "header_len": 0, "footer_len": 2, }, 0x8002: { "name": "MUL (DINT)", "header_len": 3, "footer_len": 3, }, 0x8003: { "name": "DIV (DINT)", "header_len": 3, "footer_len": 3, }, 0x8004: { "name": "SQR (DINT)", "header_len": 3, "footer_len": 3, }, 0x8005: { "name": "NEG (DINT)", "header_len": 0, "footer_len": 3, }, 0x8006: { "name": "CLR (DINT)", "header_len": 0, "footer_len": 2, }, 0x8007: { "name": "MOV (DINT)", "header_len": 0, "footer_len": 2, }, 0x8008: { "name": "ADD (REAL)", "header_len": 3, "footer_len": 3, }, 0x8009: { "name": "SUB (REAL)", "header_len": 3, "footer_len": 3, }, 0x800A: { "name": "MUL (REAL)", "header_len": 3, "footer_len": 3, }, 0x800B: { "name": "DIV (REAL)", "header_len": 3, "footer_len": 3, }, 0x800C: { "name": "SQR (REAL)", "header_len": 3, "footer_len": 3, }, 0x800D: { "name": "NEG (REAL)", "header_len": 0, "footer_len": 10, }, 0x800E: { "name": "CLR (REAL)", "header_len": 0, "footer_len": 2, }, 0x800F: { "name": "MOV (REAL)", "header_len": 0, "footer_len": 8, }, 0x8012: { "name": "ONS", "header_len": 3, "footer_len": 3, }, 0x8013: { "name": "OSR", "header_len": 3, "footer_len": 3, }, 0x8014: { "name": "OSF", "header_len": 3, "footer_len": 3, }, 0x8016: { "name": "BTD", "header_len": 3, "footer_len": 3, }, 0x8017: { "name": "FAL", "header_len": 5, "footer_len": 6, }, 0x8018: { "name": "FSC", "header_len": 5, "footer_len": 6, }, 0x801B: { "name": "EQU", "header_len": 0, "footer_len": 1, }, 0x801C: { "name": "NEQ", "header_len": 0, "footer_len": 1, }, 0x801D: { "name": "LES", "header_len": 0, "footer_len": 1, }, 0x801E: { "name": "LEQ", "header_len": 0, "footer_len": 1, }, 0x801F: { "name": "GRT", "header_len": 0, "footer_len": 1, }, 0x8020: { "name": "GEQ", "header_len": 0, "footer_len": 1, }, 0x8021: { "name": "LIM", "header_len": 0, "footer_len": 8, }, 0x8022: { "name": "MEQ", "header_len": 0, "footer_len": 3, }, 0x8023: { "name": "EQU (REAL)", "header_len": 0, "footer_len": 1, }, 0x8024: { "name": "NEQ (REAL)", "header_len": 0, "footer_len": 1, }, 0x8025: { "name": "LES (REAL)", "header_len": 0, "footer_len": 5, }, 0x8026: { "name": "LEQ (REAL)", "header_len": 0, "footer_len": 5, }, 0x8027: { "name": "GRT (REAL)", "header_len": 0, "footer_len": 5, }, 0x8028: { "name": "GEQ (REAL)", "header_len": 0, "footer_len": 5, }, 0x8029: { "name": "LIM (REAL)", "header_len": 3, "footer_len": 3, }, 0x802A: { "name": "AND", "header_len": 0, "footer_len": 2, }, 0x802B: { "name": "OR", "header_len": 0, "footer_len": 2, }, 0x802C: { "name": "XOR", "header_len": 0, "footer_len": 2, }, 0x802D: { "name": "NOT", "header_len": 0, "footer_len": 3, }, 0x802E: { "name": "MVM", "header_len": 3, "footer_len": 3, }, 0x8031: { "name": "JMP", "header_len": 1, "footer_len": 2, }, 0x8032: { "name": "LBL", "header_len": 0, "footer_len": 0, }, 0x8033: { "name": "FLL", "header_len": 3, "footer_len": 3, }, 0x8036: { "name": "COP", "header_len": 3, "footer_len": 3, }, 0x803C: { "name": "FOR", "header_len": 5, "footer_len": 5, }, 0x803E: { "name": "REAL->SINT", "header_len": 3, "footer_len": 3, }, 0x803F: { "name": "REAL->INT", "header_len": 3, "footer_len": 3, }, 0x8040: { "name": "REAL->DINT", "header_len": 3, "footer_len": 3, }, 0x8041: { "name": "SINT->DINT", "header_len": 2, "footer_len": 4, }, 0x8042: { "name": "DINT->SINT", "header_len": 4, "footer_len": 3, }, 0x8045: { "name": "INT->DINT", "header_len": 2, "footer_len": 7, }, 0x8050: { "name": "BSL", "header_len": 3, "footer_len": 3, }, 0x8051: { "name": "BSR", "header_len": 3, "footer_len": 3, }, 0x8052: { "name": "FFL (DINT)", "header_len": 3, "footer_len": 3, }, 0x8053: { "name": "LFL (DINT)", "header_len": 3, "footer_len": 3, }, 0x8054: { "name": "FFU (DINT)", "header_len": 3, "footer_len": 3, }, 0x8055: { "name": "LFU (DINT)", "header_len": 3, "footer_len": 3, }, 0x8058: { "name": "CMP", "header_len": 4, "footer_len": 5, }, 0x8059: { "name": "CPT", "header_len": 4, "footer_len": 5, }, 0x805C: { "name": "SINT->REAL", "header_len": 2, "footer_len": 16, # I have no idea why this is so long }, 0x805E: { "name": "DINT->REAL", "header_len": 4, "footer_len": 3, }, 0x807A: { "name": "JSR", "header_len": 4, "footer_len": 4, }, 0x807B: { "name": "SBR", "header_len": 3, "footer_len": 3, }, 0x807C: { "name": "RET", "header_len": 3, "footer_len": 3, }, 0x807D: { "name": "SQI", "header_len": 3, "footer_len": 3, }, 0x807E: { "name": "SQO", "header_len": 3, "footer_len": 3, }, 0x807F: { "name": "SQL", "header_len": 3, "footer_len": 3, }, 0x8080: { "name": "AVE (DINT)", "header_len": 4, "footer_len": 4, }, 0x8081: { "name": "SRT (DINT)", "header_len": 4, "footer_len": 4, }, 0x8082: { "name": "AVE (REAL)", "header_len": 4, "footer_len": 4, }, 0x8083: { "name": "STD (DINT)", "header_len": 4, "footer_len": 4, }, 0x8084: { "name": "STD (REAL)", "header_len": 4, "footer_len": 4, }, 0x808C: { "name": "AVE (SINT)", "header_len": 4, "footer_len": 4, }, 0x808D: { "name": "AVE (INT)", "header_len": 4, "footer_len": 4, }, 0x808E: { "name": "STD (SINT)", "header_len": 4, "footer_len": 4, }, 0x808F: { "name": "STD (INT)", "header_len": 4, "footer_len": 4, }, 0x8092: { "name": "SRT (SINT)", "header_len": 4, "footer_len": 4, }, 0x8093: { "name": "SRT (INT)", "header_len": 4, "footer_len": 4, }, 0x8094: { "name": "SRT (REAL)", "header_len": 4, "footer_len": 4, }, 0x8106: { "name": "FFL (SINT)", "header_len": 3, "footer_len": 3, }, # 0x8107-0x8109 are likely SINT variants on FIFO/LIFO buffers 0x80BF: { "name": "ACCESS IDX (SINT)", "header_len": 2, "footer_len": 8, }, 0x80C1: { "name": "ACCESS IDX (DINT)", "header_len": 2, "footer_len": 8, }, 0x80CE: { "name": "ABS (DINT)", "header_len": 0, "footer_len": 4, }, 0x80CF: { "name": "ABS (REAL)", "header_len": 0, "footer_len": 10, }, 0x80D0: { "name": "MOD (DINT)", "header_len": 3, "footer_len": 3, }, 0x80D1: { "name": "MOD (REAL)", "header_len": 3, "footer_len": 3, }, 0x8138: { "name": "CPS", "header_len": 3, "footer_len": 3, }, 0x8139: { "name": "SIZE", "header_len": 3, "footer_len": 3, }, 0x813C: { "name": "SWPB", "header_len": 3, "footer_len": 3, }, 0x8157: { "name": "COP", "header_len": 3, "footer_len": 3, }, 0x8158: { "name": "ALM", "header_len": 3, "footer_len": 1, }, 0x8159: { "name": "ALM END", "header_len": 0, "footer_len": 3, }, 0x8166: { "name": "ALM INPUT", "header_len": 0, "footer_len": 1, }, 0x8167: { "name": "ALM ARG", "header_len": 0, "footer_len": 7, }, 0x8175: { "name": "ALMD STRUC", "header_len": 1, "footer_len": 1, }, 0x8180: { "name": "ALMA STRUC", "header_len": 0, "footer_len": 1, }, 0x8186: { "name": "CPS", "header_len": 3, "footer_len": 3, }, } # "Opcodes" that are in the second byte of the 0x00 "SPC" instruction # These usually modify the meaning of the instructions following them EXTENDED_OPCODE: Final[dict[int, dict[str, str | Callable]]] = { 0x10: {"name": "???", "dec": _decompile_SPC_default}, # Gives the type of next operand 0x20: {"name": "STRUCTURE TYPE", "dec": _decompile_SPC_structure_type}, 0x21: {"name": "???", "dec": _decompile_SPC_default}, # Represents sub-operations? 0x30: {"name": "SUB BLOCK OPERATION", "dec": _decompile_SPC_block_op}, 0x31: {"name": "SUB BLOCK OPERATION", "dec": _decompile_SPC_block_op}, 0x32: {"name": "SUB BLOCK OPERATION", "dec": _decompile_SPC_block_op}, 0x33: {"name": "SUB BLOCK OPERATION", "dec": _decompile_SPC_block_op}, 0x34: {"name": "SUB BLOCK OPERATION", "dec": _decompile_SPC_block_op}, 0x35: {"name": "SUB BLOCK OPERATION", "dec": _decompile_SPC_block_op}, 0x36: {"name": "SUB BLOCK OPERATION", "dec": _decompile_SPC_block_op}, 0x37: {"name": "SUB BLOCK OPERATION", "dec": _decompile_SPC_block_op}, 0x38: {"name": "SUB BLOCK OPERATION", "dec": _decompile_SPC_block_op}, 0x39: {"name": "SUB BLOCK OPERATION", "dec": _decompile_SPC_block_op}, 0x3A: {"name": "SUB BLOCK OPERATION", "dec": _decompile_SPC_block_op}, 0x3B: {"name": "SUB BLOCK OPERATION", "dec": _decompile_SPC_block_op}, 0x3C: {"name": "SUB BLOCK OPERATION", "dec": _decompile_SPC_block_op}, 0x3D: {"name": "SUB BLOCK OPERATION", "dec": _decompile_SPC_block_op}, 0x3E: {"name": "SUB BLOCK OPERATION", "dec": _decompile_SPC_block_op}, 0x3F: {"name": "SUB BLOCK OPERATION", "dec": _decompile_SPC_block_op}, # Normal top-level operations 0x40: {"name": "BLOCK OPERATION", "dec": _decompile_SPC_block_op}, 0x41: {"name": "BLOCK OPERATION", "dec": _decompile_SPC_block_op}, 0x42: {"name": "BLOCK OPERATION", "dec": _decompile_SPC_block_op}, 0x43: {"name": "BLOCK OPERATION", "dec": _decompile_SPC_block_op}, 0x44: {"name": "BLOCK OPERATION", "dec": _decompile_SPC_block_op}, 0x45: {"name": "BLOCK OPERATION", "dec": _decompile_SPC_block_op}, 0x46: {"name": "BLOCK OPERATION", "dec": _decompile_SPC_block_op}, 0x47: {"name": "BLOCK OPERATION", "dec": _decompile_SPC_block_op}, 0x48: {"name": "BLOCK OPERATION", "dec": _decompile_SPC_block_op}, 0x49: {"name": "BLOCK OPERATION", "dec": _decompile_SPC_block_op}, 0x4A: {"name": "BLOCK OPERATION", "dec": _decompile_SPC_block_op}, 0x4B: {"name": "BLOCK OPERATION", "dec": _decompile_SPC_block_op}, 0x4C: {"name": "BLOCK OPERATION", "dec": _decompile_SPC_block_op}, 0x4D: {"name": "BLOCK OPERATION", "dec": _decompile_SPC_block_op}, 0x4E: {"name": "BLOCK OPERATION", "dec": _decompile_SPC_block_op}, 0x4F: {"name": "BLOCK OPERATION", "dec": _decompile_SPC_block_op}, 0x50: {"name": "NOP", "dec": _decompile_SPC_default}, # Next operand is a function pointer 0x60: {"name": "FUNCTION PTR", "dec": _decompile_SPC_function_ptr}, 0x70: {"name": "BLOCK LENGTH", "dec": _decompile_SPC_default}, 0x80: {"name": "???", "dec": _decompile_SPC_default}, 0x90: {"name": "???", "dec": _decompile_SPC_default}, 0xA0: {"name": "ARRAY DEFINITION", "dec": _decompile_SPC_array_def}, 0xB0: {"name": "???", "dec": _decompile_SPC_default}, } LOGIC_OPCODE: Final[dict[int, dict[str, str | Callable]]] = { # Represents a "special" operation 0x00: { "name": "SPC", "dec": _decompile_SPC, }, 0x17: { "name": "STR", "dec": _decompile_STR, }, 0x1A: { "name": "STR", "dec": _decompile_STR, }, 0x1B: { "name": "STR", "dec": _decompile_STR, }, # Instruction block start 0x34: { "name": "BLOCK", "dec": _decompile_BLOCK, }, 0x3F: { "name": "LOAD MEM", "dec": _decompile_LOADMEM, }, # Examine if closed 0x40: { "name": "XIC", "dec": _decompile_BIT_OP, }, 0x41: { "name": "XIC", "dec": _decompile_BIT_OP, }, 0x42: { "name": "XIC", "dec": _decompile_BIT_OP, }, 0x43: { "name": "XIC", "dec": _decompile_BIT_OP, }, 0x44: { "name": "XIC", "dec": _decompile_BIT_OP, }, 0x45: { "name": "XIC", "dec": _decompile_BIT_OP, }, 0x46: { "name": "XIC", "dec": _decompile_BIT_OP, }, 0x47: { "name": "XIC", "dec": _decompile_BIT_OP, }, # Examine if open 0x48: { "name": "XIO", "dec": _decompile_BIT_OP, }, 0x49: { "name": "XIO", "dec": _decompile_BIT_OP, }, 0x4A: { "name": "XIO", "dec": _decompile_BIT_OP, }, 0x4B: { "name": "XIO", "dec": _decompile_BIT_OP, }, 0x4C: { "name": "XIO", "dec": _decompile_BIT_OP, }, 0x4D: { "name": "XIO", "dec": _decompile_BIT_OP, }, 0x4E: { "name": "XIO", "dec": _decompile_BIT_OP, }, 0x4F: { "name": "XIO", "dec": _decompile_BIT_OP, }, # Output latch 0x50: { "name": "OTL", "dec": _decompile_BIT_OP, }, 0x51: { "name": "OTL", "dec": _decompile_BIT_OP, }, 0x52: { "name": "OTL", "dec": _decompile_BIT_OP, }, 0x53: { "name": "OTL", "dec": _decompile_BIT_OP, }, 0x54: { "name": "OTL", "dec": _decompile_BIT_OP, }, 0x55: { "name": "OTL", "dec": _decompile_BIT_OP, }, 0x56: { "name": "OTL", "dec": _decompile_BIT_OP, }, 0x57: { "name": "OTL", "dec": _decompile_BIT_OP, }, # Output unlatch 0x58: { "name": "OTU", "dec": _decompile_BIT_OP, }, 0x59: { "name": "OTU", "dec": _decompile_BIT_OP, }, 0x5A: { "name": "OTU", "dec": _decompile_BIT_OP, }, 0x5B: { "name": "OTU", "dec": _decompile_BIT_OP, }, 0x5C: { "name": "OTU", "dec": _decompile_BIT_OP, }, 0x5D: { "name": "OTU", "dec": _decompile_BIT_OP, }, 0x5E: { "name": "OTU", "dec": _decompile_BIT_OP, }, 0x5F: { "name": "OTU", "dec": _decompile_BIT_OP, }, # These are all 8 long # Output energize 0x60: { "name": "OTE", "dec": _decompile_BIT_OP, }, 0x61: { "name": "OTE", "dec": _decompile_BIT_OP, }, 0x62: { "name": "OTE", "dec": _decompile_BIT_OP, }, 0x63: { "name": "OTE", "dec": _decompile_BIT_OP, }, 0x64: { "name": "OTE", "dec": _decompile_BIT_OP, }, 0x65: { "name": "OTE", "dec": _decompile_BIT_OP, }, 0x66: { "name": "OTE", "dec": _decompile_BIT_OP, }, 0x67: { "name": "OTE", "dec": _decompile_BIT_OP, }, # Timer/counter opcodes 0x68: {"name": "TON", "dec": _decompile_TIMER}, 0x69: {"name": "TOF", "dec": _decompile_TIMER}, 0x6A: {"name": "RTO", "dec": _decompile_TIMER}, 0x6B: {"name": "RES", "dec": _decompile_TIMER}, 0x6C: {"name": "CTU", "dec": _decompile_TIMER}, 0x6D: {"name": "CTD", "dec": _decompile_TIMER}, # Branch start 0x70: { "name": "BST", "dec": _decompile_BST, }, # Next branch (separates branches that branch at the same spot) 0x71: { "name": "NXB", "dec": _decompile_NXB, }, # 0x72 is something else to do with branches? # Branch end 0x73: { "name": "BND", "dec": _decompile_BND, }, # Denotes beginning of the rung 0x78: { "name": "RUNGSTART", "dec": _decompile_RUNG, }, # Rung ending 0x7A: { "name": "RUNGEND", "dec": _decompile_RUNGEND, }, # SINT arguments (this is maybe incorrect) 0x80: { "name": "ARG (SINT)", "dec": _decompile_OPERAND, }, 0x81: { "name": "ARG (SINT)", "dec": _decompile_OPERAND, }, 0x82: { "name": "ARG (SINT)", "dec": _decompile_OPERAND, }, 0x83: { "name": "ARG (SINT)", "dec": _decompile_OPERAND, }, 0x84: { "name": "ARG (SINT)", "dec": _decompile_OPERAND, }, 0x85: { "name": "ARG (SINT)", "dec": _decompile_OPERAND, }, 0x86: { "name": "ARG (SINT)", "dec": _decompile_OPERAND, }, 0x87: { "name": "ARG (SINT)", "dec": _decompile_OPERAND, }, # Constants 0x8D: { "name": "CONST", "dec": _decompile_CONST, }, # TODO: Find better names for these two - the term "WORD" can be confusing 0x8E: { "name": "CONST (LOW WORD)", "dec": _decompile_CONST, }, 0x8F: { "name": "CONST (HIGH WORD)", "dec": _decompile_CONST, }, # Represents a pointer to someplace in memory 0x90: { "name": "PTR", "dec": _decompile_PTR, }, 0x91: { "name": "PTR", "dec": _decompile_PTR, }, 0x92: { "name": "PTR", "dec": _decompile_PTR, }, 0x93: { "name": "PTR", "dec": _decompile_PTR, }, 0x94: { "name": "PTR", "dec": _decompile_PTR, }, 0x95: { "name": "PTR", "dec": _decompile_PTR, }, 0x96: { "name": "PTR", "dec": _decompile_PTR, }, 0x97: { "name": "PTR", "dec": _decompile_PTR, }, 0x98: { "name": "PTR", "dec": _decompile_PTR, }, 0x99: { "name": "PTR", "dec": _decompile_PTR, }, 0x9A: { "name": "PTR", "dec": _decompile_PTR, }, 0x9B: { "name": "PTR", "dec": _decompile_PTR, }, 0x9C: { "name": "PTR", "dec": _decompile_PTR, }, 0x9D: { "name": "PTR", "dec": _decompile_PTR, }, 0x9E: { "name": "PTR", "dec": _decompile_PTR, }, 0x9F: { "name": "PTR", "dec": _decompile_PTR, }, 0xAC: { "name": "STR", "dec": _decompile_STR, }, # Function arguments (TODO: figure out if these also use 0xc8-0xcf) 0xC0: { "name": "ARG (DINT)", "dec": _decompile_OPERAND, }, 0xC1: { "name": "ARG (DINT)", "dec": _decompile_OPERAND, }, 0xC2: { "name": "ARG (DINT)", "dec": _decompile_OPERAND, }, 0xC3: { "name": "ARG (DINT)", "dec": _decompile_OPERAND, }, 0xC4: { "name": "ARG (DINT)", "dec": _decompile_OPERAND, }, 0xC5: { "name": "ARG (DINT)", "dec": _decompile_OPERAND, }, 0xC6: { "name": "ARG (DINT)", "dec": _decompile_OPERAND, }, 0xC7: { "name": "ARG (DINT)", "dec": _decompile_OPERAND, }, 0xE0: { "name": "?", "dec": _decompile_default, }, }
[docs] def instruction_buffer_to_instruction_list(instruction_buffer: list) -> list: """ Converts an instruction buffer to an instruction list. Args: instruction_buffer: instruction buffer as little-endian formatted list of bytes Returns: Instruction list as properly formatted list of 32-bit integers """ instruction_byte_list = list(zip(*(iter(instruction_buffer),) * 4, strict=False)) instruction_list = [ unpack_dint(bytes(instruction)) & 0xFFFFFFFF for instruction in instruction_byte_list ] return instruction_list
def _resolve_token_name(token_address: int, symbol_list: list[tuple], template_data: dict) -> str: """ Returns the name of the token at the specified memory address. If name cannot be resolved, returns the memory address of the token. Args: token_address: the memory address of the token symbol_list: a list of tuples: (address, symbol) template_data: a data structure representing all template data Returns: The name of the token """ # TODO: only works with exact offset matches. doesn't work with multibyte types yet symbol_list.append((0, None)) symbol = max(symbol for symbol in symbol_list if symbol[0] <= token_address)[1] if symbol is None: return f"0x{token_address:0>8x}" token_offset = token_address - symbol[3] if token_offset == 0: # Don"t print the offset if it's zero token_name = symbol[1] else: token_name = f"{symbol[1]}+0x{token_offset & 0xFFFFFFFF:0>8x}" symbol_type = symbol[2] token_type = symbol_type & 0x0FFF # token_type_dimension = (symbol_type & 0x6000) >> 13 is_structure_type = ((symbol_type & 0x8000) >> 15) == 1 if is_structure_type and (token_type in template_data): token_template = template_data[token_type] if token_offset in token_template["Structure"]: field_name = token_template["Structure"][token_offset]["Name"] token_name = f"{symbol[1]}.{field_name}" return token_name def _resolve_all_token_names(data: str, symbol_list: LayoutType, template_data: dict) -> str: """ Returns the original string with all token names resolved. Args data: a string with unresolved token names symbol_list: Returns: The original string with all token names resolved """ data_split = data.split("@") # odd indices are tokens to process for token_idx in range(1, len(data_split), 2): # process every token token = int(data_split[token_idx], 0) token_name = _resolve_token_name(token, symbol_list, template_data) data_split[token_idx] = token_name return "".join(data_split)
[docs] def decompile_process_logic_segment( instruction_list: list[int], starting_address: int, indent_level: int = 0 ) -> str: """ Decompiles a segment of process logic. Decompiles a segment of process logic by calling each instruction's decompile method. Note that each decompile call may consume more than one instruction and may recursively call this function. Args: instruction_list: The list of instructions to decompile. It is implicitly assumed that if any operation is in the list, the list also contains all of the instructions required by its decompiliation function. starting_address: The address of the first instruction in the list indent_level: The indentation level to use Returns: A string representing the decompiliation of the given instructions """ out = [] current_address = starting_address ending_address = starting_address + (len(instruction_list) * 4) while current_address < ending_address: instruction_start_idx = (current_address - starting_address) // 4 instruction = instruction_list[instruction_start_idx] # Write the address and instruction hex out before each instruction out.append("\n" + _get_line_prefix(current_address, instruction, indent_level)) opcode = (instruction & 0xFF000000) >> 24 operand = instruction & 0x00FFFFFF # Run the instruction's decompile function, if it has one, and set current # address to the returned ending address of the instruction if opcode in LOGIC_OPCODE: block_out, current_address = LOGIC_OPCODE[opcode]["dec"]( instruction_list[instruction_start_idx:], current_address, indent_level, operand, ) out.append("".join(block_out)) # Advance to instruction after the end of the last one current_address += 4 return "".join(out)
# NOTE(cegoes): According to one of Greg's commits, these 2 functions aren't really doing anything. # However, removing them caused issues for him, since I think they're being called # from PEAT. However, the result isn"t being used for anything. Just leave the alone for now. # NOTE(cegoes, 4/19/2018): this included disassemble_process_logic I think
[docs] def disassemble_instruction(instruction: int) -> str: """ Obtains the human-readable instruction from the bytecode instruction. Args: instruction: uint byte-code instruction Returns: String representing human-readable disassembled instruction """ opcode = (instruction & 0xFF000000) >> 24 operand = instruction & 0x00FFFFFF disassembled_instruction = "?" if opcode in LOGIC_OPCODE: opcode_name = LOGIC_OPCODE[opcode]["name"] # noqa: F841 disassemble_func = LOGIC_OPCODE[opcode]["dec"] disassembled_instruction = disassemble_func(opcode, operand) return disassembled_instruction