diff --git a/src/Chip8.Analysis/FlowEffect.cs b/src/Chip8.Analysis/FlowEffect.cs new file mode 100644 index 0000000..bb8e7f7 --- /dev/null +++ b/src/Chip8.Analysis/FlowEffect.cs @@ -0,0 +1,12 @@ +namespace Chip8.Analysis; + +public enum FlowEffect +{ + Fallthrough, + Branch, + Jump, + Call, + Return, + IndirectJump, + Terminate, +} diff --git a/src/Chip8.Analysis/FlowTraversal.cs b/src/Chip8.Analysis/FlowTraversal.cs new file mode 100644 index 0000000..904b8f0 --- /dev/null +++ b/src/Chip8.Analysis/FlowTraversal.cs @@ -0,0 +1,106 @@ +using System.Collections.Generic; + +namespace Chip8.Analysis; + +public sealed class FlowTraversal +{ + FlowTraversal( + ushort entry, + ushort baseAddress, + IReadOnlyDictionary decoded, + IReadOnlyCollection leaders, + IReadOnlyList indirectJumps, + IReadOnlyList externalTargets) + { + this.entry = entry; + this.baseAddress = baseAddress; + this.decoded = decoded; + this.leaders = leaders; + this.indirectJumps = indirectJumps; + this.externalTargets = externalTargets; + } + + public ushort entry { get; } + public ushort baseAddress { get; } + public IReadOnlyDictionary decoded { get; } + public IReadOnlyCollection leaders { get; } + public IReadOnlyList indirectJumps { get; } + public IReadOnlyList externalTargets { get; } + + public static FlowTraversal explore(byte[] program, ushort entry = InstructionDecoder.defaultBaseAddress, ushort baseAddress = InstructionDecoder.defaultBaseAddress) + { + var decoded = new Dictionary(); + var leaders = new SortedSet { entry }; + var indirectJumps = new List(); + var externalTargets = new SortedSet(); + var pending = new Queue(); + var seen = new HashSet(); + pending.Enqueue(entry); + + while (pending.Count > 0) + { + ushort address = pending.Dequeue(); + if (!seen.Add(address)) + { + continue; + } + + var info = InstructionDecoder.decodeAt(program, address, baseAddress); + if (info is null) + { + externalTargets.Add(address); + continue; + } + + decoded[address] = info; + + switch (info.flow) + { + case FlowEffect.Fallthrough: + enqueue(pending, info.nextAddress); + break; + + case FlowEffect.Branch: + { + ushort notTaken = info.nextAddress; + ushort taken = (ushort)(notTaken + InstructionDecoder.byteLengthAt(program, notTaken, baseAddress)); + leaders.Add(notTaken); + leaders.Add(taken); + enqueue(pending, notTaken); + enqueue(pending, taken); + break; + } + + case FlowEffect.Jump: + leaders.Add(info.jumpTarget!.Value); + enqueue(pending, info.jumpTarget!.Value); + break; + + case FlowEffect.Call: + { + ushort target = info.callTarget!.Value; + ushort returnAddress = info.nextAddress; + leaders.Add(target); + leaders.Add(returnAddress); + enqueue(pending, target); + enqueue(pending, returnAddress); + break; + } + + case FlowEffect.IndirectJump: + indirectJumps.Add(address); + break; + + case FlowEffect.Return: + case FlowEffect.Terminate: + break; + } + } + + var indirectList = new List(indirectJumps); + indirectList.Sort(); + return new FlowTraversal(entry, baseAddress, decoded, leaders, indirectList, new List(externalTargets)); + } + + static void enqueue(Queue pending, ushort address) => pending.Enqueue(address); +} diff --git a/src/Chip8.Analysis/ReachabilitySet.cs b/src/Chip8.Analysis/ReachabilitySet.cs new file mode 100644 index 0000000..9b6aca9 --- /dev/null +++ b/src/Chip8.Analysis/ReachabilitySet.cs @@ -0,0 +1,101 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; + +namespace Chip8.Analysis; + +public sealed class ReachabilitySet +{ + readonly SortedSet reachable; + readonly FlowTraversal traversal; + + ReachabilitySet(ushort entry, ushort baseAddress, int programLength, FlowTraversal traversal) + { + this.entry = entry; + this.baseAddress = baseAddress; + this.programLength = programLength; + this.traversal = traversal; + reachable = new SortedSet(traversal.decoded.Keys); + } + + public ushort entry { get; } + public ushort baseAddress { get; } + public int programLength { get; } + + public IReadOnlyCollection reachableAddresses => reachable; + + public int reachableInstructionCount => reachable.Count; + + public bool contains(ushort address) => reachable.Contains(address); + + public int reachableByteCount + { + get + { + int total = 0; + foreach (var info in traversal.decoded.Values) + { + total += info.byteLength; + } + return total; + } + } + + public int programByteCount => programLength; + + public double reachableByteFraction => + programLength == 0 ? 0.0 : (double)reachableByteCount / programLength; + + public HashSet reachableByteAddresses() + { + var bytes = new HashSet(); + foreach (var info in traversal.decoded.Values) + { + for (int offset = 0; offset < info.byteLength; offset++) + { + bytes.Add((ushort)(info.address + offset)); + } + } + return bytes; + } + + public IReadOnlyList deadInstructions(byte[] program) + { + var linear = InstructionDecoder.decodeLinear(program, baseAddress); + return linear.Where(info => !reachable.Contains(info.address)).ToList(); + } + + public IReadOnlyList dataByteAddresses() + { + var covered = reachableByteAddresses(); + var data = new List(); + for (int offset = 0; offset < programLength; offset++) + { + ushort address = (ushort)(baseAddress + offset); + if (!covered.Contains(address)) + { + data.Add(address); + } + } + return data; + } + + public static ReachabilitySet build(byte[] program, ushort entry = InstructionDecoder.defaultBaseAddress, ushort baseAddress = InstructionDecoder.defaultBaseAddress) + { + var traversal = FlowTraversal.explore(program, entry, baseAddress); + return new ReachabilitySet(entry, baseAddress, program.Length, traversal); + } + + public string render() + { + var builder = new StringBuilder(); + builder.Append("reachable instructions: ").Append(reachableInstructionCount); + builder.Append(" (").Append(reachableByteCount).Append(" of ").Append(programLength).Append(" bytes)\n"); + foreach (ushort address in reachable) + { + builder.Append(" 0x").Append(address.ToString("X4", CultureInfo.InvariantCulture)).Append('\n'); + } + return builder.ToString(); + } +}