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()