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

241 lines
9.7 KiB
Python

import logging
from ScEpTIC.emulator.memory.virtual_memory import VirtualMemory
from ScEpTIC.emulator.memory.virtual_memory_cell import VirtualMemoryCell
from ScEpTIC.exceptions import MemoryException
class VirtualStack(VirtualMemory):
"""
Extension of VirtualMemory which represents the stack of the simulated program.
In this implementation the stack grows from lower to higher addresses.
Write and read functions are necessary due to the memory allocation method given by LLVM-IR.
It does not handle the stack: firstly it allocates some space, then it writes to it.
PUSH and POP operations are to support function calls.
"""
def __eq__(self, other):
if not isinstance(other, VirtualStack):
return False
return super().__eq__(other)
def _get_top_cell_address(self):
"""
Returns the address of the memory cell in top of the stack.
"""
logging.debug('[{}] Getting address of cell on top of the stack.'.format(self.mem_type))
indexes = sorted(self._memory.keys())
try:
index = indexes.index(self.top_address)-1
except ValueError:
index = -1
try:
return indexes[index]
except ValueError:
raise MemoryException('[{}] Stack seems to be empty!'.format(self.mem_type))
def allocate(self, dimension, dimension_in_bits = True, metadata = None, alignment = None):
"""
Allocates elements on top of the stack.
"""
dimension = self.convert_dimension(dimension, dimension_in_bits)
# get address of first free space
address = self.top_address
logging.debug('[{}] Allocating {} bytes at address {}{}.'.format(self.mem_type, dimension, self.address_prefix, hex(address)))
if address in self._memory:
cell = self._memory[address]
if cell.dimension == dimension:
cell.metadata = metadata
# clear tracked data
cell.other_memory_trace_addresses = []
elif cell.dimension > dimension:
# redimensionate cell and create nulled space
# create new cell
new_cell = VirtualMemoryCell(address, self.address_prefix, dimension, alignment=alignment)
new_cell.set_lookup(None, self._vmstate.register_file.pc, self._vmstate.global_clock, True)
self._memory[address] = new_cell
# create remaining part of space
diff_addr = address + dimension
diff_dim = cell.dimension - dimension
diff_cell = VirtualMemoryCell(diff_addr, self.address_prefix, diff_dim, alignment=alignment)
diff_cell.set_lookup(None, self._vmstate.register_file.pc, self._vmstate.global_clock, True)
self._memory[diff_addr] = diff_cell
# For memory trace: in this case, we "allocate" a smaller cell.
# Allocation in stack happens only on function call, the start memory address remains unchanged
# so eventual anomalies are still analyzable (we are sizing down)
else:
# need more cells
required_dimension = dimension
also_track_addresses = []
# for each element which can start inside the address interval
for i in range(address, address + dimension):
# if a cell exists at such interval
if i in self._memory:
cell = self._memory[i]
# decrement required dimension
required_dimension -= cell.dimension
# For memory trace: in this case, we "allocate" a bigger cell.
# We need to trace the memory access as if a write on the whole cells happened
also_track_addresses.append(cell.absolute_address)
# remove memory cell
del self._memory[i]
# if considered cell bigger than required dimension (exceed address + dimension)
if required_dimension < 0:
diff_addr = cell.address + cell.dimension + required_dimension # NB: required_dimension is negative!!!
diff_dim = -required_dimension
diff_cell = VirtualMemoryCell(diff_addr, self.address_prefix, diff_dim, alignment=alignment)
diff_cell.set_lookup(None, self._vmstate.register_file.pc, self._vmstate.global_clock, True)
self._memory[diff_addr] = diff_cell
break
# Here two cases: Required dimension reached or not. If not, top of the modified stack have been reached and space needs to be allocated.
# If yes, cells are deleted and must be recreated
new_cell = VirtualMemoryCell(address, self.address_prefix, dimension, alignment=alignment)
new_cell.set_lookup(None, self._vmstate.register_file.pc, self._vmstate.global_clock, True)
self._memory[address] = new_cell
new_cell.other_memory_trace_addresses = also_track_addresses
self._memory[address].metadata = metadata
self.top_address = self.top_address + dimension
return '{}{}'.format(self.address_prefix, hex(address))
return self._allocate(dimension, False, metadata, alignment)
def deallocate(self, address, resolve_address = True, ignore_exception = False):
"""
Decrements the ESP (-> deallocation all the cells until a given address).
It doesn't really remove data.
"""
if resolve_address:
address = self.get_real_address(address)
logging.debug('[{}] Deallocating memory starting from address {}{}.'.format(self.mem_type, self.address_prefix, hex(address)))
if address not in self._memory:
if ignore_exception:
return
raise MemoryException('[{}] Unable to perform memory deallocation operation: no memory cell starts at {}'.format(self.mem_type, address))
self.top_address = address
def _allocate(self, dimension, dimension_in_bits = True, metadata = None, alignment = None):
"""
Allocates a single cell in top of the stack with a given dimension, and returns the address.
(PUSH-like)
"""
dimension = self.convert_dimension(dimension, dimension_in_bits)
# get address of first free space
address = self.top_address
logging.debug('[{}] Allocating {} bytes at address {}{}.'.format(self.mem_type, dimension, self.address_prefix, hex(address)))
# allocate space
cell = VirtualMemoryCell(address, self.address_prefix, dimension, alignment=alignment)
cell.set_lookup(None, self._vmstate.register_file.pc, self._vmstate.global_clock, True)
cell.metadata = metadata
# append to memory
self._memory[address] = cell
# update address of first free space
self.top_address = self.top_address + dimension
return '{}{}'.format(self.address_prefix, hex(address))
def _deallocate(self, address, resolve_address = True, ignore_exception = False):
"""
Deallocates all the cells until address. Is used to remove the cells in top of the stack.
(POP-like)
"""
if resolve_address:
address = self.get_real_address(address)
logging.debug('[{}] Deallocating memory starting from address {}{}.'.format(self.mem_type, self.address_prefix, hex(address)))
if address not in self._memory:
if ignore_exception:
return
raise MemoryException('[{}] Unable to perform memory deallocation operation: no memory cell starts at {}. {}'.format(self.mem_type, address, sorted(self._memory.keys())))
# use list(...) to avoid RuntimeError: dictionary changed size during iteration
for addr in list(self._memory.keys()):
if addr >= address:
logging.debug('[{}] Deallocating memory cell at address {}{} ({} bytes).'.format(self.mem_type, self.address_prefix, hex(addr), self._memory[addr].dimension))
del self._memory[addr]
self.top_address = address
def _set_metadata(self, address, metadata):
"""
Sets metadata information to help finding anomalies in stack.
"""
address = self.get_real_address(address)
self._memory[address].metadata = metadata
def push(self, dimension, content, metadata = None, dimension_in_bits = True):
"""
Push a given value on top of the stack
"""
address = self.allocate(dimension, dimension_in_bits, metadata)
self.write(address, dimension, content, dimension_in_bits)
# metadata info for stack anomalies
if metadata is not None:
self._set_metadata(address, metadata)
def pop(self, dimension, dimension_in_bits = True):
"""
Pops the value on the top of the stack
"""
address = self._get_top_cell_address()
# address is the real one
value = self.read(address, dimension, dimension_in_bits, False)
self.deallocate(address, False)
return value