492 lines
18 KiB
Python
492 lines
18 KiB
Python
import copy
|
|
import logging
|
|
|
|
from ScEpTIC import tools
|
|
from ScEpTIC.AST import builtins
|
|
from ScEpTIC.AST import register_allocation
|
|
from ScEpTIC.AST.builtins.linker import BuiltinLinker
|
|
from ScEpTIC.AST.elements.instruction import Instruction
|
|
from ScEpTIC.AST.elements.instructions.termination_instructions import ReturnOperation
|
|
from ScEpTIC.AST.elements.types import Type
|
|
from ScEpTIC.AST.elements.value import Value
|
|
from ScEpTIC.AST.transformations import apply_transformation
|
|
from ScEpTIC.emulator import register_file
|
|
from ScEpTIC.emulator.custom_metrics_manager import CustomMetricsManager
|
|
from ScEpTIC.emulator.memory import Memory
|
|
from ScEpTIC.emulator.intermittent_executor import profiling
|
|
from ScEpTIC.emulator.io.interrupts_manager import InterruptsManager
|
|
from ScEpTIC.emulator.timers.timers_manager import TimersManager
|
|
from ScEpTIC.emulator.memory.virtual_memory import VirtualMemory
|
|
from ScEpTIC.emulator.memory.virtual_memory_cell import VirtualMemoryCell
|
|
from ScEpTIC.emulator.register_file.program_counter import ProgramCounter
|
|
from ScEpTIC.emulator.stats import ScEpTICStats
|
|
from ScEpTIC.exceptions import ConfigurationException, InitializationException, MemoryException, RuntimeException, StopAnomalyFoundException, StopException
|
|
from ScEpTIC.AST.transformations.base.instructions.simulate_clock_cycles import SimulateClockCycles
|
|
from ScEpTIC.emulator.memory.physical.physical_memory_management_unit import PhysicalMemoryManagementUnit
|
|
from ScEpTIC.emulator.memory.physical.physical_memory_array import PhysicalMemoryArray
|
|
|
|
class VMState:
|
|
"""
|
|
State of the ScEpTIC execution environment
|
|
"""
|
|
|
|
def _deprecated_config(self):
|
|
self.execution_depth = self.config.deprecated.execution_depth
|
|
self.stop_on_first_anomaly = self.config.deprecated.stop_on_first_anomaly
|
|
|
|
# Used for anomaly checks (data, input); auto-configured by InterruptionManager
|
|
self.do_data_anomaly_check = False
|
|
self.input_lookup_enabled = False
|
|
self.collect_memory_trace = False
|
|
|
|
# Used for anomaly check using memory traces
|
|
self.memory_trace = {}
|
|
self.memory_trace_prefixes = []
|
|
|
|
self.anomalies = []
|
|
self.profiling = {}
|
|
self.observations = {}
|
|
self.energy_metrics = {}
|
|
|
|
if self.memory.memory_positions['stack'] == 'non_volatile':
|
|
self.memory_trace_prefixes.append(self.memory.address_prefixes['stack'])
|
|
|
|
if self.memory.memory_positions['heap'] == 'non_volatile':
|
|
self.memory_trace_prefixes.append(self.memory.address_prefixes['heap'])
|
|
|
|
if 'non_volatile' in self.memory.memory_positions['gst']:
|
|
self.memory_trace_prefixes.append(self.memory.address_prefixes['non_volatile_gst'])
|
|
|
|
|
|
def __init__(self, vm, config):
|
|
logging.debug('Initializing VMState.')
|
|
self.initialized = False
|
|
self.vm = vm
|
|
self.config = config
|
|
|
|
# Initialize Register File and program counter
|
|
ProgramCounter._vmstate = self
|
|
self.register_file = register_file.create_register_file(self.config.register_file)
|
|
self.register_file._vmstate = self
|
|
|
|
PhysicalMemoryManagementUnit._vmstate = self
|
|
PhysicalMemoryArray._vmstate = self
|
|
|
|
# Initialize Memory
|
|
VirtualMemory._vmstate = self
|
|
VirtualMemoryCell._vmstate = self
|
|
self.memory = Memory(self.config.memory)
|
|
Type.address_dimension = self.memory.address_dimension
|
|
|
|
# used by regiser allocation for saving registers, so to not interfere with the stack content (and thus with anomalies)
|
|
self.reg_saving_pool = []
|
|
|
|
# used to differentiate events
|
|
self.global_clock = 0
|
|
self.state_save_clock = 0
|
|
|
|
# used to map state-save clock to a program counter
|
|
self.state_save_clock_pc_maps = {}
|
|
|
|
# contains the return value of the main function
|
|
self.main_return_value = None
|
|
|
|
# Force stop
|
|
self.force_stop = False
|
|
|
|
# Link VMState to Value
|
|
Value._vmstate = self
|
|
Instruction._vmstate = self
|
|
|
|
# Custom callbacks
|
|
self.custom_callbacks = {}
|
|
|
|
# IR Prefix
|
|
self.ir_function_prefix = self.config.program.ir_function_prefix
|
|
BuiltinLinker.prefix = self.ir_function_prefix
|
|
|
|
# set main function name
|
|
self.main_function_name = self.config.program.main_function_name
|
|
self.main_function_len = 0
|
|
|
|
# AST transformations
|
|
self.transformations = self.config.ast_transformations.transformations
|
|
|
|
# lookup information for stack anomalies
|
|
self._init_function_call_lookup()
|
|
|
|
self.stats = ScEpTICStats()
|
|
|
|
# Used by anomaly analysis
|
|
self.raise_memory_anomaly_exceptions = True
|
|
self.trigger_reset_and_restore = False
|
|
|
|
self._deprecated_config()
|
|
|
|
self.custom_metrics = CustomMetricsManager(self)
|
|
|
|
self.interrupts_manager = InterruptsManager(self)
|
|
self.timers_manager = TimersManager(self)
|
|
|
|
logging.info('VMState initialized.')
|
|
|
|
self.log_dir = None
|
|
|
|
|
|
def _init_function_call_lookup(self):
|
|
"""
|
|
Initializes function call lookup information.
|
|
"""
|
|
self.function_call_lookup = {'regs': [], 'args': [], 'pc': None, 'ebp': None}
|
|
|
|
|
|
def init_gst(self, global_vars):
|
|
"""
|
|
Initializes the global symbol table.
|
|
"""
|
|
logging.debug('Initializing Global Symbol Table')
|
|
|
|
for name in global_vars:
|
|
global_var = global_vars[name]
|
|
section = global_var.section
|
|
composition = global_var.type.get_memory_composition(True)
|
|
|
|
initial_val = global_var.initial_val.get_val()
|
|
|
|
# force a list for initial value, since composition is always a list
|
|
if initial_val is not None and not isinstance(initial_val, list):
|
|
initial_val = [initial_val]
|
|
|
|
self.memory.gst.allocate(name, section, composition, initial_val, alignment=global_var.align)
|
|
|
|
self.memory.gst.set_state_as_base_state()
|
|
self.memory.gst.sync_mmu()
|
|
|
|
|
|
def init_code(self, functions, declarations):
|
|
"""
|
|
Stores the function's code information to be run and performs register allocation if required.
|
|
"""
|
|
logging.debug('Initializing Code')
|
|
|
|
self.functions = functions
|
|
|
|
if self.main_function_name not in self.functions:
|
|
raise ConfigurationException('Unable to find main function "{}" in the provided code.'.format(self.main_function_name))
|
|
|
|
self.register_file.pc.update(self.main_function_name, 0)
|
|
self.state_save_clock_pc_maps[0] = copy.deepcopy(self.register_file.pc)
|
|
|
|
# load builtins
|
|
builtins.load_libraries()
|
|
builtins.link_builtins()
|
|
|
|
# process profiling markers
|
|
profiling.process_profiling_calls(self.ir_function_prefix, self.functions, declarations)
|
|
|
|
if self.register_file.requires_register_allocation:
|
|
# load the correct register allocator
|
|
allocator = register_allocation.get_register_allocator(self.config.register_allocation)
|
|
allocator(self.functions, len(self.register_file), self.config.register_file)
|
|
|
|
# apply program transformations
|
|
for transformation in self.transformations:
|
|
apply_transformation(transformation, self.functions, self, declarations)
|
|
|
|
if len(declarations) > 0:
|
|
raise InitializationException('The following functions do not have any definition:\n{}'.format(declarations.keys()))
|
|
|
|
self.main_function_len = len(self.functions[self.main_function_name])
|
|
|
|
for name in self.functions:
|
|
self.functions[name].adjust_alloca_ticks()
|
|
|
|
# Adjust return operation for functions executed before/after checkpoint/restore
|
|
for name, f_name in self.vm.state_retention.callbacks.items():
|
|
if f_name in self.functions:
|
|
self.custom_callbacks[name] = {'name': f_name, 'ret_ops': []}
|
|
|
|
for instruction in self.functions[f_name].body:
|
|
if isinstance(instruction, ReturnOperation):
|
|
instruction.update_pc = False
|
|
self.custom_callbacks[name]['ret_ops'].append(instruction)
|
|
|
|
ProgramCounter.populate_base_addresses()
|
|
|
|
|
|
def on_function_call(self, function_name):
|
|
"""
|
|
Callback that executes the operations needed before executing a function call.
|
|
"""
|
|
|
|
logging.debug("Called on_function_call({})".format(function_name))
|
|
|
|
if function_name not in self.functions:
|
|
raise RuntimeException('Unable to execute function call to "{}": function not found.'.format(function_name))
|
|
|
|
# callback for registers
|
|
self.register_file.on_function_call()
|
|
|
|
# save program counter
|
|
pc = self.register_file.pc.save()
|
|
|
|
if self.do_data_anomaly_check:
|
|
self.function_call_lookup['pc'] = self.memory.stack.top_address
|
|
|
|
self.memory.stack.push(self.memory.address_dimension, pc, 'PC')
|
|
|
|
# save tracking informations
|
|
self.register_file.pc._pc_tracking.append(pc)
|
|
|
|
# save stack base pointer
|
|
ebp = self.register_file.ebp
|
|
|
|
if self.do_data_anomaly_check:
|
|
self.function_call_lookup['ebp'] = self.memory.stack.top_address
|
|
|
|
self.memory.stack.push(self.memory.address_dimension, ebp, 'EBP')
|
|
|
|
# update ebp
|
|
self.register_file.ebp = self.memory.stack.top_address
|
|
|
|
# update pc with function_name at instruction #0
|
|
self.register_file.pc.update(function_name, 0)
|
|
|
|
|
|
def on_function_return(self, return_value, input_lookup_data, tick_count, update_program_counter = True):
|
|
"""
|
|
Callback that executes the operations needed after returning from a function call.
|
|
"""
|
|
|
|
logging.debug("Called on_function_return()")
|
|
|
|
self._init_function_call_lookup()
|
|
|
|
try:
|
|
# callback for registers
|
|
self.register_file.on_function_return()
|
|
|
|
# deallocate stack; ebp is direct address, without prefix
|
|
try:
|
|
self.memory.stack.deallocate(self.register_file.ebp, False, True)
|
|
except MemoryException:
|
|
# stack has not grown
|
|
pass
|
|
|
|
return_op = self.current_instruction
|
|
|
|
# pop ebp
|
|
ebp = self.memory.stack.pop(self.memory.address_dimension)
|
|
self.register_file.ebp = ebp
|
|
|
|
# pop pc
|
|
pc = self.memory.stack.pop(self.memory.address_dimension)
|
|
if not isinstance(pc, dict):
|
|
raise Exception(f"Return error: PC popped from stack does not map to any instruction")
|
|
|
|
self.register_file.pc.restore(pc)
|
|
|
|
self.register_file.pc._pc_tracking.pop()
|
|
|
|
# When returning from a function call, the current instruction should be the call to the function returning
|
|
# If we are returning from ISR, the function call is not in the code. Therefore, we force to consider as current instruction the call to the ISR
|
|
if self.interrupts_manager.is_isr_running() and return_op.is_isr_return:
|
|
current_instruction = return_op.isr_call
|
|
# Otherwise, current instruction is current instruction
|
|
else:
|
|
current_instruction = self.current_instruction
|
|
|
|
# pop parameters
|
|
function_args = current_instruction.function_args
|
|
params_count = len(function_args)
|
|
saved_params_number = min(params_count - self.register_file.param_regs_count, 0)
|
|
|
|
params_count -= 1
|
|
|
|
for i in range(params_count, params_count-saved_params_number, -1):
|
|
arg = function_args[i]
|
|
arg_len = len(arg.type)
|
|
self.memory.stack.pop(arg_len)
|
|
|
|
|
|
# get target of returning call
|
|
target = current_instruction.target
|
|
|
|
# write return_value into target
|
|
if target is not None:
|
|
self.register_file.write(target.value, return_value)
|
|
|
|
# if input lookup tracking is enabled, set lookup info
|
|
if self.input_lookup_enabled:
|
|
function = self.functions[current_instruction.name]
|
|
|
|
# If returning function is an input -> set target register input lookup data
|
|
if function.is_input:
|
|
lookup_data = tools.build_input_lookup_data(function.input_name, self.state_save_clock)
|
|
self.register_file.set_input_lookup(target.value, lookup_data)
|
|
|
|
# else set target input lookup data from the one of the returned register value.
|
|
elif target is not None:
|
|
self.register_file.set_input_lookup(target.value, input_lookup_data)
|
|
|
|
# increment pc and clock
|
|
self.on_run(tick_count, update_program_counter)
|
|
|
|
# end of program reached (pop will fail)
|
|
except (MemoryException, IndexError):
|
|
# set pc to len of main
|
|
self.register_file.pc.update(self.main_function_name, self.main_function_len)
|
|
|
|
# set return value
|
|
self.main_return_value = return_value
|
|
|
|
# increment clock
|
|
self.on_run(tick_count, False)
|
|
|
|
|
|
def on_branch(self, label, basic_block_id, tick_count):
|
|
"""
|
|
Callback that executes the operations needed to perform the branch operation, which updates program counter and last_basic_block.
|
|
"""
|
|
|
|
logging.debug("Called on_branch()")
|
|
|
|
self.register_file.last_basic_block = basic_block_id
|
|
|
|
# get current function name to retrieve the instruction_id corresponding to the given label
|
|
current_function_name = self.register_file.pc.function_name
|
|
|
|
# get instruction_id
|
|
next_instruction_id = self.functions[current_function_name].get_instruction_id(label)
|
|
|
|
# update program counter
|
|
self.register_file.pc.update(current_function_name, next_instruction_id)
|
|
|
|
# call run's callback, without incrementing the program counter (have just been updated)
|
|
self.on_run(tick_count, False)
|
|
|
|
|
|
def on_run(self, tick_count, update_program_counter = True):
|
|
"""
|
|
Callback that is executed when an instruction is run. It increments the program counter and the global_clock.
|
|
"""
|
|
|
|
logging.debug("Called on_run()")
|
|
|
|
# statistics
|
|
self.stats.instruction_executed()
|
|
|
|
self.global_clock += tick_count
|
|
|
|
if update_program_counter:
|
|
self.register_file.pc.increment_pc()
|
|
|
|
|
|
@property
|
|
def current_instruction(self):
|
|
"""
|
|
Returns the current instruction.
|
|
"""
|
|
logging.debug("Get current instruction")
|
|
|
|
if self.interrupts_manager.interrupt_firing():
|
|
return self.interrupts_manager.get_firing_interrupt(return_isr_call=True)
|
|
|
|
pc = self.register_file.pc
|
|
|
|
if pc.function_name not in self.functions:
|
|
raise RuntimeException('Function {} does not exist.'.format(pc.instruction_number, pc.function_name))
|
|
|
|
instruction = self.functions[pc.function_name].body[pc.instruction_number]
|
|
|
|
if instruction is None:
|
|
raise RuntimeException('Instruction #{} in function {} does not exist.'.format(pc.instruction_number, pc.function_name))
|
|
|
|
return instruction
|
|
|
|
|
|
def get_instruction_from_pc(self, pc):
|
|
"""
|
|
Returns an instruction corresponding to a given program counter
|
|
"""
|
|
|
|
logging.debug("Getting instruction with program counter {}".format(pc))
|
|
|
|
if pc.function_name not in self.functions:
|
|
raise RuntimeException('Function {} does not exist.'.format(pc.instruction_number, pc.function_name))
|
|
|
|
instruction = self.functions[pc.function_name].body[pc.instruction_number]
|
|
|
|
if instruction is None:
|
|
raise RuntimeException('Instruction #{} in function {} does not exist.'.format(pc.instruction_number, pc.function_name))
|
|
|
|
return instruction
|
|
|
|
|
|
@property
|
|
def program_end_reached(self):
|
|
"""
|
|
Returns if the end of the program has been reached.
|
|
"""
|
|
pc = self.register_file.pc
|
|
|
|
# given a main() of 5 instructions (0 to 4), end of program is reached if pc is on instruction 5 (with doesn't exists)
|
|
return self.force_stop or pc.function_name == self.main_function_name and pc.instruction_number >= self.main_function_len
|
|
|
|
|
|
def run_step(self):
|
|
"""
|
|
Runs the current instruction.
|
|
"""
|
|
|
|
logging.debug("Called run_step()")
|
|
self.timers_manager.check_timers()
|
|
|
|
if self.program_end_reached:
|
|
|
|
if self.force_stop:
|
|
raise StopException('Stop by user')
|
|
|
|
raise RuntimeException('Program ended with return value {}'.format(self.main_return_value))
|
|
|
|
instruction = self.current_instruction
|
|
|
|
logging.info('({}) Running instruction {}.'.format(self.register_file.pc, instruction.instruction_type))
|
|
|
|
instruction.run()
|
|
|
|
if self.config.memory.check_memory_size:
|
|
self.memory.check_memory_size()
|
|
|
|
|
|
def reset(self):
|
|
"""
|
|
Performs the CPU reset operations.
|
|
"""
|
|
|
|
self.memory.reset()
|
|
self.register_file.reset(self.main_function_name)
|
|
SimulateClockCycles.reset()
|
|
self._init_function_call_lookup()
|
|
self.trigger_reset_and_restore = False
|
|
self.global_clock = 0
|
|
self.state_save_clock = 0
|
|
self.main_return_value = None
|
|
self.force_stop = False
|
|
|
|
|
|
def handle_stop_request(self, anomaly):
|
|
"""
|
|
Stops the analysis if stop_on_first_anomaly is set to True, accordingly to the user input.
|
|
"""
|
|
|
|
if not self.stop_on_first_anomaly:
|
|
return
|
|
|
|
print("An anomaly has been found:\n{}\nContinue? [y/N]".format(anomaly))
|
|
|
|
res = input()
|
|
|
|
if res != 'y':
|
|
raise StopAnomalyFoundException()
|