Secure your code as it's written. Use Snyk Code to scan source code in minutes - no build needed - and fix issues immediately.
def call(self, i: Instruction, targets=None) -> bool:
# Keep track of the state before the call.
targets = self._calculate_targets(i, targets)
saved_state, saved_state_change = copy(self.state), copy(self.state_change)
possible_states = set()
for _, target in targets:
if target is None or self.rom.is_ram(target):
# If we can't reliably derive the address of the subroutine
# being called, we're left in an unknown state.
return self._unknown_subroutine_state(
i, unknown_reason=UnknownReason.INDIRECT_JUMP
)
# Run a parallel instance of the CPU to execute
# the subroutine that is being called.
cpu = self.copy(new_subroutine=True)
call_size = 2 if i.operation in (Op.JSR, Op.RTS) else 3
cpu.stack.push(i, i.pc, call_size)
cpu.stack_trace.append(self.subroutine_pc)
cpu.subroutine_pc = target
cpu.pc = target
# Emulate the called subroutine.
self.log.add_reference(i, target)
self.log.add_subroutine(target, stack_trace=cpu.stack_trace)
cpu.run()
def _propagate_subroutine_state(
self, call_pc: int, subroutine_pc: int
) -> Tuple[bool, UnknownReason]:
known = True
unknown_reason = UnknownReason.KNOWN
# If the user defined a state assertion for the current instruction.
if call_pc in self.log.instruction_assertions:
return (known, unknown_reason) # Execution can proceed.
# If the subroutine can return in more than one distinct state, or its
# state is unknown, we can't reliably propagate the state to the caller.
subroutine = self.log.subroutines[subroutine_pc]
return_states = subroutine.simplify_return_states(self.state)
# Multiple known return states.
if len([s for s in return_states if not s.unknown]) > 1:
known = False
unknown_reason = UnknownReason.MULTIPLE_RETURN_STATES
self.log.add_subroutine_state(
self.subroutine_pc, call_pc, StateChange(unknown_reason=unknown_reason)
def execute(self, instruction: Instruction) -> bool:
self.pc += instruction.size
# See if we can learn something about the *required*
# state of the CPU based on the current instruction.
self._derive_state_inference(instruction)
if instruction.is_return:
return self.ret(instruction)
elif instruction.is_interrupt:
self._unknown_subroutine_state(
instruction, unknown_reason=UnknownReason.SUSPECT_INSTRUCTION
)
return False
elif instruction.is_jump:
self.jump(instruction)
return False
elif instruction.is_call:
return self.call(instruction)
elif instruction.is_branch:
self.branch(instruction)
elif instruction.is_sep_rep:
self.sep_rep(instruction)
elif instruction.does_change_stack:
self.change_stack(instruction)
elif instruction.does_change_a:
self.change_a(instruction)
elif instruction.is_pop:
known = True
unknown_reason = UnknownReason.KNOWN
# If the user defined a state assertion for the current instruction.
if call_pc in self.log.instruction_assertions:
return (known, unknown_reason) # Execution can proceed.
# If the subroutine can return in more than one distinct state, or its
# state is unknown, we can't reliably propagate the state to the caller.
subroutine = self.log.subroutines[subroutine_pc]
return_states = subroutine.simplify_return_states(self.state)
# Multiple known return states.
if len([s for s in return_states if not s.unknown]) > 1:
known = False
unknown_reason = UnknownReason.MULTIPLE_RETURN_STATES
self.log.add_subroutine_state(
self.subroutine_pc, call_pc, StateChange(unknown_reason=unknown_reason)
)
# Unknown state with some reason.
elif any(s.unknown for s in return_states):
known = False
unknown_state = [s for s in return_states if s.unknown][0]
unknown_reason = unknown_state.unknown_reason
self.log.add_subroutine_state(self.subroutine_pc, call_pc, unknown_state)
# Unique return state, apply it.
if known:
assert len(return_states) == 1
self._apply_state_change(return_states.pop())
return (known, unknown_reason)
def simplify_return_states(self, state: State) -> List[StateChange]:
if len(self.state_changes) == 0:
self.is_recursive = True
return [StateChange(unknown_reason=UnknownReason.RECURSION)]
# Simplify the state changes based on the caller state.
changes = set()
for change in self.state_changes.values():
changes.add(change.simplify(state))
return list(changes)
def unknown(self):
return self.unknown_reason != UnknownReason.KNOWN
def unified_state_suggestion():
unified = i.subroutine.unified_state_change
if unified is not None:
return [("subroutine", unified)]
elif i.subroutine.does_save_state_in_incipit:
return [("subroutine", StateChange())]
return []
if not i.state_change_after.unknown:
return []
if i.has_asserted_state_change or i.asserted_subroutine_state_change:
return []
reason = i.state_change_after.unknown_reason
if i.is_call and reason == UnknownReason.INDIRECT_JUMP:
return [*jump_table_suggestion(), ("instruction", StateChange())]
elif i.is_jump and reason == UnknownReason.INDIRECT_JUMP:
if i.subroutine.does_save_state_in_incipit:
return [*jump_table_suggestion(), ("subroutine", StateChange())]
else:
return [*jump_table_suggestion(), *unified_state_suggestion()]
elif i.is_return and reason == UnknownReason.STACK_MANIPULATION:
return unified_state_suggestion()
elif i.is_return and reason == UnknownReason.INDIRECT_JUMP:
if i.ret_indirect_type == RetIndirectType.CALL:
return [*jump_table_suggestion(), ("instruction", StateChange())]
elif i.ret_indirect_type == RetIndirectType.JUMP:
return [*jump_table_suggestion(), *unified_state_suggestion()]
def pop(self, i: Instruction) -> bool:
if i.operation == Op.PLP:
entry = self.stack.pop_one()
if entry.instruction and entry.instruction.operation == Op.PHP:
self.state, self.state_change = entry.data
# We can't trust the disassembly if we don't know
# which state the PLP instruction is restoring.
else:
return self._unknown_subroutine_state(
i,
unknown_reason=UnknownReason.STACK_MANIPULATION,
stack_manipulator=entry.instruction,
)
elif i.operation in (Op.PLX, Op.PLY):
self.stack.pop(self.state.x_size)
elif i.operation == Op.PLB:
self.stack.pop_one()
elif i.operation == Op.PLD:
self.stack.pop(2)
else:
assert False
return True
def change_sort(t: Tuple[int, StateChange]) -> int:
return {
UnknownReason.STACK_MANIPULATION: 1,
UnknownReason.RECURSION: 2,
}.get(t[1].unknown_reason, 0)
def _unknown_subroutine_state(
self,
instruction: Instruction,
unknown_reason: Optional[UnknownReason] = None,
stack_manipulator: Optional[Instruction] = None,
) -> bool:
# Check if the user defined a state assertion for the current instruction.
if instruction.pc in self.log.instruction_assertions:
return True # Execution can proceed.
# No custom assertion, we need to stop here.
unknown_reason = unknown_reason or UnknownReason.UNKNOWN
self.state_change = StateChange(unknown_reason=unknown_reason)
self.log.add_subroutine_state(
self.subroutine_pc, instruction.pc, copy(self.state_change)
)
# If the unknown state is due to stack manipulation:
if unknown_reason == UnknownReason.STACK_MANIPULATION:
# If we know which instruction performed the
# manipulation, we flag it.
if stack_manipulator:
self.subroutine.has_stack_manipulation = True
stack_manipulator.stack_manipulation = (
StackManipulation.CAUSES_UNKNOWN_STATE
)
return False