573 lines
19 KiB
Python
573 lines
19 KiB
Python
import json
|
|
import os
|
|
import traceback
|
|
from datetime import datetime
|
|
|
|
from ScEpTIC import llvmir_parser, tools
|
|
from ScEpTIC.analysis import get_analysis
|
|
from ScEpTIC.analysis.options import AnalysisResultFormat
|
|
from ScEpTIC.emulator.intermittent_executor import interruption_managers
|
|
from ScEpTIC.emulator.state_retention_manager import StateRetentionManager
|
|
from ScEpTIC.emulator.io.input import InputManager
|
|
from ScEpTIC.emulator.io.output import OutputManager, OutputSkeleton
|
|
from ScEpTIC.emulator.vmstate import VMState
|
|
from ScEpTIC.exceptions import ConfigurationException, StopAnomalyFoundException, StopException
|
|
from ScEpTIC.AST.transformations.base.instructions.custom_log import CustomLog
|
|
|
|
from ScEpTIC.emulator.memory.physical.physical_memory_management_unit import PhysicalMemoryManagementUnit
|
|
|
|
|
|
class VM:
|
|
"""
|
|
ScEpTIC VM
|
|
"""
|
|
|
|
def _deprecated_config(self, config):
|
|
self.run_continuous = config.deprecated.run_continuous
|
|
self.run_locate_memory_test = config.deprecated.run_locate_memory_test
|
|
self.run_evaluate_memory_test = config.deprecated.run_evaluate_memory_test
|
|
self.run_input_consistency_test = config.deprecated.run_input_consistency_test
|
|
self.run_output_profiling = config.deprecated.run_output_profiling_test
|
|
self.run_profiling = config.deprecated.run_profiling
|
|
self.run_memory_size_measure = config.deprecated.run_memory_size_measure
|
|
|
|
|
|
def __init__(self, config):
|
|
self.config = config
|
|
self.parsed = llvmir_parser.parse_file(self.config.program.file, self.config.logging.parser_log_level, self.config.logging.log_section_content)
|
|
self.state = VMState(self, self.config)
|
|
|
|
self.state_retention = StateRetentionManager(self.state, self.config.state_retention)
|
|
self.state_retention.process_state_save_routines(self.parsed['function_definitions'], self.parsed['function_declarations'])
|
|
|
|
self.state.init_code(self.parsed['function_definitions'], self.parsed['function_declarations'])
|
|
self.state.init_gst(self.parsed['global_vars'])
|
|
|
|
if config.result_output.dir_append_datetime:
|
|
current_datetime = datetime.now().strftime('%Y_%m_%d_%H_%M_%S')
|
|
result_dir = f"{config.result_output.test_name}_{current_datetime}"
|
|
else:
|
|
result_dir = f"{config.result_output.test_name}"
|
|
|
|
self.result_dir = os.path.join(os.getcwd(), config.result_output.save_directory, result_dir)
|
|
self.save_analysis_output = config.result_output.save_analysis_output
|
|
|
|
self.state.stats.test_name = config.result_output.test_name
|
|
|
|
self.state.log_dir = os.path.join(self.result_dir, 'logs')
|
|
|
|
self._deprecated_config(config)
|
|
|
|
self.state.initialized = True
|
|
|
|
self.termination_reason = {'event': None, 'event_info': None}
|
|
|
|
for mmu in PhysicalMemoryManagementUnit._mmus:
|
|
mmu.init_log_handlers()
|
|
|
|
|
|
def _save_program_code(self):
|
|
"""
|
|
If save_llvmir_code is set, saves the program code into "code" folder inside the save_dir.
|
|
"""
|
|
|
|
if not self.state.config.result_output.save_code:
|
|
return
|
|
|
|
save_dir = os.path.join(self.result_dir, 'code')
|
|
|
|
if os.path.exists(save_dir):
|
|
return
|
|
|
|
os.makedirs(save_dir, exist_ok=True)
|
|
|
|
for function_name in self.state.functions:
|
|
name = function_name.replace(self.state.ir_function_prefix, '')
|
|
file_name = os.path.join(save_dir, f'{name}.txt')
|
|
|
|
with open(file_name, 'w') as fp:
|
|
fp.write('{}'.format(self.state.functions[function_name]))
|
|
|
|
|
|
def _save_vm_state(self, name):
|
|
"""
|
|
If save_vm_state is set, saves the simulator state after each test into a states folder.
|
|
"""
|
|
|
|
if not self.state.config.result_output.save_state:
|
|
return
|
|
|
|
save_dir = os.path.join(self.result_dir, 'states')
|
|
os.makedirs(save_dir, exist_ok=True)
|
|
|
|
save_file = os.path.join(save_dir, '{}.txt'.format(name))
|
|
|
|
with open(save_file, 'w') as fp:
|
|
fp.write(self.get_visual_dump())
|
|
|
|
if len(PhysicalMemoryManagementUnit._mmus) == 0:
|
|
return
|
|
|
|
mmu_save_dir = os.path.join(save_dir, 'physical_memory')
|
|
os.makedirs(mmu_save_dir, exist_ok=True)
|
|
|
|
for memory in PhysicalMemoryManagementUnit._mmus:
|
|
if AnalysisResultFormat.TEXT in self.state.config.analysis.results_formats:
|
|
txt_file = os.path.join(mmu_save_dir, '{}.txt'.format(memory.get_label()))
|
|
with open(txt_file, 'w') as fp:
|
|
fp.write(memory.get_visual_dump())
|
|
|
|
if AnalysisResultFormat.JSON in self.state.config.analysis.results_formats:
|
|
json_file = os.path.join(mmu_save_dir, '{}.json'.format(memory.get_label()))
|
|
with open(json_file, 'w') as fp:
|
|
json.dump(memory.get_dump(), fp)
|
|
|
|
def set_termination_reason(self, event, event_info=None):
|
|
"""
|
|
Sets the analysis termination reason
|
|
"""
|
|
self.termination_reason = {'event': event, 'event_info': event_info}
|
|
|
|
def reset(self):
|
|
"""
|
|
Resets the whole status of the simulator.
|
|
"""
|
|
|
|
self.state_retention.reset()
|
|
self.state.reset()
|
|
self.state.memory.force_nvm_reset()
|
|
|
|
self.state.stats.reset()
|
|
self.state.custom_metrics.reset()
|
|
|
|
|
|
def execute_analysis(self):
|
|
"""
|
|
Run all the analysis
|
|
"""
|
|
for analysis_name in self.state.config.analysis.enabled_analysis:
|
|
self.run_single_analysis(analysis_name)
|
|
|
|
for mmu in PhysicalMemoryManagementUnit._mmus:
|
|
mmu.close_log_handlers()
|
|
|
|
if CustomLog.handler is not None:
|
|
CustomLog.handler.close()
|
|
|
|
|
|
def run_single_analysis(self, analysis_name):
|
|
"""
|
|
Executes an analysis and saves its results
|
|
:param analysis_name: analysis name
|
|
"""
|
|
# get analysis
|
|
analysis = get_analysis(analysis_name, self)
|
|
|
|
self._save_program_code()
|
|
|
|
run_exception = None
|
|
|
|
print(f"Running {analysis_name} analysis")
|
|
try:
|
|
analysis.run()
|
|
print("Done!")
|
|
|
|
except KeyboardInterrupt:
|
|
self.set_termination_reason("KEYBOARD_INTERRUPT")
|
|
print("Analysis stopped. Saving output data...")
|
|
|
|
except BaseException as e:
|
|
if not self.state.config.result_output.save_results_after_exception:
|
|
raise e
|
|
|
|
self.set_termination_reason("EXCEPTION", {'exception': f"{e}", 'stack_trace': f"{traceback.format_exc()}", 'pc': f"{self.state.register_file.pc}"})
|
|
print("An exception stopped the analysis. Saving output data...")
|
|
print(f"PC: {self.state.register_file.pc}")
|
|
|
|
try:
|
|
print(f"Current instruction: {self.state.current_instruction}")
|
|
|
|
if self.state.current_instruction.metadata is not None:
|
|
print(f" Metadata: {self.state.current_instruction.metadata.retrieve()}")
|
|
|
|
except BaseException:
|
|
pass
|
|
|
|
run_exception = e
|
|
|
|
try:
|
|
self._save_vm_state(analysis_name)
|
|
except BaseException as e:
|
|
print(e)
|
|
print(traceback.format_exc())
|
|
|
|
if self.state.config.analysis.save_results:
|
|
print(f"Saving {analysis_name} analysis results")
|
|
|
|
save_dir = os.path.join(self.result_dir, 'analysis', analysis_name)
|
|
|
|
if not os.path.exists(save_dir):
|
|
os.makedirs(save_dir, exist_ok=True)
|
|
|
|
file_base_name = os.path.join(save_dir, analysis_name)
|
|
|
|
for result_format in self.state.config.analysis.results_formats:
|
|
analysis.save_result(file_base_name, result_format)
|
|
self.state.custom_metrics.save_metrics(save_dir, result_format)
|
|
|
|
with open(os.path.join(save_dir, 'termination_reason.json'), 'w') as fp:
|
|
json.dump(self.termination_reason, fp, indent=4, sort_keys=True)
|
|
|
|
if run_exception is not None:
|
|
raise run_exception
|
|
|
|
|
|
def _save_memory_size(self, memory_size_interruption_manager):
|
|
"""
|
|
DEPRECATED
|
|
:param memory_size_interruption_manager:
|
|
:return:
|
|
"""
|
|
|
|
if memory_size_interruption_manager.__class__.__name__ != 'MemorySizeIdentifier':
|
|
return
|
|
|
|
save_dir = os.path.join(self.result_dir, 'memory_size')
|
|
|
|
if not os.path.exists(save_dir):
|
|
os.makedirs(save_dir, exist_ok=True)
|
|
|
|
save_file = os.path.join(save_dir, f"results.json")
|
|
|
|
with open(save_file, "w") as fp:
|
|
json.dump(self.state.memory_size, fp)
|
|
|
|
|
|
def _load_interruption_manager(self, module_name, class_name):
|
|
"""
|
|
DEPRECATED.
|
|
Loads a given interruption manager module.
|
|
It also enables/disables the controls to be done during runtime operations on data, accordingly
|
|
to the loaded module.
|
|
- For data module enables data anomaly check
|
|
- For input module enables input lookup anomaly check
|
|
"""
|
|
|
|
interruption_manager = interruption_managers.get_interruption_manager(module_name, class_name)(self.state, self.state_retention)
|
|
|
|
if interruption_manager.requires_static_checkpoint and not self.state_retention.static_placement:
|
|
raise ConfigurationException('Unable to run "{}, {}" test: static checkpoint placement required.'.format(module_name, class_name))
|
|
|
|
self.state.do_data_anomaly_check = interruption_manager.do_data_anomaly_check
|
|
self.state.input_lookup_enabled = interruption_manager.input_lookup_enabled
|
|
self.state.collect_memory_trace = interruption_manager.collect_memory_trace
|
|
|
|
return interruption_manager
|
|
|
|
|
|
def _get_stop_info(self):
|
|
"""
|
|
Returns stop information
|
|
"""
|
|
|
|
pc_stop_at = ''
|
|
space = ' '
|
|
for pc in self.state.register_file.pc._pc_tracking:
|
|
pc_stop_at += '{}{} -> {}\n'.format(space, pc['function_name'], pc['instruction_number'])
|
|
space += ' '*4
|
|
|
|
pc_stop_at += '{}{}\n'.format(space, self.state.register_file.pc)
|
|
|
|
return pc_stop_at, self.state.global_clock
|
|
|
|
|
|
def stop_current_test(self):
|
|
"""
|
|
Stops the current test
|
|
"""
|
|
|
|
self.state.force_stop = True
|
|
|
|
pc_stop_at, clock = self._get_stop_info()
|
|
self.state.stats.stop_at(pc_stop_at, clock)
|
|
|
|
|
|
def run_test(self, module_name, class_name):
|
|
"""
|
|
DEPRECATED
|
|
Runs completely a test, given the test module (interruption manager)
|
|
"""
|
|
|
|
print('Running {} test using {}'.format(module_name, class_name))
|
|
|
|
interruption_manager = self._load_interruption_manager(module_name, class_name)
|
|
|
|
self._save_program_code()
|
|
self.reset_anomalies()
|
|
self.reset_profiling()
|
|
self.reset()
|
|
|
|
while not self.state.program_end_reached:
|
|
try:
|
|
if interruption_manager.intermittent_execution_required():
|
|
interruption_manager.run_with_intermittent_execution()
|
|
|
|
else:
|
|
self.state.run_step()
|
|
|
|
except StopAnomalyFoundException:
|
|
pc_stop_at, clock = self._get_stop_info()
|
|
self.state.stats.stop_at(pc_stop_at, clock, True)
|
|
break
|
|
|
|
except StopException:
|
|
pc_stop_at, clock = self._get_stop_info()
|
|
self.state.stats.stop_at(pc_stop_at, clock)
|
|
break
|
|
|
|
if self.save_analysis_output:
|
|
|
|
if not os.path.exists(self.result_dir):
|
|
os.makedirs(self.result_dir, exist_ok=True)
|
|
|
|
anomalies_file_name = os.path.join(self.result_dir, '{}_anomalies.txt'.format(module_name))
|
|
profiling_file_name = os.path.join(self.result_dir, '{}_profiling.txt'.format(module_name))
|
|
|
|
self._save_memory_size(interruption_manager)
|
|
|
|
else:
|
|
anomalies_file_name = None
|
|
profiling_file_name = None
|
|
|
|
self.get_found_anomalies(anomalies_file_name)
|
|
|
|
if not self.get_profiling_info(profiling_file_name, module_name):
|
|
self.get_observation_info(profiling_file_name)
|
|
|
|
self._save_vm_state(module_name)
|
|
|
|
|
|
def run_tests(self):
|
|
"""
|
|
DEPRECATED
|
|
Runs the tests configured in the config file.
|
|
"""
|
|
|
|
if self.run_continuous:
|
|
self.run_test('base', 'InterruptionManager')
|
|
|
|
if self.run_evaluate_memory_test:
|
|
self.run_test('memory_evaluate', 'EvaluateMemoryAnomaliesInterruptionManager')
|
|
|
|
if self.run_locate_memory_test:
|
|
self.run_test('memory_locate', 'LocateMemoryAnomaliesInterruptionManager')
|
|
|
|
if self.run_input_consistency_test:
|
|
self.run_test('input', 'InputInterruptionManager')
|
|
|
|
if self.run_output_profiling:
|
|
self.run_test('output', 'OutputInterruptionManager')
|
|
|
|
if self.run_profiling:
|
|
self.run_test('profiling', 'ProfilingInterruptionManager')
|
|
|
|
if self.run_memory_size_measure:
|
|
self.run_test('memory_size', 'MemorySizeIdentifier')
|
|
|
|
|
|
def get_visual_dump(self):
|
|
"""
|
|
Returns a visual dump of the current state of the simulator.
|
|
"""
|
|
|
|
# registers
|
|
retstr = '[REGISTERS]\n'+str(self.state.register_file.get_visual_dump())
|
|
retstr += "\n"
|
|
|
|
# stack
|
|
retstr += '[STACK]\n'+str(self.state.memory.stack)
|
|
retstr += self.state.memory.stack.get_visual_dump()
|
|
retstr += "\n\n"
|
|
|
|
# heap
|
|
retstr += '[HEAP]\n'+str(self.state.memory.heap)
|
|
retstr += self.state.memory.heap.get_visual_dump()
|
|
retstr += "\n"
|
|
|
|
# gst
|
|
retstr += '[GST]\n'+self.state.memory.gst.get_visual_dump()
|
|
retstr += "\n\n"
|
|
|
|
# input table
|
|
retstr += '[INPUT TABLE]\n{}\n\n'.format(tools.fancy_dict_to_str(InputManager.input_table))
|
|
|
|
# output table
|
|
retstr += '[OUTPUT TABLE]\n{}\n\n'.format(tools.fancy_dict_to_str(OutputManager.output_table))
|
|
|
|
# output idempotency
|
|
retstr += '[OUTPUT IDEMPOTENCY]\n{}\n\n'.format(tools.fancy_dict_to_str(OutputManager.get_measured_idempotency()))
|
|
|
|
# global clock
|
|
retstr += 'Global Clock: {}\n\n'.format(self.state.global_clock)
|
|
|
|
# stats
|
|
retstr += '[Stats]\n {}\n\n'.format(str(self.state.stats).replace('\n', '\n '))
|
|
|
|
return retstr
|
|
|
|
def get_found_anomalies(self, outfile = None):
|
|
"""
|
|
Prints out the found anomalies. If an outfile is specified, such information is written inside it.
|
|
"""
|
|
|
|
if len(self.state.anomalies) == 0:
|
|
return None
|
|
|
|
retval = "Found anomalies: {}\n\n".format(len(self.state.anomalies))
|
|
|
|
for anomaly in self.state.anomalies:
|
|
retval += '{}\n'.format(anomaly)
|
|
|
|
if outfile is None:
|
|
print(retval)
|
|
|
|
else:
|
|
with open(outfile, 'w') as fp:
|
|
fp.write(retval)
|
|
|
|
|
|
def get_profiling_info(self, outfile = None, module_name = ''):
|
|
"""
|
|
Prints out the gethered profiling information. If an outfile is specified, such information is written inside it.
|
|
"""
|
|
|
|
if len(self.state.profiling) == 0:
|
|
return False
|
|
|
|
if module_name == 'output':
|
|
return self.get_output_profiling_info(outfile)
|
|
|
|
retval = 'Observed clock cycles: {}\n\n'.format(len(self.state.profiling))
|
|
spaces = ' ' * 4
|
|
|
|
for clock_id in sorted(self.state.profiling):
|
|
checkpoint_pc = self.state.state_save_clock_pc_maps[clock_id]
|
|
retval += 'Checkpoint: {}Global clock: {}\n\n'.format(checkpoint_pc.resolve(), clock_id)
|
|
|
|
runs = self.state.profiling[clock_id]
|
|
|
|
for run_id in sorted(runs):
|
|
if run_id == 0:
|
|
retval += '{}Normal execution:\n'.format(spaces)
|
|
else:
|
|
retval += '{}#{} power failure generated:\n'.format(spaces, run_id)
|
|
|
|
run = runs[run_id]
|
|
|
|
for io in run:
|
|
io_str = str(io).replace('\n', '\n{}'.format(spaces*3))
|
|
retval += '{}{}\n'.format(spaces*2, io_str)
|
|
|
|
retval += '\n'
|
|
|
|
retval += '\n'
|
|
|
|
if outfile is None:
|
|
print(retval)
|
|
|
|
else:
|
|
with open(outfile, 'w') as fp:
|
|
fp.write(retval)
|
|
|
|
return True
|
|
|
|
|
|
def get_output_profiling_info(self, outfile = None):
|
|
if len(self.state.profiling) == 0:
|
|
return False
|
|
|
|
retval = ''
|
|
|
|
spaces = ' ' * 4
|
|
|
|
for clock_id in sorted(self.state.profiling):
|
|
checkpoint_pc = self.state.state_save_clock_pc_maps[clock_id]
|
|
retval += 'Checkpoint: {}\n'.format(checkpoint_pc.resolve(), clock_id)
|
|
|
|
outs = self.state.profiling[clock_id]
|
|
|
|
for out in outs:
|
|
tracking = outs[out]
|
|
|
|
if len(tracking) == 0:
|
|
continue
|
|
|
|
retval += '{}Output {}\n'.format(spaces, OutputSkeleton.output_names[out.replace(self.state.ir_function_prefix, '')])
|
|
|
|
|
|
for track in tracking:
|
|
out_str = str(track).replace('\n', '\n{}'.format(spaces*3))
|
|
retval += '{}{}\n'.format(spaces*2, out_str)
|
|
|
|
retval += '\n'
|
|
|
|
retval += '\n'
|
|
|
|
if outfile is None:
|
|
print(retval)
|
|
|
|
else:
|
|
with open(outfile, 'w') as fp:
|
|
fp.write(retval)
|
|
|
|
return True
|
|
|
|
|
|
def get_observation_info(self, outfile = None):
|
|
"""
|
|
Prints out the gethered profiling information. If an outfile is specified, such information is written inside it.
|
|
"""
|
|
|
|
if len(self.state.observations) == 0:
|
|
return False
|
|
|
|
retval = ''
|
|
|
|
for first_checkpoint in self.state.observations:
|
|
retval += 'Checkpoint: {}\n'.format(first_checkpoint.resolve())
|
|
|
|
for second_checkpoint in self.state.observations[first_checkpoint]:
|
|
try:
|
|
second_checkpoint_str = second_checkpoint.resolve()
|
|
except IndexError:
|
|
second_checkpoint_str = 'Program End\n'
|
|
|
|
retval += '{}End checkpoint: {}'.format(' '*4, second_checkpoint_str)
|
|
retval += '{}Input access models:\n{}'.format(' '*6, tools.fancy_dict_to_str(self.state.observations[first_checkpoint][second_checkpoint], 7))
|
|
|
|
retval += '\n'
|
|
|
|
retval += '\n'
|
|
|
|
if outfile is None:
|
|
print(retval)
|
|
|
|
else:
|
|
with open(outfile, 'w') as fp:
|
|
fp.write(retval)
|
|
|
|
return True
|
|
|
|
|
|
def reset_anomalies(self):
|
|
"""
|
|
Resets the found anomalies.
|
|
"""
|
|
self.state.anomalies = []
|
|
|
|
def reset_profiling(self):
|
|
"""
|
|
Resets the gathered profiling information.
|
|
"""
|
|
self.state.profiling = {}
|