Source code for ducky.devices.storage

"""
Persistent storage support.

Several different persistent storages can be attached to a virtual machine, each
with its own id. This module provides methods for manipulating their content.
Storages operate with blocks of constant, standard size, though this is not
a mandatory requirement - storage with different block size, or even with variable
block size can be implemented.

Block IO subsystem transfers blocks between storages and VM,
"""

import enum
import os
import six

from ..errors import InvalidResourceError
from . import Device, MMIOMemoryPage
from ..util import UINT8_FMT, UINT32_FMT
from ..mm import addr_to_page

#: Size of block, in bytes.
BLOCK_SIZE = 1024

[docs]class StorageAccessError(Exception): """ Base class for storage-related exceptions. """ pass
[docs]class Storage(Device): """ Base class for all block storages. :param ducky.machine.Machine machine: machine storage is attached to. :param int sid: id of storage. :param int size: size of storage, in bytes. """ def __init__(self, machine, name, sid = None, size = None, *args, **kwargs): super(Storage, self).__init__(machine, 'storage', name, *args, **kwargs) self.sid = sid self.size = size
[docs] def do_read_blocks(self, start, cnt): """ Read one or more blocks from device to internal buffer. Child classes are supposed to reimplement this particular method. :param u32_t start: index of the first requested block. :param u32_t cnt: number of blocks to read. """ raise NotImplementedError()
[docs] def do_write_blocks(self, start, cnt, buff): """ Write one or more blocks from internal buffer to device. Child classes are supposed to reimplement this particular method. :param u32_t start: index of the first requested block. :param u32_t cnt: number of blocks to write. """ raise NotImplementedError()
[docs] def read_blocks(self, start, cnt): """ Read one or more blocks from device to internal buffer. Child classes should not reimplement this method, as it provides checks common for (probably) all child classes. :param u32_t start: index of the first requested block. :param u32_t cnt: number of blocks to read. """ self.machine.DEBUG('%s.read_blocks: id=%s, start=%s, cnt=%s', self.__class__.__name__, self.sid, start, cnt) if (start + cnt) * BLOCK_SIZE > self.size: raise StorageAccessError('Out of bounds access: storage size {} is too small'.format(self.size)) return self.do_read_blocks(start, cnt)
[docs] def write_blocks(self, start, cnt, buff): """ Write one or more blocks from internal buffer to device. Child classes should not reimplement this method, as it provides checks common for (probably) all child classes. :param u32_t start: index of the first requested block. :param u32_t cnt: number of blocks to write. """ self.machine.DEBUG('%s.write_blocks: id=%s, start=%s, cnt=%s', self.__class__.__name__, self.sid, start, cnt) if (start + cnt) * BLOCK_SIZE > self.size: raise StorageAccessError('Out of bounds access: storage size {} is too small'.format(self.size)) self.do_write_blocks(start, cnt, buff)
[docs]class FileBackedStorage(Storage): """ Storage that saves its content into a regular file. """ def __init__(self, machine, name, filepath = None, *args, **kwargs): """ :param machine.Machine machine: virtual machine this storage is attached to :param int sid: storage id :param path: path to a underlying file """ self.filepath = filepath st = os.stat(filepath) super(FileBackedStorage, self).__init__(machine, name, size = st.st_size, *args, **kwargs) self.filepath = filepath self.file = None @staticmethod
[docs] def create_from_config(machine, config, section): return FileBackedStorage(machine, section, sid = config.getint(section, 'sid', None), filepath = config.get(section, 'filepath', None))
[docs] def boot(self): self.machine.DEBUG('FileBackedStorage.boot') self.file = open(self.filepath, 'r+b') self.machine.tenh('storage: file %s as storage #%i (%s)', self.filepath, self.sid, self.name)
[docs] def halt(self): self.machine.DEBUG('FileBackedStorage.halt') self.file.flush() self.file.close()
if six.PY2: def _read(self, cnt): return bytearray([ord(c) for c in self.file.read(cnt)]) def _write(self, buff): self.file.write(''.join([chr(b) for b in buff])) else:
[docs] def _read(self, cnt): return self.file.read(cnt)
[docs] def _write(self, buff): self.file.write(bytes(buff))
[docs] def do_read_blocks(self, start, cnt): self.machine.DEBUG('%s.do_read_blocks: start=%s, cnt=%s', self.__class__.__name__, start, cnt) self.file.seek(start * BLOCK_SIZE) return self._read(cnt * BLOCK_SIZE)
[docs] def do_write_blocks(self, start, cnt, buff): self.machine.DEBUG('%s.do_write_blocks: start=%s, cnt=%s', self.__class__.__name__, start, cnt) self.file.seek(start * BLOCK_SIZE) self._write(buff) self.file.flush()
# # Block IO subsystem # DEFAULT_IRQ = 0x02 BIO_RDY = 0x00000001 #: Operation is completed, user can access data and/or request another operation BIO_ERR = 0x00000002 #: Error happened while performing the operation. BIO_READ = 0x00000004 #: Request data read - transfer data from storage to memory. BIO_WRITE = 0x00000008 #: Request data write - transfer data from memory to storage. BIO_BUSY = 0x00000010 #: Data transfer in progress. BIO_DMA = 0x00000020 #: Request direct memory access - data will be transfered directly between storage and RAM.. BIO_SRST = 0x00000040 #: Reset BIO. BIO_USER = BIO_READ | BIO_WRITE | BIO_DMA | BIO_SRST #: Flags that user can set - others are read-only. DEFAULT_MMIO_ADDRESS = 0x8400
[docs]class BlockIOPorts(enum.IntEnum): """ MMIO ports, in form of offsets from a base MMIO address. """ STATUS = 0x00 #: Status port - query BIO status, and submit commands by setting flags SID = 0x04 #: ID of selected storage device BLOCK = 0x08 #: Block ID COUNT = 0x0C #: Number of blocks ADDR = 0x10 #: Address of a memory buffer DATA = 0x14 #: Data port, for non-DMA access
[docs]class BlockIOMMIOMemoryPage(MMIOMemoryPage):
[docs] def read_u32(self, offset): self.DEBUG('%s.read_u32: offset=%s', self.__class__.__name__, UINT8_FMT(offset)) dev = self._device if offset == BlockIOPorts.STATUS: return dev._flags if offset == BlockIOPorts.DATA: return dev.read_data() self.WARN('%s.read_u32: attempt to read unhandled MMIO offset: offset=%s', self.__class__.__name__, UINT8_FMT(offset)) return 0x00000000
[docs] def write_u32(self, offset, value): self.DEBUG('%s.write_u32: offset=%s, value=%s', self.__class__.__name__, UINT8_FMT(offset), UINT32_FMT(value)) value &= 0xFFFFFFFF dev = self._device if offset == BlockIOPorts.STATUS: dev.status_write(value) return if offset == BlockIOPorts.SID: dev.select_storage(value) return if offset == BlockIOPorts.BLOCK: dev._block = value return if offset == BlockIOPorts.COUNT: dev._count = value return if offset == BlockIOPorts.ADDR: dev._address = value return if offset == BlockIOPorts.DATA: dev.write_data(value) return self.WARN('%s.read_u32: attempt to write unhandled MMIO offset: offset=%s, value=%s', self.__class__.__name__, UINT8_FMT(offset), UINT32_FMT(value))
[docs]class BlockIO(Device): def __init__(self, machine, name, mmio_address = None, irq = None, *args, **kwargs): super(BlockIO, self).__init__(machine, 'bio', name, *args, **kwargs) self.DEBUG = self.machine.DEBUG self._mmio_address = mmio_address or DEFAULT_MMIO_ADDRESS self._mmio_page = None self.reset() @staticmethod
[docs] def create_from_config(machine, config, section): return BlockIO(machine, section, mmio_address = config.getint(section, 'mmio-address', DEFAULT_MMIO_ADDRESS), irq = config.getint(section, 'irq', DEFAULT_IRQ))
[docs] def boot(self): self.DEBUG('%s.boot', self.__class__.__name__) self._mmio_page = BlockIOMMIOMemoryPage(self, self.machine.memory, addr_to_page(self._mmio_address)) self.machine.memory.register_page(self._mmio_page) self.machine.tenh('BIO: controller on [%s] as %s', UINT32_FMT(self._mmio_address), self.name)
[docs] def halt(self): self.DEBUG('%s.halt', self.__class__.__name__) self.machine.memory.unregister_page(self._mmio_page)
[docs] def reset(self): self.DEBUG('%s.reset', self.__class__.__name__) self._buffer = None self._buffer_length = None self._buffer_index = None self._storage = None self._device = 0xFFFFFFFF self._flags = BIO_RDY self._block = 0x00000000 self._count = 0x00000000 self._address = 0x00000000 self._dma = False self._busy = False
[docs] def buff_to_memory(self, addr, buff): self.DEBUG('%s.buff_to_memory: addr=%s', self.__class__.__name__, UINT32_FMT(addr)) for i in range(0, len(buff)): self.machine.memory.write_u8(addr + i, buff[i])
[docs] def memory_to_buff(self, addr, length): self.DEBUG('%s.memory_to_buff: addr=%s, length=%s', self.__class__.__name__, UINT32_FMT(addr), length) return bytearray([self.machine.memory.read_u8(addr + i) for i in range(0, length)])
[docs] def _flag_busy(self): """ Signals BIO is running an operation: `BIO_BUSY` is set, and `BIO_RDY` is cleared. """ self.DEBUG('%s._flag_busy', self.__class__.__name__) self._flags |= BIO_BUSY self._flags &= ~BIO_RDY
[docs] def _flag_finished(self): """ Signals BIO is ready to accept new request: `BIO_RDY` is set, and `BIO_BUSY` is cleared. If there was an request running, it is finished now. User can queue another request, or access data in case read by the last request. """ self.DEBUG('%s._flag_finished', self.__class__.__name__) self._flags &= ~BIO_BUSY self._flags |= BIO_RDY
[docs] def _flag_error(self): """ Signals BIO request failed: `BIO_ERR` is set, and both `BIO_RDY` and `BIO_BUSY` are cleared. """ self.DEBUG('%s._flag_error', self.__class__.__name__) self._flags &= ~BIO_RDY self._flags &= ~BIO_BUSY self._flags |= BIO_ERR
[docs] def status_write(self, value): """ Handles writes to `STATUS` register. Starts the IO requested when `BIO_READ` or `BIO_WRITE` were set. """ self.DEBUG('%s.status_write: value=%s', self.__class__.__name__, UINT32_FMT(value)) value &= BIO_USER if value & BIO_SRST: self.DEBUG('%s.status_write: SRST', self.__class__.__name__) self.reset() if value & BIO_DMA: self.DEBUG('%s.status_write: DMA', self.__class__.__name__) self._dma = True if value & BIO_READ or value & BIO_WRITE: self.DEBUG('%s.status_write: IO', self.__class__.__name__) self._flag_busy() self._buffer_index = 0 if self._storage is None: self._flag_error() return if value & BIO_READ: try: self._buffer = self._storage.read_blocks(self._block, self._count) except StorageAccessError: self._flag_error() else: if self._dma is True: self.buff_to_memory(self._address, self._buffer) self._flag_finished() else: if self._dma is True: self._buffer = self.memory_to_buff(self._address, self._count * BLOCK_SIZE) try: self._storage.write_blocks(self._block, self._count, self._buffer) except StorageAccessError: self._flag_error() else: self._flag_finished()
[docs] def select_storage(self, sid): self.DEBUG('%s.select_storage: sid=%s', self.__class__.__name__, UINT32_FMT(sid)) self._device_id = 0xFFFFFFFF self._storage = None try: self._storage = self.machine.get_storage_by_id(sid) self._device_id = sid except InvalidResourceError: self._flag_error()
[docs] def read_data(self): if self._buffer_index == self._buffer_length: self._flag_error() return 0xFFFFFFFF i = self._buffer_index v = (self._buffer[i + 3] << 24) | (self._buffer[i + 2] << 16) | (self._buffer[i + 1] << 8) | self._buffer[i] self._buffer_index += 4 return v
[docs] def write_data(self, value): if self._buffer_index == self._buffer_length: self._flag_error() return i = self._buffer_index self._buffer[i] = value & 0xFF self._buffer[i + 1] = (value >> 8) & 0xFF self._buffer[i + 1] = (value >> 16) & 0xFF self._buffer[i + 1] = (value >> 24) & 0xFF self._buffer_index += 4