blob: b0a7d9d9f4516d4dbff68cb47f7e2094769ee1eb [file] [log] [blame]
# Copyright lowRISC contributors.
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0
import struct
from typing import List, Sequence
from shared.mem_layout import get_memory_layout
from .alert import BadAddrError
from .trace import Trace
class TraceDmemStore(Trace):
def __init__(self, addr: int, value: int, is_wide: bool):
self.addr = addr
self.value = value
self.is_wide = is_wide
def trace(self) -> str:
num_bytes = 32 if self.is_wide else 4
top = self.addr + num_bytes - 1
return 'dmem[{:#x}..{:#x}] = {:#x}'.format(self.addr, top, self.value)
class Dmem:
'''An object representing OTBN's DMEM.
Memory is stored as an array of 32-byte words (the native width for the
OTBN wide side). These words are stored as 256-bit unsigned integers. This
is the same width as the wide-side registers (to avoid unnecessary
packing/unpacking work), but the unsigned values simplify tracing.
'''
def __init__(self) -> None:
_, dmem_size = get_memory_layout()['DMEM']
# Check the arguments look sensible, to avoid allocating massive chunks
# of memory. We know we won't have more than 1 MiB of DMEM.
if dmem_size > 1024 * 1024:
raise RuntimeError('Implausibly large DMEM size: {}'
.format(dmem_size))
# We're going to store DMEM as an array of 32-byte words (the native
# width for the OTBN wide side). Of course, that means dmem_size needs
# to be divisible by 32.
if dmem_size % 32:
raise RuntimeError('DMEM size ({}) is not divisible by 32.'
.format(dmem_size))
num_words = dmem_size // 32
# Initialise the memory to an arbitrary "bad" constant (here,
# 0xdeadbeef). We could initialise to a random value, but maybe it's
# more helpful to generate something recognisable for now.
uninit = 0
for i in range(32 // 4):
uninit = (uninit << 32) | 0xdeadbeef
self.data = [uninit] * num_words
self.trace = [] # type: List[TraceDmemStore]
def _get_u32s(self, idx: int) -> List[int]:
'''Return the value at idx as 8 uint32's
These are ordered by increasing address, so the first word from
_get_u32s(0) will correspond to bytes at addresses 0x0 through 0x3.
These uint32's are the words that get loaded by word accesses to
memory. Since these accesses are also little-endian, the byte at memory
address 0x0 (which holds the LSB for the first 256-bit value) will also
be the LSB of the first u32 returned by _get_u32s(0).
'''
assert 0 <= idx < len(self.data)
word = self.data[idx]
assert 0 <= word <= 1 << 256
# Pack this unsigned word into w32s as 8 32-bit numbers. Note that
# this packing is "little-endian": the first unsigned word contains
# the LSB of the value.
ret = []
w32_mask = (1 << 32) - 1
for subidx in range(8):
ret.append((word >> (32 * subidx)) & w32_mask)
return ret
def _set_u32s(self, idx: int, u32s: List[int]) -> None:
'''Set the value at idx with 8 uint32's in little-endian order'''
assert 0 <= idx < len(self.data)
assert len(u32s) == 8
# Accumulate the u32s into a 256-bit unsigned number (in a
# little-endian fashion)
u256 = 0
for u32 in reversed(u32s):
assert 0 <= u32 <= (1 << 32) - 1
u256 = (u256 << 32) | u32
# Store it!
self.data[idx] = u256
def load_le_words(self, data: bytes) -> None:
'''Replace the start of memory with data'''
if len(data) > 32 * len(self.data):
raise ValueError('Trying to load {} bytes of data, but DMEM '
'is only {} bytes long.'
.format(len(data), 32 * len(self.data)))
# Zero-pad bytes up to the next multiple of 256 bits (because things
# are little-endian, is like zero-extending the last word).
if len(data) % 32:
data = data + b'0' * (32 - (len(data) % 32))
acc = []
for idx32, u32 in enumerate(struct.iter_unpack('<I', data)):
acc.append(u32[0])
if len(acc) == 8:
self._set_u32s(idx32 // 8, acc)
acc = []
# Our zero-extension should have guaranteed we finished on a multiple
# of 8, but it can't hurt to check.
assert acc == []
def dump_le_words(self) -> bytes:
'''Return the contents of memory as bytes.
The bytes are formatted as little-endian 32-bit words. These
words are themselves packed little-endian into 256-bit words.
'''
u32s = [] # type: List[int]
for idx in range(len(self.data)):
u32s += self._get_u32s(idx)
return struct.pack('<{}I'.format(len(u32s)), *u32s)
def load_u256(self, addr: int) -> int:
'''Read a u256 little-endian value from an aligned address'''
assert addr >= 0
if addr & 31:
raise BadAddrError('wide load', addr,
'address is not 32-byte aligned')
word_addr = addr // 32
if word_addr >= len(self.data):
raise BadAddrError('wide load', addr,
'address is above the top of dmem')
return self.data[word_addr]
def store_u256(self, addr: int, value: int) -> None:
'''Write a u256 little-endian value to an aligned address'''
assert addr >= 0
assert 0 <= value < (1 << 256)
if addr & 31:
raise BadAddrError('wide store', addr,
'address is not 32-byte aligned')
word_addr = addr // 32
if word_addr >= len(self.data):
raise BadAddrError('wide store', addr,
'address is above the top of dmem')
self.trace.append(TraceDmemStore(addr, value, True))
def load_u32(self, addr: int) -> int:
'''Read a 32-bit value from memory.
addr should be 4-byte aligned. The result is returned as an unsigned
32-bit integer.
'''
assert addr >= 0
if addr & 3:
raise BadAddrError('narrow load', addr,
'address is not 4-byte aligned')
if (addr + 3) // 32 >= len(self.data):
raise BadAddrError('narrow load', addr,
'address is above the top of dmem')
idx32 = addr // 4
idxW = idx32 // 8
offW = idx32 % 8
return self._get_u32s(idxW)[offW]
def store_u32(self, addr: int, value: int) -> None:
'''Store a 32-bit unsigned value to memory.
addr should be 4-byte aligned.
'''
assert addr >= 0
assert 0 <= value <= (1 << 32) - 1
if addr & 3:
raise BadAddrError('narrow load', addr,
'address is not 4-byte aligned')
if (addr + 3) // 32 >= len(self.data):
raise BadAddrError('narrow load', addr,
'address is above the top of dmem')
self.trace.append(TraceDmemStore(addr, value, False))
def changes(self) -> Sequence[Trace]:
return self.trace
def _commit_store(self, item: TraceDmemStore) -> None:
if item.is_wide:
assert 0 <= item.value < (1 << 256)
self.data[item.addr // 32] = item.value
return
idx32 = item.addr // 4
idxW = idx32 // 8
offW = idx32 % 8
# Since we store data in wide form, we have to do a read/modify/write
# to update. Grab the old word and expand it to a list of 8 u32s.
u32s = self._get_u32s(idxW)
# Replace the word we're setting
assert 0 <= item.value <= (1 << 32) - 1
u32s[offW] = item.value
# And write back
self._set_u32s(idxW, u32s)
def commit(self) -> None:
for item in self.trace:
self._commit_store(item)
self.trace = []
def abort(self) -> None:
self.trace = []