feat(analysis): add flow traversal and reachability analysis

This commit is contained in:
dualfroz committed 2026-08-25 13:27:44 +02:00
1 parent 42c16f3ce0
commit 1f3ed9595e
3 files changed
+219

No files matched your search

+12
View File
@@ -0,0 +1,12 @@
namespace Chip8.Analysis;
public enum FlowEffect
{
Fallthrough,
Branch,
Jump,
Call,
Return,
IndirectJump,
Terminate,
}
+106
View File
@@ -0,0 +1,106 @@
using System.Collections.Generic;
namespace Chip8.Analysis;
public sealed class FlowTraversal
{
FlowTraversal(
ushort entry,
ushort baseAddress,
IReadOnlyDictionary<ushort, InstructionInfo> decoded,
IReadOnlyCollection<ushort> leaders,
IReadOnlyList<ushort> indirectJumps,
IReadOnlyList<ushort> 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<ushort, InstructionInfo> decoded { get; }
public IReadOnlyCollection<ushort> leaders { get; }
public IReadOnlyList<ushort> indirectJumps { get; }
public IReadOnlyList<ushort> externalTargets { get; }
public static FlowTraversal explore(byte[] program, ushort entry = InstructionDecoder.defaultBaseAddress, ushort baseAddress = InstructionDecoder.defaultBaseAddress)
{
var decoded = new Dictionary<ushort, InstructionInfo>();
var leaders = new SortedSet<ushort> { entry };
var indirectJumps = new List<ushort>();
var externalTargets = new SortedSet<ushort>();
var pending = new Queue<ushort>();
var seen = new HashSet<ushort>();
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<ushort>(indirectJumps);
indirectList.Sort();
return new FlowTraversal(entry, baseAddress, decoded, leaders, indirectList, new List<ushort>(externalTargets));
}
static void enqueue(Queue<ushort> pending, ushort address) => pending.Enqueue(address);
}
+101
View File
@@ -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<ushort> 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<ushort>(traversal.decoded.Keys);
}
public ushort entry { get; }
public ushort baseAddress { get; }
public int programLength { get; }
public IReadOnlyCollection<ushort> 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<ushort> reachableByteAddresses()
{
var bytes = new HashSet<ushort>();
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<InstructionInfo> deadInstructions(byte[] program)
{
var linear = InstructionDecoder.decodeLinear(program, baseAddress);
return linear.Where(info => !reachable.Contains(info.address)).ToList();
}
public IReadOnlyList<ushort> dataByteAddresses()
{
var covered = reachableByteAddresses();
var data = new List<ushort>();
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();
}
}