feat(analysis): add call graph builder
This commit is contained in:
1 parent
3894bbc1e8
commit
696c7991ad
1 file changed
+229
@@ -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<ushort, CallNode> nodeByAddress;
|
||||
readonly List<ushort> unreachableSubroutines;
|
||||
|
||||
CallGraph(ushort entry, ushort baseAddress, Dictionary<ushort, CallNode> nodeByAddress, List<ushort> unreachableSubroutines)
|
||||
{
|
||||
this.entry = entry;
|
||||
this.baseAddress = baseAddress;
|
||||
this.nodeByAddress = nodeByAddress;
|
||||
this.unreachableSubroutines = unreachableSubroutines;
|
||||
}
|
||||
|
||||
public ushort entry { get; }
|
||||
public ushort baseAddress { get; }
|
||||
|
||||
public IReadOnlyCollection<CallNode> nodes => nodeByAddress.Values;
|
||||
public int nodeCount => nodeByAddress.Count;
|
||||
|
||||
public int subroutineCount => nodeByAddress.Count - (nodeByAddress.ContainsKey(entry) ? 1 : 0);
|
||||
|
||||
public IReadOnlyList<ushort> 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<CallNode> recursiveSubroutines =>
|
||||
nodeByAddress.Values.Where(node => node.isRecursive).OrderBy(node => node.address).ToList();
|
||||
|
||||
public IReadOnlyList<CallNode> leafSubroutines =>
|
||||
nodeByAddress.Values.Where(node => node.isLeaf).OrderBy(node => node.address).ToList();
|
||||
|
||||
public IReadOnlyList<CallNode> 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<ushort, CallNode> { [entry] = new CallNode(entry, true) };
|
||||
var subroutineQueue = new Queue<ushort>();
|
||||
var processed = new HashSet<ushort>();
|
||||
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<ushort> scanBody(byte[] program, ushort subroutine, ushort baseAddress)
|
||||
{
|
||||
var callees = new List<ushort>();
|
||||
var pending = new Queue<ushort>();
|
||||
var seen = new HashSet<ushort>();
|
||||
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<ushort, CallNode> nodeByAddress)
|
||||
{
|
||||
foreach (var node in nodeByAddress.Values)
|
||||
{
|
||||
if (reaches(nodeByAddress, node.address, node.address))
|
||||
{
|
||||
node.isRecursive = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool reaches(Dictionary<ushort, CallNode> nodeByAddress, ushort start, ushort goal)
|
||||
{
|
||||
var pending = new Queue<ushort>();
|
||||
var seen = new HashSet<ushort>();
|
||||
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<ushort> findOrphans(byte[] program, ushort baseAddress, Dictionary<ushort, CallNode> nodeByAddress)
|
||||
{
|
||||
var orphans = new SortedSet<ushort>();
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user