// // Copyright (c) 2010-2025 Antmicro // Copyright (c) 2011-2015 Realtime Embedded // // This file is licensed under the MIT License. // Full license text is available in 'licenses/MIT.txt'. // using System; using System.Runtime.InteropServices; using Microsoft.CSharp.RuntimeBinder; using Antmicro.Renode.Peripherals.Bus; using Antmicro.Renode.Utilities; using System.Linq; using System.Collections.Generic; using System.Diagnostics; using Antmicro.Migrant; using Antmicro.Renode.Logging; using System.IO; using System.IO.MemoryMappedFiles; using System.Threading.Tasks; using System.Threading; #if NET // LZ4 library (lz4net) used on mono/.NET Framework relies on dll dynamically generated in // /tmp/f82a68ada1d833f8838dd859bcb27a61/50495300240756128a79c0c0850f98e5.dll. // That generated dll caused problems when switching from dotnet to mono build (in that order), // as dll stayed in /tmp/ and was not regenerated. Such auto-generated dll // was incompatible with mono and caused crash at runtime during serialization. // Newer version of library doesn't rely on autogenerated dll so it doesn't conflict with mono. using K4os.Compression.LZ4; #else using LZ4; #endif using Antmicro.Renode.UserInterface; using Antmicro.Renode.Core; using Antmicro.Renode.Peripherals.CPU; #if PLATFORM_WINDOWS using System.Reflection.Emit; using System.Reflection; #endif using Antmicro.Renode.Exceptions; using Endianess = ELFSharp.ELF.Endianess; namespace Antmicro.Renode.Peripherals.Memory { [Icon("memory")] public sealed class MappedMemory : IBytePeripheral, IWordPeripheral, IDoubleWordPeripheral, IQuadWordPeripheral, IMapped, IDisposable, IKnownSize, ISpeciallySerializable, IMemory, IMultibyteWritePeripheral, ICanLoadFiles, IEndiannessAware { #if PLATFORM_WINDOWS static MappedMemory() { var dynamicMethod = new DynamicMethod("Memset", MethodAttributes.Public | MethodAttributes.Static, CallingConventions.Standard, null, new [] { typeof(IntPtr), typeof(byte), typeof(int) }, typeof(MappedMemory), true); var generator = dynamicMethod.GetILGenerator(); generator.Emit(OpCodes.Ldarg_0); generator.Emit(OpCodes.Ldarg_1); generator.Emit(OpCodes.Ldarg_2); generator.Emit(OpCodes.Initblk); generator.Emit(OpCodes.Ret); MemsetDelegate = (Action)dynamicMethod.CreateDelegate(typeof(Action)); } #endif public MappedMemory(IMachine machine, long size, int? segmentSize = null, string sharedMemoryFileRoot = null) { if(size == 0) { throw new ConstructionException("Memory size cannot be 0"); } if(segmentSize == null) { var proposedSegmentSize = Math.Min(MaximalSegmentSize, Math.Max(MinimalSegmentSize, size / RecommendedNumberOfSegments)); // align it segmentSize = (int)(Math.Ceiling(1.0 * proposedSegmentSize / MinimalSegmentSize) * MinimalSegmentSize); this.DebugLog("Segment size automatically calculated to value {0}B", Misc.NormalizeBinary(segmentSize.Value)); } this.machine = machine; this.size = size; SegmentSize = segmentSize.Value; if(sharedMemoryFileRoot != null && !Directory.Exists(sharedMemoryFileRoot)) { throw new ConstructionException($"Requested {nameof(sharedMemoryFileRoot)} path {sharedMemoryFileRoot} doesn't exist."); } this.sharedMemoryFileRoot = sharedMemoryFileRoot; Init(); } public event Action SegmentTouched; public int SegmentCount { get { return segments.Length; } } public int SegmentSize { get; private set; } public bool UsingSharedMemory { get { return sharedMemoryFileRoot != null; } } public IEnumerable MappedSegments { get { return describedSegments; } } public byte ReadByte(long offset) { if(offset < 0 || offset >= size) { this.Log(LogLevel.Error, "Tried to read byte at offset 0x{0:X} outside the range of the peripheral 0x0 - 0x{1:X}", offset, size); return 0; } var localOffset = GetLocalOffset(offset); var segment = segments[GetSegmentNo(offset)]; return Marshal.ReadByte(new IntPtr(segment.ToInt64() + localOffset)); } public void WriteByte(long offset, byte value) { if(offset < 0 || offset >= size) { this.Log(LogLevel.Error, "Tried to write byte value 0x{0:X} to offset 0x{1:X} outside the range of the peripheral 0x0 - 0x{2:X}", value, offset, size); return; } var localOffset = GetLocalOffset(offset); var segment = segments[GetSegmentNo(offset)]; Marshal.WriteByte(new IntPtr(segment.ToInt64() + localOffset), value); InvalidateMemoryFragment(offset, 1); } public ushort ReadWord(long offset) { if(offset < 0 || offset > size - sizeof(ushort)) { this.Log(LogLevel.Error, "Tried to read word at offset 0x{0:X} outside the range of the peripheral 0x0 - 0x{1:X}", offset, size); return 0; } var localOffset = GetLocalOffset(offset); var segment = segments[GetSegmentNo(offset)]; if(localOffset > SegmentSize - sizeof(ushort)) // cross segment read { var bytes = new byte[2]; bytes[0] = Marshal.ReadByte(new IntPtr(segment.ToInt64() + localOffset)); var secondSegment = segments[GetSegmentNo(offset + 1)]; bytes[1] = Marshal.ReadByte(secondSegment); return BitConverter.ToUInt16(bytes, 0); } return unchecked((ushort)Marshal.ReadInt16(new IntPtr(segment.ToInt64() + localOffset))); } public void WriteWord(long offset, ushort value) { if(offset < 0 || offset > size - sizeof(ushort)) { this.Log(LogLevel.Error, "Tried to write word value 0x{0:X} to offset 0x{1:X} outside the range of the peripheral 0x0 - 0x{2:X}", value, offset, size); return; } var localOffset = GetLocalOffset(offset); var segment = segments[GetSegmentNo(offset)]; if(localOffset > SegmentSize - sizeof(ushort)) // cross segment write { var bytes = BitConverter.GetBytes(value); Marshal.WriteByte(new IntPtr(segment.ToInt64() + localOffset), bytes[0]); var secondSegment = segments[GetSegmentNo(offset + 1)]; Marshal.WriteByte(secondSegment, bytes[1]); InvalidateMemoryFragment(offset, 1); InvalidateMemoryFragment(offset + 1, 1); } else { Marshal.WriteInt16(new IntPtr(segment.ToInt64() + localOffset), unchecked((short)value)); InvalidateMemoryFragment(offset, sizeof(ushort)); } } public uint ReadDoubleWord(long offset) { if(offset < 0 || offset > size - sizeof(uint)) { this.Log(LogLevel.Error, "Tried to read double word at offset 0x{0:X} outside the range of the peripheral 0x0 - 0x{1:X}", offset, size); return 0; } var localOffset = GetLocalOffset(offset); var segment = segments[GetSegmentNo(offset)]; if(localOffset > SegmentSize - sizeof(uint)) // cross segment read { var bytes = ReadBytes(offset, sizeof(uint)); return BitConverter.ToUInt32(bytes, 0); } return unchecked((uint)Marshal.ReadInt32(new IntPtr(segment.ToInt64() + localOffset))); } public void WriteDoubleWord(long offset, uint value) { if(offset < 0 || offset > size - sizeof(uint)) { this.Log(LogLevel.Error, "Tried to write double word value 0x{0:X} to offset 0x{1:X} outside the range of the peripheral 0x0 - 0x{2:X}", value, offset, size); return; } var localOffset = GetLocalOffset(offset); var segment = segments[GetSegmentNo(offset)]; if(localOffset > SegmentSize - sizeof(uint)) // cross segment write { var bytes = BitConverter.GetBytes(value); WriteBytes(offset, bytes); // the memory will be invalidated by `WriteBytes` } else { Marshal.WriteInt32(new IntPtr(segment.ToInt64() + localOffset), unchecked((int)value)); InvalidateMemoryFragment(offset, sizeof(uint)); } } public ulong ReadQuadWord(long offset) { if(offset < 0 || offset > size - sizeof(ulong)) { this.Log(LogLevel.Error, "Tried to read quad word at offset 0x{0:X} outside the range of the peripheral 0x0 - 0x{1:X}", offset, size); return 0; } var localOffset = GetLocalOffset(offset); var segment = segments[GetSegmentNo(offset)]; if(localOffset > SegmentSize - sizeof(ulong)) // cross segment read { var bytes = ReadBytes(offset, sizeof(ulong)); return BitConverter.ToUInt64(bytes, 0); } return unchecked((ulong)Marshal.ReadInt64(new IntPtr(segment.ToInt64() + localOffset))); } public void WriteQuadWord(long offset, ulong value) { if(offset < 0 || offset > size - sizeof(ulong)) { this.Log(LogLevel.Error, "Tried to write quad word value 0x{0:X} to offset 0x{1:X} outside the range of the peripheral 0x0 - 0x{2:X}", value, offset, size); return; } var localOffset = GetLocalOffset(offset); var segment = segments[GetSegmentNo(offset)]; if(localOffset > SegmentSize - sizeof(ulong)) // cross segment write { var bytes = BitConverter.GetBytes(value); WriteBytes(offset, bytes); // the memory will be invalidated by `WriteBytes` } else { Marshal.WriteInt64(new IntPtr(segment.ToInt64() + localOffset), unchecked((long)value)); InvalidateMemoryFragment(offset, sizeof(ulong)); } } public void ReadBytes(long offset, int count, byte[] destination, int startIndex) { if(offset < 0 || offset > size - count) { this.Log(LogLevel.Error, "Tried to read {0} bytes at offset 0x{1:X} outside the range of the peripheral 0x0 - 0x{2:X}", count, offset, size); return; } var read = 0; while(read < count) { var currentOffset = offset + read; var localOffset = GetLocalOffset(currentOffset); var segment = segments[GetSegmentNo(currentOffset)]; var length = Math.Min(count - read, (int)(SegmentSize - localOffset)); Marshal.Copy(new IntPtr(segment.ToInt64() + localOffset), destination, read + startIndex, length); read += length; } } public byte[] ReadBytes(long offset, int count, IPeripheral context = null) { var result = new byte[count]; ReadBytes(offset, count, result, 0); return result; } public void WriteBytes(long offset, byte[] value) { WriteBytes(offset, value, 0, value.Length); } public void WriteBytes(long offset, byte[] value, int count) { WriteBytes(offset, value, 0, count); } public void WriteBytes(long offset, byte[] array, int startingIndex, int count, IPeripheral context = null) { if(offset < 0 || offset > size - count) { this.Log(LogLevel.Error, "Tried to write {0} bytes at offset 0x{1:X} outside the range of the peripheral 0x0 - 0x{2:X}", count, offset, size); return; } var written = 0; while(written < count) { var currentOffset = offset + written; var localOffset = GetLocalOffset(currentOffset); var segment = segments[GetSegmentNo(currentOffset)]; var length = Math.Min(count - written, (int)(SegmentSize - localOffset)); Marshal.Copy(array, startingIndex + written, new IntPtr(segment.ToInt64() + localOffset), length); written += length; InvalidateMemoryFragment(currentOffset, length); } } public void WriteString(long offset, string value) { WriteBytes(offset, new System.Text.ASCIIEncoding().GetBytes(value).Concat(new []{ (byte)'\0' }).ToArray()); } public void LoadFileChunks(string path, IEnumerable chunks, ICPU cpu) { this.LoadFileChunks(chunks, cpu); } public void Reset() { // nothing happens with memory // we do not reset segments (as we do in init), since we do in init only // to have deterministic behaviour (i.e. given script executed two times will // give the same results; not zeroing during reset will however is not necessary // (starting values are not random anyway) } public IntPtr GetSegment(int segmentNo) { if(segmentNo < 0 || segmentNo > segments.Length) { throw new ArgumentOutOfRangeException("segmentNo"); } return segments[segmentNo]; } public void TouchAllSegments() { for(var i = 0; i < segments.Length; i++) { TouchSegment(i); } } public bool IsTouched(int segmentNo) { CheckSegmentNo(segmentNo); return segments[segmentNo] != IntPtr.Zero; } public void TouchSegment(int segmentNo) { CheckSegmentNo(segmentNo); if(segments[segmentNo] == IntPtr.Zero) { var allocSeg = AllocateSegment(segmentNo); var originalPointer = (long)allocSeg; var alignedPointer = (IntPtr)((originalPointer + Alignment) & ~(Alignment - 1)); segments[segmentNo] = alignedPointer; if(UsingSharedMemory) { sharedSegments[segmentNo].AlignmentOffset = (ulong)alignedPointer - (ulong)allocSeg; } this.NoisyLog(string.Format("Segment no {1} allocated at 0x{0:X} (aligned to 0x{2:X}).", allocSeg.ToInt64(), segmentNo, alignedPointer.ToInt64())); originalPointers[segmentNo] = allocSeg; MemSet(alignedPointer, ResetByte, SegmentSize); var segmentTouched = SegmentTouched; if(segmentTouched != null) { segmentTouched(segmentNo); } } } public byte ResetByte { get; set; } public long Size { get { return size; } } // The endianness of MappedMemory matches the host endianness because it is directly backed by host memory public Endianess Endianness => BitConverter.IsLittleEndian ? Endianess.LittleEndian : Endianess.BigEndian; public void InitWithRandomData() { var rand = EmulationManager.Instance.CurrentEmulation.RandomGenerator; var buf = new byte[SegmentSize]; for(var i = 0; i < segments.Length; ++i) { rand.NextBytes(buf); WriteBytes(i * SegmentSize, buf); } } public void ZeroAll() { foreach(var segment in segments.Where(x => x != IntPtr.Zero)) { MemSet(segment, ResetByte, SegmentSize); } } public void ZeroRange(long rangeStart, long rangeLength) { SetRange(rangeStart, rangeLength, ResetByte); } public void SetRange(long rangeStart, long rangeLength, byte value) { var array = new byte[rangeLength]; for(long i = 0; i < rangeLength; ++i) { array[i] = value; } WriteBytes(rangeStart, array); } public void Dispose() { Free(); GC.SuppressFinalize(this); } public void Load(PrimitiveReader reader) { // checking magic var magic = reader.ReadUInt32(); if(magic != Magic) { throw new InvalidOperationException("Memory: Cannot resume state from stream: Invalid magic."); } SegmentSize = reader.ReadInt32(); size = reader.ReadInt64(); ResetByte = reader.ReadByte(); if(emptyCtorUsed) { Init(); } var realSegmentsCount = 0; for(var i = 0; i < segments.Length; i++) { var isTouched = reader.ReadBoolean(); if(!isTouched) { continue; } var compressedSegmentSize = reader.ReadInt32(); var compressedBuffer = reader.ReadBytes(compressedSegmentSize); TouchSegment(i); realSegmentsCount++; #if NET var decodedBuffer = new byte[SegmentSize]; var decodedLength = LZ4Codec.Decode(compressedBuffer, 0, compressedBuffer.Length, decodedBuffer, 0, decodedBuffer.Length); #else var decodedBuffer = LZ4Codec.Decode(compressedBuffer, 0, compressedBuffer.Length, SegmentSize); var decodedLength = decodedBuffer.Length; #endif Marshal.Copy(decodedBuffer, 0, segments[i], decodedLength); } this.NoisyLog(string.Format("{0} segments loaded from stream, of which {1} had content.", segments.Length, realSegmentsCount)); } public void Save(PrimitiveWriter writer) { var globalStopwatch = Stopwatch.StartNew(); var realSegmentsCount = 0; writer.Write(Magic); writer.Write(SegmentSize); writer.Write(size); writer.Write(ResetByte); byte[][] outputBuffers = new byte[segments.Length][]; int[] encodedLengths = new int[segments.Length]; Parallel.For(0, segments.Length, i => { if(segments[i] == IntPtr.Zero) { return; } Interlocked.Increment(ref realSegmentsCount); var localBuffer = new byte[SegmentSize]; Marshal.Copy(segments[i], localBuffer, 0, localBuffer.Length); #if NET var outputBuffer = new byte[LZ4Codec.MaximumOutputSize(localBuffer.Length)]; encodedLengths[i] = LZ4Codec.Encode(localBuffer, 0, localBuffer.Length, outputBuffer, 0, outputBuffer.Length); outputBuffers[i] = outputBuffer; #else outputBuffers[i] = LZ4Codec.Encode(localBuffer, 0, localBuffer.Length); encodedLengths[i] = outputBuffers[i].Length; #endif }); for(var i = 0; i < segments.Length; i++) { if(segments[i] == IntPtr.Zero) { writer.Write(false); continue; } writer.Write(true); writer.Write(encodedLengths[i]); writer.Write(outputBuffers[i], 0, encodedLengths[i]); } this.NoisyLog(string.Format("{0} segments saved to stream, of which {1} had contents.", segments.Length, realSegmentsCount)); globalStopwatch.Stop(); this.NoisyLog("Memory serialization ended in {0}s.", Misc.NormalizeDecimal(globalStopwatch.Elapsed.TotalSeconds)); } public string GetSegmentPath(int segmentNo) { if(segmentNo >= 0 && segmentNo < sharedSegments.Length) { return sharedSegments[segmentNo].MMFPath; } return ""; } public ulong GetSegmentAlignmentOffset(int segmentNo) { if(segmentNo >= 0 && segmentNo < sharedSegments.Length) { return sharedSegments[segmentNo].AlignmentOffset; } return 0UL; } /// /// This constructor is only to be used with serialization. Deserializer has to invoke Load method after such /// construction. /// private MappedMemory() { emptyCtorUsed = true; } private void CheckAlignment(IntPtr segment) { if((segment.ToInt64() & 7) != 0) { throw new ArgumentException(string.Format("Segment address has to be aligned to 8 bytes, but it is 0x{0:X}.", segment)); } } private void Init() { PrepareSegments(); } private void Free() { if(!disposed) { if(UsingSharedMemory) { for(var i = 0; i < segments.Length; i++) { sharedSegments[i].Free(this); this.NoisyLog("Shared segment {0} freed.", i); } } else { for(var i = 0; i < segments.Length; i++) { if(segments[i] != IntPtr.Zero) { var segment = originalPointers[i]; Marshal.FreeHGlobal(segment); segments[i] = IntPtr.Zero; originalPointers[i] = IntPtr.Zero; this.NoisyLog("Segment {0} freed.", i); } } } } disposed = true; } private long GetLocalOffset(long offset) { return (offset % SegmentSize); } void CheckSegmentNo(int segmentNo) { if(segmentNo < 0 || segmentNo >= SegmentCount) { throw new ArgumentOutOfRangeException("segmentNo"); } } private void PrepareSegments() { if(segments != null) { // this is because in case of loading the starting memory snapshot // after deserialization (i.e. resetting after deserialization) // memory segments would have been lost return; } // how many segments we need? var segmentsNo = size / SegmentSize + (size % SegmentSize != 0 ? 1 : 0); this.NoisyLog(string.Format("Preparing {0} segments for {1} bytes of memory, each {2} bytes long.", segmentsNo, size, SegmentSize)); segments = new IntPtr[segmentsNo]; originalPointers = new IntPtr[segmentsNo]; // segments are not allocated until they are used by read, write, load etc (or touched) describedSegments = new IMappedSegment[segmentsNo]; for(var i = 0; i < describedSegments.Length - 1; i++) { describedSegments[i] = new MappedSegment(this, i, (uint)SegmentSize); } var last = describedSegments.Length - 1; var sizeOfLast = (uint)(size % SegmentSize); if(sizeOfLast == 0) { sizeOfLast = (uint)SegmentSize; } describedSegments[last] = new MappedSegment(this, last, sizeOfLast); sharedSegments = new SharedSegment[segmentsNo]; for(var i = 0; i < sharedSegments.Length; i++) { sharedSegments[i] = new SharedSegment(sharedMemoryFileRoot); } } private int GetSegmentNo(long offset) { var segmentNo = (int)(offset / SegmentSize); #if DEBUG // check bounds if(segmentNo >= segments.Length || segmentNo < 0) { throw new IndexOutOfRangeException(string.Format( "Memory: Attemption to use segment number {0}, which does not exist. Total number of segments is {1}.", segmentNo, segments.Length )); } #endif // if such segment is not currently allocated, // allocate it TouchSegment(segmentNo); return segmentNo; } private IntPtr AllocateSegment(int segmentNo) { this.NoisyLog("Allocating segment of size {0}.", SegmentSize); if(UsingSharedMemory) { return sharedSegments[segmentNo].Allocate(SegmentSize + Alignment); } else { return Marshal.AllocHGlobal(SegmentSize + Alignment); } } private void InvalidateMemoryFragment(long start, int length) { if(machine == null) { // this peripheral is not connected to any machine, so there is nothing we can do return; } this.NoisyLog("Invalidating memory fragment at 0x{0:X} of size {1} bytes.", start, length); var registrationPoints = GetRegistrationPoints(); foreach(var cpu in machine.SystemBus.GetCPUs().OfType()) { foreach(var regPoint in registrationPoints) { try { //it's dynamic to avoid cyclic dependency to TranslationCPU ((dynamic)cpu).InvalidateTranslationBlocks(new IntPtr(regPoint + start), new IntPtr(regPoint + start + length)); } catch(RuntimeBinderException) { // CPU does not implement `InvalidateTranslationBlocks`, there is not much we can do } } } } private List GetRegistrationPoints() { if(registrationPointsCached == null) { registrationPointsCached = machine.SystemBus.GetRegistrationPoints(this).Select(x => (long)(x.Range.StartAddress + x.Offset)).ToList(); } return registrationPointsCached; } #if PLATFORM_WINDOWS private static void MemSet(IntPtr pointer, byte value, int length) { MemsetDelegate(pointer, value, length); } private static Action MemsetDelegate; #else [DllImport("libc", EntryPoint = "memset")] private static extern IntPtr MemSet(IntPtr pointer, byte value, int length); #endif private readonly bool emptyCtorUsed; private IntPtr[] segments; private SharedSegment[] sharedSegments; private IntPtr[] originalPointers; private IMappedSegment[] describedSegments; private bool disposed; private long size; private string sharedMemoryFileRoot; private List registrationPointsCached; private readonly IMachine machine; private const uint Magic = 0xABCD6366; private const int Alignment = 0x1000; private const int MinimalSegmentSize = 64 * 1024; private const int MaximalSegmentSize = 16 * 1024 * 1024; private const int RecommendedNumberOfSegments = 16; private class MappedSegment : IMappedSegment { public IntPtr Pointer { get { return parent.GetSegment(index); } } public ulong Size { get { return size; } } public ulong StartingOffset { get { return checked((ulong)index * (ulong)parent.SegmentSize); } } public MappedSegment(MappedMemory parent, int index, uint size) { this.index = index; this.parent = parent; this.size = size; } public void Touch() { parent.TouchSegment(index); } public override string ToString() { return string.Format("[MappedSegment: Size=0x{0:X}, StartingOffset=0x{1:X}]", Size, StartingOffset); } private readonly MappedMemory parent; private readonly int index; private readonly uint size; } private class SharedSegment { public SharedSegment(string sharedMemoryFileRoot) { this.sharedMemoryFileRoot = sharedMemoryFileRoot; } private string MMFFileName { get { return "renode-sharedSegment-" + guid.Value.ToString(); } } public string MMFPath { get { return Path.Combine(sharedMemoryFileRoot, MMFFileName); } } public ulong AlignmentOffset { get; set; } public unsafe IntPtr Allocate(int bytes) { if(Directory.Exists(sharedMemoryFileRoot)) { guid = Guid.NewGuid(); // generate a Guid to be used in a unique filename for this segment // NOTE: It would be preferable to use shared memory objects here instead of files, but // this is currently not supported for Linux on both Mono (v. 6.12.0.200) and dotnet (8.0.403) mmf = MemoryMappedFile.CreateFromFile(MMFPath, FileMode.CreateNew, null, bytes); mmva = mmf.CreateViewAccessor(); byte* ptr = null; mmva.SafeMemoryMappedViewHandle.AcquirePointer(ref ptr); return (IntPtr)ptr; } else { throw new InvalidOperationException(string.Format("Directory {0} not found", sharedMemoryFileRoot)); } } public void Free(MappedMemory mem) { if(!disposed && guid != null) { if(mmva != null) { mmva.SafeMemoryMappedViewHandle.ReleasePointer(); } try { File.Delete(MMFPath); } catch { mem.Log(LogLevel.Warning, "Failed to delete temporary file {0}", MMFPath); } guid = null; mmva = null; mmf = null; disposed = true; } } private string sharedMemoryFileRoot; private Nullable guid; private MemoryMappedFile mmf; private MemoryMappedViewAccessor mmva; private bool disposed; } } }