First commit

This commit is contained in:
Matteo Visotto 2026-07-10 10:38:57 +02:00
commit ace3695e48
Signed by: matteovisotto
GPG Key ID: 995A56D6EE338A27
500 changed files with 331297 additions and 0 deletions

BIN
ScEpTIC/.DS_Store vendored Normal file

Binary file not shown.

0
ScEpTIC/AST/__init__.py Normal file
View File

Binary file not shown.

View File

@ -0,0 +1,18 @@
from ScEpTIC.AST.builtins.builtin import Builtin
def load_libraries():
"""
Loads the libraries of builtins
"""
import ScEpTIC.AST.builtins.libs.math
import ScEpTIC.AST.builtins.libs.mem
import ScEpTIC.AST.builtins.libs.std
import ScEpTIC.AST.builtins.libs.llvm
import ScEpTIC.AST.builtins.libs.sceptic
def link_builtins():
"""
Links the builtins (both library-defined and user-defined)
"""
Builtin.link()

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,129 @@
import logging
from ScEpTIC.AST.builtins.linker import BuiltinLinker
from ScEpTIC.AST.elements.instruction import Instruction
class Builtin(Instruction):
"""
Base object to be extended for builtin functions.
HOW TO:
1) create the new instruction to be executed
2) Implement the get_val() method, returning the result of the instruction.
Arguments can be obatined using the list self.args (index = index of arg), their
values will be automatically resolved on runtime.
3) Call the class method define_builtin on the class
4) All the builtins will be loaded by calling link()
Example:
class LenTestInstruction(Builtin):
def get_val(self):
return len(self.args)
LenTestInstruction.define_builtin('let_test_instruction', 'i32, i32, double, float, i8', 'i64')
"""
# this value is used to increment the global clock.
# Set this in the Builtin implementations equal to the number
# of corresponding assemly instruction of the builtin.
tick_count = 1
builtins = []
def __init__(self):
super().__init__()
@staticmethod
def link():
"""
Links the builtin Instructions creating a function.
"""
for builtin in Builtin.builtins:
logging.debug('[Builtin] Linking {}'.format(builtin['name']))
# Resolve lambdas
for arg in builtin.keys():
if arg != "instruction" and callable(builtin[arg]):
builtin[arg] = builtin[arg]()
BuiltinLinker(builtin['name'], builtin['arguments'], builtin['return_type'], builtin['instruction'])
@classmethod
def define_builtin(cls, name, arguments, return_type):
"""
Define builtins, which will be loaded by the link() function.
"""
element = {'instruction': cls, 'name': name, 'arguments': arguments, 'return_type': return_type}
cls.builtins.append(element)
def set_args(self, args):
"""
Sets the arguments of the builtin.
"""
self._args = args
@property
def args(self):
"""
Returns the resolved value of the arguments.
"""
args = []
for arg in self._args:
arg = arg.get_val()
args.append(arg)
return args
def get_val(self):
"""
NB: must be implemented by each builtin!
"""
raise NotImplementedError('get_val() method for builtins is mandatory to be implemented!')
def get_uses(self):
"""
Returns the registers used by this instruction as a list of strings.
(used by register allocation)
"""
uses = []
for arg in self._args:
# support for var_args
if isinstance(arg, list):
for arg_i in arg:
uses = uses + arg_i.get_uses()
else:
uses = uses + arg.get_uses()
return uses
def replace_reg_name(self, old_reg_name, new_reg_name):
"""
Replaces the name of a register used by the instruction with a new one.
(used by register allocation)
"""
for arg in self._args:
# support for var_args
if isinstance(arg, list):
for arg_i in arg:
arg_i.replace_reg_name(old_reg_name, new_reg_name)
else:
arg.replace_reg_name(old_reg_name, new_reg_name)
if 'target' in self.__dict__ and self.target is not None:
self.target.replace_reg_name(old_reg_name, new_reg_name)
def __str__(self):
retstr = super().__str__()
retstr += '{}({})'.format(self.__class__.__name__, self._args)
return retstr

View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,165 @@
from ScEpTIC.AST.builtins.builtin import Builtin
from ScEpTIC.AST.elements.value import Value
"""
LLVM library functions
"""
def get_unaligned_cells(cells, nbytes, stack_limit_address, stack_prefix):
"""
ScEpTIC align memory to single bytes.
To make llvm mem-based functions work, we need to count the number of cells instead of using the memory size.
Further, when allocating memory without initializing the cells with a value, ScEpTIC creates a single cell of the entire data structure size.
To ensure we do not smash the stack, this function limits the cells to EBP-2 cells.
This is possible as these Builtins are function calls and allocate a new stack frame.
:param cells: List of cells
:param nbytes: number of requested bytes
:param stack_limit_address: start address of the new frame (EBP - 2 cells, for saved PC and EBP)
:param stack_prefix: address prefix of the stack
"""
# LLVM uses cell's alignment to calculate nbytes
if not isinstance(cells, list) or len(cells) == 0:
return cells
alignment = cells[0].alignment
if alignment is not None and alignment > 0:
# Calculate real number of cells
n_cells = nbytes // alignment
# Resize cells using ScEpTIC no-alignment policy
cells = cells[:n_cells]
if len(cells) > 0 and cells[0].address_prefix == stack_prefix:
for idx, cell in enumerate(cells):
if cell.address >= stack_limit_address:
return cells[:idx]
return cells
class msp430_dma_word_copy(Builtin):
"""
msp430 __dma_word_copy() builtin
"""
def get_val(self):
args = self.args
destination = args[0]
source = args[1]
# compiled for x86 -> word size 32 bit -> 4 bytes
nbytes = args[2]
# Stack limit address
stack_limit_address = self._vmstate.register_file.ebp - 2 * (self._vmstate.memory.address_dimension // 8)
stack_prefix = self._vmstate.memory.stack.address_prefix
cells = self._vmstate.memory.get_cells_from_address(source, nbytes, False)
# No cells alignment to be considered
self._vmstate.memory.simulate_mmu_read_on_cells(cells)
self._vmstate.memory.set_cells_from_address(destination, cells)
class memcpy(Builtin):
"""
memcpy() builtin
"""
def get_val(self):
args = self.args
destination = args[0]
source = args[1]
nbytes = args[2]
# Stack limit address
stack_limit_address = self._vmstate.register_file.ebp - 2 * (self._vmstate.memory.address_dimension // 8)
stack_prefix = self._vmstate.memory.stack.address_prefix
cells = self._vmstate.memory.get_cells_from_address(source, nbytes, False)
cells = get_unaligned_cells(cells, nbytes, stack_limit_address, stack_prefix)
self._vmstate.memory.simulate_mmu_read_on_cells(cells)
self._vmstate.memory.set_cells_from_address(destination, cells)
class memmove(Builtin):
"""
memmove() builtin
"""
def get_val(self):
args = self.args
destination = args[0]
source = args[1]
nbytes = args[2]
# Stack limit address
stack_limit_address = self._vmstate.register_file.ebp - 2 * (self._vmstate.memory.address_dimension // 8)
stack_prefix = self._vmstate.memory.stack.address_prefix
cells = self._vmstate.memory.get_cells_from_address(source, nbytes, False)
cells = get_unaligned_cells(cells, nbytes, stack_limit_address, stack_prefix)
self._vmstate.memory.simulate_mmu_read_on_cells(cells)
self._vmstate.memory.set_cells_from_address(destination, cells)
cells = self._vmstate.memory.get_cells_from_address(destination, nbytes, False)
cells = get_unaligned_cells(cells, nbytes, stack_limit_address, stack_prefix)
self._vmstate.memory.simulate_mmu_read_on_cells(cells)
for cell in cells:
self._vmstate.memory.write(cell.absolute_address, cell.dimension, None, False)
class memset(Builtin):
"""
memset() builtin
"""
def get_val(self):
args = self.args
destination = args[0]
nbytes = args[2]
value = args[1]
# Stack limit address
stack_limit_address = self._vmstate.register_file.ebp - 2 * (self._vmstate.memory.address_dimension // 8)
stack_prefix = self._vmstate.memory.stack.address_prefix
cells = self._vmstate.memory.get_cells_from_address(destination, nbytes, False)
# Keep this here to ensure data structures works
# TODO: change alignment to the one of the operation, and not the one of the cell!!
cells = get_unaligned_cells(cells, nbytes, stack_limit_address, stack_prefix)
self._vmstate.memory.simulate_mmu_read_on_cells(cells)
for cell in cells:
self._vmstate.memory.write(cell.absolute_address, cell.dimension, value, False)
"""
Definitions for the various versions of llvm.memcpy, llvm.memmove, llvm.memset
"""
# Called on linking, as it depends on vmstate
def address_dimension_16():
dim = f"i{Builtin._vmstate.memory.address_dimension}"
return f"{dim}, {dim}, i16"
def address_dimension_32():
dim = f"i{Builtin._vmstate.memory.address_dimension}"
return f"{dim}, {dim}, i32"
def address_dimension_64():
dim = f"i{Builtin._vmstate.memory.address_dimension}"
return f"{dim}, {dim}, i64"
# in llvm 5, source destination len align isvolatile, in llvm7 align is missing.
# To support every version, it is defined only as if it has 3 arguments.
memcpy.define_builtin('llvm.memcpy.p0i8.p0i8.i16', address_dimension_16, 'void')
memcpy.define_builtin('llvm.memcpy.p0i8.p0i8.i32', address_dimension_32, 'void')
memcpy.define_builtin('llvm.memcpy.p0i8.p0i8.i64', address_dimension_64, 'void')
memmove.define_builtin('llvm.memmove.p0i8.p0i8.i16', address_dimension_16, 'void')
memmove.define_builtin('llvm.memmove.p0i8.p0i8.i32', address_dimension_32, 'void')
memmove.define_builtin('llvm.memmove.p0i8.p0i8.i64', address_dimension_64, 'void')
memset.define_builtin('llvm.memset.p0i8.i16', address_dimension_16, 'void')
memset.define_builtin('llvm.memset.p0i8.i32', address_dimension_32, 'void')
memset.define_builtin('llvm.memset.p0i8.i64', address_dimension_64, 'void')
memcpy.define_builtin('__memcpy_chk', address_dimension_64, 'void')
msp430_dma_word_copy.define_builtin('__dma_word_copy', address_dimension_32, 'void')

View File

@ -0,0 +1,246 @@
import math
from ScEpTIC.AST.builtins.builtin import Builtin
"""
math.h most used library functions
"""
class ceil(Builtin):
"""
Definition of the ceil() builtin
"""
def get_val(self):
return math.ceil(self.args[0])
class floor(Builtin):
"""
Definition of the floor() builtin
"""
def get_val(self):
return math.floor(self.args[0])
class fabs(Builtin):
"""
Definition of the fabs() builtin
"""
def get_val(self):
return math.fabs(self.args[0])
class abs(Builtin):
"""
Definition of the abs() builtin
"""
def get_val(self):
return math.abs(self.args[0])
class fmod(Builtin):
"""
Definition of the fmod() builtin
"""
def get_val(self):
return math.fmod(self.args[0], self.args[1])
class sqrt(Builtin):
"""
Definition of the sqrt() builtin
"""
def get_val(self):
return math.sqrt(self.args[0])
class pow(Builtin):
"""
Definition of the pow() builtin
"""
def get_val(self):
return math.pow(self.args[0], self.args[1])
class exp(Builtin):
"""
definition of the exp() builtin
"""
def get_val(self):
return math.exp(self.args[0])
class log(Builtin):
"""
definition of the log() builtin
"""
def get_val(self):
return math.log(self.args[0])
class log10(Builtin):
"""
definition of the log10() builtin
"""
def get_val(self):
return math.log10(self.args[0])
class acos(Builtin):
"""
definition of the acos() builtin
"""
def get_val(self):
return math.acos(self.args[0])
class asin(Builtin):
"""
Definition of the asin() builtin
"""
def get_val(self):
return math.asin(self.args[0])
class atan(Builtin):
"""
Definition of the atan() builtin
"""
def get_val(self):
return math.atan(self.args[0])
class atan2(Builtin):
"""
Definition of the atan2() builtin
"""
def get_val(self):
return math.atan2(self.args[0], self.args[1])
class cos(Builtin):
"""
Definition of the cos() builtin
"""
def get_val(self):
return math.cos(self.args[0])
class cosh(Builtin):
"""
Definition of the cosh() builtin
"""
def get_val(self):
return math.cosh(self.args[0])
class sin(Builtin):
"""
Definition of the sin() builtin
"""
def get_val(self):
return math.sin(self.args[0])
class sinh(Builtin):
"""
Definition of the sinh() builtin
"""
def get_val(self):
return math.sinh(self.args[0])
class tan(Builtin):
"""
Definition of the tan() builtin
"""
def get_val(self):
return math.tan(self.args[0])
class tanh(Builtin):
"""
Definition of the tanh() builtin
"""
def get_val(self):
return math.tanh(self.args[0])
class math_max(Builtin):
"""
Definition of the MAX() builtin
"""
def get_val(self):
return max(self.args[0], self.args[1])
class math_min(Builtin):
"""
Definition of the MIN() builtin
"""
def get_val(self):
return min(self.args[0], self.args[1])
ceil.define_builtin('ceil', 'double', 'double')
ceil.define_builtin('llvm.ceil.f64', 'double', 'double')
floor.define_builtin('floor', 'double', 'double')
floor.define_builtin('llvm.floor.f64', 'double', 'double')
fabs.define_builtin('fabs', 'double', 'double')
fabs.define_builtin('llvm.fabs.f64', 'double', 'double')
abs.define_builtin('abs', 'double', 'double')
abs.define_builtin('llvm.abs.f64', 'double', 'double')
fmod.define_builtin('fmod', 'double, double', 'double')
fmod.define_builtin('llvm.fmod.f64', 'double, double', 'double')
sqrt.define_builtin('sqrt', 'double', 'double')
sqrt.define_builtin('llvm.sqrt.f64', 'double', 'double')
pow.define_builtin('pow', 'double, double', 'double')
pow.define_builtin('llvm.pow.f64', 'double, double', 'double')
exp.define_builtin('exp', 'double', 'double')
exp.define_builtin('llvm.exp.f64', 'double', 'double')
log.define_builtin('log', 'double', 'double')
log.define_builtin('llvm.log.f64', 'double', 'double')
log10.define_builtin('log10', 'double', 'double')
log10.define_builtin('llvm.log10.f64', 'double', 'double')
acos.define_builtin('acos', 'double', 'double')
acos.define_builtin('llvm.acos.f64', 'double', 'double')
asin.define_builtin('asin', 'double', 'double')
asin.define_builtin('llvm.asin.f64', 'double', 'double')
atan.define_builtin('atan', 'double', 'double')
atan.define_builtin('llvm.atan.f64', 'double', 'double')
atan2.define_builtin('atan2', 'double, double', 'double')
atan2.define_builtin('llvm.atan2.f64', 'double, double', 'double')
cos.define_builtin('cos', 'double', 'double')
cos.define_builtin('llvm.cos.f64', 'double', 'double')
cosh.define_builtin('cosh', 'double', 'double')
cosh.define_builtin('llvm.cosh.f64', 'double', 'double')
sin.define_builtin('sin', 'double', 'double')
sin.define_builtin('llvm.sin.f64', 'double', 'double')
sinh.define_builtin('sinh', 'double', 'double')
sinh.define_builtin('llvm.sinh.f64', 'double', 'double')
tan.define_builtin('tan', 'double', 'double')
tan.define_builtin('llvm.tan.f64', 'double', 'double')
tanh.define_builtin('tanh', 'double', 'double')
tanh.define_builtin('llvm.tanh.f64', 'double', 'double')
math_max.define_builtin('MAX', 'double, double', 'double')
math_min.define_builtin('MAX', 'double, double', 'double')

View File

@ -0,0 +1,72 @@
from ScEpTIC.AST.builtins.builtin import Builtin
"""
Heap-related library functions
"""
class malloc(Builtin):
"""
Definition of the malloc() builtin
"""
def get_val(self):
address = self._vmstate.memory.heap.allocate(self.args[0], False)
return address
class calloc(Builtin):
"""
Definition of the calloc() builtin
"""
def get_val(self):
# two parameters: number of elements and element dimension
num = self.args[0]
dim = self.args[1]
dimension = num * dim
address = self._vmstate.memory.heap.allocate(dimension, False)
# initialize to 0
for i in range(0, num):
addr = self._vmstate.memory.add_offset(address, i*dim, False)
self._vmstate.memory.heap.write(addr, dim, 0, False)
return address
class realloc(Builtin):
"""
Definition of the realloc() builtin
"""
def get_val(self):
address = self._vmstate.memory.heap.reallocate(self.args[0], self.args[1], False)
return address
class free(Builtin):
"""
Definition of the free() builtin
"""
def get_val(self):
self._vmstate.memory.heap.deallocate(self.args[0])
return 0
# Called on linking, as it depends on vmstate
def address_dimension():
return f"i{Builtin._vmstate.memory.address_dimension}"
# define malloc()
malloc.define_builtin('malloc', 'i32', address_dimension)
# define calloc()
calloc.define_builtin('calloc', 'i32, i32', address_dimension)
# define realloc()
realloc.define_builtin('realloc', '{}, i32'.format(address_dimension), address_dimension)
# define free()
free.define_builtin('free', 'i32', address_dimension)

View File

@ -0,0 +1,6 @@
float sceptic_get_capacitor_voltage(void);
int sceptic_get_mcu_clock_cycles(void);
float sceptic_get_mcu_time(void);
void sceptic_set_mcu_power_on_voltage(float);
void sceptic_mcu_lpm(void);
float sceptic_timekeeper_get_time(void);

View File

@ -0,0 +1,480 @@
import math
from ScEpTIC.AST.builtins.builtin import Builtin
from ScEpTIC.AST.elements.value import Value
from ScEpTIC.emulator.energy.mcu import ADCPowerState
from ScEpTIC.emulator.energy.mcu.options import MCUClockCycleAction
from ScEpTIC.emulator.energy.options import OpModeName
class voc_read_data(Builtin):
"""
voc_read_data() builtin that reads voc from SGP40 sensor
"""
def get_val(self):
try:
sgp40 = self._vmstate.config.analysis.energy.system_model.get_component('SGP40')
except BaseException:
raise Exception("SystemModel not attached to ScEpTIC")
if sgp40 is None:
raise Exception("SHT85 not attached to ScEpTIC")
# read
self._vmstate.global_clock += sgp40.simulate_read()
class temperature_read_data(Builtin):
"""
temperature_read_data() builtin that reads temperature from SHT85 sensor
"""
def get_val(self):
try:
sht85 = self._vmstate.config.analysis.energy.system_model.get_component('SHT85')
except BaseException:
raise Exception("SystemModel not attached to ScEpTIC")
if sht85 is None:
raise Exception("SHT85 not attached to ScEpTIC")
# read
self._vmstate.global_clock += sht85.simulate_read()
class camera_read_data(Builtin):
"""
camera_read_data() builtin that reads a camera
"""
def get_val(self):
try:
camera = self._vmstate.config.analysis.energy.system_model.get_component('0V7620')
except BaseException:
raise Exception("SystemModel not attached to ScEpTIC")
if camera is None:
raise Exception("Camera not attached to ScEpTIC")
# word size
self._vmstate.global_clock += camera.simulate_read(int(self.args[0]))
class generic_adc_sensor_read(Builtin):
"""
generic_adc_sensor_read() builtin that reads a generic ADC sensor
"""
def get_val(self):
try:
generic_sensor = self._vmstate.config.analysis.energy.system_model.get_component('generic_sensor')
except BaseException:
raise Exception("SystemModel not attached to ScEpTIC")
if generic_sensor is None:
raise Exception("GenericADCSensor not attached to ScEpTIC")
self._vmstate.global_clock += generic_sensor.simulate_read(int(self.args[0]))
class cc1101_set_state(Builtin):
"""
cc1101_set_state() builtin that sets the state of cc1101
"""
states = {0: 'off', 1: 'inactive', 2: 'active'}
def get_val(self):
try:
cc1101 = self._vmstate.config.analysis.energy.system_model.get_component('cc1101')
except BaseException:
raise Exception("SystemModel not attached to ScEpTIC")
if cc1101 is None:
raise Exception("CC1101 not attached to ScEpTIC")
self._vmstate.global_clock += cc1101.simulate_set_state(self.states[int(self.args[0])])
class cc1101_transmit(Builtin):
"""
cc1101_transmit() builtin that turns on the cc1101, transmit data, and turns it off
"""
def get_val(self):
try:
cc1101 = self._vmstate.config.analysis.energy.system_model.get_component('cc1101')
except BaseException:
raise Exception("SystemModel not attached to ScEpTIC")
if cc1101 is None:
raise Exception("CC1101 not attached to ScEpTIC")
# args: bytes, word_size
self._vmstate.global_clock += cc1101.simulate_transmit(int(self.args[0]), int(self.args[1]))
class adc_off(Builtin):
"""
adc_off() builtin that turns off the ADCon
"""
def get_val(self):
try:
system_model = self._vmstate.config.analysis.energy.system_model
except BaseException:
raise Exception("SystemModel not attached to ScEpTIC")
# Turn off ADC
self._vmstate.global_clock += system_model.run_step(MCUClockCycleAction.NO_MEMORY_ACCESS, OpModeName.SIMULATE_ADC)
system_model.mcu.set_adc_state(ADCPowerState.OFF)
class adc_on(Builtin):
"""
adc_on() builtin that turns on the ADC
"""
def get_val(self):
try:
system_model = self._vmstate.config.analysis.energy.system_model
except BaseException:
raise Exception("SystemModel not attached to ScEpTIC")
# Turn on ADC
# - Run instructions
# - ADCPowerState -> ON
for _ in range(system_model.mcu.adc_instructions['on']):
self._vmstate.global_clock += system_model.run_step(MCUClockCycleAction.NO_MEMORY_ACCESS, OpModeName.SIMULATE_ADC)
system_model.mcu.set_adc_state(ADCPowerState.ON)
class adc_read(Builtin):
"""
adc_read() builtin that reads data from ADC
"""
def get_val(self):
try:
system_model = self._vmstate.config.analysis.energy.system_model
except BaseException:
raise Exception("SystemModel not attached to ScEpTIC")
# Wait for startup and data to be ready
for _ in range(system_model.mcu.get_ADC_wait_cycles()):
self._vmstate.global_clock += system_model.run_step(MCUClockCycleAction.NO_MEMORY_ACCESS, self.op_mode_name)
# Transfer
for _ in range(system_model.mcu.adc_instructions['transfer']):
self._vmstate.global_clock += system_model.run_step(MCUClockCycleAction.NO_MEMORY_ACCESS, self.op_mode_name)
class adc_get_data(Builtin):
"""
adc_get_data(n_samples, wait_between_samples) builtin that turns the ADC on, collects n_samples (one every wait_between_samples ms), and turns the ADC off.
"""
op_mode_name = OpModeName.SIMULATE_ADC
def __adc_on(self, system_model):
# Turn on ADC
# - Run instructions
# - ADCPowerState -> ON
for _ in range(system_model.mcu.adc_instructions['on']):
self._vmstate.global_clock += system_model.run_step(MCUClockCycleAction.NO_MEMORY_ACCESS, self.op_mode_name)
system_model.mcu.set_adc_state(ADCPowerState.ON)
def __adc_off(self, system_model):
# Turn off ADC
self._vmstate.global_clock += system_model.run_step(MCUClockCycleAction.NO_MEMORY_ACCESS, self.op_mode_name)
system_model.mcu.set_adc_state(ADCPowerState.OFF)
def __adc_transfer_data(self, system_model):
# Wait for startup and data to be ready
for _ in range(system_model.mcu.get_ADC_wait_cycles()):
self._vmstate.global_clock += system_model.run_step(MCUClockCycleAction.NO_MEMORY_ACCESS, self.op_mode_name)
# Transfer
for _ in range(system_model.mcu.adc_instructions['transfer']):
self._vmstate.global_clock += system_model.run_step(MCUClockCycleAction.NO_MEMORY_ACCESS, self.op_mode_name)
def __wait_cycles(self, system_model, wait_cycles):
if wait_cycles <= 0:
return
for _ in range(wait_cycles):
self._vmstate.global_clock += system_model.run_step(MCUClockCycleAction.NO_MEMORY_ACCESS, self.op_mode_name)
def __adc_get_samples_on_off(self, system_model, n_samples, wait_cycles):
for i in range(n_samples):
self.__adc_on(system_model)
self.__adc_transfer_data(system_model)
self.__adc_off(system_model)
if i < (n_samples - 1):
self.__wait_cycles(system_model, wait_cycles)
def __adc_get_samples(self, system_model, n_samples, wait_cycles):
self.__adc_on(system_model)
for i in range(n_samples):
self.__adc_transfer_data(system_model)
if i < (n_samples - 1):
self.__wait_cycles(system_model, wait_cycles)
self.__adc_off(system_model)
def get_val(self):
try:
system_model = self._vmstate.config.analysis.energy.system_model
except BaseException:
raise Exception("SystemModel not attached to ScEpTIC")
# Samples to collect
n_samples = self.args[0]
# ms to wait between samples
samples_s_wait = float(self.args[1]) / 1000.0
frequency = system_model.mcu.get_frequency()
sample_wait_cycles = max(0, math.ceil(samples_s_wait*frequency) - system_model.mcu.get_ADC_wait_cycles())
wait_cycles = sample_wait_cycles - system_model.mcu.adc_instructions['on'] - 1
if self.args[2] == 0:
self.__adc_get_samples(system_model, n_samples, sample_wait_cycles)
else:
self.__adc_get_samples_on_off(system_model, n_samples, wait_cycles)
class timekeeper_get_time_us(Builtin):
"""
timekeeper_get_time_us() builtin that returns timekeeper power-off time in us
"""
def get_val(self):
try:
return self._vmstate.config.analysis.energy.system_model.timekeeper.get_time() * 1000000.0
except BaseException:
raise Exception("SystemModel/Timekeeper not attached to ScEpTIC")
class timekeeper_get_time(Builtin):
"""
timekeeper_get_time() builtin that returns timekeeper power-off time
"""
def get_val(self):
try:
return self._vmstate.config.analysis.energy.system_model.timekeeper.get_time()
except BaseException:
raise Exception("SystemModel/Timekeeper not attached to ScEpTIC")
class set_mcu_power_on_voltage(Builtin):
"""
set_mcu_power_on_voltage() builtin that sets the power on voltage of the MCU
"""
def get_val(self):
try:
voltage = self.args[0]
#print(f"Set MCU Von={voltage}V")
return self._vmstate.config.analysis.energy.system_model.mcu.set_v_on(voltage)
except BaseException:
raise Exception("SystemModel/MCU not attached to ScEpTIC")
class get_mcu_time(Builtin):
"""
get_mcu_time() builtin that returns the mcu time
"""
def get_val(self):
try:
#t = self._vmstate.config.analysis.energy.system_model.mcu.get_time()
#print(f"Time: {t}")
return self._vmstate.config.analysis.energy.system_model.mcu.get_time()
except BaseException:
raise Exception("SystemModel/MCU not attached to ScEpTIC")
class get_mcu_time_us(Builtin):
"""
get_mcu_time_us() builtin that returns the mcu time in us
"""
def get_val(self):
try:
return self._vmstate.config.analysis.energy.system_model.mcu.get_time() * 1000000
except BaseException:
raise Exception("SystemModel/MCU not attached to ScEpTIC")
class set_mcu_time(Builtin):
"""
set_mcu_time() builtin that sets the mcu time
"""
def get_val(self):
try:
t = float(self.args[0])
self._vmstate.config.analysis.energy.system_model.mcu.set_time(t)
except BaseException:
raise Exception("SystemModel/MCU not attached to ScEpTIC")
class set_mcu_time_us(Builtin):
"""
set_mcu_time_us() builtin that sets the mcu time in us
"""
def get_val(self):
try:
t = float(self.args[0]) / 1000000.0
self._vmstate.config.analysis.energy.system_model.mcu.set_time(t)
except BaseException as e:
print(self.args)
print(e)
raise Exception("SystemModel/MCU not attached to ScEpTIC")
class get_simulation_time(Builtin):
"""
get_simulation_time() builtin that returns the simulation time
"""
print_enabled = False
def get_val(self):
try:
t = self._vmstate.config.analysis.energy.system_model.get_simulation_time()
if self.print_enabled:
t_us = t * 1000000.0
print(f"Time (us): {round(t_us, 2)}")
return t
except BaseException:
raise Exception("SystemModel/MCU not attached to ScEpTIC")
class reset_simulation_time(Builtin):
"""
reset_simulation_time() builtin that resets the simulation time
"""
def get_val(self):
try:
self._vmstate.config.analysis.energy.system_model.reset_simulation_time()
except BaseException:
raise Exception("SystemModel/MCU not attached to ScEpTIC")
class get_mcu_clock_cycles(Builtin):
"""
get_mcu_clock_cycles() builtin that returns the number of clock cycles in the latest active cycle
"""
def get_val(self):
try:
return self._vmstate.config.analysis.energy.system_model.mcu.get_clock_cycles()
except BaseException:
raise Exception("SystemModel/MCU not attached to ScEpTIC")
class get_used_energy(Builtin):
"""
get_used_energy() builtin that returns the used energy
"""
print_enabled = False
def get_val(self):
try:
e = self._vmstate.config.analysis.energy.system_model.get_used_energy()
if self.print_enabled:
e_uj = e * 1000000.0
print(f"Energy consumption (uJ): {round(e_uj, 2)}")
return e
except BaseException:
raise Exception("SystemModel/MCU not attached to ScEpTIC")
class set_capacitor_voltage(Builtin):
"""
set_voltage() bultin that sets the energy buffer voltage
"""
def get_val(self):
try:
system_model = self._vmstate.config.analysis.energy.system_model
except BaseException:
raise Exception("SystemModel not attached to ScEpTIC")
system_model.energy_buffer.set_voltage(self.args[0])
class reset_used_energy(Builtin):
"""
reset_energy() builtin that resets the energy consumption
"""
def get_val(self):
try:
self._vmstate.config.analysis.energy.system_model.reset_used_energy()
except BaseException:
raise Exception("SystemModel/MCU not attached to ScEpTIC")
class get_capacitor_voltage(Builtin):
"""
get_voltage() builtin that returns capacitor voltage
"""
def get_val(self):
try:
system_model = self._vmstate.config.analysis.energy.system_model
except BaseException:
raise Exception("SystemModel not attached to ScEpTIC")
op_mode_name = OpModeName.PROBE_ENERGY_BUFFER
# Turn on ADC
# - Run instructions
# - ADCPowerState -> ON
for _ in range(system_model.mcu.adc_instructions['on']):
self._vmstate.global_clock += system_model.run_step(MCUClockCycleAction.NO_MEMORY_ACCESS, op_mode_name)
system_model.mcu.set_adc_state(ADCPowerState.ON)
# Wait for startup and data to be ready
for _ in range(system_model.mcu.get_ADC_wait_cycles()):
self._vmstate.global_clock += system_model.run_step(MCUClockCycleAction.NO_MEMORY_ACCESS, op_mode_name, True)
# Transfer data to reg
for _ in range(system_model.mcu.adc_instructions['transfer']):
self._vmstate.global_clock += system_model.run_step(MCUClockCycleAction.NO_MEMORY_ACCESS, op_mode_name)
v = system_model.energy_buffer.get_voltage()
# Turn off ADC
self._vmstate.global_clock += system_model.run_step(MCUClockCycleAction.NO_MEMORY_ACCESS, op_mode_name)
system_model.mcu.set_adc_state(ADCPowerState.OFF)
return v
get_capacitor_voltage.define_builtin('sceptic_get_capacitor_voltage', '', 'double')
set_capacitor_voltage.define_builtin('sceptic_set_capacitor_voltage', 'double', 'void')
get_mcu_clock_cycles.define_builtin('sceptic_get_mcu_clock_cycles', '', 'i32')
get_mcu_time.define_builtin('sceptic_get_mcu_time', '', 'double')
get_mcu_time_us.define_builtin('sceptic_get_mcu_time_us', '', 'double')
get_used_energy.define_builtin('sceptic_get_used_energy', '', 'double')
reset_used_energy.define_builtin('sceptic_reset_used_energy', '', 'void')
get_simulation_time.define_builtin('sceptic_get_simulation_time', '', 'double')
set_mcu_time.define_builtin('sceptic_set_mcu_time', 'double', 'void')
set_mcu_time_us.define_builtin('sceptic_set_mcu_time_us', 'double', 'void')
reset_simulation_time.define_builtin('sceptic_reset_simulation_time', '', 'void')
# voltage
set_mcu_power_on_voltage.define_builtin('sceptic_set_mcu_power_on_voltage', 'double', 'void')
timekeeper_get_time.define_builtin('sceptic_timekeeper_get_time', '', 'double')
timekeeper_get_time_us.define_builtin('sceptic_timekeeper_get_time_us', '', 'double')
# n_samples, t_wait (ms), on_off
adc_get_data.define_builtin('sceptic_adc_get_data', 'i32, double, i32', 'void')
adc_on.define_builtin('sceptic_adc_on', '', 'void')
adc_off.define_builtin('sceptic_adc_off', '', 'void')
adc_read.define_builtin('sceptic_adc_read', '', 'void')
# bytes, word_size
cc1101_transmit.define_builtin('sceptic_cc1101_transmit', 'i32, i32', 'void')
# state_id
cc1101_set_state.define_builtin('sceptic_cc1101_set_state', 'i32', 'void')
# n_samples
generic_adc_sensor_read.define_builtin('sceptic_generic_adc_sensor_read', 'i32', 'void')
# word size
camera_read_data.define_builtin('sceptic_camera_read_data', 'i32', 'void')
temperature_read_data.define_builtin('sceptic_read_temperature', '', 'void')
voc_read_data.define_builtin('sceptic_read_voc', '', 'void')

View File

@ -0,0 +1,143 @@
import random
import time
from ScEpTIC.AST.builtins.builtin import Builtin
from ScEpTIC.AST.elements.value import Value
"""
some stdio.h library functions
"""
class debug_print(Builtin):
"""
Definition of the debug_print() builtin
Note: you can use debug_print() inside your code and will print all the arguments that it receives.
"""
def get_val(self):
print('[Debug Print]: {}'.format(self.args))
return 0
class memcmp(Builtin):
"""
Definition of the memcmp() builtin
"""
def get_val(self):
buffer_1_addr = self.args[0]
buffer_2_addr = self.args[1]
# element number of i8 -> bytes
buffer_length = self.args[2]
buffer_1 = self._vmstate.memory.get_cells_from_address(buffer_1_addr, buffer_length, False)
self._vmstate.memory.simulate_mmu_read_on_cells(buffer_1)
buffer_2 = self._vmstate.memory.get_cells_from_address(buffer_2_addr, buffer_length, False)
self._vmstate.memory.simulate_mmu_read_on_cells(buffer_2)
for i in range(0, len(buffer_1)):
val_1 = buffer_1[i].content
if isinstance(val_1, str):
val_1 = ord(val_1)
val_2 = buffer_2[i].content
if isinstance(val_2, str):
val_2 = ord(val_2)
# compensate for memory representation
if val_1 < 0:
val_1 = Value.convert_sint_to_uint(val_1, buffer_1[i].dimension*8)
if val_2 < 0:
val_2 = Value.convert_sint_to_uint(val_2, buffer_2[i].dimension*8)
if val_1 != val_2:
return val_1 - val_2
return 0
class randombuiltin(Builtin):
"""
Definition of the rand() builtin
"""
def get_val(self):
RAND_MAX = 32767
return random.randint(0, RAND_MAX)
class srandombuiltin(Builtin):
"""
Definition of the srand() builtin
"""
def get_val(self):
random.seed(int(self.args[0]))
class exitbuiltin(Builtin):
"""
Definition of the exit() builtin
"""
def get_val(self):
# set program to its end
self._vmstate.register_file.pc.update(self._vmstate.main_function_name, self._vmstate.main_function_len)
# set main return value to the one passed to exit
self._vmstate.main_return_value = self.args[0]
class timebuiltin(Builtin):
"""
Definiton of the time() builtin
"""
def get_val(self):
return int(time.time())
class strlen(Builtin):
"""
Definition of the strlen() builtin
"""
def get_val(self):
base_addr = self.args[0]
str_len = 0
bits = 8
while True:
addr = self._vmstate.memory.add_offset(base_addr, bits*str_len)
data = self._vmstate.memory.get_cells_from_address(addr, bits)
self._vmstate.memory.simulate_mmu_read_on_cells(data)
if len(data) != 1:
raise Exception("strlen() error: multiple cells within 8 bits!")
if data[0].content == 0:
break
str_len += 1
return str_len
# custom print
debug_print.define_builtin('debug_print', 'i32', 'void')
# definition for memcmp() and strncmp()
memcmp.define_builtin('memcmp', 'i8, i8, i64', 'i32')
memcmp.define_builtin('strncmp', 'i8, i8, i64', 'i32')
# random functions
randombuiltin.define_builtin('rand', '', 'i32')
srandombuiltin.define_builtin('srand', 'i32', 'void')
# definition for exit()
exitbuiltin.define_builtin('exit', 'i32', 'void')
strlen.define_builtin('strlen', 'i8', 'i64')
timebuiltin.define_builtin('time', '', 'i32')

View File

@ -0,0 +1,249 @@
import logging
from ScEpTIC.AST.elements.function import Function
from ScEpTIC.AST.elements.instructions.memory_operations import AllocaOperation, LoadOperation, StoreOperation
from ScEpTIC.AST.elements.instructions.termination_instructions import ReturnOperation
from ScEpTIC.AST.elements.value import Value
from ScEpTIC.llvmir_parser.sections_parser import global_vars
class BuiltinLinker:
"""
This class generates the code of a builtin function.
Builtin functions are implementation of library functions (e.g. sqrt, pow) that performs some
architecture-dependent operations, which doesn't modify the control flow nor the memory (SRAM, NVM).
In case control-flow modification or memory accesses are needed, it is better to directly create some C code
for them and include it in the final code to be analyzed.
Builtins are just used to not include in the final code library operations, and should not be used to
emulate memory accesses, global variables accesses, direct-memory writes and other major operations,
which can be easily implemented in the program code.
An example of a builtins are mathematical functions and printing functions.
The core of a builtin is an extension of the Instruction operation. This is needed to be able to analyze the code.
Normally LLVM will consider the arguments to be from virtual register %0 to %(#args - 1)
and the %(#args) is the ID of the first basic block. Since the builtins should not have labels and control-flow operations,
it is omitted.
Arguments are passed from %0 to %(len(args) - 1)
Arguments addresses in stack are stored from %(len(args)) to %(len(args)*2 - 1)
Arguments values are loaded from %(len(args) * 2) to %(len(args) * 3 - 1)
Return value will be on %(len(args) * 3)
"""
prefix = None
def __init__(self, name, arguments, return_type, instruction):
self.name = "{}{}".format(self.prefix if self.prefix is not None else '', name)
# link only if a declaration exists, which means that the function is used inside the program.
if self.name not in Function.declarations:
return None
self.arguments = self._parse_arguments(arguments)
self.return_type = global_vars.parse_type(return_type, True, True)
self.body = []
self._attach_instruction(instruction)
logging.info('[BuiltinLinker] Linking {} into {}'.format(instruction.__name__, self.name))
self.is_input = instruction.is_input if hasattr(instruction, 'is_input') else False
self.input_name = instruction.input_name if self.is_input else None
self._link()
@staticmethod
def _parse_arguments(arguments):
"""
Parses the argument string and returns a list of types
"""
# no arguments
if len(arguments) == 0:
return []
arguments = arguments.split(',')
args = []
for i in arguments:
# remove initial spaces
if i[0] == ' ':
i = i[1:]
arg = global_vars.parse_type(i, True, True)
args.append(arg)
return args
def _link(self):
"""
Creates the function, attach the body and maps it into function_definitions.
"""
f = Function(self.name, self.return_type, self.arguments, None, None)
f.attach_body(self.body)
f.is_input = self.is_input
f.input_name = self.input_name
f.is_builtin = True
def _attach_instruction(self, instruction):
"""
Attach the actual ad-hoc code.
"""
self._arguments_init()
# set target register
target = self._get_return_register()
instruction = instruction()
instruction.target = target
args = self._get_arguments()
instruction.set_args(args)
self.body.append(instruction)
self._generate_return()
def _get_argument_type(self, argument_id):
"""
Returns the type of a given argument
"""
return self.arguments[argument_id]
def _get_argument_register_address(self, argument_id):
"""
Returns the register which contains the address in which the argument will be, as a Value.
"""
reg_id = '%{}'.format(len(self.arguments) + argument_id)
reg_type = self._get_argument_type(argument_id)
return Value('virtual_reg', reg_id, reg_type)
def _get_argument_register(self, argument_id):
"""
Returns the register containing the value of the given argument, as a Value.
"""
reg_id = '%{}'.format((len(self.arguments) * 2) + argument_id)
reg_type = self._get_argument_type(argument_id)
return Value('virtual_reg', reg_id, reg_type)
def _get_passed_argument_register(self, argument_id):
"""
Returns the virtual register in which the passed argument is.
"""
reg_id = '%{}'.format(argument_id)
reg_type = self._get_argument_type(argument_id)
return Value('virtual_reg', reg_id, reg_type)
def _generate_save_argument_in_stack(self, argument_id):
"""
Generates the operations needed to save the given argument onto the stack.
"""
# allocate space on stack
target = self._get_argument_register_address(argument_id)
elements_number = 1
align = 0
alloca = AllocaOperation(target, target.type, elements_number, align)
self.body.append(alloca)
# save passed argument onto the stack
value = self._get_passed_argument_register(argument_id)
align = 0
volatile = False
store = StoreOperation(target, value, align, volatile)
self.body.append(store)
def _save_arguments(self):
"""
Saves the arguments onto the stack.
"""
for i in range(0, len(self.arguments)):
self._generate_save_argument_in_stack(i)
def _generate_load_argument_from_stack(self, argument_id):
"""
Generates the load operation which loads from stack into a register the given argument.
"""
target = self._get_argument_register(argument_id)
load_type = self._get_argument_type(argument_id)
element = self._get_argument_register_address(argument_id)
align = 0
volatile = False
load = LoadOperation(target, load_type, element, align, volatile)
self.body.append(load)
def _load_arguments(self):
"""
Loads the arguments onto the stack.
"""
for i in range(0, len(self.arguments)):
self._generate_load_argument_from_stack(i)
def _arguments_init(self):
"""
Saves onto the stack the arguments and loads them into the corresponding register.
"""
self._save_arguments()
self._load_arguments()
def _get_arguments(self):
"""
Returns the arguments (as Value virtual_reg) of the builtin.
This must be used by the actual builtin-code.
"""
args = []
for i in range(0, len(self.arguments)):
arg = self._get_argument_register(i)
args.append(arg)
return args
def _get_return_register(self):
"""
Returns the register in which the return value will be contained.
"""
reg_id = '%{}'.format(len(self.arguments) * 3)
return Value('virtual_reg', reg_id, self.return_type)
def _generate_return(self):
"""
Generates the return instruction needed at the end of the builtin.
"""
value = self._get_return_register()
ret = ReturnOperation(value)
self.body.append(ret)

View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,226 @@
import copy
from ScEpTIC import tools
from ScEpTIC.AST.elements.instructions.memory_operations import AllocaOperation
from ScEpTIC.AST.elements.instructions.other_operations import CallOperation
from ScEpTIC.AST.elements.instructions.termination_instructions import BranchOperation
from ScEpTIC.exceptions import RuntimeException
class Function:
"""
AST node representing a function.
Contains the function's definition and body.
"""
elements = {}
declarations = {}
def __init__(self, name, return_type, arguments, attr_groups, metadata):
self.name = name
self.type = return_type
# is a list of Types with additional attributes field.
self.arguments = arguments
self.attr_groups = attr_groups
self.metadata = metadata
self.body = None
# dictionary that maps a label with the first instruction of the corresponding basic block
self._instr_label_maps = {}
self.declarations[self.name] = self
# instance of the object in charge of performing register allocation over this function.
# it is assigned by the register allocation and is used to perform pre and post processing.
self.register_allocator = None
# contains the names of the functions called by the current one. It is used by register
# allocation, for finding the overall number of used registers, to emulate the saving of registers
# before running a function. It also helps in finding the best possible allocation of registers
# w.r.t. different functions.
# NB: in self._calls recursion is not considered, since the number of registers to be saved
# will be equal to the number of registers used by the function.
self._calls = []
self.is_input = False
self.input_name = None
self.is_builtin = False
def __str__(self):
retval = '{} {}()'.format(self.type, self.name, self.arguments)
if self.body is not None:
retval += '\n'+tools.fancy_list_to_str(self.body)
return retval
def __len__(self):
return len(self.body)
@property
def first_basic_block_id(self):
"""
Returns the id of the first basic block.
If the function has some arguments, each argument is mapped starting from %0 to %(#args - 1)
The first basic block label will be %(#args)
"""
return '%{}'.format(len(self.arguments))
def attach_body(self, body):
"""
Sets the body of a function definition and creates the mapping between instruction id and labels.
"""
self.elements[self.name] = self
del self.declarations[self.name]
self.body = body
self.update_labels()
self._populate_calls()
self._adjust_useless_branch_ticks()
def update_body(self, new_body):
"""
Updates the body of the function
"""
self.body = copy.deepcopy(new_body)
self.update_labels()
self._populate_calls()
def update_labels(self):
"""
Updates the label-instruction mappings
"""
self._instr_label_maps = {}
for i in range(0, len(self.body)):
instruction = self.body[i]
if instruction.label is not None:
self._instr_label_maps[instruction.label] = i
def get_instruction_id(self, label):
"""
Returns the instruction which has a given label.
"""
if label not in self._instr_label_maps:
raise RuntimeException('Unable to find label {} in function {}.'.format(label, self.name))
return self._instr_label_maps[label]
def get_ignore(self):
"""
Returns a list of register names to be ignored by register allocation.
For a function corresponds to the registers used to pass the parameters, which are from
%0 to %(n-1) (n = number of arguments).
"""
ignores = []
for i in range(0, len(self.arguments)):
ignores.append('%{}'.format(i))
return ignores
def _populate_calls(self):
"""
Populates the self._call list.
"""
self._calls = []
for i in self.body:
# if is a call operation, and is not a call to the same function append it to self._calls
# recursion is not considered, since the obtained list is used to verify the maximun number of
# register used by a function and a call to the same function won't change the number of registers
# used by the function.
if isinstance(i, CallOperation):
name = i.name
if name != self.name and name not in self._calls:
self._calls.append(name)
def _remove_from_calls(self, name):
"""
Removes a given function name from the self._call list
"""
try:
self._calls.remove(name)
# element not in list
except ValueError:
pass
def _adjust_useless_branch_ticks(self):
"""
This method virtually removes the useless branch instructions, which are
unconditional branches that jumps to the next instruction. Those branches are
necessary to have a basic-block division, but in the real final machine code they will
be removed. For such reason, the number of clock ticks to be increased by those branches is set to 0,
so to run them but to do not interfere with the analysis. (This allows me to not remove them)
"""
body_len = len(self.body)
# nb: unconditional branches have condition = None
for i in range(0, body_len):
code = self.body[i]
if isinstance(code, BranchOperation):
# if is not an unconditional branch, skip
if code.condition is not None:
continue
# next instruction id
next_instr_index = i+1
# if a next instruction exists && if the branch jumps to the next instruction, set its tick to 0
if next_instr_index < body_len and code.target_true == self.body[next_instr_index].label:
code.tick_count = 0
def adjust_alloca_ticks(self):
"""
This method sets all the ticks for alloca operations equal to 0, except for the last one.
Alloca operations are merged together during datalayout into a single ebp increment.
The last alloca is the one with tick = 1 for skipping the entire alloca section when running in
intermittent execution (otherwise multiple alloca operations might be run, but the result would be wrong)
"""
body_len = len(self.body)
last_alloca_id = -1
args = len(self.arguments)
# scan the code for alloca operations
for i in range(0, body_len):
code = self.body[i]
if not isinstance(code, AllocaOperation):
continue
last_alloca_id = i
code.tick_count = 0
var_metadata = code.metadata.retrieve() if code.metadata is not None else None
if var_metadata is None or 'variable_name' not in var_metadata:
metadata_append = ''
else:
metadata_append = ' "{}"'.format(var_metadata['variable_name'])
metadata_prepend = 'function argument' if i < args else 'local variable'
code.metadata = '{}{}'.format(metadata_prepend, metadata_append)
if last_alloca_id >= 0:
self.body[last_alloca_id].tick_count = 1

View File

@ -0,0 +1,43 @@
import collections
class GlobalVar:
"""
AST node representing a global variable
"""
# use a ordered dict to keep global variables in the same order as specified by the programmer.
elements = collections.OrderedDict()
def __init__(self, name, var_type, initial_val, is_constant, align, section, comdat, metadata):
self.name = name
self.is_constant = is_constant
self.type = var_type
self.initial_val = initial_val
if align is None:
align = 0
self.align = int(align)
self.section = section
self.comdat = comdat
self.metadata = metadata
self.elements[self.name] = self
def __str__(self):
retval = 'global_var: '
if self.section is not None:
retval += '['+str(self.section)+'] '
retval += '{} {}'.format(self.type, self.name)
if self.initial_val is not None:
retval += ' = {}'.format(self.initial_val)
return retval

View File

@ -0,0 +1,142 @@
import logging
from ScEpTIC import tools
class Instruction:
"""
Generic AST Instruction
"""
_vmstate = None
# number of clock tick to be increased by this instruction
tick_count = 1
memory_tick_count = 0
def __init__(self):
self.basic_block_id = None
self.label = None
self.preds = None
self.metadata = None
self._omit_target = False
self.memory_tag = None
self.memory_tag_dependency = None
self.is_part_of_dummy_write = False
self.is_dummy_write_master = False
self.dummy_write_master = None
def __str__(self):
retstr = ''
if self.label is not None:
retstr += '[{}] '.format(self.label)
if 'target' in self.__dict__ and not self._omit_target and self.target is not None:
retstr += '{} = '.format(self.target)
return retstr
def save_in_target_register(self, value):
"""
Saves a given value in the target register of the instruction, if present.
"""
if self._omit_target or self.target is None:
return
# get target register name
target = self.target.value
self._vmstate.register_file.write(target, value)
if self._vmstate.input_lookup_enabled:
input_lookup_data = self.get_input_lookup()
self._vmstate.register_file.set_input_lookup(target, input_lookup_data)
logging.info('[{}] Saving result in {}'.format(self.instruction_type, target))
def run(self, update_program_counter=True):
"""
Executes the operation and the target assignment.
"""
value = self.get_val()
self.save_in_target_register(value)
# call run's callback
self._vmstate.on_run(self.tick_count, update_program_counter)
def get_defs(self):
"""
Returns a list of registers defined by this instruction. Usually its len will be 1 or 0.
(used by register allocation)
"""
if 'target' in self.__dict__.keys() and self.target is not None:
return self.target.get_uses()
return []
def get_uses(self):
"""
Returns a list containing the names of the registers used by this instruction.
(used by register allocation)
"""
return []
def get_ignore(self):
"""
Returns a list of register names to be ignored by register allocation.
"""
return []
def get_input_lookup(self):
"""
Returns the input lookup data for the current operation
"""
return tools.build_input_lookup_data(None, None)
@property
def instruction_type(self):
"""
Returns the instruction type
"""
return self.__class__.__name__
def replace_reg_name(self, old_reg_name, new_reg_name):
"""
Skeleton.
Replaces the name of a register used by the instruction with a new one.
(used by register allocation)
"""
pass
def resolve_memory_tag(self, elements):
"""
Resolves and returns the memory tag of the targeted element
"""
raise NotImplementedError(f"{self.__class__.__name__} does not implement resolve_memory_tag()")
def resolve_memory_address_chain(self, elements):
"""
Returns a list of all the instructions required to get the address of the targeted element(s)
"""
raise NotImplementedError(f"{self.__class__.__name__} does not implement resolve_memory_address_chain()")

View File

@ -0,0 +1,342 @@
import logging
from ScEpTIC import tools
from ScEpTIC.AST.elements.instruction import Instruction
from ScEpTIC.exceptions import RuntimeException
class BinaryOperation(Instruction):
"""
AST nodes for the LLVM Binary Instructions group
https://llvm.org/docs/LangRef.html#binaryops
NB: each result is stored as SIGNED int, even if the operation is unsigned.
The sign bit is an interpretation of UNSIGNED operations, which will manage that by converting the int to its unsigned equivalent.
"""
def __init__(self, operation_type, first_operand, second_operand, target, is_bitwise, specific_attributes):
super().__init__()
self.operation_type = operation_type
self.first_operand = first_operand
self.second_operand = second_operand
self.target = target
self.is_bitwise = is_bitwise
# just used to determine if a result is a "poison value", which is for back-end optimizations.
self.exact = 'exact' in specific_attributes
self.no_unsigned_wrap = 'nuw' in specific_attributes
self.no_signed_wrap = 'nsw' in specific_attributes
def __str__(self):
retstr = super().__str__()
retstr += '{} {}, {}'.format(self.operation_type, self.first_operand, self.second_operand)
return retstr
def get_input_lookup(self):
"""
Returns the input lookup data for the current operation
"""
first = self.first_operand.get_input_lookup()
second = self.second_operand.get_input_lookup()
return tools.merge_input_lookup_data(first, second)
def get_val(self):
"""
Returns the value obtained from the operation.
It converts address operands (if present) to relative spaces, applies the operation and converts them back to the absolute space.
"""
first_operand = self.first_operand.get_val()
second_operand = self.second_operand.get_val()
# address prefixes (populated if an operand is an address)
prefix1 = None
prefix2 = None
# if first_operand is an address, get prefix and relative address (which can be used in operations)
if self._vmstate.memory._is_absolute_address(first_operand):
prefix1, first_operand = self._vmstate.memory._parse_absolute_address(first_operand)
# if second_operand is an address, get prefix and relative address (which can be used in operations)
if self._vmstate.memory._is_absolute_address(second_operand):
prefix2, second_operand = self._vmstate.memory._parse_absolute_address(second_operand)
# normalize prefixes: if one is none, just copy from the other.
if prefix1 is None:
prefix1 = prefix2
elif prefix2 is None:
prefix2 = prefix1
val = self._get_val(first_operand, second_operand)
# if prefix is set the result is an address, so I must convert it from relative to absolute space.
if prefix1 is not None:
# NB: prefixes must be the same
if prefix1 != prefix2:
raise RuntimeException('Uncompatible address space {} {} for binary operation {}.'.format(prefix1, prefix2, self.operation_type))
val = self._vmstate.memory._convert_to_absolute_address(prefix1, val)
logging.info('[{}] Executing {} {}, {} with result {}'.format(self.instruction_type, self.operation_type, first_operand, second_operand, val))
return val
def get_uses(self):
"""
Returns a list containing the names of the registers used by this instruction.
(used by register allocation)
"""
first_reg = self.first_operand.get_uses()
second_reg = self.second_operand.get_uses()
return first_reg + second_reg
def replace_reg_name(self, old_reg_name, new_reg_name):
"""
Replaces the name of a register used by the instruction with a new one.
(used by register allocation)
"""
self.first_operand.replace_reg_name(old_reg_name, new_reg_name)
self.second_operand.replace_reg_name(old_reg_name, new_reg_name)
self.target.replace_reg_name(old_reg_name, new_reg_name)
def _get_val(self, first_operand, second_operand):
"""
Returns the value returned from the operation, given its operands.
"""
dim = len(self.first_operand.type)
if self.operation_type == 'add':
# force encoding
first_operand = int(first_operand)
second_operand = int(second_operand)
val = first_operand + second_operand
# force a number of bits
return self.first_operand.convert_sint_to_sint(val, dim)
elif self.operation_type == 'fadd':
first_operand = float(first_operand)
second_operand = float(second_operand)
return first_operand + second_operand
elif self.operation_type == 'sub':
first_operand = int(first_operand)
second_operand = int(second_operand)
val = first_operand - second_operand
return self.first_operand.convert_sint_to_sint(val, dim)
elif self.operation_type == 'fsub':
first_operand = float(first_operand)
second_operand = float(second_operand)
return first_operand - second_operand
elif self.operation_type == 'mul':
first_operand = int(first_operand)
second_operand = int(second_operand)
val = int(first_operand * second_operand)
return self.first_operand.convert_sint_to_sint(val, dim)
elif self.operation_type == 'fmul':
first_operand = float(first_operand)
second_operand = float(second_operand)
return float(first_operand * second_operand)
elif self.operation_type == 'udiv':
# unsigned operations considers their operands as unsigned.
# http://lists.llvm.org/pipermail/llvm-dev/2017-July/115975.html
dim = len(self.first_operand)
first_operand = int(first_operand)
second_operand = int(second_operand)
first_operand = self.first_operand.convert_sint_to_uint(first_operand, dim)
second_operand = self.second_operand.convert_sint_to_uint(second_operand, dim)
val = int(first_operand // second_operand)
# result is unsigned. Memory cells in my representation uses signed only.
return self.first_operand.convert_uint_to_sint(val, dim)
elif self.operation_type == 'sdiv':
first_operand = int(first_operand)
second_operand = int(second_operand)
val = int(first_operand // second_operand)
return self.first_operand.convert_sint_to_sint(val, dim)
elif self.operation_type == 'fdiv':
return float(first_operand / second_operand)
elif self.operation_type == 'urem':
# unsigned operations considers their operands as unsigned.
# http://lists.llvm.org/pipermail/llvm-dev/2017-July/115975.html
first_operand = int(first_operand)
second_operand = int(second_operand)
dim = len(self.first_operand)
first_operand = self.first_operand.convert_sint_to_uint(first_operand, dim)
second_operand = self.second_operand.convert_sint_to_uint(second_operand, dim)
val = int(first_operand % second_operand)
# result is unsigned. Memory cells in my representation uses signed only.
return self.first_operand.convert_uint_to_sint(val, dim)
elif self.operation_type == 'srem':
first_operand = int(first_operand)
second_operand = int(second_operand)
val = int(first_operand % second_operand)
return self.first_operand.convert_sint_to_sint(val, dim)
elif self.operation_type == 'frem':
first_operand = float(first_operand)
second_operand = float(second_operand)
return float(first_operand % second_operand)
elif self.operation_type == 'shl':
# logic implemented to overcome discrepancies between signed and unsigned results
# e.g. -1 << 2 != 65535 << 2 for 16bit operations
first_operand = self.first_operand.convert_sint_to_bin(first_operand, dim)
sh_len = int(second_operand)
shifted = first_operand[sh_len:]+sh_len*'0'
return self.first_operand.convert_bin_to_sint(shifted)
elif self.operation_type == 'lshr':
# logical shift right: append 0 to added left bits
first_operand = self.first_operand.convert_sint_to_bin(first_operand, dim)
sh_len = int(second_operand)
shifted = sh_len*'0'+first_operand[:dim-sh_len]
return self.first_operand.convert_bin_to_sint(shifted)
elif self.operation_type == 'ashr':
# arithmetical shift right: append sign bit to added left bits
first_operand = self.first_operand.convert_sint_to_bin(first_operand, dim)
sh_len = int(second_operand)
shifted = sh_len*first_operand[0]+first_operand[:dim-sh_len]
return self.first_operand.convert_bin_to_sint(shifted)
elif self.operation_type == 'and':
first_operand = int(first_operand)
second_operand = int(second_operand)
return int(first_operand & second_operand)
elif self.operation_type == 'or':
first_operand = int(first_operand)
second_operand = int(second_operand)
return int(first_operand | second_operand)
elif self.operation_type == 'xor':
first_operand = int(first_operand)
second_operand = int(second_operand)
return int(first_operand ^ second_operand)
# if gets there, the operation is not supported right now.
raise NotImplementedError('{} is not supported for now!'.format(self.operation_type))
def resolve_memory_tag(self, elements):
"""
Resolves and returns the memory tag of the targeted element
"""
if self.memory_tag is None:
if self.operation_type == 'add' or self.operation_type == 'fadd':
operation_tag = '+'
elif self.operation_type == 'sub' or self.operation_type == 'fsub':
operation_tag = '-'
elif self.operation_type == 'mul' or self.operation_type == 'fmul':
operation_tag = '*'
elif self.operation_type == 'udiv' or self.operation_type == 'sdiv' or self.operation_type == 'fdiv':
operation_tag = '/'
elif self.operation_type == 'urem' or self.operation_type == 'srem' or self.operation_type == 'frem':
operation_tag = '%'
elif self.operation_type == 'shl':
operation_tag = '<<'
elif self.operation_type == 'lshr' or self.operation_type == 'ashr':
operation_tag = '>>'
elif self.operation_type == 'and':
operation_tag = '&&'
elif self.operation_type == 'or':
operation_tag = '||'
elif self.operation_type == 'xor':
operation_tag = '^'
else:
raise NotImplementedError(f"resolve_memory_tag() not implemented for binary operation type {self.operation_type}")
first_operand_tag = self.first_operand.resolve_memory_tag(elements)
second_operand_tag = self.second_operand.resolve_memory_tag(elements)
self.memory_tag = f"{first_operand_tag} {operation_tag} {second_operand_tag}"
return self.memory_tag
def resolve_memory_tag_dependency(self, elements):
if self.memory_tag_dependency is None:
first_operand_tag = self.first_operand.resolve_memory_tag_dependency(elements)
second_operand_tag = self.second_operand.resolve_memory_tag_dependency(elements)
self.memory_tag_dependency = [first_operand_tag, second_operand_tag]
return self.memory_tag_dependency
def resolve_memory_address_chain(self, elements):
"""
Returns a list of all the instructions required to get the address of the targeted element(s)
"""
chain = []
if self.target is not None:
chain.append(self)
chain.append(self.first_operand.resolve_memory_address_chain(elements))
chain.append(self.second_operand.resolve_memory_address_chain(elements))
return chain

View File

@ -0,0 +1,239 @@
import logging
from ScEpTIC import tools
from ScEpTIC.AST.elements.instruction import Instruction
class ConversionOperation(Instruction):
"""
AST nodes for the LLVM Bitwise Instructions group
https://llvm.org/docs/LangRef.html#bitwiseops
"""
def __init__(self, conversion_type, operand, target_type, target):
super().__init__()
self.conversion_type = conversion_type
self.operand = operand
self.target_type = target_type
self.target = target
# instructions that do not correspond to any ISA instruction
if self.conversion_type in ['bitcast', 'addrspacecast', 'inttoptr', 'ptrtoint']:
self.tick_count = 0
def __str__(self):
retstr = super().__str__()
retstr += '{} {} to {}'.format(self.conversion_type, self.operand, self.target_type)
return retstr
def get_input_lookup(self):
"""
Returns the input lookup data for the current operation
"""
operand = self.operand.get_input_lookup()
return tools.merge_input_lookup_data(operand, tools.build_input_lookup_data(None, None))
def get_val(self):
"""
Returns the value obtained from the operation.
It converts address operands (if present) to relative spaces, applies the operation and converts them back to the absolute space.
"""
operand = self.operand.get_val()
prefix = None
# if operand is an address, get prefix and relative address (which can be used in conversion)
if self._vmstate.memory._is_absolute_address(operand):
prefix, operand = self._vmstate.memory._parse_absolute_address(operand)
value = self._get_val(operand)
# if prefix is set, value is an address. If so, convert it from relative to absolute space.
if prefix is not None:
value = self._vmstate.memory._convert_to_absolute_address(prefix, value)
logging.info('[{}] Executing {} on {} with result {}'.format(self.instruction_type, self.conversion_type, operand, value))
return value
def get_uses(self):
"""
Returns a list containing the names of the registers used by this instruction.
(used by register allocation)
"""
return self.operand.get_uses()
def replace_reg_name(self, old_reg_name, new_reg_name):
"""
Replaces the name of a register used by the instruction with a new one.
(used by register allocation)
"""
self.operand.replace_reg_name(old_reg_name, new_reg_name)
if self.target is not None:
self.target.replace_reg_name(old_reg_name, new_reg_name)
def _get_val(self, value):
"""
Returns the value returned from the operation, given its operands.
"""
if self.conversion_type == 'addrspacecast':
# is like bitcast, but for converting the address space
return value
elif self.conversion_type == 'fpext':
value = float(value)
# converts a fp value to a larger-sized one.
# for how data is represented in the simulator, it does nothing
return value
elif self.conversion_type == 'fptosi':
# fp to signed int conversion
value = float(value)
dim = len(self.target_type)
# convert to int value
value = int(round(value))
# apply max dimension
value = self.operand.convert_sint_to_sint(value, dim)
return value
elif self.conversion_type == 'fptoui':
# float to unsigned int conversion
value = float(value)
dim = len(self.target_type)
# convert to int value
value = int(round(value))
# apply max dimension
value = self.operand.convert_sint_to_uint(value, dim)
# NB: elements in registers / memory are saved as SIGNED int.
# Operations that uses UNSIGNED operands, first converts the SIGNED operand to UNSIGNED one.
# (SIGNED VS UNSIGNED is just a mode of interpretation)
return value
elif self.conversion_type == 'fptrunc':
# converts a fp value to a smaller-sized one.
# for how data is represented in the simulator, it does nothing
value = float(value)
return value
elif self.conversion_type == 'bitcast':
# bitcast does not apply any conversion.
# it just tells the compiler to treat data as it was already written as "target_type"
return value
elif self.conversion_type == 'inttoptr':
# returns back the integer as memory location (address)
# for how addresses are represented, it does nothing.
return value
elif self.conversion_type == 'ptrtoint':
# returns the memory address as an integer
# for how addresses are represented, it does nothing.
return value
elif self.conversion_type == 'sext':
value = int(value)
initial_dim = len(self.operand)
value = self.operand.convert_sint_to_bin(value, initial_dim)
target_dim = len(self.target_type)
prefix = value[0] * (target_dim - initial_dim)
value = prefix + value
return self.operand.convert_bin_to_sint(value)
elif self.conversion_type == 'sitofp':
# convert an integer to a float
return float(value)
elif self.conversion_type == 'uitofp':
# convert an unsigned integer to a float
# convert memory cell value to unsigned int.
value = int(value)
value = self.operand.convert_sint_to_uint(value, len(self.operand))
return float(value)
elif self.conversion_type == 'trunc':
dim = len(self.operand)
value = self.operand.convert_sint_to_bin(int(value), dim)
trunc = dim-len(self.target_type)
value = self.operand.convert_bin_to_sint(value[trunc:])
return value
elif self.conversion_type == 'zext':
initial_dim = len(self.operand)
value = self.operand.convert_sint_to_bin(value, initial_dim)
dim = len(self.target_type)
value = '0' * (dim - initial_dim) + value
# for how data is represented, zero expansion does nothing
return self.operand.convert_bin_to_sint(value)
# if gets there, the conversion mode is not supported.
raise NotImplementedError('{} is not supported for now!'.format(self.conversion_type))
def resolve_memory_tag(self, elements):
"""
Resolves and returns the memory tag of the targeted element
"""
if self.memory_tag is None:
self.memory_tag = self.operand.resolve_memory_tag(elements)
return self.memory_tag
def resolve_memory_tag_dependency(self, elements):
if self.memory_tag_dependency is None:
self.memory_tag_dependency = self.operand.resolve_memory_tag_dependency(elements)
return self.memory_tag_dependency
def resolve_memory_address_chain(self, elements):
"""
Returns a list of all the instructions required to get the address of the targeted element(s)
"""
chain = []
if self.target is not None:
chain.append(self)
chain.append(self.operand.resolve_memory_address_chain(elements))
return chain

View File

@ -0,0 +1,588 @@
import logging
from ScEpTIC import tools
from ScEpTIC.AST.elements.instruction import Instruction
from ScEpTIC.AST.misc.virtual_memory_enum import VirtualMemoryEnum
class AllocaOperation(Instruction):
"""
AST node of the LLVM Memory Instructions group - Alloca Instruction
https://llvm.org/docs/LangRef.html#memoryops
"""
def __init__(self, target, element_type, elements_number, align):
super().__init__()
self.target = target
self.type = element_type
self.elements_number = int(elements_number)
self.align = int(align)
self.is_first = False
def __str__(self):
retstr = super().__str__()
retstr += 'alloca {} x {}'.format(self.type, self.elements_number)
return retstr
def run(self, update_program_counter=True):
"""
Executes the operation and the target assignment.
(Update program counter ignored -> always True)
"""
target = self.target.value
dimension = len(self.type) * self.elements_number
address = self._vmstate.memory.stack.allocate(dimension, True, self.metadata, self.align)
# use special write_address (datalayout omitted, value can't be stored in a physical register
# since it is resolved and directly-placed by the compiler's backend)
self._vmstate.register_file.write_address(target, address)
# call run's callback
self._vmstate.on_run(self.tick_count)
logging.info('[{}] Allocating {} bits in stack at address {} ({}).'.format(self.instruction_type, dimension, address, target))
def get_uses(self):
"""
Returns a list containing the names of the registers used by this instruction.
(used by register allocation)
"""
# no register used
return []
# TODO: check if this is necessary (before was returning an empty set)
#def get_defs(self):
# """
# Returns a list of registers defined by this instruction.
# (used by register allocation)
# """
#
# # no register defined
# return []
def get_ignore(self):
"""
Returns a list of register names to be ignored by register allocation.
For alloca operation it is the target register.
"""
return self.target.get_uses()
def get_input_lookup(self):
"""
Returns the input lookup data for the current operation
"""
return tools.build_input_lookup_data(None, None)
def resolve_memory_tag(self, elements):
"""
Resolves and returns the memory tag of the targeted element
"""
if self.memory_tag is None:
if self.metadata is None:
if self.is_first:
self.memory_tag = "first_alloca"
else:
raise Exception(f"No metadata available for {self}")
else:
metadata = self.metadata.retrieve()
self.memory_tag = metadata['variable_name']
return self.memory_tag
def resolve_memory_tag_dependency(self, elements):
if self.memory_tag_dependency is None:
self.memory_tag_dependency = self.resolve_memory_tag(elements)
return self.memory_tag_dependency
def resolve_memory_address_chain(self, elements):
"""
Returns a list of all the instructions required to get the address of the targeted element(s)
"""
return [self]
class LoadOperation(Instruction):
"""
AST node of the LLVM Memory Instructions group - Load Instruction
https://llvm.org/docs/LangRef.html#memoryops
"""
def __init__(self, target, load_type, element, align, volatile):
super().__init__()
self.target = target
self.type = load_type
self.element = element
self.align = int(align)
self.is_volatile = volatile
self.virtual_memory_target = VirtualMemoryEnum.VOLATILE
self.virtual_memory_normalized = False
self.has_virtual_memory_copy = False
self.virtual_memory_copy = None
self.is_virtual_memory_copy = False
# In llvmir the arguments of a function are not stored in stack, but passed as virtual registers
# or immediate values to the call().
# To emulate the storing of the values onto the stack, save those values as address registers.
# The register allocation step needs only to set self.is_arg_of_function_call to True, without touching
# the target virtual register.
# The arguments are loaded then onto the stack when the function needs them (the store is generated in the llvm ir already)
self.is_arg_of_function_call = False
def __str__(self):
retstr = super().__str__()
s_type = str(self.type)
if self.element.type is not None:
s_type = ''
retstr += 'load {} {}{}'.format(s_type, self.element, ' [arg]' if self.is_arg_of_function_call else '')
retstr += f" (${self.virtual_memory_target.value})"
if self.virtual_memory_normalized:
retstr += " {NORMALIZED}"
if self.is_virtual_memory_copy:
retstr += " {COPY}"
return retstr
def run(self, update_program_counter=True):
"""
Executes the load operation and the target assignment.
(Update program counter ignored -> always True)
"""
value = self.get_val()
# explained in __init__ comment
if self.is_arg_of_function_call:
target = self.target.value
self._vmstate.register_file.write_address(target, value)
if self._vmstate.input_lookup_enabled:
input_lookup_data = self.get_input_lookup()
self._vmstate.register_file.set_address_input_lookup(target, input_lookup_data)
else:
self.save_in_target_register(value)
# call run's callback
self._vmstate.on_run(self.tick_count)
logging.info('[{}] Loading {} into {}.'.format(self.instruction_type, value, self.target.value))
def get_val(self):
"""
Executes the operation and the target assignment.
"""
address = self.element.get_val()
dimension = len(self.type)
# read from memory
value = self._vmstate.memory.read(address, dimension)
return value
def get_input_lookup(self):
"""
Returns the input lookup data for the current operation
"""
address = self.element.get_val()
return self._vmstate.memory.get_cell_input_lookup(address)
def get_load_address(self):
"""
Returns the address to be loaded.
"""
return self.element.get_val()
def replace_reg_name(self, old_reg_name, new_reg_name):
"""
Replaces the name of a register used by the instruction with a new one.
(used by register allocation)
"""
self.element.replace_reg_name(old_reg_name, new_reg_name)
self.target.replace_reg_name(old_reg_name, new_reg_name)
def get_uses(self):
"""
Returns a list containing the names of the registers used by this instruction.
(used by register allocation)
"""
return self.element.get_uses()
def get_ignore(self):
"""
Returns a list of register names to be ignored by register allocation.
For load operation it is the target register, if the load operation is marked to be used
as argument of a function call, since it will be loaded as a stack offset.
"""
# if loads an argument of a function call, it is an address register (stack location)
# so it must be ignored on register allocation.
if self.is_arg_of_function_call:
return self.target.get_uses()
return []
def resolve_memory_tag(self, elements):
"""
Resolves and returns the memory tag of the targeted element
"""
if self.memory_tag is None:
self.memory_tag = self.element.resolve_memory_tag(elements)
return self.memory_tag
def resolve_memory_tag_dependency(self, elements):
if self.memory_tag_dependency is None:
self.memory_tag_dependency = self.element.resolve_memory_tag_dependency(elements)
return self.memory_tag_dependency
def resolve_memory_address_chain(self, elements):
"""
Returns a list of all the instructions required to get the address of the targeted element(s)
"""
return [self, self.element.resolve_memory_address_chain(elements)]
def get_memory_address(self):
"""
:return: the address of the targeted memory cell
"""
return self.get_load_address()
class StoreOperation(Instruction):
"""
AST node of the LLVM Memory Instructions group - Store Instruction
https://llvm.org/docs/LangRef.html#memoryops
"""
def __init__(self, target, value, align, volatile):
super().__init__()
self.target = target
self.value = value
self.align = int(align)
self.is_volatile = volatile
self.virtual_memory_target = VirtualMemoryEnum.VOLATILE
self.virtual_memory_normalized = False
self.has_virtual_memory_copy = False
self.virtual_memory_copy = None
self.is_virtual_memory_copy = False
self._omit_target = True
def __str__(self):
retstr = super().__str__()
retstr += 'store {} in {}'.format(self.value, self.target)
retstr += f" (${self.virtual_memory_target.value})"
if self.virtual_memory_normalized:
retstr += " {NORMALIZED}"
if self.is_virtual_memory_copy:
retstr += " {COPY}"
return retstr
def run(self, update_program_counter=True):
"""
Executes the operation and the target assignment.
(Update program counter ignored -> always True)
"""
address = self.target.get_val()
dimension = len(self.value.type)
content = self.value.get_val()
# write into memory if it is not a dummy write
if not self.is_part_of_dummy_write:
self._vmstate.memory.write(address, dimension, content)
if self._vmstate.input_lookup_enabled:
input_lookup_data = self.value.get_input_lookup()
self._vmstate.memory.set_cell_input_lookup(address, input_lookup_data)
# call run's callback
self._vmstate.on_run(self.tick_count)
logging.info('[{}] Saving {} into {}.'.format(self.instruction_type, content, self.target.value))
def get_uses(self):
"""
Returns a list containing the names of the registers used by this instruction.
(used by register allocation)
"""
return self.value.get_uses() + self.target.get_uses()
def get_defs(self):
"""
Returns a list of registers defined by this instruction.
(used by register allocation)
"""
return []
def get_store_address(self):
"""
Returns the address in which the value will be stored.
"""
return self.target.get_val()
def replace_reg_name(self, old_reg_name, new_reg_name):
"""
Replaces the name of a register used by the instruction with a new one.
(used by register allocation)
"""
self.value.replace_reg_name(old_reg_name, new_reg_name)
self.target.replace_reg_name(old_reg_name, new_reg_name)
def resolve_memory_tag(self, elements):
"""
Resolves and returns the memory tag of the targeted element
"""
if self.memory_tag is None:
self.memory_tag = self.target.resolve_memory_tag(elements)
return self.memory_tag
def resolve_memory_tag_dependency(self, elements):
if self.memory_tag_dependency is None:
self.memory_tag_dependency = self.target.resolve_memory_tag_dependency(elements)
return self.memory_tag_dependency
def resolve_memory_address_chain(self, elements):
"""
Returns a list of all the instructions required to get the address of the targeted element(s)
"""
return [self, self.target.resolve_memory_address_chain(elements)]
def get_memory_address(self):
"""
:return: the target address of the store
"""
return self.get_store_address()
class GetElementPointerOperation(Instruction):
"""
AST node of the LLVM Memory Instructions group - GetElementPointer Instruction
https://llvm.org/docs/LangRef.html#memoryops
"""
def __init__(self, target, element, base_type, indexes, inbounds):
super().__init__()
self.target = target
self.element = element
self.type = base_type
# indexes is a list of Values with an additional inrage attribute (either True or False)
self.indexes = indexes
self.inbounds = inbounds
def __str__(self):
retstr = super().__str__()
s_indexes = ''
for i in self.indexes:
s_indexes = '{}[{}]'.format(s_indexes, i)
retstr += 'getelementpointer {} {}'.format(self.element, s_indexes)
return retstr
def get_val(self):
"""
Returns the represented absolute address.
"""
# get relative address to perform computation
address = self.element.get_val()
if address in self._vmstate.functions:
raise NotImplementedError("Direct manipulation of function pointers not supported!")
prefix, base_address = self._vmstate.memory._parse_absolute_address(address)
# first index is the offset from the base_address (dimension of spacing = overall size of one element of composition self.type)
# (is like "pointer" spacing)
offset = len(self.type) * self.indexes[0].get_val()
# C-like indexes starts from the second element of self.indexes
indexes = self.indexes[1:]
composition = self.type.get_memory_composition()
#print(f"Index: {indexes}; Composition: {composition}; Initial offset = {offset}; First index: {self.indexes[0]}")
# each index is an instance of Value
for index in indexes:
index = index.get_val()
composition_elements = composition[0]
flat = []
# Array index (composition_elements = number of elements in the array)
# E.g. [3, 32] or [3, [3, 32]]
if isinstance(composition_elements, int):
self.type.flat_composition(composition[1], flat)
offset += index * sum(flat)
composition = composition[1]
# Struct selector (composition is the entire struct data)
# E.g. [[1, 32], [1, 16], [1, 18]]
elif isinstance(composition_elements, list):
prev_elements = composition[:index]
self.type.flat_composition(prev_elements, flat)
offset += sum(flat)
# next element -> sub-struct element
composition = composition[index]
else:
raise NotImplementedError("GetElementPointerOperation() error")
# offset is calculated in bits, address is in bytes
if offset % 8 != 0:
raise ValueError('Offset {} is not a multiple of a byte.'.format(offset))
#print("-> Offset: {}\n".format(offset))
offset = offset // 8 # int division
value = '{}{}'.format(prefix, hex(base_address + offset))
logging.info('[{}] Address resolved to {}.'.format(self.instruction_type, value))
return value
def get_uses(self):
"""
Returns a list containing the names of the registers used by this instruction.
(used by register allocation)
"""
uses = self.element.get_uses()
for index in self.indexes:
uses = uses + index.get_uses()
return uses
def replace_reg_name(self, old_reg_name, new_reg_name):
"""
Replaces the name of a register used by the instruction with a new one.
(used by register allocation)
"""
self.element.replace_reg_name(old_reg_name, new_reg_name)
for index in self.indexes:
index.replace_reg_name(old_reg_name, new_reg_name)
if self.target is not None:
self.target.replace_reg_name(old_reg_name, new_reg_name)
def get_input_lookup(self):
"""
Returns the input lookup data for the current operation
"""
# element is an address (or a reference to it), so does not have any input lookup info.
# indexes may have input lookup infos
lookup = tools.build_input_lookup_data(None, None)
for index in self.indexes:
lookup = tools.merge_input_lookup_data(lookup, index.get_input_lookup())
return lookup
def resolve_memory_tag(self, elements):
"""
Resolves and returns the memory tag of the targeted element
"""
if self.memory_tag is None:
memory_tag = str(self.element.resolve_memory_tag(elements))
for index in self.indexes:
memory_tag += f"[{index.resolve_memory_tag(elements)}]"
self.memory_tag = memory_tag
return self.memory_tag
def resolve_memory_tag_dependency(self, elements):
if self.memory_tag_dependency is None:
dep = [None]
for index in self.indexes:
dep.append(index.resolve_memory_tag(elements))
dep.append(index.resolve_memory_tag_dependency(elements))
self.memory_tag_dependency = dep
return self.memory_tag_dependency
def resolve_memory_address_chain(self, elements):
"""
Returns a list of all the instructions required to get the address of the targeted element(s)
"""
chain = []
if self.target is not None:
chain.append(self)
chain.append(self.element.resolve_memory_address_chain(elements))
for index in self.indexes:
chain.append(index.resolve_memory_address_chain(elements))
return chain

View File

@ -0,0 +1,646 @@
import copy
import logging
import math
from ScEpTIC import tools
from ScEpTIC.AST.elements.instruction import Instruction
from ScEpTIC.AST.elements.types import BaseType
from ScEpTIC.AST.misc.virtual_memory_enum import VirtualMemoryEnum
from ScEpTIC.exceptions import RuntimeException, RegAllocException
class CompareOperation(Instruction):
"""
AST node of the LLVM Other Instructions group - Compare Instruction
https://llvm.org/docs/LangRef.html#otherops
"""
def __init__(self, target, first_operand, second_operand, condition, comparation_type, operation_code):
super().__init__()
self.target = target
self.first_operand = first_operand
self.second_operand = second_operand
self.condition = condition
self.type = comparation_type
self.operation_code = operation_code
def __str__(self):
retstr = super().__str__()
retstr += 'cmp {}: {} vs {}'.format(self.condition, self.first_operand, self.second_operand)
return retstr
@staticmethod
def is_qnan(value):
"""
Returns if a value is a QNAN
"""
# if value is None -> QNAN (for my memory representation)
return value is None or math.isnan(value)
@staticmethod
def is_one_qnan(value1, value2):
"""
Returns if at least one of two values are QNAN
"""
return CompareOperation.is_qnan(value1) or CompareOperation.is_qnan(value2)
def _check_prefix(self, prefix1, prefix2):
"""
Verify if address prefixes are consistent.
"""
if prefix1 != prefix2:
raise RuntimeException('Unable to compare different address spaces ({} with {}) for the compare operation {}.'.format(prefix1, prefix2, self.condition))
def get_val(self):
"""
Returns the value obtained from the operation.
It converts address operands (if present) to relative spaces, and applies the comparisons.
"""
dim = len(self.first_operand.type)
first_operand = self.first_operand.get_val()
second_operand = self.second_operand.get_val()
# address prefixes (populated if an operand is an address)
prefix1 = None
prefix2 = None
# if first_operand is an address, get prefix and relative address (which can be used in comparisons)
if self._vmstate.memory._is_absolute_address(first_operand):
prefix1, first_operand = self._vmstate.memory._parse_absolute_address(first_operand)
# if second_operand is an address, get prefix and relative address (which can be used in comparisons)
if self._vmstate.memory._is_absolute_address(second_operand):
prefix2, second_operand = self._vmstate.memory._parse_absolute_address(second_operand)
# adjust prefixes if only one element is an address
# NB: is possible to compare an address with a direct value, and in this case the prefix can be ignored.
# (so I just copy it to the other prefix)
if prefix1 is None:
prefix1 = prefix2
elif prefix2 is None:
prefix2 = prefix1
logging.info('[{}] Executing {} {}{}, {}{}'.format(self.instruction_type, self.condition, '' if prefix1 is None else prefix1, first_operand, '' if prefix2 is None else prefix2, second_operand))
# comparison types
if self.condition == 'eq':
return prefix1 == prefix2 and first_operand == second_operand
elif self.condition == 'ne':
return prefix1 != prefix2 or first_operand != second_operand
elif self.condition == 'sgt':
self._check_prefix(prefix1, prefix2)
return first_operand > second_operand
elif self.condition == 'sge':
self._check_prefix(prefix1, prefix2)
return first_operand >= second_operand
elif self.condition == 'slt':
self._check_prefix(prefix1, prefix2)
return first_operand < second_operand
elif self.condition == 'sle':
self._check_prefix(prefix1, prefix2)
return first_operand <= second_operand
elif self.condition == 'ugt':
self._check_prefix(prefix1, prefix2)
if self.operation_code == 'fcmp':
# unordered check -> true if one is QNAN
return self.is_one_qnan(first_operand, second_operand) or first_operand > second_operand
else:
# converts to unsigned ints
first_operand = self.first_operand.convert_sint_to_uint(first_operand, dim)
second_operand = self.second_operand.convert_sint_to_uint(second_operand, dim)
return first_operand > second_operand
elif self.condition == 'uge':
self._check_prefix(prefix1, prefix2)
if self.operation_code == 'fcmp':
# unordered check -> true if one is QNAN
return self.is_one_qnan(first_operand, second_operand) or first_operand >= second_operand
else:
# converts to unsigned ints
first_operand = self.first_operand.convert_sint_to_uint(first_operand, dim)
second_operand = self.second_operand.convert_sint_to_uint(second_operand, dim)
return first_operand >= second_operand
elif self.condition == 'ult':
self._check_prefix(prefix1, prefix2)
if self.operation_code == 'fcmp':
# unordered check -> true if one is QNAN
return self.is_one_qnan(first_operand, second_operand) or first_operand < second_operand
else:
# converts to unsigned ints
first_operand = self.first_operand.convert_sint_to_uint(first_operand, dim)
second_operand = self.second_operand.convert_sint_to_uint(second_operand, dim)
return first_operand < second_operand
elif self.condition == 'ule':
self._check_prefix(prefix1, prefix2)
if self.operation_code == 'fcmp':
# unordered check -> true if one is QNAN
return self.is_one_qnan(first_operand, second_operand) or first_operand <= second_operand
else:
# converts to unsigned ints
first_operand = self.first_operand.convert_sint_to_uint(first_operand, dim)
second_operand = self.second_operand.convert_sint_to_uint(second_operand, dim)
return first_operand <= second_operand
elif self.condition == 'true':
return True
elif self.condition == 'false':
return False
elif self.condition == 'ord':
self._check_prefix(prefix1, prefix2)
return not self.is_one_qnan(first_operand, second_operand)
elif self.condition == 'oeq':
return not self.is_one_qnan(first_operand, second_operand) and prefix1 == prefix2 and first_operand == second_operand
elif self.condition == 'one':
return not self.is_one_qnan(first_operand, second_operand) and (prefix1 != prefix2 or first_operand != second_operand)
elif self.condition == 'ogt':
self._check_prefix(prefix1, prefix2)
return not self.is_one_qnan(first_operand, second_operand) and first_operand > second_operand
elif self.condition == 'oge':
self._check_prefix(prefix1, prefix2)
return not self.is_one_qnan(first_operand, second_operand) and first_operand >= second_operand
elif self.condition == 'olt':
self._check_prefix(prefix1, prefix2)
return not self.is_one_qnan(first_operand, second_operand) and first_operand < second_operand
elif self.condition == 'ole':
self._check_prefix(prefix1, prefix2)
return not self.is_one_qnan(first_operand, second_operand) and first_operand <= second_operand
elif self.condition == 'uno':
self._check_prefix(prefix1, prefix2)
return self.is_one_qnan(first_operand, second_operand)
elif self.condition == 'ueq':
return self.is_one_qnan(first_operand, second_operand) or (prefix1 == prefix2 and first_operand == second_operand)
elif self.condition == 'une':
return self.is_one_qnan(first_operand, second_operand) or (prefix1 != prefix2 or first_operand != second_operand)
# if gets there, the comparison mode is not supported.
raise NotImplementedError('Comparison mode "{}" not supported for now!'.format(self.condition))
def get_uses(self):
"""
Returns a list containing the names of the registers used by this instruction.
(used by register allocation)
"""
first_reg = self.first_operand.get_uses()
second_reg = self.second_operand.get_uses()
return first_reg + second_reg
def get_input_lookup(self):
"""
Returns the input lookup data for the current operation
"""
first = self.first_operand.get_input_lookup()
second = self.second_operand.get_input_lookup()
return tools.merge_input_lookup_data(first, second)
def replace_reg_name(self, old_reg_name, new_reg_name):
"""
Replaces the name of a register used by the instruction with a new one.
(used by register allocation)
"""
self.first_operand.replace_reg_name(old_reg_name, new_reg_name)
self.second_operand.replace_reg_name(old_reg_name, new_reg_name)
self.target.replace_reg_name(old_reg_name, new_reg_name)
class PhiOperation(Instruction):
"""
AST node of the LLVM Other Instructions group - Phi Instruction
https://llvm.org/docs/LangRef.html#otherops
"""
def __init__(self, target, return_type, values):
super().__init__()
self.target = target
self.type = return_type
# values is a dict with label as keys {labelid: value}
self.values = values
def __str__(self):
retstr = super().__str__()
retstr += 'phi {}: {}'.format(self.type, self.values)
return retstr
def get_val(self):
"""
Returns the value obtained from the operation.
"""
# get the basic block from which the current one is entered.
label = self._vmstate.register_file.last_basic_block
if label not in self.values:
raise RuntimeException('Basic Block {} not present in the choices of phi operation.'.format(label))
# get value
value = self.values[label].get_val()
logging.info('[{}] Executing PHI operation with result {} ({} taken).'.format(self.instruction_type, value, label))
return value
def get_uses(self):
"""
Returns a list containing the names of the registers used by this instruction.
(used by register allocation)
"""
uses = []
for value in self.values:
value = self.values[value].get_uses()
uses = uses + value
return uses
def get_input_lookup(self):
"""
Returns the input lookup data for the current operation
"""
label = self._vmstate.register_file.last_basic_block
return self.values[label].get_input_lookup()
def replace_reg_name(self, old_reg_name, new_reg_name):
"""
Replaces the name of a register used by the instruction with a new one.
(used by register allocation)
"""
for value in self.values:
self.values[value].replace_reg_name(old_reg_name, new_reg_name)
self.target.replace_reg_name(old_reg_name, new_reg_name)
class SelectOperation(Instruction):
"""
AST node of the LLVM Other Instructions group - Select Instruction
https://llvm.org/docs/LangRef.html#otherops
"""
def __init__(self, target, condition, first_operand, second_operand):
super().__init__()
self.target = target
self.condition = condition
self.first_operand = first_operand
self.second_operand = second_operand
def __str__(self):
retstr = super().__str__()
retstr += 'select {}: {}, {}'.format(self.condition, self.first_operand, self.second_operand)
return retstr
def get_val(self):
"""
Returns the result of the operation.
"""
# if condition resolves to 1 returns the first operand, otherwise the second one.
condition = self.condition.get_val()
logging.info('[{}] Executing Select operation with condition {}.'.format(self.instruction_type, condition))
if int(condition) == 1:
return self.first_operand.get_val()
else:
return self.second_operand.get_val()
def get_input_lookup(self):
"""
Returns the input lookup data for the current operation
"""
condition = self.condition.get_val()
if int(condition) == 1:
return self.first_operand.get_input_lookup()
else:
return self.second_operand.get_input_lookup()
def get_uses(self):
"""
Returns a list containing the names of the registers used by this instruction.
(used by register allocation)
"""
first_reg = self.first_operand.get_uses()
second_reg = self.second_operand.get_uses()
cond_reg = self.condition.get_uses()
return first_reg + second_reg + cond_reg
def replace_reg_name(self, old_reg_name, new_reg_name):
"""
Replaces the name of a register used by the instruction with a new one.
(used by register allocation)
"""
self.first_operand.replace_reg_name(old_reg_name, new_reg_name)
self.second_operand.replace_reg_name(old_reg_name, new_reg_name)
self.condition.replace_reg_name(old_reg_name, new_reg_name)
self.target.replace_reg_name(old_reg_name, new_reg_name)
class CallOperation(Instruction):
"""
AST node of the LLVM Other Instructions group - Call Instruction
https://llvm.org/docs/LangRef.html#otherops
"""
# push pc
# push ebp
# esp <- ebp
# pc = 0x...
# args are pushed/loaded by previously-inserted operations
tick_count = 4
memory_tick_count = 2
n_memory_instructions = 2
ignore = []
def __init__(self, target, function_name, return_type, function_signature, function_args, attrs):
super().__init__()
self.target = target
self.target_function = function_name
self.type = return_type
# is a possible empty list containing the function signature when var_args is used
# is a list of Type with additional attributes field
self.function_signature = function_signature
# is a list of Values with additional attributes field.
self.function_args = function_args
# all attributes used for back-end / middle-end optimizations.
# the only one needed are from 'return' and are zeroext and signext.
# the same applies for the function arguments's attributes.
# for how data is represented, only signext applies a modification on data,
# and since functions are user-defined and no 1bit data structure exists in C,
# sext won't have any unattended behaviour (e.g. i1 1 = -1), so can be omitted.
self.attributes = attrs
self.virtual_memory_target = VirtualMemoryEnum.VOLATILE
# checkpoint-related infos
self.checkpoint_save_regs = set()
self.checkpoint_save_pc = False
self.checkpoint_save_esp = False
self.checkpoint_save_ram = False
# Custom callback used for ISR and other functionalities
self.custom_callback = lambda: None
self.is_isr_call = False
def __str__(self):
retstr = super().__str__()
retstr += 'call {}({})'.format(self.name, self.function_args)
retstr += f" (${self.virtual_memory_target.value})"
if self.checkpoint_save_pc:
retstr += f"\n - save_pc: {self.checkpoint_save_pc}"
retstr += f"\n - save_esp: {self.checkpoint_save_esp}"
retstr += f"\n - save_regs: {self.checkpoint_save_regs}"
return retstr
def get_function_max_reg_usage(self):
"""
Returns the number of physical registers used by this function.
"""
function = self._vmstate.functions[self.name]
if 'max_reg_usage' not in function.__dict__:
raise RegAllocException('Register allocation not done on function {}'.format(self.name))
return function.max_reg_usage
@property
def name(self):
if isinstance(self.target_function, str):
return self.target_function
return self.target_function.value
def resolve_function_name(self):
if isinstance(self.target_function, str):
return self.target_function
return self.target_function.get_val()
def run(self, update_program_counter=True):
"""
Runs the call operation.
(Update program counter ignored -> always True)
"""
# run custom callback
self.custom_callback()
function_name = self.resolve_function_name()
logging.info('[{}] Calling function {}.'.format(self.instruction_type, function_name))
# instructions to be skipped (checkpoint, restore)
if function_name in self.ignore:
self._vmstate.register_file.pc.increment_pc()
return
args = []
input_lookups = []
# In a real scenario, args are saved onto the stack. In this case, instead, they are re-loaded before the
# function call and directly passed as values.
# The loading of each arguments results in a very similar number of operations as saving them into the stack
# but since the RegisterFile is stacked when the function call happens, I need to save their values and restore
# them back as virtual_registers or address_registers (in case of physical regfile) to be able to use them.
for arg in self.function_args:
arg_val = arg.get_val()
args.append(arg_val)
if self._vmstate.input_lookup_enabled:
if arg.value_class == 'virtual_reg':
input_lookup_data = self._vmstate.register_file.get_input_lookup(arg.value)
input_lookups.append(input_lookup_data)
else:
input_lookups.append(None)
# emulate stacking of arguments for stack's anomalies analysis
arg_count = len(self.function_args)
saved_params_number = min(arg_count - self._vmstate.register_file.param_regs_count, 0)
arg_count -= 1
# push parameters in reverse order. NB: some parameters are passed using registers
for i in range(arg_count, arg_count-saved_params_number, -1):
arg = self.function_args[i]
arg_len = len(arg.type)
arg_val = arg.get_val()
if self._vmstate.do_data_anomaly_check:
self._vmstate.function_call_lookup['args'].append(self._vmstate.memory.stack.top_address)
self._vmstate.memory.stack.push(arg_len, arg_val, 'function argument')
# callback
self._vmstate.on_function_call(function_name)
self._vmstate.global_clock += self.tick_count
f_args = self._vmstate.functions[function_name].arguments
var_arg = BaseType('var_args', '8')
for i in range(0, len(f_args)):
is_var_arg = f_args[i].base_type == var_arg
# if is a variable argument, from now insert elements inside a single list
if is_var_arg:
arg = []
for j in range(i, len(args)):
arg.append(args[j])
else:
arg = args[i]
# NB: here I use write_address, since for virtual regfile is like write()
# and for physical one is needed since the stack placement is emulated as a sequence of
# laod before their actual usage.
target = '%{}'.format(i)
self._vmstate.register_file.write_address(target, arg)
# set input lookup
if self._vmstate.input_lookup_enabled and not is_var_arg and input_lookups[i] is not None:
self._vmstate.register_file.set_address_input_lookup(target, input_lookups[i])
# if is var_arg, all arguments have been passed, so stop
if is_var_arg:
break
def get_type_from_virtual_reg(self, virtual_reg):
"""
Returns the type of an argument given its virtual register name.
"""
for arg in self.function_args:
# if the virtual reg is in the uses of the argument, the
# given type is the one of the argument.
if virtual_reg in arg.get_uses():
# returns a copy so to not change the original element
return copy.deepcopy(arg.type)
return None
def get_uses(self):
"""
Returns a list containing the names of the registers used by this instruction.
(used by register allocation)
"""
uses = []
for arg in self.function_args:
uses = uses + arg.get_uses()
return uses
def replace_reg_name(self, old_reg_name, new_reg_name):
"""
Replaces the name of a register used by the instruction with a new one.
(used by register allocation)
"""
for arg in self.function_args:
arg.replace_reg_name(old_reg_name, new_reg_name)
if self.target is not None:
self.target.replace_reg_name(old_reg_name, new_reg_name)
class VaArgOperation(Instruction):
"""
AST node of the LLVM Other Instructions group - VaArg Instruction
NOT SUPPORTED IN ScEpTIC
https://llvm.org/docs/LangRef.html#otherops
"""
def __init__(self, target, element, arg_type):
super().__init__()
self.target = target
self.element = element
self.type = arg_type
def __str__(self):
retstr = super().__str__()
retstr += 'va_arg {} {}'.format(self.condition, self.element, self.type)
return retstr
def run(self, update_program_counter=True):
raise RuntimeException('va_arg llvm instruction not supported for now.')

View File

@ -0,0 +1,208 @@
import logging
from ScEpTIC.AST.elements.instruction import Instruction
from ScEpTIC.AST.misc.virtual_memory_enum import VirtualMemoryEnum
class ReturnOperation(Instruction):
"""
AST node of the LLVM Termination Instructions group - Return Instruction
https://llvm.org/docs/LangRef.html#terminators
"""
# esp = ebp
# pop ebp
# ebp = ebp_pop
# pop pc
# pc = pc_pop
# store retval / place into a reg
tick_count = 6
memory_tick_count = 2
n_memory_instructions = 2
def __init__(self, value):
super().__init__()
self.value = value
self.update_pc = True
self.virtual_memory_target = VirtualMemoryEnum.VOLATILE
# Custom callback used for ISR and other functionalities
self.custom_callback = lambda: None
self.is_isr_return = False
self.isr_call = None
def __str__(self):
retstr = super().__str__()
retstr += 'return {}'.format(self.value)
retstr += f" (${self.virtual_memory_target.value})"
return retstr
def run(self, update_program_counter=True):
"""
Retrieves the value of the return operation and calls the return's callback.
(Update program counter ignored -> always True)
"""
value = self.value.get_val()
input_lookup = self.value.get_input_lookup()
logging.info('[{}] Returning value {}.'.format(self.instruction_type, value))
self._vmstate.on_function_return(value, input_lookup, self.tick_count, self.update_pc)
# run custom callback
self.custom_callback()
def get_uses(self):
"""
Returns a list containing the names of the registers used by this instruction.
(used by register allocation)
"""
return self.value.get_uses()
def replace_reg_name(self, old_reg_name, new_reg_name):
"""
Replaces the name of a register used by the instruction with a new one.
(used by register allocation)
"""
self.value.replace_reg_name(old_reg_name, new_reg_name)
class BranchOperation(Instruction):
"""
AST node of the LLVM Termination Instructions group - Branch Instruction
https://llvm.org/docs/LangRef.html#terminators
"""
# if condition is None: target = target_true directly
def __init__(self, condition, target_true, target_false):
super().__init__()
self.condition = condition
self.target_true = target_true
self.target_false = target_false
def __str__(self):
retstr = super().__str__()
retstr += 'branch {} {} {}{}'.format(self.condition, self.target_true, self.target_false, ' [Useless]' if self.tick_count == 0 else '')
return retstr
def run(self, update_program_counter=True):
"""
Executes the branch operation.
(Update program counter ignored -> always True)
"""
# if condition is none, is an unconditional branch
if self.condition is None:
target = self.target_true
else:
# evaluates the condition
value = self.condition.get_val()
if int(value) == 1:
target = self.target_true
else:
target = self.target_false
# performs callback call
self._vmstate.on_branch(target, self.basic_block_id, self.tick_count)
logging.info('[{}] Branching to {} (condition is {}).'.format(self.instruction_type, target, True if self.condition is None else value))
def get_uses(self):
"""
Returns a list containing the names of the registers used by this instruction.
(used by register allocation)
"""
if self.condition is not None:
return self.condition.get_uses()
return []
def replace_reg_name(self, old_reg_name, new_reg_name):
"""
Replaces the name of a register used by the instruction with a new one.
(used by register allocation)
"""
if self.condition is not None:
self.condition.replace_reg_name(old_reg_name, new_reg_name)
class SwitchOperation(Instruction):
"""
AST node of the LLVM Termination Instructions group - Switch Instruction
https://llvm.org/docs/LangRef.html#terminators
"""
def __init__(self, element, default_label, switch_pairs):
super().__init__()
self.element = element
self.default_label = default_label
# switch_pairs is a list of Value with additional label attribute
self.switch_pairs = switch_pairs
def __str__(self):
retstr = super().__str__()
retstr += 'switch {}; default: {}; {}'.format(self.element, self.default_label, self.switch_pairs)
return retstr
def run(self, update_program_counter=True):
"""
Executes the switch operation.
(Update program counter ignored -> always True)
"""
target_value = self.element.get_val()
label = None
for value in self.switch_pairs:
val = value.get_val()
# if value found, set label
if val == target_value:
label = value.label
break
# if label not set, use default_label
if label is None:
label = self.default_label
# performs callback call
self._vmstate.on_branch(label, self.basic_block_id, self.tick_count)
logging.info('[{}] Branching to {}.'.format(self.instruction_type, label))
def get_uses(self):
"""
Returns a list containing the names of the registers used by this instruction.
(used by register allocation)
"""
return self.element.get_uses()
def replace_reg_name(self, old_reg_name, new_reg_name):
"""
Replaces the name of a register used by the instruction with a new one.
(used by register allocation)
"""
self.element.replace_reg_name(old_reg_name, new_reg_name)

View File

@ -0,0 +1,120 @@
class Metadata:
"""
AST node for LLVM metadata
"""
elements = {}
def __init__(self, name, metadata_type, metadata_function_name, values, includes):
self.name = name
self.type = metadata_type
self.type_name = metadata_function_name
# values can be a dictionary or a list. each element can be a string, a reference to other metadata or a Metadata object.
self.values = values
self.includes = includes
if self.name is not None:
self.elements[self.name] = self
def __str__(self):
return '{} = {}(includes: {}; values: {})'.format(self.name, self.type_name, self.includes, self.values)
@staticmethod
def fancy_format(metadata):
"""
Formats the metadata and returns it
"""
retval = ''
if 'line' in metadata:
retval += 'Line: {}; '.format(metadata['line'])
if 'column' in metadata:
retval += 'Column: {}; '.format(metadata['column'])
if 'variable_name' in metadata:
retval += 'Variable name: {}; '.format(metadata['variable_name'])
if 'subprogram_name' in metadata:
retval += 'Function name: {}; '.format(metadata['subprogram_name'])
if 'file' in metadata:
retval += 'File: {}; '.format(metadata['file'])
if retval[-2:] == '; ':
retval = retval[:-2]
return retval
def retrieve(self):
"""
Resolve metadata tree and return only relevant metadata information
"""
if len(self.values) == 0 and len(self.includes) == 0:
return {}
if self.type_name == 'DIFile':
return {'file': '{}/{}'.format(self.values['directory'], self.values['filename'])}
if self.type_name == 'DILocation':
# retrieve scope
retrived_vals = self.elements[self.values['scope']].retrieve()
return {**retrived_vals, **{'line': self.values['line'], 'column': self.values['column']}}
if self.type_name == 'DISubprogram':
# get file name
file = self.elements[self.values['file']].retrieve()
return {**file, **{'subprogram_name': self.values['name']}}
if self.type_name == 'DILexicalBlock':
# retrieve scope
retrived_vals = self.elements[self.values['scope']].retrieve()
if 'file' not in retrived_vals:
retrived_vals['file'] = self.elements[self.values['file']].retrieve()['file']
return retrived_vals
if self.type_name == 'DILocalVariable':
# retrieve scope
retrived_vals = self.elements[self.values['scope']].retrieve()
try:
return {**retrived_vals, **{'variable_name': self.values['name'], 'line': self.values['line'], 'column': self.values['column']}}
except Exception:
return {**retrived_vals, **{'variable_name': self.values['name'], 'line': self.values['line']}}
location = None
local_var = None
# find the element that contains the relevant metadata informations.
for i in self.includes:
# empty DIExpression()
if i == 'empty':
#return None
continue
if self.elements[i].type_name == 'DILocation':
location = i
if self.elements[i].type_name == 'DILocalVariable':
local_var = i
# Local Variable has higher priority (contains more information)
# NB: is only for local variable declarations
if local_var is not None:
return self.elements[local_var].retrieve()
# If the metadata is not associated with local variable
# or DILocalVariable not found, use DILocation
if location is not None:
return self.elements[location].retrieve()
return None

View File

@ -0,0 +1,330 @@
class BaseType:
"""
AST node of LLVM Base Type
NOTE: enum data type doesn't appear because it is directly resolved and converted in int by the llvm front-end.
"""
def __init__(self, base_type, bits):
self.type = base_type
self.bits = int(bits)
def __len__(self):
return self.bits
def __str__(self):
return '{} {}bit'.format(self.type, self.bits)
def __repr__(self):
return 'BaseType({}, {})'.format(self.type, self.bits)
def __eq__(self, other):
if not isinstance(other, BaseType):
return False
return self.type == other.type and self.bits == other.bits
class Type:
"""
AST node representing a LLVM generic type (Array, Vector, Pointer, Custom Type)
"""
address_dimension = 0
def __init__(self, is_pointer, pointer_level, is_array, array_composition, is_vector, vector_dimension, is_base_type, custom_type_name, is_ct_defined, custom_type_def, base_type):
self.is_pointer = is_pointer
self.pointer_level = int(pointer_level)
self.is_array = is_array
self._set_array_composition(array_composition)
self.is_vector = is_vector
self.vector_dimension = int(vector_dimension)
self.is_base_type = is_base_type
self.custom_type_name = custom_type_name
self.is_ct_defined = is_ct_defined
self.custom_type_def = custom_type_def
self.base_type = base_type
def _set_array_composition(self, array_composition):
"""
Sets the array composition of the type.
"""
self.array_composition = []
for elem in array_composition:
elem = int(elem)
self.array_composition.append(elem)
self.array_dimensions = len(self.array_composition)
def _get_array_composition(self, element_dimension):
"""
Returns the overall array cells number.
"""
array_composition = self.array_composition.copy()
array_composition.reverse()
dim = element_dimension
for i in array_composition:
dim = [i, dim]
return dim
def get_memory_composition(self, flat_composition = False):
"""
Returns the memory composition of the Type, which is a list [a, b]
with a representing the number of elements and b representing the size of each element.
b can be a list of memory composition in case the Type refers to a customtype.
"""
element_dimension = 0
# set dimension
if self.is_pointer:
element_dimension = self.address_dimension
elif self.is_base_type:
element_dimension = len(self.base_type)
elif self.is_ct_defined:
raise NotImplementedError('Type not supported')
else:
element_dimension = CustomType.elements[self.custom_type_name].get_memory_composition()
# is is array, create proper composition
if self.is_array:
composition = self._get_array_composition(element_dimension)
elif self.is_vector:
elements_number = self.vector_dimension
composition = [elements_number, element_dimension]
elif self.is_base_type:
composition = [1, element_dimension]
else:
composition = element_dimension
if flat_composition:
flat = []
self.flat_composition(composition, flat)
return flat
return composition
@staticmethod
def flat_composition(composition, lst):
"""
flatterns a memory composition, turning it into a single list of dimensions
array: [2, 32] -> [32, 32]
array of struct: [3, [[3, 8], [1, 32]]] -> [8, 8, 8, 32, 8, 8, 8, 32, 8, 8, 8, 32]
"""
# If composition is an integer -> recursion ended
if isinstance(composition, int):
lst.append(composition)
return
elif isinstance(composition, list):
# Empty list
if len(composition) == 0:
return
if len(composition) == 1:
composition = composition[0]
n_elements = composition[0]
elements_size = composition[1]
# n_elements is an int -> array (consider the element_size now)
if isinstance(n_elements, int):
for _ in range(0, n_elements):
Type.flat_composition(elements_size, lst)
# n_elements is a list -> struct (consider the whole composition now)
elif isinstance(n_elements, list):
for sub_comp in composition:
Type.flat_composition(sub_comp, lst)
else:
raise NotImplementedError('Type.flat_composition() error 1')
else:
raise NotImplementedError('Type.flat_composition() error 2')
@classmethod
def empty(cls):
"""
Creates an empty type.
"""
is_pointer = False
pointer_level = 0
is_array = False
array_composition = []
is_vector = False
vector_dimension = 0
is_base_type = True
custom_type_name = None
is_ct_defined = False
custom_type_def = None
base_type = None
return cls(is_pointer, pointer_level, is_array, array_composition, is_vector, vector_dimension, is_base_type, custom_type_name, is_ct_defined, custom_type_def, base_type)
def __len__(self):
"""
Returns the size in bits of the type
"""
len_multiplier = 1
if self.is_vector:
len_multiplier = self.vector_dimension
if self.is_array:
for i in self.array_composition:
len_multiplier = len_multiplier * i
if self.is_pointer:
return len_multiplier * self.address_dimension
elif self.is_base_type:
return len_multiplier * len(self.base_type)
elif self.is_ct_defined:
tmp_len = 0
for elem in self.custom_type_def:
tmp_len = tmp_len + len(elem)
return len_multiplier * tmp_len
else:
if self.custom_type_name not in CustomType.elements:
raise ValueError('Custom type {} not found!'.format(self.custom_type_name))
return len_multiplier * len(CustomType.elements[self.custom_type_name])
def __str__(self):
if self.is_base_type:
retstr = str(self.base_type)+'*'*self.pointer_level
elif self.is_ct_defined:
retstr = 'CustomType: {'
length = len(self.custom_type_def)-1
for i in range(0, length+1):
retstr += '{}'.format(self.custom_type_def[i])
if i < length:
retstr += ', '
retstr += '}'
else:
retstr = str(self.custom_type_name)
if self.is_vector:
retstr = '<{} x {}>'.format(self.vector_dimension, retstr)
if self.is_array:
retstr = '({})'.format(retstr)
for i in self.array_composition:
retstr = '{}[{}]'.format(retstr, i)
return retstr
def __repr__(self):
return str(self)
class CustomType:
"""
AST node representing a LLVM custom type
"""
elements = {}
def __init__(self, fullname, ct_type, name, type_composition):
self.fullname = fullname
self.type = ct_type
self.name = name
self._set_type_composition(type_composition)
self.elements[self.fullname] = self
def _set_type_composition(self, type_composition):
"""
Sets the type composition of the custom type
"""
self.type_composition = []
for elem in type_composition:
self.type_composition.append(elem)
def __len__(self):
"""
Returns the size in bits of the customtype
"""
total_len = 0
for elem in self.type_composition:
total_len = total_len + len(elem)
return total_len
def __str__(self):
retstr = ''
length = len(self.type_composition)-1
for i in range(0, length+1):
retstr += '{}'.format(self.type_composition[i])
if i < length:
retstr += ', '
# two {{ }} for printing single { }
# middle {} to print retstr
return '{}: {{{}}}'.format(self.fullname, retstr)
def get_memory_composition(self):
"""
Returns a list of lists, in which each element is a memory composition of the represented type.
"""
elements = []
for elem in self.type_composition:
elements.append(elem.get_memory_composition())
return elements

View File

@ -0,0 +1,365 @@
import logging
import struct
from ScEpTIC import tools
from ScEpTIC.exceptions import MemoryException
class Value:
"""
AST node for data representation (immediate, register data, etc)
"""
_vmstate = None
def __init__(self, value_class, operand_value, value_type):
self.value_class = value_class
self.value = operand_value
self.type = value_type
def __repr__(self):
return 'Value({}, {}, {})'.format(self.value_class, self.value, self.type)
def __str__(self):
if self.type is None:
return str(self.value)
return 'Value: ({}) {}'.format(self.type, self.value)
def __len__(self):
return len(self.type)
def is_llvm_string(self):
try:
return self.value_class == 'address' and '@.str' in self.value.element.value
except:
return False
def get_val(self):
"""
Retrieve and returns the value represented by the object.
"""
if self._vmstate is None:
raise MemoryException('VM Status not initialized!')
if self.value_class == 'immediate':
value = self._resolve_immediate()
elif self.value_class == 'array_val':
value = self._resolve_array_val()
elif self.value_class == 'struct_val':
value = self._resolve_array_val()
elif self.value_class == 'virtual_reg':
value = self._vmstate.register_file.read(self.value)
elif self.value_class == 'global_var':
# Function pointer -> same initial token of global_vars
# If function exists -> return value
if self.value in self._vmstate.functions:
value = self.value
else:
value = self._vmstate.memory.gst.get_symbol_address(self.value)
elif self.value_class == 'vector':
raise NotImplementedError('Vector operations not supported at the moment. Please compile with a different target.')
elif self.value_class == 'address':
# GetElementPointerOperation has method get_val that resolves the correct value
value = self.value.get_val()
elif self.value_class == 'conversion':
# ConversionOperation has method get_val that resolves the correct value
value = self.value.get_val()
elif self.value_class == 'metadata':
# appears only in @llvm.dbg functions, so no need to resolve that address
raise ValueError('Metadata should not be present in running functions.')
logging.info('[Value] Resolved value of {} with {}.'.format(self.value, value))
return value
def resolve_memory_tag(self, elements):
"""
Resolves and returns the memory tag of the targeted element
"""
if self.value_class == 'immediate':
return self._resolve_immediate()
elif self.value_class == 'virtual_reg':
return elements[self.value].resolve_memory_tag(elements)
elif self.value_class == 'global_var':
return self.value
elif self.value_class == 'address':
return self.value.resolve_memory_tag(elements)
elif self.value_class == 'conversion':
return self.value.resolve_memory_tag(elements)
raise NotImplementedError(f"resolve_memory_tag() not implemented for value class {self.value_class}")
def resolve_memory_tag_dependency(self, elements):
if self.value_class == 'immediate':
return []
elif self.value_class == 'virtual_reg':
return elements[self.value].resolve_memory_tag_dependency(elements)
elif self.value_class == 'global_var':
return [self.value]
elif self.value_class == 'address':
return self.value.resolve_memory_tag_dependency(elements)
elif self.value_class == 'conversion':
return self.value.resolve_memory_tag_dependency(elements)
raise NotImplementedError(f"resolve_memory_tag() not implemented for value class {self.value_class}")
def resolve_memory_address_chain(self, elements):
"""
Returns a list of all the instructions required to get the address of the targeted element(s)
"""
if self.value_class == 'immediate':
return []
elif self.value_class == 'virtual_reg':
return [elements[self.value].resolve_memory_address_chain(elements)]
elif self.value_class == 'global_var':
return []
elif self.value_class == 'address':
return [self.value.resolve_memory_address_chain(elements)]
elif self.value_class == 'conversion':
return [self.value.resolve_memory_address_chain(elements)]
raise NotImplementedError(f"resolve_memory_address_chain() not implemented for value class {self.value_class}")
def get_input_lookup(self):
"""
Returns the input lookup information for the current Value object.
"""
if self._vmstate.input_lookup_enabled:
if self.value_class == 'virtual_reg':
return self._vmstate.register_file.get_input_lookup(self.value)
elif self.value_class == 'address':
return self.value.get_input_lookup()
elif self.value_class == 'conversion':
return self.value.get_input_lookup()
return tools.build_input_lookup_data(None, None)
def get_uses(self):
"""
Returns a list containing the names of the registers used by this value.
(used by register allocation)
"""
if self.value_class == 'virtual_reg':
return [self.value]
elif self.value_class == 'address':
return self.value.get_uses()
elif self.value_class == 'conversion':
return self.value.get_uses()
return []
def get_memory_uses(self):
if self.value_class == 'global_var':
return [self.value]
def get_defs(self):
"""
Returns a list of registers defined by this instruction.
(used by register allocation)
"""
return []
def replace_reg_name(self, old_reg_name, new_reg_name):
"""
Replaces the name of a register contained in this Value object.
(used by register allocation)
"""
if self.value_class == 'virtual_reg':
if self.value == old_reg_name:
self.value = new_reg_name
elif self.value_class == 'address':
self.value.replace_reg_name(old_reg_name, new_reg_name)
elif self.value_class == 'conversion':
self.value.replace_reg_name(old_reg_name, new_reg_name)
def _resolve_immediate(self):
"""
Resolves the value of a immediate.
"""
if self.value is None:
return None
if self.value == 'null':
return 0
if self.value == 'true':
return 1
if self.value == 'false':
return 0
try:
return int(self.value)
except ValueError:
pass
try:
return float(self.value)
except ValueError:
pass
if 'e+' in self.value:
return float(self.value)
if '0x' in self.value:
# self.value[2:] to remove 0x
# each value is converted as a double from llvm, even if is a float
# NB: target architecture could be in little endian (e in target_datalayout)
# or big endian (E in target_datalayout). The conversion in the llvm ir stays the same,
# independently of the type of endianness used by the backend compiler.
# So the endianness conversion stays ! (network - big-endian)
return struct.unpack('!d', bytes.fromhex(self.value[2:]))[0]
if self.value == 'zeroinitializer':
composition = self.type.get_memory_composition(True)
for i in range(0, len(composition)):
composition[i] = 0
return composition
return self.value
def _resolve_array_val(self):
"""
Resolves the value of array initialization.
"""
flat_values = []
tools.inf_depth_lst_flat(self.value, flat_values)
for i in range(0, len(flat_values)):
flat_values[i] = flat_values[i].get_val()
return flat_values
@staticmethod
def convert_sint_to_bin(val, bits):
"""
Converts a signed integer to its binary representation, using a certain number of bits
"""
format_str = '{:0'+str(bits)+'b}'
if val < 0:
# all 1s
mask = int('1' * bits, 2)
val = format_str.format(val & mask)
else:
val = format_str.format(val)
# return correct bit dimension
return val[-bits:]
@staticmethod
def convert_uint_to_bin(val, bits):
"""
Converts an unsigned integer to its binary representation.
For how data is represented, it is the same as converting a signed integer to binary.
"""
return Value.convert_sint_to_bin(val, bits)
@staticmethod
def convert_bin_to_uint(val):
"""
Converts a number in binary format to its equivalent unsigned decimal.
"""
return int(val, 2)
@staticmethod
def convert_bin_to_sint(val):
"""
Converts a number in binary format to its equivalent signed decimal.
"""
if val[0] == '0':
return int(val, 2)
if len(val) == 1:
return int(val, 2)
return int(val[1:], 2) - (2 ** (len(val) - 1))
@staticmethod
def convert_sint_to_uint(val, bits):
"""
Converts a number in signed integer form to its equivalent unsigned integer.
"""
val = Value.convert_sint_to_bin(val, bits)
return Value.convert_bin_to_uint(val)
@staticmethod
def convert_uint_to_sint(val, bits):
"""
Converts a number in unsigned integer form to its equivalent signed integer.
"""
val = Value.convert_uint_to_bin(val, bits)
return Value.convert_bin_to_sint(val)
@staticmethod
def convert_sint_to_sint(val, bits):
"""
Convert a signed integer to a signed integer, using a maximum number of bits.
"""
val = Value.convert_sint_to_bin(val, bits)
return Value.convert_bin_to_sint(val)

View File

Binary file not shown.

View File

@ -0,0 +1,5 @@
from enum import Enum
class VirtualMemoryEnum(Enum):
VOLATILE = 'VM'
NON_VOLATILE = 'NVM'

View File

@ -0,0 +1,17 @@
import logging
def get_register_allocator(config):
"""
Function that dynamically loads the requested register allocation module and returns its main function,
to be used to perform it.
"""
logging.debug('[RegisterAllocation] from {}.{} import {}'.format(config.module_location, config.module_name, config.allocation_function_name))
# set absolute module name
module_name = '{}.{}'.format(config.module_location, config.module_name)
# import the corresponding module
module = __import__(module_name, fromlist=[config.allocation_function_name])
# return requested function
return getattr(module, config.allocation_function_name)

View File

@ -0,0 +1,96 @@
import copy
from ScEpTIC.AST.register_allocation.linear_scan.linear_scan import LinearScanRegisterAllocator
def allocate_registers(functions, registers_number, config):
"""
This functions performs the register allocation on the whole code (analysis, pre-processing, register allocation, and post-processing)
"""
# perform register allocation on each function
for function_name in functions:
func = functions[function_name]
# set the register allocator and run it
func.register_allocator = LinearScanRegisterAllocator(func, registers_number, config.reg_prefix, config.spill_prefix, config.spill_type)
func.register_allocator.run_register_allocation()
# analyze register usage, to support call_post_processing
compute_registers_usage(functions, registers_number)
# do post-processing of functions calls. (insert register saving/restoring operations and set their tick_count)
for function_name in functions:
func = functions[function_name]
func.register_allocator.do_call_post_processing()
def compute_registers_usage(functions, registers_number):
"""
This function estimates the maximum register usage of each code's function and is used to
estimate the actual number of operation performed for saving and restoring registers before
a function call.
Register allocation must be performed before running this function, so to have the number of registers used.
A good partitioned register allocation will make registers from frequent functions call to not overlap, so to minimize
the register saving operations before a function call.
To estimate this scenario, is sufficient to find the number of registers used by each function (including the ones from the
calls present in such function).
The number of registers needed by a function is computed as:
max(registers_used_by_internal_calls) + registers_used_by_function
NB: recursive calls are ignored, since they will save the number of registers used by the current function.
If this number is lower than the number of available registers, no saving is needed before the call.
The overall number of register usage is calculated iteratively, since cycles can be possible (a calls b; b calls a;)
If the number of needed registers exceed the number of available ones, a cycle-dependecy is found and thus all registers should be
saved before such call.
Initial setup: each function uses reg_count as max_reg_usage, calls is initialized with the functions called
Iterate:
find the max_reg_usage among the functions in calls
set max_reg_usage = max_reg_usage of function calls + reg_count
if max_reg_usage > max_available_registers -> max_reg_usage = max_available_registers
until a fixed point is reached
Once the max_reg_usage is found, call post-processing can be done (addition of registers saving/restoring routines)
"""
function_data = {}
old_function_data = {}
# initialization step
for function_name in functions:
func = functions[function_name]
function_data[function_name] = {'reg_count': func.reg_count, 'calls': func._calls, 'max_reg_usage': func.reg_count}
# iterate until a fixed point is reached
while function_data != old_function_data:
old_function_data = copy.deepcopy(function_data)
# update each value
for name in function_data:
f_data = function_data[name]
# if the considered function exceed registers_number, skip (computation already done)
if len(f_data['calls']) == 0 or f_data['max_reg_usage'] >= registers_number:
continue
# find max reg usage for called functions and compute current max_reg_usage
calls = []
for sub in f_data['calls']:
calls.append(function_data[sub]['max_reg_usage'])
max_reg_usage = max(calls) + f_data['reg_count']
# limit max_reg_usage to registers_number
f_data['max_reg_usage'] = min(max_reg_usage, registers_number)
# update functions with final results.
for function_name in functions:
func = functions[function_name]
func.max_reg_usage = function_data[function_name]['max_reg_usage']

View File

@ -0,0 +1,610 @@
import copy
import logging
from ScEpTIC import tools
from ScEpTIC.AST.elements import value
from ScEpTIC.AST.elements.instructions import memory_operations
from ScEpTIC.AST.elements.instructions import other_operations
from ScEpTIC.AST.elements.metadata import Metadata
from ScEpTIC.AST.register_allocation.linear_scan.register_operations import SaveRegistersOperation, RestoreRegistersOperation
from ScEpTIC.AST.register_allocation.linear_scan.register_pool import RegisterPool
from ScEpTIC.exceptions import RegAllocException
from ScEpTIC.llvmir_parser.sections_parser import global_vars
class LinearScanRegisterAllocator:
"""
Implementation of linear scan register allocation.
It must be done for each function and this class refers to a single function body.
Implementation details can be found here:
https://www2.seas.gwu.edu/~hchoi/teaching/cs160d/linearscan.pdf
"""
def __init__(self, function, regs_number, reg_prefix = 'R', spill_prefix = '%spill_', spill_type = 'i32'):
self.function = function
self.code = function.body
self.virtual_regs = {}
self.intervals = []
self.active = []
self.register_pool = RegisterPool(regs_number, reg_prefix)
# prefix of spill registers
self.spill_prefix = spill_prefix
self.spill_count = 0
self.spill_dimension = global_vars.parse_type(spill_type)
self.reg_count = 0
# registers to be ignored due to data layout
self.ignores = []
# identifies the line of the last alloca
# it is used to put alloca operations in the same area of the code, as they are translated with a single ESP increment.
self.last_alloca = 0
# calculate latest alloca operation
for i in range(0, len(self.code)):
self.last_alloca = i
if not isinstance(self.code[i], memory_operations.AllocaOperation):
break
def run_register_allocation(self):
"""
Runs the actual register allocation.
"""
self.do_call_pre_processing()
self.do_liveness_analysis()
self.do_register_allocation()
@property
def spill_type(self):
"""
Returns an instance of the spill type, to be used in the operations.
"""
return copy.deepcopy(self.spill_dimension)
def _name_to_virtual_reg(self, name):
"""
Returns a Value representing a virtual register with the provided name
"""
return value.Value('virtual_reg', name, None)
def _create_virtual_reg(self):
"""
Creates a new virtual register.
"""
name = '{}{}'.format(self.spill_prefix, self.spill_count)
self.spill_count += 1
logging.debug('[RegisterAllocation] Creating new virtual register {}.'.format(name))
return self._name_to_virtual_reg(name)
def append_alloca_operation(self, alloca_type):
"""
Appends an alloca operation in the code at the end of the alloca section.
It returns the virtual registers contining the alloca address.
"""
logging.debug('[RegisterAllocation] Appending alloca({}) operation on top.'.format(len(alloca_type)))
spill_reg = self._create_virtual_reg()
target = copy.deepcopy(spill_reg)
# create the operation
alloca = memory_operations.AllocaOperation(target, alloca_type, 1, 1)
alloca.metadata = Metadata(None, None, None, [], [])
# insert it in the code
self.code.insert(self.last_alloca, alloca)
# update last_alloca, so to consider this added one.
self.last_alloca += 1
return spill_reg
def append_store_operation(self, position, target, value, target_type, label, metadata):
"""
Appends a store operation at the required position.
"""
logging.debug('[RegisterAllocation] Appending store {}, {} at {}.'.format(target.value, value.value, position))
target = copy.deepcopy(target)
value = copy.deepcopy(value)
value.type = copy.deepcopy(target_type)
# create the operation
store = memory_operations.StoreOperation(target, value, 1, False)
store.label = label
store.metadata = metadata
# insert in the code the operation
self.code.insert(position, store)
def append_load_operation(self, position, element, load_type, is_arg_of_function_call, metadata):
"""
Appends a load operation at the required position.
It returns the virtual register containing the loaded value.
"""
logging.debug('[RegisterAllocation] Appending load {} at {}.'.format(element.value, position))
spill_reg = self._create_virtual_reg()
target = copy.deepcopy(spill_reg)
element = copy.deepcopy(element)
# create the operation
load = memory_operations.LoadOperation(target, load_type, element, 1, False)
load.metadata = metadata
# eventually set if it loads a function call's argument
if is_arg_of_function_call:
load.is_arg_of_function_call = True
# insert in the code the operation
self.code.insert(position, load)
return spill_reg
def do_call_pre_processing(self):
"""
Applies call pre-processing, consisting in:
- marking load of arguments as is_arg_of_function_call
- creating alloca-store-load for immediate
- creating alloca-store-load for conversion and other operation s.t. their
results are passed as arguments of the function call.
"""
search_from = 0
search_to = 0
i = 0
# scan the code by line. while is used since the lines number may change
while i < len(self.code):
operation = self.code[i]
# process only call operations which are not part of the checkpoint mechanism.
if isinstance(operation, other_operations.CallOperation) and operation.name in self.function._calls:
targets = operation.get_uses()
search_to = i
j = search_from
# adjust label (if present)
append_label = operation.label
# search for instructions s.t. their target is in function arguments
while j < search_to:
op = self.code[j]
# target of operation is an argument of the call
if 'target' in op.__dict__ and op.target is not None and op.target.value in targets:
targets.remove(op.target.value)
# if is a load instruction, mark it
if isinstance(op, memory_operations.LoadOperation):
op.is_arg_of_function_call = True
# else generate alloca, store and load, as it would be done
# in a real scenario (value saved onto the stack to be passed)
# NB: load is marked as is_arg_of_function_call (so to emulate stack passing)
else:
# get type of argument
arg_type = operation.get_type_from_virtual_reg(op.target.value)
# append alloca and compensate for the added instruction
tmp_reg = self.append_alloca_operation(arg_type)
# append store before call and compensate for the added instruction
# search_to = call position
# nb: here is set the eventual label of the call operation.
self.append_store_operation(search_to+1, tmp_reg, op.target, arg_type, append_label, operation.metadata)
# set the lable to be appended to None
append_label = None
# append load before call and compensate for the added instruction
tmp_reg = self.append_load_operation(search_to+2, tmp_reg, arg_type, True, operation.metadata)
# update the new register name containing the argument value.
operation.replace_reg_name(op.target.value, tmp_reg.value)
# compensate for the 3 added instructions
i += 3
# update search_to value, to consider added instructions
search_to = i
# update code index
j += 1
# create alloca, store and load for immediate values, as it is done
# for operations on the above lines.
for arg in operation.function_args:
if arg.value_class == 'immediate':
# llvm builtins may have 1bit operations, but memory can only work
# with bytes, so modify the memory dimension to do not have problems.
# data integrity is preserved.
if arg.type is not None and arg.type.base_type.bits < 8:
arg.type.base_type.bits = 8
tmp_reg = self.append_alloca_operation(arg.type)
i += 1
# nb: here is set the eventual label of the call operation.
self.append_store_operation(i, tmp_reg, arg, arg.type, append_label, operation.metadata)
# set the lable to be appended to None
append_label = None
i += 1
tmp_reg = self.append_load_operation(i, tmp_reg, arg.type, True, operation.metadata)
i += 1
# update the arguments to be a virtual register instead of an immediate
arg.value_class = 'virtual_reg'
# set the virutal register name
arg.value = tmp_reg.value
# update search from. it is used to limit the area in which function arguments are searched.
# in llvm all arguments are reloaded if multiple calls happens from them, so there is no need
# to search arguments from the top (it is sufficient to go from the previous call to the current one)
search_from = i + 1
# refresh the operation label: if it is assigned to another operation, it will be set to false
# otherwise is unchanged.
operation.label = append_label
# update code index
i += 1
def do_call_post_processing(self):
"""
Applies function call post-processing, which consists in:
- Insertion of SaveRegistersOperation and RestoreRegistersOperation respectively before and after
a function call
- Updating the labels mappings (if code is appended, a label is mapped with a line which won't be exact)
"""
i = 0
# scan the code by line. while is used since the lines number may change
while i < len(self.code):
operation = self.code[i]
# process only call operations which are not part of the checkpoint mechanism.
if isinstance(operation, other_operations.CallOperation) and operation.name in self.function._calls:
# if recursive call, save used registers
if operation.name == self.function.name:
tick_count = self.function.reg_count
# else estimates the number of registers to be saved in case of a well-partitioned
# register allocation among functions.
# e.g. if main uses 2 registers and calls pow which uses 2 registers, a well-partitioned
# register allocation will give R0 and R1 to main, and R2 and R3 to pow
# with the result of no register saving needed
else:
# usage = reg used by current function + max reg used by the called function (and calls inside it)
# registers to be saved = usage - available
# if I have 10 registers, the function uses 2 registers and the called one uses 2 registers,
# no saving needed.
# max_reg_usage is calculated iteratively before running do_call_post_processing
tick_count = self.function.reg_count + operation.get_function_max_reg_usage() - self.register_pool.regs_number
# if result is < 0 means that no register should be saved.
if tick_count < 0:
tick_count = 0
# get target register name
if 'target' in operation.__dict__ and operation.target is not None:
target = operation.target.get_uses()[0]
else:
target = None
# create the saving and restoring operations
save_reg_op = SaveRegistersOperation(tick_count, target, self.register_pool)
save_reg_op.metadata = operation.metadata
# compensate label
save_reg_op.label = operation.label
operation.label = None
load_reg_op = RestoreRegistersOperation(save_reg_op)
load_reg_op.metadata = operation.metadata
# insert the save operation before the function call
self.code.insert(i, save_reg_op)
# 1 for compensating save_reg_op, 1 for finding the operation after the call
i += 2
# insert the restore operation after the function call
self.code.insert(i, load_reg_op)
i += 1
# update the function's label mappings.
self.function.update_labels()
def _init_intervals(self):
"""
Inits the intervals used for calculating the active interva of the registers for the linear scan.
"""
for _ in range(0, len(self.code)):
self.intervals.append(None)
def do_liveness_analysis(self):
"""
This method performs the liveness analysis, which finds the live interval for each register.
NB: LLVM IR is in Static Single Assignment (SSA) foarm, so for obtaining the liveness intervals
is sufficient to map the definition with its last use.
"""
# registers to be ignored, since will be considered as stack offsets/addresses
# which will be resolved by datalayout operation.
self.ignores = self.function.get_ignore()
# for each line, compute the uses and definitions of each register.
for i in range(0, len(self.code)):
self.ignores = self.ignores + self.code[i].get_ignore()
defs = tools.list_sanitize_from_list(self.code[i].get_defs(), self.ignores)
uses = tools.list_sanitize_from_list(self.code[i].get_uses(), self.ignores)
for reg in defs:
self.virtual_regs[reg] = {'uses': [], 'def': i, 'target': None}
for reg in uses:
self.virtual_regs[reg]['uses'].append(i)
self._init_intervals()
# find last use of each register and create interval dict
for reg_id, reg in self.virtual_regs.items():
# if no use, the last use will be in the same instruction in which the register
# is defined
if len(reg['uses']) == 0:
reg['uses'].append(reg['def'])
reg['last_use'] = max(reg['uses'])
# set register intervals
self.intervals[reg['def']] = {'reg': reg_id, 'last_use': reg['last_use']}
def _compensate_for_alloca(self):
"""
Updates the liveness analysis to account for the insertion of an alloca operations.
"""
logging.debug('[RegisterAllocation] Compensating for alloca operation.')
# alloca operations do not introduces any use/def and are appended on the top of the code
# so i can insert a None interval on top of the list (since all alloca operations are in the same region)
self.intervals.insert(0, None)
# increment uses of each interval by 1
for interval in self.intervals:
if interval is None:
continue
interval['last_use'] += 1
# increament uses and definitions of each virtual register by one
for virtual_reg in self.virtual_regs.values():
virtual_reg['def'] += 1
virtual_reg['uses'] = [x+1 for x in virtual_reg['uses']]
virtual_reg['last_use'] += 1
def _compensate_for_operation(self, instant):
"""
Updates the liveness analysis to account for the insertion of an operation which may
insert use/def and is inserted in a precise line of code.
instant specifies also the line of code
"""
logging.debug('[RegisterAllocation] Compensating for operation at {}.'.format(instant))
# insert the proper interval
self.intervals.insert(instant, None)
# increment data by 1 for subsequent intervals
for interval in self.intervals:
if interval is None:
continue
# if subsequent interval, it needs to be updated
if interval['last_use'] >= instant:
interval['last_use'] += 1
# get corresponding virtual register
virtual_reg = self.virtual_regs[interval['reg']]
# increment its definition instant if happens after the given instant
if virtual_reg['def'] >= instant:
virtual_reg['def'] += 1
# increment all the uses by 1 if they are after the given instant
virtual_reg['uses'] = [x+1 if x >= instant else x for x in virtual_reg['uses']]
# increment last use by 1, since it will be certainly after the given instant
virtual_reg['last_use'] += 1
def _assign_register(self, instant, new_reg_name):
"""
Assigns a physical register to a virtual register which is defined in a given instant.
"""
interval = self.intervals[instant]
reg_name = interval['reg']
logging.debug('[RegisterAllocation] Assigning register {} to virtual register {} at {}.'.format(new_reg_name, reg_name, instant))
# update each occurrence of the old register with the new register
for line in self.virtual_regs[reg_name]['uses']:
self.code[line].replace_reg_name(reg_name, new_reg_name)
self.code[instant].replace_reg_name(reg_name, new_reg_name)
self.virtual_regs[reg_name]['target'] = new_reg_name
# insert register into the active ones.
self.active.append(interval['reg'])
def do_register_allocation(self):
"""
Performs the linear scan register allocation over the given function.
"""
instant = 0
# scan all intervals and insert register spills / promotions
while instant < len(self.intervals):
interval = self.intervals[instant]
if interval is None:
instant += 1
continue
# expire old registers
self.expire_old(instant)
# set max reg count (used to estimate the tick_count)
self.reg_count = max(self.reg_count, len(self.active))
# if all registers used, need to spill.
if len(self.active) == self.register_pool.regs_number:
# spill at current instant
self.spill_at(instant)
# compensate for alloca and store
instant += 2
# else assign register
else:
new_reg_name = self.register_pool.get_reg()
self._assign_register(instant, new_reg_name)
# increment instant index
instant += 1
# set function regiser count.
self.function.reg_count = self.reg_count
def expire_old(self, instant):
"""
Updates the active registers by removing expired ones.
"""
logging.debug('[RegisterAllocation] Expiring registers at {}.'.format(instant))
# scan active registers to find if some of them can be freed.
for reg in self.active:
# if the last use is before current instant, the register can be
# considered as free.
if self.virtual_regs[reg]['last_use'] < instant:
# remove from active registers
self.active.remove(reg)
# free register
reg_name = self.virtual_regs[reg]['target']
self.register_pool.free_reg(reg_name)
def _get_fist_use_after_instant(self, reg, instant):
"""
Returns the first use of a register after a given instant.
"""
uses = self.virtual_regs[reg]['uses']
first_use = self.virtual_regs[reg]['last_use']
for i in uses:
if instant < i < first_use:
first_use = i
return first_use
def spill_at(self, instant):
"""
Selects a register to be spilled in the given instant and spills it.
"""
selected_spill = None
selected_reg = None
latest_use = -1
# scans each active register.
for reg in self.active:
reg_data = self.virtual_regs[reg]
# if register can be spilled (= not used in this instant nor defined)
if instant not in reg_data['uses'] and instant != reg_data['def']:
# get first use after current instant
first_use = self._get_fist_use_after_instant(reg, instant)
# update max usage informations, used for spilling selection
if first_use > latest_use:
selected_spill = reg_data['target']
selected_reg = reg
latest_use = first_use
# no register can be spilled, exit.
if selected_spill is None:
raise RegAllocException('Unable to select a register for spilling')
logging.debug('[RegisterAllocation] Spilling register {} at instant {}'.format(selected_spill, instant))
# append alloca operation, to account for stack space
tmp_reg = self.append_alloca_operation(self.spill_type)
# compensate for alloca
self._compensate_for_alloca()
instant += 1
# save register value onto the stack before current instant
value_reg = self._name_to_virtual_reg(selected_spill)
self.append_store_operation(instant, tmp_reg, value_reg, self.spill_type, None, self.code[instant].metadata)
# compensate for store
self._compensate_for_operation(instant)
instant += 1
# compensate for alloca and store. NB: use is after alloca and store
latest_use += 2
# create load before the first use of spilled register and
tmp_reg = self.append_load_operation(latest_use, tmp_reg, self.spill_type, False, self.code[instant].metadata)
self._compensate_for_operation(latest_use)
# retarget uses of spilled register to created load (new reg name, which for now is virtual and in
# a next step will be updated with a physical one)
uses = [x for x in self.virtual_regs[selected_reg]['uses'] if x >= latest_use]
for i in uses:
self.code[i].replace_reg_name(selected_spill, tmp_reg.value)
# create interval and register for load, so to assign a physical register to it in a next iteration.
reg_id = tmp_reg.value
last_use = max(uses)
virtual_reg = {'uses': uses, 'def': latest_use, 'target': None, 'last_use': last_use}
self.virtual_regs[reg_id] = virtual_reg
self.intervals[latest_use] = {'reg': reg_id, 'last_use': last_use}
# assign register
self.active.remove(selected_reg)
self._assign_register(instant, selected_spill)

View File

@ -0,0 +1,128 @@
import copy
from ScEpTIC.AST.elements.instruction import Instruction
from ScEpTIC.exceptions import MemoryException
class SaveRegistersOperation(Instruction):
"""
Operation that emulates the saving of registers before a function call.
All the register savings are done as caller-save, since saving register in the callee
function will lead to multiple SaveRegistersOperation (one before each return).
The instruction is set up with:
- tick_count: consists in the number of registers used by the called function.
In a real scenario, 1 store operation (Rx -> stack) will be inserted for each register
that needs to be saved. Once this instruction is executed, the global clock will be incremented
by this value ( = number of store operations that should be present)
- target_reg: is the name of the register that will contain the return value of the call. It won't be restored, nor saved.
NB: all registers are saved from R0 to R_n, so it is sufficient to provide the number of registers to be saved
to save the first n registers.
"""
def __init__(self, tick_count, target_reg, register_pool):
super().__init__()
target_reg_id = register_pool._get_id_from_reg_name(target_reg)
if target_reg_id is None:
target_reg_id = tick_count
"""
Tick count corresponds to the number of registers to be saved, which
can be converted as a R_i with i in [0, tick_count)
If the target register is inside such range, tick_count should be decreased by 1
since the target register will be replaced by the function call and won't be restored
(it will contain the call's return value)
"""
if target_reg_id < tick_count:
tick_count -= 1
self.tick_count = tick_count
self.target_reg = target_reg
self._omit_target = True
def get_val(self):
"""
Save all registers, except for the target one.
They will be restored by RestoreRegistersOperation.
Since saving all registers or a sub-group of them won't change anything,
this function saves all of them in order to remove a control of which register
needs to be saved. The overall result is the same as it only some registers are saved,
since registers computed inside a function won't have any value needed by the caller,
except from the return register.
"""
# variables used to store registers
registers = {}
registers_input_lookup = {}
for reg in self._vmstate.register_file._registers:
if reg != self.target_reg:
reg = self._vmstate.register_file._registers[reg]
# save register's value
registers[reg.name] = copy.deepcopy(reg.value)
# save register's input lookup information
if self._vmstate.input_lookup_enabled:
input_lookup = copy.deepcopy(self._vmstate.register_file.get_input_lookup(reg.name))
if input_lookup is not None:
registers_input_lookup[reg.name] = input_lookup
# simulate register saving.
for _ in range(0, self.tick_count):
if self._vmstate.do_data_anomaly_check:
self._vmstate.function_call_lookup['regs'].append(self._vmstate.memory.stack.top_address)
self._vmstate.memory.stack.push(self._vmstate.memory.address_dimension, copy.deepcopy(registers), 'register')
# save registers into saving_pool
to_save = {'registers': registers, 'registers_input_lookup': registers_input_lookup}
self._vmstate.reg_saving_pool.append(to_save)
def __str__(self):
return 'SaveRegistersOperation(~{}) [{} ops]'.format(self.target_reg, self.tick_count)
class RestoreRegistersOperation(Instruction):
"""
Represents an operation that emulates the restoring of registers after a called function returns.
It is setup by proiding the corresponding SaveRegistersOperation, from which takes the tick_count and the
registers values to be restored.
"""
def __init__(self, save_registers_operation):
super().__init__()
self.tick_count = save_registers_operation.tick_count
self._omit_target = True
def get_val(self):
to_restore = self._vmstate.reg_saving_pool.pop()
# restore each register saved by the SaveRegistersOperation
# NB: the target register is not included here by construction of the
# SaveRegistersOperation method.
for reg in to_restore['registers']:
self._vmstate.register_file.write(reg, copy.deepcopy(to_restore['registers'][reg]))
# restore input lookup information.
if self._vmstate.input_lookup_enabled:
for reg in to_restore['registers_input_lookup']:
self._vmstate.register_file._input_lookup[reg] = copy.deepcopy(to_restore['registers_input_lookup'][reg])
# simulate registers restoring.
for _ in range(0, self.tick_count):
stacked_reg_file = self._vmstate.memory.stack.pop(self._vmstate.memory.address_dimension)
if to_restore['registers'] != stacked_reg_file:
raise MemoryException('[RestoreRegistersOperation] Register file saved in stack is anomalous (wrong register values).')
def __str__(self):
return 'RestoreRegistersOperation() [{} ops]'.format(self.tick_count)

View File

@ -0,0 +1,48 @@
from ScEpTIC.exceptions import RegAllocException
class RegisterPool:
"""
Pool of registers used by register allocation to get available registers.
"""
def __init__(self, regs_number, reg_prefix):
# list of registers state: True -> free; False -> in use
self.registers = [True for x in range(regs_number)]
self.regs_number = regs_number
self.reg_prefix = reg_prefix
def get_reg(self):
"""
Returns a free register. If no register is available, it raises a RegAllocException
The returned register is marked as busy.
"""
for reg_id in range(0, self.regs_number):
if self.registers[reg_id]:
# set the register to in use
self.registers[reg_id] = False
return '{}{}'.format(self.reg_prefix, reg_id)
raise RegAllocException('No register available for register allocation!')
def _get_id_from_reg_name(self, reg_name):
"""
Returns the id of a register, given its name.
"""
if reg_name is None:
return None
reg_name = reg_name.replace(self.reg_prefix, '')
return int(reg_name)
def free_reg(self, reg_name):
"""
Marks a given register as free.
"""
reg_id = self._get_id_from_reg_name(reg_name)
self.registers[reg_id] = True

View File

@ -0,0 +1,21 @@
import logging
CONFIG_FUNCTION_NAME = 'get_config'
def apply_transformation(transformation, functions_ast, vmstate, f_declarations):
"""
Applies a program transformation.
Note that any transformation must be specified as a sub-module of the transformation module.
The name of the module represents the name of the transformation, which is passed as argument to this method.
Each transformation sub-module need a apply_transformation() function that takes as argument the functions AST.
"""
logging.info(f"Applying transformation {transformation}")
# Import custom module
module_name = 'ScEpTIC.AST.transformations.{}.main'.format(transformation)
function_name = 'apply_transformation'
module = __import__(module_name, fromlist=[function_name])
transformation_function = getattr(module, function_name)
# Apply transformation
transformation_function(functions_ast, vmstate, f_declarations)

View File

@ -0,0 +1,60 @@
from ScEpTIC.config.base_config import ScEpTICBaseConfig
class BaseTransformationsConfig(ScEpTICBaseConfig):
"""
Configuration for default transformations
"""
_config_domain = {
'simulate_clock_cycles_function_name': {'class': str, 'mode': 'set'},
'increment_custom_metric_function_name': {'class': str, 'mode': 'set'},
'enter_mcu_lpm_function_name': {'class': str, 'mode': 'set'},
'print_dump_function_name': {'class': str, 'mode': 'set'},
'interrupts_enable_firing_function_name': {'class': str, 'mode': 'set'},
'interrupts_disable_firing_function_name': {'class': str, 'mode': 'set'},
'interrupts_enable_function_name': {'class': str, 'mode': 'set'},
'interrupts_disable_function_name': {'class': str, 'mode': 'set'},
'timer_start_function_name': {'class': str, 'mode': 'set'},
'timer_stop_function_name': {'class': str, 'mode': 'set'},
'timer_set_period_function_name': {'class': str, 'mode': 'set'},
'printf_function_name': {'class': str, 'mode': 'set'},
'custom_log_function_name': {'class': str, 'mode': 'set'},
'custom_log_dump_function_name': {'class': str, 'mode': 'set'},
}
def __init__(self, main_config):
super().__init__(main_config)
self.__ir_prefix = f"{main_config.program.ir_function_prefix}"
self.simulate_clock_cycles_function_name = f"{self.__ir_prefix}sceptic_simulate_cycles"
self.increment_custom_metric_function_name = f"{self.__ir_prefix}sceptic_increment_custom_metric"
self.enter_mcu_lpm_function_name = f"{self.__ir_prefix}sceptic_mcu_lpm"
self.print_dump_function_name = f"{self.__ir_prefix}sceptic_print_dump"
self.interrupts_enable_firing_function_name = f"{self.__ir_prefix}sceptic_interrupts_enable_firing"
self.interrupts_disable_firing_function_name = f"{self.__ir_prefix}sceptic_interrupts_disable_firing"
self.interrupts_enable_function_name = f"{self.__ir_prefix}sceptic_interrupts_enable"
self.interrupts_disable_function_name = f"{self.__ir_prefix}sceptic_interrupts_disable"
self.timer_start_function_name = f"{self.__ir_prefix}sceptic_timer_start"
self.timer_stop_function_name = f"{self.__ir_prefix}sceptic_timer_stop"
self.timer_set_period_function_name = f"{self.__ir_prefix}sceptic_timer_set_period"
self.printf_function_name = f"{self.__ir_prefix}printf"
self.custom_log_function_name = f"{self.__ir_prefix}sceptic_custom_log"
self.custom_log_dump_function_name = f"{self.__ir_prefix}sceptic_custom_log_dump"
for function_name in self._config_domain.keys():
self._register_config_callback(function_name, self._append_ir_callback)
def _append_ir_callback(self, config_name, config_value):
if not config_value.startswith("sceptic_"):
config_value = f"sceptic_{config_value}"
setattr(self, config_name, f"{self.__ir_prefix}{config_value}")
# Returns the configuration
def get_config(main_config):
return BaseTransformationsConfig(main_config)

View File

@ -0,0 +1,52 @@
from ScEpTIC.AST.elements.instruction import Instruction
class CustomInstruction(Instruction):
"""
CustomInstruction() base class
"""
_omit_target = True
tick_count = 0
def __init__(self, call_operation):
super().__init__()
self.basic_block_id = call_operation.basic_block_id
self.label = call_operation.label
self.preds = call_operation.preds
self.metadata = call_operation.metadata
self.virtual_memory_target = call_operation.virtual_memory_target
self.target = None
self.args = call_operation.function_args
self._str_params = None
def get_val(self):
return
def __str__(self):
retstr = super().__str__()
if self._str_params is None:
params = [self.get_arg(i) for i in range(len(self.args))]
else:
params = self._str_params
str_params = ', '.join([f"\"{x}\"" if isinstance(x, str) else f"{x}" for x in params])
retstr += f"{self.__class__.__name__}({str_params})"
return retstr
def get_arg(self, id):
try:
# llvm string
if self.args[id].is_llvm_string():
address = self.args[id].get_val()
return self._vmstate.memory.gst._get_gst_from_address(address).read_string_from_address(address)
return self.args[id].get_val()
except BaseException:
return self.args[id]
def argc(self):
return len(self.args)

View File

@ -0,0 +1,51 @@
from . import CustomInstruction
import os
class CustomLog(CustomInstruction):
"""
CustomLog() logs data
"""
tick_count = 1
handler = None
def __init__(self, call_operation, no_pointer=True):
super(CustomLog, self).__init__(call_operation)
self.no_pointer = no_pointer
def run(self, update_program_counter=True):
if CustomLog.handler is None:
os.makedirs(self._vmstate.log_dir, exist_ok=True)
CustomLog.handler = open(os.path.join(self._vmstate.log_dir, 'custom_logs.csv'), "w")
CustomLog.handler.write("timestamp, program_counter, custom_log_data\n")
format_str = self.get_arg(0)
if self.no_pointer:
args = tuple([self.get_arg(i) for i in range(1, self.argc())])
#print(f"CustomLog: {args}")
else:
args = tuple([self._vmstate.memory.read_addr(self.get_arg(i)) for i in range(1, self.argc())])
#print(f"CustomLogDump: {args}")
try:
log_line = "{}".format(format_str % args)
except TypeError:
#log_line = "{} {}".format(format_str, args)
log_line = f"{args}"
if ',' in log_line:
log_line = f"\"{log_line}\""
t = self._vmstate.config.analysis.energy.system_model.get_simulation_time()
pc = f"{self._vmstate.register_file.pc}"
CustomLog.handler.write(f"{t}, {pc}, {log_line}\n")
super().run(update_program_counter)
class CustomLogDump(CustomLog):
def __init__(self, call_operation):
super(CustomLogDump, self).__init__(call_operation, False)

View File

@ -0,0 +1,18 @@
from . import CustomInstruction
class EnterMCULPM(CustomInstruction):
"""
EnterMCULPM() sets the MCU into LPM
params for the equivalent function call:
- wakeup_time
"""
tick_count = 1
def __init__(self, call_operation):
super().__init__(call_operation)
self.lpm_time = int(self.get_arg(0)) if len(self.args) > 0 else 0
def get_lpm_time(self):
# us
return float(self.lpm_time / 1000000.0)

View File

@ -0,0 +1,25 @@
from . import CustomInstruction
class IncrementCustomMetric(CustomInstruction):
"""
IncrementCustomMetric() increments a custom metric
params for the equivalent function call:
- custom metric id
"""
def __init__(self, call_operation):
super().__init__(call_operation)
self.metric_id = self.get_arg(0)
if not self._vmstate.custom_metrics.exists(self.metric_id):
raise Exception(f"Metric #{self.metric_id} not found")
self.val = int(self.get_arg(1)) if len(self.args) > 1 else 1
name = self._vmstate.custom_metrics.get_name(self.metric_id)
self._str_params = [name, self.val]
def run(self, update_program_counter=True):
super().run(update_program_counter)
self._vmstate.custom_metrics.increment(self.metric_id, self.val)

View File

@ -0,0 +1,37 @@
from . import CustomInstruction
class InterruptsDisableFiring(CustomInstruction):
"""
InterruptsDisableFiring() disable interrupts firing
"""
def run(self, update_program_counter=True):
self._vmstate.interrupts_manager.disable_firing()
super().run(update_program_counter)
class InterruptsEnableFiring(CustomInstruction):
"""
InterruptsEnableFiring() enable interrupts firing
"""
def run(self, update_program_counter=True):
self._vmstate.interrupts_manager.enable_firing()
super().run(update_program_counter)
class InterruptsDisable(CustomInstruction):
"""
InterruptsDisable() disable interrupts
"""
def run(self, update_program_counter=True):
self._vmstate.interrupts_manager.disable()
super().run(update_program_counter)
class InterruptsEnable(CustomInstruction):
"""
InterruptsEnable() enable interrupts
"""
def run(self, update_program_counter=True):
self._vmstate.interrupts_manager.enable()
super().run(update_program_counter)

View File

@ -0,0 +1,26 @@
from . import CustomInstruction
class PrintVMDump(CustomInstruction):
"""
PrintVMDump() prints ScEpTIC memory dump
"""
def run(self, update_program_counter=True):
print("START DUMP >")
print()
print(self._vmstate.register_file.pc.pc_tree())
print("[REGISTER FILE]")
print(self._vmstate.register_file.get_visual_dump())
print()
print("[GST]")
print(self._vmstate.memory.gst.get_visual_dump())
print()
print("[STACK]")
print(self._vmstate.memory.stack.get_visual_dump())
print()
print("[HEAP]")
print(self._vmstate.memory.heap.get_visual_dump())
print()
print("END DUMP <")
super().run(update_program_counter)

View File

@ -0,0 +1,22 @@
from . import CustomInstruction
class Printf(CustomInstruction):
"""
printf() function
"""
tick_count = 0
stdout_enabled = True
def run(self, update_program_counter=True):
if self.stdout_enabled:
format_str = self.get_arg(0)
args = tuple([self.get_arg(i) for i in range(1, self.argc())])
try:
print("[PRINTF]: {}".format(format_str % args))
except TypeError:
print("[PRINTF]: {}, {}".format(format_str, args))
super().run(update_program_counter)

View File

@ -0,0 +1,113 @@
from ScEpTIC.emulator.energy.mcu.options import MCUClockCycleAction
from . import CustomInstruction
class SimulateClockCycles(CustomInstruction):
"""
SimulateClockCycles() simulates the execution of a given number of clock cycles
params for the equivalent function call:
- number of clock cycles to simulate
- ratio of register operations
- ratio of volatile memory accesses
- ratio of non-volatile memory accesses
"""
entities = []
@classmethod
def reset(cls):
for entity in cls.entities:
entity.executed_instructions = 0
def __init__(self, call_operation):
super().__init__(call_operation)
self.n_cycles = self.get_arg(0)
self.ratios = [float(self.get_arg(1)), float(self.get_arg(2)), float(self.get_arg(3))]
if sum(self.ratios) != 1.0:
raise Exception("Operation ratios do not sum up to 1")
self.t_base, self.schedule = self.__generate_schedule(self.ratios)
self.executed_instructions = 0
self.current_instruction = None
self.entities.append(self)
def get_instruction_type(self):
"""
Returns the instruction to execute
"""
return self.current_instruction
def get_simulation_data(self):
return {'schedule': self.schedule, 'cycles': self.n_cycles, 'base_period': self.t_base}
def __generate_schedule(self, ratios):
# Possible periods length: possible base periods that lead to integer deadlines
t_bases = [10, 20, 50, 100, 200, 500, 1000]
ops = [MCUClockCycleAction.NO_MEMORY_ACCESS, MCUClockCycleAction.VOLATILE_MEMORY_ACCESS, MCUClockCycleAction.NON_VOLATILE_MEMORY_ACCESS]
t_base = 0
periods = []
int_ratios = []
for t in t_bases:
# Create integer ratios to identify the minimum possible base period
int_ratios = [int(x * t) for x in ratios if x * t == int(x * t)]
# Check if all operations have an integer ratio
if len(int_ratios) == len(ratios):
t_base = t
# Generate periods, accounting for operations never executed (i.e. ratio = 0)
periods = [t / x if x != 0 else t_base+1 for x in int_ratios]
break
# If t_base not set -> an operation has a non-integer period -> not supported (max 2 numbers after comma)
if t_base == 0:
raise Exception("Operation ratios not supported in a period of up to 100ops!")
schedule = []
# Initial deadlines
deadlines = [x for x in periods]
# Generate schedule
for _ in range(0, t_base):
# Element with the lowest deadline
op = min(range(len(deadlines)), key=deadlines.__getitem__)
schedule.append(ops[op])
# Update deadline
deadlines[op] = deadlines[op] + periods[op]
n = {}
error = False
# Test schedule
for i in range(len(ops)):
# Count occurrences of operation ops[i]
n[i] = len([x for x in schedule if x == ops[i]])
if n[i] != int_ratios[i]:
error = True
if error:
raise Exception(f"Wrong schedule: {ops}\nRequested: {int_ratios}\nActual: {n}\n")
return t_base, schedule
def run(self, update_program_counter=True):
# Set current instruction
self.current_instruction = self.schedule[self.executed_instructions % self.t_base]
# Increment executed instructions
self.executed_instructions += 1
# Update program counter if end reached
end_reached = self.executed_instructions >= self.n_cycles
# Reset if end reached
if end_reached:
self.executed_instructions = 0
super().run(update_program_counter=end_reached)

View File

@ -0,0 +1,41 @@
from . import CustomInstruction
class TimerCustomInstruction(CustomInstruction):
"""
Timer custom instruction
"""
@property
def timer_name(self):
return self.get_arg(0)
class TimerStart(TimerCustomInstruction):
"""
TimerStart() starts a given timer
"""
def run(self, update_program_counter=True):
self._vmstate.timers_manager.start_timer(self.timer_name)
super().run(update_program_counter)
class TimerStop(TimerCustomInstruction):
"""
TimerStop() stops a given timer
"""
def run(self, update_program_counter=True):
self._vmstate.timers_manager.stop_timer(self.timer_name)
super().run(update_program_counter)
class TimerSetPeriod(TimerCustomInstruction):
"""
TimerSetPeriod() stops a given timer
"""
def __init__(self, call_operation):
super().__init__(call_operation)
self.period = float(self.get_arg(1))
def run(self, update_program_counter=True):
period = float(self.get_arg(1))
self._vmstate.timers_manager.set_timer_period(self.timer_name, self.period)
super().run(update_program_counter)

View File

@ -0,0 +1,45 @@
from ScEpTIC.AST.elements.instructions.other_operations import CallOperation
from ScEpTIC.AST.transformations.base.instructions.custom_log import CustomLog, CustomLogDump
from ScEpTIC.AST.transformations.base.instructions.printf import Printf
from ScEpTIC.AST.transformations.base.instructions.enter_mcu_lpm import EnterMCULPM
from ScEpTIC.AST.transformations.base.instructions.increment_custom_metric import IncrementCustomMetric
from ScEpTIC.AST.transformations.base.instructions.simulate_clock_cycles import SimulateClockCycles
from ScEpTIC.AST.transformations.base.instructions.print_vm_dump import PrintVMDump
from ScEpTIC.AST.transformations.base.instructions.interrupts import InterruptsEnable, InterruptsDisable, InterruptsEnableFiring, InterruptsDisableFiring
from ScEpTIC.AST.transformations.base.instructions.timers import TimerStart, TimerStop, TimerSetPeriod
def apply_transformation(functions, vmstate, f_declarations):
"""
:param functions: ScEpTIC AST functions
:param vmstate: vmstate
"""
config = vmstate.config.ast_transformations.base
function_map = {
config.simulate_clock_cycles_function_name: SimulateClockCycles,
config.increment_custom_metric_function_name: IncrementCustomMetric,
config.enter_mcu_lpm_function_name: EnterMCULPM,
config.print_dump_function_name: PrintVMDump,
config.interrupts_enable_firing_function_name: InterruptsEnableFiring,
config.interrupts_disable_firing_function_name: InterruptsDisableFiring,
config.interrupts_enable_function_name: InterruptsEnable,
config.interrupts_disable_function_name: InterruptsDisable,
config.timer_start_function_name: TimerStart,
config.timer_stop_function_name: TimerStop,
config.timer_set_period_function_name: TimerSetPeriod,
config.printf_function_name: Printf,
config.custom_log_function_name: CustomLog,
config.custom_log_dump_function_name: CustomLogDump,
}
for f_name, function in functions.items():
for n, instruction in enumerate(function.body):
if isinstance(instruction, CallOperation):
if instruction.name in function_map.keys():
op = function_map[instruction.name](instruction)
function.body[n] = op
for f_name in function_map.keys():
if f_name in f_declarations:
del f_declarations[f_name]

View File

@ -0,0 +1,16 @@
from ScEpTIC.config.base_config import ScEpTICBaseConfig
class MementosMemoryMapConfig(ScEpTICBaseConfig):
"""
Configuration for the Mementos Memory Map Transformation
"""
_config_domain = {
}
def __init__(self, main_config):
super().__init__(main_config)
# Returns the configuration
def get_config(main_config):
return MementosMemoryMapConfig(main_config)

View File

@ -0,0 +1,24 @@
import copy
from ScEpTIC.AST.elements.instructions.other_operations import CallOperation
from ScEpTIC.AST.misc.virtual_memory_enum import VirtualMemoryEnum
def apply_transformation(functions, vmstate, f_declarations):
"""
Sets the correct memory map for Mementos
"""
checkpoint_function_name = vmstate.config.state_retention.state_save_function_name
registers_to_save = set([x for x in range(0, vmstate.config.register_file.n_physical_registers)])
for function in functions.values():
for instruction in function.body:
if "virtual_memory_target" in dir(instruction):
instruction.virtual_memory_target = VirtualMemoryEnum.VOLATILE
if isinstance(instruction, CallOperation) and instruction.name == checkpoint_function_name:
instruction.checkpoint_save_pc = True
instruction.checkpoint_save_esp = True
instruction.checkpoint_save_ram = True
instruction.checkpoint_save_regs = copy.deepcopy(registers_to_save)

View File

@ -0,0 +1,16 @@
from ScEpTIC.config.base_config import ScEpTICBaseConfig
class NVMemoryMapConfig(ScEpTICBaseConfig):
"""
Configuration for the NV Memory Map Transformation
"""
_config_domain = {
}
def __init__(self, main_config):
super().__init__(main_config)
# Returns the configuration
def get_config(main_config):
return NVMemoryMapConfig(main_config)

View File

@ -0,0 +1,31 @@
from ScEpTIC.AST.elements.instructions.memory_operations import StoreOperation
from ScEpTIC.AST.misc.virtual_memory_enum import VirtualMemoryEnum
from ScEpTIC.AST.transformations import virtual_memory
def apply_transformation(functions, vmstate, f_declarations):
"""
Sets all the memory operations to target NVM
"""
virtual_memory.apply_transformation(functions, vmstate)
checkpoint_function_name = vmstate.config.state_retention.state_save_function_name
for function in functions.values():
to_remove = []
for instruction in function.body:
# Set all to NVM
if "virtual_memory_target" in dir(instruction):
instruction.virtual_memory_target = VirtualMemoryEnum.NON_VOLATILE
# Note: dummy writes resolve uncertainty and we need them to apply our versioning technique
# Remove volatile copies
if isinstance(instruction, StoreOperation):
if instruction.is_virtual_memory_copy:
to_remove.append(instruction)
for instruction in to_remove:
function.body.remove(instruction)
function.update_labels()

View File

@ -0,0 +1,36 @@
from ScEpTIC.config.base_config import ScEpTICBaseConfig
class PlaceStaticFrequencyScalingConfig(ScEpTICBaseConfig):
"""
Configuration for placing static frequency scaling operations
"""
_config_domain = {
'scale_up_function_name': {'class': str, 'mode': 'set'},
'scale_down_function_name': {'class': str, 'mode': 'set'},
'scale_up_frequency': {'class': str, 'mode': 'set'},
'scale_down_frequency': {'class': str, 'mode': 'set'},
'scale_up_instructions': {'class': int, 'mode': 'set'},
'scale_down_instructions': {'class': int, 'mode': 'set'},
}
def __init__(self, main_config):
super().__init__(main_config)
self.__ir_prefix = f"{main_config.program.ir_function_prefix}"
self.scale_up_function_name = f"{self.__ir_prefix}scale_up"
self.scale_down_function_name = f"{self.__ir_prefix}scale_down"
self.scale_up_frequency = '16MHz'
self.scale_down_frequency = '8MHz'
self.scale_up_instructions = 3
self.scale_down_instructions = 3
self._register_config_callback("scale_up_function_name", self._append_ir_callback)
self._register_config_callback("scale_down_function_name", self._append_ir_callback)
def _append_ir_callback(self, config_name, config_value):
setattr(self, config_name, f"{self.__ir_prefix}{config_value}")
# Returns the configuration
def get_config(main_config):
return PlaceStaticFrequencyScalingConfig(main_config)

View File

@ -0,0 +1,33 @@
from collections import defaultdict
from ScEpTIC.AST.transformations.place_static_frequency_scaling_ops.scaling_instructions import ScaleUpInstruction, ScaleDownInstruction
from ScEpTIC.AST.transformations.virtual_memory.parsers.ast_parser import ASTParser
from ScEpTIC.AST.transformations.virtual_memory.parsers.memory_tags_parser import MemoryTagsParser
from ScEpTIC.AST.elements.instructions.memory_operations import LoadOperation, StoreOperation
from ScEpTIC.AST.elements.instructions.other_operations import CallOperation
from ScEpTIC.emulator.energy.system_energy_model import SystemEnergyModel
def apply_transformation(functions, vmstate, f_declarations):
"""
:param functions: ScEpTIC AST functions
:param vmstate: vmstate
"""
config = vmstate.config.ast_transformations.place_static_frequency_scaling_ops
system_model = vmstate.config.analysis.energy.system_model
if not isinstance(system_model, SystemEnergyModel):
raise ConfigurationException(f"SystemEnergyModel required by place_static_frequency_scaling_ops transformation!")
for f_name, function in functions.items():
for n, instruction in enumerate(function.body):
if isinstance(instruction, CallOperation):
if instruction.name == config.scale_up_function_name:
op = ScaleUpInstruction(instruction)
function.body[n] = op
elif instruction.name == config.scale_down_function_name:
op = ScaleDownInstruction(instruction)
function.body[n] = op
for f_name in [config.scale_up_function_name, config.scale_down_function_name]:
if f_name in f_declarations:
del f_declarations[f_name]

View File

@ -0,0 +1,57 @@
from ScEpTIC.AST.elements.instruction import Instruction
from ScEpTIC.emulator.energy.mcu.options import MCUClockCycleAction
from ScEpTIC.emulator.energy.options import OpModeName
class StaticScalingInstruction(Instruction):
def __init__(self, call_operation):
super().__init__()
self.basic_block_id = call_operation.basic_block_id
self.label = call_operation.label
self.preds = call_operation.preds
self.metadata = call_operation.metadata
self.target = None
self._omit_target = True
self.virtual_memory_target = call_operation.virtual_memory_target
def get_val(self):
#print(self._vmstate.register_file.pc)
#print(f"Scaling frequency to {self.frequency} {self.__class__.__name__}")
system_model = self._vmstate.config.analysis.energy.system_model
# Simulate scaling instructions execution
for _ in range(self.n_ticks):
system_model.run_step(MCUClockCycleAction.NO_MEMORY_ACCESS, self.op_mode_name)
# Change frequency
system_model.mcu.set_frequency(self.frequency)
class ScaleUpInstruction(StaticScalingInstruction):
def __init__(self, call_operation):
super().__init__(call_operation)
self.n_ticks = self._vmstate.config.ast_transformations.place_static_frequency_scaling_ops.scale_up_instructions
self.frequency = self._vmstate.config.ast_transformations.place_static_frequency_scaling_ops.scale_up_frequency
self.op_mode_name = OpModeName.STATIC_SCALE_UP
def __str__(self):
retstr = super().__str__()
retstr += 'static_scale_up()'
return retstr
class ScaleDownInstruction(StaticScalingInstruction):
def __init__(self, call_operation):
super().__init__(call_operation)
self.n_ticks = self._vmstate.config.ast_transformations.place_static_frequency_scaling_ops.scale_down_instructions
self.frequency = self._vmstate.config.ast_transformations.place_static_frequency_scaling_ops.scale_down_frequency
self.op_mode_name = OpModeName.STATIC_SCALE_DOWN
def __str__(self):
retstr = super().__str__()
retstr += 'static_scale_down()'
return retstr

View File

@ -0,0 +1,362 @@
from ScEpTIC.AST.elements.instructions.memory_operations import LoadOperation, StoreOperation
from ScEpTIC.AST.elements.instructions.other_operations import CallOperation
from ScEpTIC.AST.elements.types import BaseType
from ScEpTIC.AST.misc.virtual_memory_enum import VirtualMemoryEnum
from ScEpTIC.AST.transformations.ratchet.memory_tags_identifier import MemoryTagsIdentifier
class CheckpointPlacer:
def __init__(self, functions, functions_with_outside_frame_accesses, checkpoint_function_name):
self.functions = functions
self.functions_with_outside_frame_accesses = []
self.checkpoint_function_name = checkpoint_function_name
self.function_body_slices = {}
# adjust for LLVM IR
for function_name in functions_with_outside_frame_accesses:
name = f"@{function_name}"
self.functions_with_outside_frame_accesses.append(name)
def place_checkpoints(self):
"""
Places checkpoints in each function
"""
self._identify_memory_tags()
self._place_checkpoints_for_function_calls()
self._remove_duplicate_checkpoints()
self._split_function_bodies()
self._place_checkpoints()
self._synchronize_slices_to_body()
self._remove_duplicate_checkpoints()
self._set_nvm_default_memory()
def print_slices(self):
"""
Prints all the slices
"""
for name, slices in self.function_body_slices.items():
print(name)
for slice_index, slice in enumerate(slices):
print(f" --- Slice {slice_index} ---")
for index, instruction in enumerate(slice):
print(f" {index}: {instruction}")
print()
print()
def _set_nvm_default_memory(self):
"""
Sets each instruction to target NVM
"""
for function in self.functions.values():
for instruction in function.body:
if "virtual_memory_target" in dir(instruction):
instruction.virtual_memory_target = VirtualMemoryEnum.NON_VOLATILE
def _synchronize_slices_to_body(self):
"""
Updates the functions body with the modified slices
"""
for name, slices in self.function_body_slices.items():
instructions = []
for slice in slices:
for instruction in slice:
instructions.append(instruction)
self.functions[name].update_body(instructions)
def _place_checkpoints(self):
"""
Places checkpoints in function slices
"""
for slices in self.function_body_slices.values():
for slice in slices:
self._place_checkpoints_in_slice(slice)
def _identify_memory_tags(self):
"""
Identifies the memory tag of each instruction
"""
memory_tags = MemoryTagsIdentifier(self.functions)
memory_tags.resolve_memory_tags()
def _split_function_bodies(self):
"""
Splits the function body in slices
"""
for name, function in self.functions.items():
# Skip builtins processing
if function.is_builtin or function.is_input:
continue
self.function_body_slices[name] = []
slice = []
for instruction in function.body:
slice.append(instruction)
# slice end reached -> add to slices and start a new slice
if isinstance(instruction, CallOperation) and instruction.name == self.checkpoint_function_name:
self.function_body_slices[name].append(slice)
slice = []
# account for last slice
if len(slice) > 0:
self.function_body_slices[name].append(slice)
def _identify_possible_checkpoints(self, slice):
"""
Scans a slice and identifies all the pairs of store/load, which makes idempotent sections.
1. LOAD
2. LOAD
3. STORE
4. STORE
5. LOAD
Lines 3-4 makes an idempotent section, so we need a checkpoint between line 2 and 3
"""
checkpoint_ranges = {}
reads = {}
writes = {}
# scan in reverse order
for index, instruction in reversed(list(enumerate(slice))):
if isinstance(instruction, LoadOperation):
memory_tag = instruction.memory_tag
# if there is a write after the read -> checkpoint needed
if memory_tag in writes:
write = writes[memory_tag]
del writes[memory_tag]
if memory_tag not in checkpoint_ranges:
checkpoint_ranges[memory_tag] = []
data = {"locations": set(range(index, write)), "memory_tag": memory_tag, "covered": False}
checkpoint_ranges[memory_tag].append(data)
if isinstance(instruction, StoreOperation):
memory_tag = instruction.memory_tag
# only last identified store needed
writes[memory_tag] = index
return checkpoint_ranges
def _identify_ordered_possible_checkpoints(self, slice):
"""
Returns an ordered list of all the possible checkpoint locations
"""
possible_checkpoints = self._identify_possible_checkpoints(slice)
checkpoint_locations = {}
for data in possible_checkpoints.values():
for element in data:
index = len(element["locations"])
if index not in checkpoint_locations:
checkpoint_locations[index] = []
checkpoint_locations[index].append(element)
ordered_locations = []
for _, possible_locations in sorted(checkpoint_locations.items()):
for possible_location in possible_locations:
ordered_locations.append(possible_location)
return ordered_locations
def _place_checkpoints_in_slice(self, slice):
"""
Places checkpoints to create idempotent code sections in the provided slice of code
"""
checkpoints = self._identify_checkpoints(slice)
placed_checkpoints = 0
for checkpoint_index in checkpoints:
index = checkpoint_index + placed_checkpoints
# instruction at index
instruction = slice[index]
# build checkpoint
new_checkpoint = self._create_checkpoint()
new_checkpoint.basic_block_id = instruction.basic_block_id
# update labels
new_checkpoint.label = instruction.label
instruction.label = None
slice.insert(index, new_checkpoint)
placed_checkpoints += 1
def _identify_checkpoints(self, slice):
"""
Returns a list that contains the indexes where to place checkpoints.
The list is ordered.
Approximation: uses a map colouring-like algorithm.
Assumption valid for our benchmarks, as non-idempotent sections cannot be avoided inside loops
"""
ordered_locations = self._identify_ordered_possible_checkpoints(slice)
checkpoints = []
for location in ordered_locations:
# skip if already covered
if location["covered"]:
continue
candidates = {}
# tries each possible checkpoint placement
for candidate_checkpoint in location["locations"]:
candidates[candidate_checkpoint] = []
# check which locations the current candidate covers
for next_location in ordered_locations:
# skip previous locations and already covered locations
# Note: the current location is instead considered
if next_location["covered"]:
continue
if candidate_checkpoint in next_location["locations"]:
candidates[candidate_checkpoint].append(next_location)
# order possible checkpoint placements by covered locations
ordered_candidates = {}
# parse in order of checkpoint location
for candidate_checkpoint, data in sorted(candidates.items()):
index = len(data)
if index not in ordered_candidates:
ordered_candidates[index] = []
ordered_candidates[index].append(candidate_checkpoint)
# get the elements that cover the highest number of locations
max_covered_locations = max(ordered_candidates.keys())
# get the lowest location (instruction index), that is, where a lower number of live registers are (LOAD)
# Note: placing a checkpoint before a STORE requires to save two registers: target and value
target_checkpoint = ordered_candidates[max_covered_locations][0]
checkpoints.append(target_checkpoint)
# mark as covered the covered locations
for location in ordered_locations:
if target_checkpoint in location["locations"]:
location["covered"] = True
return sorted(checkpoints)
def _place_checkpoints_for_function_calls(self):
"""
Places a checkpoint before and after function calls that have outside frame accesses
"""
for function in self.functions.values():
# Skip builtins processing
if function.is_builtin or function.is_input:
continue
target_calls = []
for index, instruction in enumerate(function.body):
if isinstance(instruction, CallOperation) and instruction.name in self.functions_with_outside_frame_accesses:
target_calls.append(index)
new_checkpoints = 0
for call_index in sorted(target_calls):
index = call_index + new_checkpoints
target_call = function.body[index]
# create checkpoints
checkpoint_after = self._create_checkpoint()
checkpoint_before = self._create_checkpoint()
# set basic block id
checkpoint_after.basic_block_id = target_call.basic_block_id
checkpoint_before.basic_block_id = target_call.basic_block_id
# adjust label
checkpoint_before.label = target_call.label
target_call.label = None
# add checkpoint
function.body.insert(index, checkpoint_before)
# new index = index+1 -> add after -> index + 2
function.body.insert(index+2, checkpoint_after)
new_checkpoints += 2
def _remove_duplicate_checkpoints(self):
"""
Removes duplicate checkpoints
"""
for function in self.functions.values():
# Skip builtins processing
if function.is_builtin or function.is_input:
continue
to_remove = []
for index, instruction in enumerate(function.body[:-1]):
if isinstance(instruction, CallOperation) and instruction.name == self.checkpoint_function_name:
next_instruction = function.body[index+1]
if isinstance(next_instruction, CallOperation) and next_instruction.name == self.checkpoint_function_name:
to_remove.append(index)
removed = 0
for checkpoint_index in sorted(to_remove):
index = checkpoint_index - removed
checkpoint = function.body[index]
# sets the index on the next checkpoint
if checkpoint.label is not None:
function.body[index+1].label = checkpoint.label
del function.body[index]
removed += 1
def _create_checkpoint(self):
"""
Creates and returns a custom checkpoint function call
"""
return CallOperation(None, self.checkpoint_function_name, BaseType("int", 32), [], [], {})

View File

@ -0,0 +1,205 @@
from ScEpTIC.AST.elements.instructions.memory_operations import AllocaOperation
from ScEpTIC.AST.elements.instructions.other_operations import CallOperation
class RegisterSavingOptimization:
"""
Identifies the minimum set of registes that each checkpoint need to save
"""
def __init__(self, checkpoint_placer):
self.checkpoint_placer = checkpoint_placer
def set_checkpoints_registers(self):
"""
Identifies and sets the registers that each checkpoints need to save
"""
self.checkpoint_placer._split_function_bodies()
for name in self.checkpoint_placer.function_body_slices.keys():
self._reset_registers_info(name)
self._set_checkpoints_general_purpose_regs(name)
self._set_checkpoints_esp_reg(name)
self.checkpoint_placer._synchronize_slices_to_body()
def _reset_registers_info(self, name):
"""
Resets the register-saving information associated to each checkpoint
"""
slices = self.checkpoint_placer.function_body_slices[name]
for slice in slices:
checkpoints = self._get_checkpoints(slice)
for checkpoint in checkpoints:
checkpoint.checkpoint_save_pc = True
checkpoint.checkpoint_save_esp = False
checkpoint.checkpoint_save_regs = set()
def _get_checkpoints(self, slice):
"""
Returns a list of all the checkpoints included in the slices
"""
instructions = []
for instruction in slice:
if isinstance(instruction, CallOperation) and instruction.name == self.checkpoint_placer.checkpoint_function_name:
instructions.append(instruction)
return instructions
def _set_checkpoints_general_purpose_regs(self, name):
"""
Sets the general purpose registers that each checkpoint in the function need to save
"""
# account for function's argument registers
n_args = len(self.checkpoint_placer.functions[name].arguments)
slices = self.checkpoint_placer.function_body_slices[name]
alloca_regs = self._get_alloca_regs(slices, n_args)
alive_regs = set()
# analyze slices in reverse order to identify general purpose registers to save
for slice in reversed(slices):
to_save = set()
# retrieve regs infos
regs_external_uses, regs_defs = self._get_regs_first_use_def(slice, alloca_regs)
# add live set to registers to save
for reg in alive_regs:
to_save.add(reg)
# if a defined reg is in alive_regs -> remove it from alive_regs
for reg in regs_defs:
if reg in alive_regs:
alive_regs.remove(reg)
# add to alive_regs the used regs
for reg in regs_external_uses:
alive_regs.add(reg)
self._set_checkpoints_save_regs(slice, to_save)
def _get_alloca_regs(self, slices, n_args):
"""
Returns the registers that are target of alloca instructions
"""
alloca_regs = set()
# accounts for registers that contain arguments
for i in range(n_args):
alloca_regs.add(f"%{i}")
# order does not matter
for slice in slices:
for instruction in slice:
if isinstance(instruction, AllocaOperation):
alloca_regs.add(instruction.target.value)
return alloca_regs
def _get_regs_first_use_def(self, slice, alloca_regs):
"""
Returns two lists:
- registers whose use happens before their defs
- registers defined
"""
reg_first_uses = set()
reg_defs = set()
for instruction in slice:
for reg in instruction.get_uses():
# reg not defined + ignore alloca regs
if reg not in reg_defs and reg not in alloca_regs:
reg_first_uses.add(reg)
for reg in instruction.get_defs():
# ignore alloca regs
if reg not in alloca_regs:
reg_defs.add(reg)
return reg_first_uses, reg_defs
def _set_checkpoints_save_regs(self, slice, to_save):
"""
Sets the checkpoints contained in the slice to save the given registers
Assumption (safe) -> no reg is used after a function call
This is a safe assumption for the benchmarks we are considering, as we consider LLVM IR virtual regs
"""
checkpoints = self._get_checkpoints(slice)
for checkpoint in checkpoints:
checkpoint.checkpoint_save_pc = True
for reg in to_save:
checkpoint.checkpoint_save_regs.add(reg)
if len(to_save) > 0:
checkpoint.checkpoint_save_esp = True
def _set_checkpoints_esp_reg(self, name):
"""
Sets which checkpoint in the function need to save the stack pointer
"""
slices = self.checkpoint_placer.function_body_slices[name]
# all checkpoints need to save stack pointer
#for slice in slices:
# self._set_checkpoint_save_esp(slice)
# first slice always need to save stack pointer
self._set_checkpoint_save_esp(slices[0])
# a checkpoint at the end of a slice need to save the stack pointer only if the slice calls a function with a checkpoint
for slice in slices[1:]:
for instruction in slice:
# call to a function that is not a checkpoint
if isinstance(instruction, CallOperation) and instruction.name != self.checkpoint_placer.checkpoint_function_name:
# the function contains a checkpoint, that is, saves the stack pointer
if self._function_has_checkpoint(instruction.name):
self._set_checkpoint_save_esp(slice)
# go to next slice
break
def _set_checkpoint_save_esp(self, slice):
"""
Sets the checkpoints contained in the given slice to save the stack pointer
"""
checkpoints = self._get_checkpoints(slice)
for checkpoint in checkpoints:
checkpoint.checkpoint_save_esp = True
def _function_has_checkpoint(self, name):
"""
Returns if a given function contains a checkpoint
"""
# builtin or input function
if name not in self.checkpoint_placer.function_body_slices:
return False
slices = self.checkpoint_placer.function_body_slices[name]
for slice in slices:
for instruction in slice:
if isinstance(instruction, CallOperation) and instruction.name == self.checkpoint_placer.checkpoint_function_name:
return True
return False

View File

@ -0,0 +1,20 @@
from ScEpTIC.config.base_config import ScEpTICBaseConfig
class RatchetMemoryConfig(ScEpTICBaseConfig):
"""
Configuration for the Virtual Memory Transformation
"""
_config_domain = {
'functions_with_outside_frame_accesses': {'class': str, 'mode': 'add'},
'optimize_saved_registers': {'class': bool, 'mode': 'set'},
}
def __init__(self, main_config):
super().__init__(main_config)
self.functions_with_outside_frame_accesses = []
self.optimize_saved_registers = True
# Returns the configuration
def get_config(main_config):
return RatchetMemoryConfig(main_config)

View File

@ -0,0 +1,21 @@
from ScEpTIC.AST.transformations.ratchet.checkpoint_placer import CheckpointPlacer
from ScEpTIC.AST.transformations.ratchet.checkpoint_registers_optimization import RegisterSavingOptimization
def apply_transformation(functions, vmstate, f_declarations):
"""
Apply ratchet transformation
:param functions: ScEpTIC AST functions
:param vmstate: vmstate
"""
config = vmstate.config.ast_transformations.ratchet
functions_with_outside_frame_accesses = config.functions_with_outside_frame_accesses
checkpoint_function_name = vmstate.config.state_retention.state_save_function_name
checkpoint_placer = CheckpointPlacer(functions, functions_with_outside_frame_accesses, checkpoint_function_name)
checkpoint_placer.place_checkpoints()
if config.optimize_saved_registers:
checkpoint_register_optimization = RegisterSavingOptimization(checkpoint_placer)
checkpoint_register_optimization.set_checkpoints_registers()

View File

@ -0,0 +1,48 @@
from ScEpTIC.AST.elements.instructions.memory_operations import AllocaOperation, StoreOperation, LoadOperation
class MemoryTagsIdentifier:
"""
A memory tag is a virtual representation of an accessed memory location, which captures also the access pattern.
A memory tag is associated to each LoadOperation and StoreOperation.
This class contains a collection of methods for parsing the AST and identifying the memory tag of each operation.
"""
def __init__(self, functions):
self.functions = functions
def resolve_memory_tags(self):
"""
Populates the memory tags of the instructions in each function
"""
for name, function in self.functions.items():
# Skip builtins processing
if function.is_builtin or function.is_input:
continue
self._resolve_memory_tags(function.body)
def _resolve_memory_tags(self, function_body):
"""
Populates the memory tags of the instructions of a given function
"""
elements = {}
first_alloca = True
for instruction in function_body:
# If instruction has a target virtual register, keep track of it to resolve memory tags
if 'target' in dir(instruction) and not instruction.target is None and not instruction._omit_target:
target_reg = instruction.target.value
elements[target_reg] = instruction
# Fix for LLVM 6/7/8 first alloca
if isinstance(instruction, AllocaOperation):
# Set first Alloca
instruction.is_first = first_alloca
first_alloca = False
# Resolve the memory tag of each LoadOperation and StoreOperation
if isinstance(instruction, LoadOperation) or isinstance(instruction, StoreOperation):
instruction.resolve_memory_tag(elements)

Some files were not shown because too many files have changed in this diff Show More