Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added simple control flow graph view #829

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ gradle-checker-processor = "2.0.2"
javafx-plugin = "0.1.0"
shadow = "8.1.1"
peterabeles-gversion = "1.10.3"
jgraphx = "v4.0.0"
fxgraphics2d = "2.1.4"

[libraries]
asm-core = { module = "org.ow2.asm:asm", version.ref = "asm" }
Expand Down Expand Up @@ -129,6 +131,8 @@ treemapfx = { module = "software.coley:treemap-fx", version.ref = "treemapfx" }
vineflower = { module = "org.vineflower:vineflower", version.ref = "vineflower" }

wordwrap = { module = "com.github.davidmoten:word-wrap", version.ref = "wordwrap" }
jgraphx = { module = "com.github.jgraph:jgraphx", version.ref = "jgraphx" }
fxgraphics2d = { module = "org.jfree:org.jfree.fxgraphics2d", version.ref = "fxgraphics2d" }

[bundles]
asm = [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package software.coley.recaf.services.cfg;

import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodNode;

import java.util.ArrayList;
import java.util.List;

public class ControlFlowGraph {

final List<ControlFlowVertex> vertices = new ArrayList<>();
final ClassNode klass;
final MethodNode method;

public ControlFlowGraph(ClassNode klass, MethodNode method) {
this.klass = klass;
this.method = method;
}

public List<ControlFlowVertex> getVertices() {
return vertices;
}

public ClassNode getKlass() {
return klass;
}

public MethodNode getMethod() {
return method;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package software.coley.recaf.services.cfg;

import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.*;

import java.util.HashMap;
import java.util.Map;

public class ControlFlowGraphBuilder {

final Map<AbstractInsnNode, ControlFlowVertex> vertexByInsn = new HashMap<>();

public ControlFlowGraph build(ClassNode klass, MethodNode method) {
AbstractInsnNode[] insns = method.instructions.toArray();
if (insns == null || insns.length == 0) {
return null;
}

ControlFlowGraph graph = new ControlFlowGraph(klass, method);
ControlFlowVertex vertex = new ControlFlowVertex();
for (AbstractInsnNode insn : insns) {
if (insn.getType() == AbstractInsnNode.LABEL) {
if (insn != insns[0] && !vertex.getInsns().isEmpty()) {
graph.getVertices().add(vertex);
}

ControlFlowVertex last = vertex;
vertex = new ControlFlowVertex();
vertex.getInsns().add(insn);
this.vertexByInsn.put(insn, vertex);

vertex.getInRefs().add(new ControlFlowVertexReference(ControlFlowVertexReferenceKind.LABEL, last, insn));
last.getOutRefs().add(new ControlFlowVertexReference(ControlFlowVertexReferenceKind.LABEL, vertex, insn));

continue;
}

vertex.getInsns().add(insn);
this.vertexByInsn.put(insn, vertex);
if (this.isTerminalInsn(insn)) {
graph.getVertices().add(vertex);
vertex = new ControlFlowVertex();
}
}

for (AbstractInsnNode insn : insns) {
if (!this.isTerminalInsn(insn)) {
continue;
}
ControlFlowVertex v = this.vertexByInsn.get(insn);
if (v == null) {
continue;
}
this.computeRefs(v, insn);
}

return graph;
}

boolean isTerminalInsn(AbstractInsnNode insn) {
return insn.getType() == AbstractInsnNode.JUMP_INSN
|| (insn.getOpcode() >= Opcodes.IRETURN && insn.getOpcode() <= Opcodes.RETURN);
}

// FEQ, IFNE, IFLT, IFGE, IFGT, IFLE, IF_ICMPEQ, IF_ICMPNE, IF_ICMPLT, IF_ICMPGE,
// IF_ICMPGT, IF_ICMPLE, IF_ACMPEQ, IF_ACMPNE, GOTO, JSR, IFNULL or IFNONNULL
// TABLESWITCH
void computeRefs(ControlFlowVertex vertex, AbstractInsnNode node) {
if (node.getType() == AbstractInsnNode.JUMP_INSN) {
JumpInsnNode jump = (JumpInsnNode) node;
ControlFlowVertex jumpVertex = this.vertexByInsn.get(jump.label);
ControlFlowVertex nextVertex = node.getNext() == null ? null : this.vertexByInsn.get(node.getNext());
switch (node.getOpcode()) {
case Opcodes.IFEQ:
case Opcodes.IFNE:
case Opcodes.IFLT:
case Opcodes.IFGE:
case Opcodes.IFGT:
case Opcodes.IFLE:

case Opcodes.IF_ICMPEQ:
case Opcodes.IF_ICMPNE:
case Opcodes.IF_ICMPLT:
case Opcodes.IF_ICMPGE:
case Opcodes.IF_ICMPGT:
case Opcodes.IF_ICMPLE:
case Opcodes.IF_ACMPEQ:
case Opcodes.IF_ACMPNE:

case Opcodes.IFNULL:
case Opcodes.IFNONNULL:
if (jumpVertex != null) {
vertex.getOutRefs().add(new ControlFlowVertexReference(ControlFlowVertexReferenceKind.JUMP, jumpVertex, node));
jumpVertex.getInRefs().add(new ControlFlowVertexReference(ControlFlowVertexReferenceKind.JUMP, vertex, node));
}
if (nextVertex != null) {
vertex.getOutRefs().add(new ControlFlowVertexReference(ControlFlowVertexReferenceKind.GOTO, nextVertex, node));
nextVertex.getInRefs().add(new ControlFlowVertexReference(ControlFlowVertexReferenceKind.GOTO, vertex, node));
}
break;

case Opcodes.GOTO:
if (jumpVertex != null) {
vertex.getOutRefs().add(new ControlFlowVertexReference(ControlFlowVertexReferenceKind.GOTO, jumpVertex, node));
jumpVertex.getInRefs().add(new ControlFlowVertexReference(ControlFlowVertexReferenceKind.GOTO, vertex, node));
}
break;

}
} else if (node.getType() == AbstractInsnNode.TABLESWITCH_INSN) {
TableSwitchInsnNode tableSwitchInsn = (TableSwitchInsnNode) node;
ControlFlowVertex defaultVertex = this.vertexByInsn.get(tableSwitchInsn.dflt);
if (defaultVertex != null) {
vertex.getOutRefs().add(new ControlFlowVertexReference(ControlFlowVertexReferenceKind.SWITCH_DEFAULT, defaultVertex, node));
defaultVertex.getOutRefs().add(new ControlFlowVertexReference(ControlFlowVertexReferenceKind.SWITCH_DEFAULT, vertex, node));
}

for (LabelNode label : tableSwitchInsn.labels) {
ControlFlowVertex labelVertex = this.vertexByInsn.get(label);
if (labelVertex == null) {
continue;
}
vertex.getOutRefs().add(new ControlFlowVertexReference(ControlFlowVertexReferenceKind.SWITCH, labelVertex, node));
labelVertex.getOutRefs().add(new ControlFlowVertexReference(ControlFlowVertexReferenceKind.SWITCH, vertex, node));
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package software.coley.recaf.services.cfg;

import jakarta.inject.Inject;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.tree.ClassNode;
import org.objectweb.asm.tree.MethodNode;
import software.coley.recaf.cdi.WorkspaceScoped;
import software.coley.recaf.info.ClassInfo;
import software.coley.recaf.info.JvmClassInfo;
import software.coley.recaf.info.member.MethodMember;
import software.coley.recaf.workspace.model.Workspace;

@WorkspaceScoped
public class ControlFlowGraphService {

final Workspace workspace;

@Inject
public ControlFlowGraphService(Workspace workspace) {
this.workspace = workspace;
}

public ControlFlowGraph createControlFlow(ClassInfo klass, MethodMember member) {
if (!member.isMethod()) {
return null;
}
JvmClassInfo jvmClassInfo = this.workspace.getPrimaryResource().getJvmClassBundle().get(klass.getName());
if (jvmClassInfo == null) {
return null;
}
ClassReader reader = jvmClassInfo.getClassReader();
ClassNode node = new ClassNode();
reader.accept(node, ClassReader.SKIP_DEBUG);
MethodNode method = node.methods.stream()
.filter(it -> it.name.equals(member.getName()) && it.desc.equals(member.getDescriptor()))
.findFirst()
.orElse(null);
if (method == null) {
return null;
}
return new ControlFlowGraphBuilder().build(node, method);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package software.coley.recaf.services.cfg;

import com.google.common.collect.Iterables;
import org.objectweb.asm.tree.AbstractInsnNode;

import java.util.ArrayList;
import java.util.List;

public class ControlFlowVertex {
final List<ControlFlowVertexReference> inRefs = new ArrayList<>();
final List<ControlFlowVertexReference> outRefs = new ArrayList<>();

final List<AbstractInsnNode> insns = new ArrayList<>();

public List<ControlFlowVertexReference> getInRefs() {
return inRefs;
}

public List<ControlFlowVertexReference> getOutRefs() {
return outRefs;
}

public List<AbstractInsnNode> getInsns() {
return insns;
}

public AbstractInsnNode getTerminalNode() {
return Iterables.getLast(this.insns);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package software.coley.recaf.services.cfg;

import org.objectweb.asm.tree.AbstractInsnNode;

public class ControlFlowVertexReference {
final ControlFlowVertexReferenceKind kind;
final ControlFlowVertex vertex;
final AbstractInsnNode insn;

public ControlFlowVertexReference(ControlFlowVertexReferenceKind kind, ControlFlowVertex vertex, AbstractInsnNode insn) {
this.kind = kind;
this.vertex = vertex;
this.insn = insn;
}

public ControlFlowVertexReferenceKind getKind() {
return kind;
}

public ControlFlowVertex getVertex() {
return vertex;
}

public AbstractInsnNode getInsn() {
return insn;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package software.coley.recaf.services.cfg;

public enum ControlFlowVertexReferenceKind {
JUMP,
GOTO,
LABEL,
SWITCH,
SWITCH_DEFAULT
}
2 changes: 2 additions & 0 deletions recaf-ui/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ dependencies {
implementation(libs.reactfx)
implementation(libs.richtextfx)
implementation(libs.treemapfx)
implementation(libs.jgraphx)
implementation(libs.fxgraphics2d)
}

application {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ public ContextMenuProvider getMethodContextMenuProvider(@Nonnull ContextSource s
JvmClassBundle jvmBundle = (JvmClassBundle) bundle;
JvmClassInfo declaringJvmClass = declaringClass.asJvmClass();
view.item("menu.view.methodcallgraph", FLOW, () -> actions.openMethodCallGraph(workspace, resource, jvmBundle,declaringJvmClass, method));
view.item("menu.view.methodcfg", FLOW_CONNECTION, () -> actions.openMethodCFG(workspace, resource, jvmBundle,declaringJvmClass, method));
}

// TODO: implement additional operations
Expand Down
Loading