2026-07-10 10:38:57 +02:00

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 = {}