diff --git a/src/Chip8.Analysis/CallGraph.cs b/src/Chip8.Analysis/CallGraph.cs new file mode 100644 index 0000000..a7d9cec --- /dev/null +++ b/src/Chip8.Analysis/CallGraph.cs @@ -0,0 +1,229 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; + +namespace Chip8.Analysis; + +public sealed class CallGraph +{ + readonly Dictionary nodeByAddress; + readonly List unreachableSubroutines; + + CallGraph(ushort entry, ushort baseAddress, Dictionary nodeByAddress, List unreachableSubroutines) + { + this.entry = entry; + this.baseAddress = baseAddress; + this.nodeByAddress = nodeByAddress; + this.unreachableSubroutines = unreachableSubroutines; + } + + public ushort entry { get; } + public ushort baseAddress { get; } + + public IReadOnlyCollection nodes => nodeByAddress.Values; + public int nodeCount => nodeByAddress.Count; + + public int subroutineCount => nodeByAddress.Count - (nodeByAddress.ContainsKey(entry) ? 1 : 0); + + public IReadOnlyList orphanedSubroutines => unreachableSubroutines; + + public bool hasNode(ushort address) => nodeByAddress.ContainsKey(address); + + public CallNode? nodeAt(ushort address) => + nodeByAddress.TryGetValue(address, out var node) ? node : null; + + public IReadOnlyList recursiveSubroutines => + nodeByAddress.Values.Where(node => node.isRecursive).OrderBy(node => node.address).ToList(); + + public IReadOnlyList leafSubroutines => + nodeByAddress.Values.Where(node => node.isLeaf).OrderBy(node => node.address).ToList(); + + public IReadOnlyList nodesInOrder => + nodeByAddress.Values.OrderBy(node => node.address).ToList(); + + public IReadOnlyList<(ushort caller, ushort callee)> callEdges + { + get + { + var edges = new List<(ushort, ushort)>(); + foreach (var node in nodesInOrder) + { + foreach (ushort callee in node.callees) + { + edges.Add((node.address, callee)); + } + } + return edges; + } + } + + public static CallGraph build(byte[] program, ushort entry = InstructionDecoder.defaultBaseAddress, ushort baseAddress = InstructionDecoder.defaultBaseAddress) + { + var nodeByAddress = new Dictionary { [entry] = new CallNode(entry, true) }; + var subroutineQueue = new Queue(); + var processed = new HashSet(); + subroutineQueue.Enqueue(entry); + + while (subroutineQueue.Count > 0) + { + ushort subroutine = subroutineQueue.Dequeue(); + if (!processed.Add(subroutine)) + { + continue; + } + + var callerNode = nodeByAddress[subroutine]; + foreach (ushort callee in scanBody(program, subroutine, baseAddress)) + { + callerNode.addCallee(callee); + if (!nodeByAddress.TryGetValue(callee, out var calleeNode)) + { + calleeNode = new CallNode(callee, false); + nodeByAddress[callee] = calleeNode; + } + calleeNode.addCaller(subroutine); + subroutineQueue.Enqueue(callee); + } + } + + markRecursion(nodeByAddress); + var orphans = findOrphans(program, baseAddress, nodeByAddress); + return new CallGraph(entry, baseAddress, nodeByAddress, orphans); + } + + static IEnumerable scanBody(byte[] program, ushort subroutine, ushort baseAddress) + { + var callees = new List(); + var pending = new Queue(); + var seen = new HashSet(); + pending.Enqueue(subroutine); + + while (pending.Count > 0) + { + ushort address = pending.Dequeue(); + if (!seen.Add(address)) + { + continue; + } + + var info = InstructionDecoder.decodeAt(program, address, baseAddress); + if (info is null) + { + continue; + } + + switch (info.flow) + { + case FlowEffect.Fallthrough: + pending.Enqueue(info.nextAddress); + break; + + case FlowEffect.Branch: + pending.Enqueue(info.nextAddress); + pending.Enqueue((ushort)(info.nextAddress + InstructionDecoder.byteLengthAt(program, info.nextAddress, baseAddress))); + break; + + case FlowEffect.Jump: + pending.Enqueue(info.jumpTarget!.Value); + break; + + case FlowEffect.Call: + callees.Add(info.callTarget!.Value); + pending.Enqueue(info.nextAddress); + break; + + case FlowEffect.Return: + case FlowEffect.Terminate: + case FlowEffect.IndirectJump: + break; + } + } + + return callees; + } + + static void markRecursion(Dictionary nodeByAddress) + { + foreach (var node in nodeByAddress.Values) + { + if (reaches(nodeByAddress, node.address, node.address)) + { + node.isRecursive = true; + } + } + } + + static bool reaches(Dictionary nodeByAddress, ushort start, ushort goal) + { + var pending = new Queue(); + var seen = new HashSet(); + foreach (ushort callee in nodeByAddress[start].callees) + { + pending.Enqueue(callee); + } + + while (pending.Count > 0) + { + ushort current = pending.Dequeue(); + if (current == goal) + { + return true; + } + if (!seen.Add(current)) + { + continue; + } + if (nodeByAddress.TryGetValue(current, out var node)) + { + foreach (ushort callee in node.callees) + { + pending.Enqueue(callee); + } + } + } + + return false; + } + + static List findOrphans(byte[] program, ushort baseAddress, Dictionary nodeByAddress) + { + var orphans = new SortedSet(); + foreach (var info in InstructionDecoder.decodeLinear(program, baseAddress)) + { + if (info.kind == InstructionKind.Call) + { + ushort target = info.callTarget!.Value; + if (!nodeByAddress.ContainsKey(target)) + { + orphans.Add(target); + } + } + } + return orphans.ToList(); + } + + public string render() + { + var builder = new StringBuilder(); + builder.Append("call graph: entry 0x").Append(entry.ToString("X4", CultureInfo.InvariantCulture)); + builder.Append(", ").Append(subroutineCount).Append(" subroutines\n"); + foreach (var node in nodesInOrder) + { + builder.Append(" ").Append(node).Append('\n'); + foreach (ushort callee in node.callees) + { + builder.Append(" calls 0x").Append(callee.ToString("X4", CultureInfo.InvariantCulture)).Append('\n'); + } + } + if (unreachableSubroutines.Count > 0) + { + builder.Append(" orphaned targets:\n"); + foreach (ushort orphan in unreachableSubroutines) + { + builder.Append(" 0x").Append(orphan.ToString("X4", CultureInfo.InvariantCulture)).Append('\n'); + } + } + return builder.ToString(); + } +}