+ *
+ * To the extent possible under law, the author(s) have dedicated all
+ * copyright and related and neighboring rights to this software to
+ * the public domain worldwide. This software is distributed without
+ * any warranty.
+ *
+ * You should have received a copy of the CC0 Public Domain Dedication
+ * along with this software. If not,
+ * see .
+ */
+
+package gr.gousiosg.javacg.stat;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.bcel.classfile.Attribute;
+import org.apache.bcel.classfile.BootstrapMethod;
+import org.apache.bcel.classfile.BootstrapMethods;
+import org.apache.bcel.classfile.ConstantCP;
+import org.apache.bcel.classfile.ConstantMethodHandle;
+import org.apache.bcel.classfile.ConstantNameAndType;
+import org.apache.bcel.classfile.ConstantPool;
+import org.apache.bcel.classfile.ConstantUtf8;
+import org.apache.bcel.classfile.JavaClass;
+import org.apache.bcel.classfile.Method;
+
+/**
+ * {@link DynamicCallManager} provides facilities to retrieve information about
+ * dynamic calls statically.
+ *
+ * Most of the time, call relationships are explicit, which allows to properly
+ * build the call graph statically. But in the case of dynamic linking, i.e.
+ * invokedynamic instructions, this relationship might be unknown
+ * until the code is actually executed. Indeed, bootstrap methods are used to
+ * dynamically link the code at first call. One can read details about the
+ * invokedynamic
+ * instruction to know more about this mechanism.
+ *
+ * Nested lambdas are particularly subject to such absence of concrete caller,
+ * which lead us to produce method names like lambda$null$0, which
+ * breaks the call graph. This information can however be retrieved statically
+ * through the code of the bootstrap method called.
+ *
+ * In {@link #retrieveCalls(Method, JavaClass)}, we retrieve the (called,
+ * caller) relationships by analyzing the code of the caller {@link Method}.
+ * This information is then used in {@link #linkCalls(Method)} to rename the
+ * called {@link Method} properly.
+ *
+ * @author Matthieu Vergne
+ */
+public class DynamicCallManager {
+ private static final Pattern BOOTSTRAP_CALL_PATTERN = Pattern
+ .compile("invokedynamic\t(\\d+):\\S+ \\S+ \\(\\d+\\)");
+ private static final int CALL_HANDLE_INDEX_ARGUMENT = 1;
+
+ private final Map dynamicCallers = new HashMap<>();
+
+ /**
+ * Retrieve dynamic call relationships based on the code of the provided
+ * {@link Method}.
+ *
+ * @param method {@link Method} to analyze the code
+ * @param jc {@link JavaClass} info, which contains the bootstrap methods
+ * @see #linkCalls(Method)
+ */
+ public void retrieveCalls(Method method, JavaClass jc) {
+ if (method.isAbstract() || method.isNative()) {
+ // No code to consider
+ return;
+ }
+ ConstantPool cp = method.getConstantPool();
+ BootstrapMethod[] boots = getBootstrapMethods(jc);
+ String code = method.getCode().toString();
+ Matcher matcher = BOOTSTRAP_CALL_PATTERN.matcher(code);
+ while (matcher.find()) {
+ int bootIndex = Integer.parseInt(matcher.group(1));
+ BootstrapMethod bootMethod = boots[bootIndex];
+ int calledIndex = bootMethod.getBootstrapArguments()[CALL_HANDLE_INDEX_ARGUMENT];
+ String calledName = getMethodNameFromHandleIndex(cp, calledIndex);
+ String callerName = method.getName();
+ dynamicCallers.put(calledName, callerName);
+ }
+ }
+
+ private String getMethodNameFromHandleIndex(ConstantPool cp, int callIndex) {
+ ConstantMethodHandle handle = (ConstantMethodHandle) cp.getConstant(callIndex);
+ ConstantCP ref = (ConstantCP) cp.getConstant(handle.getReferenceIndex());
+ ConstantNameAndType nameAndType = (ConstantNameAndType) cp.getConstant(ref.getNameAndTypeIndex());
+ return nameAndType.getName(cp);
+ }
+
+ /**
+ * Link the {@link Method}'s name to its concrete caller if required.
+ *
+ * @param method {@link Method} to analyze
+ * @see #retrieveCalls(Method, JavaClass)
+ */
+ public void linkCalls(Method method) {
+ int nameIndex = method.getNameIndex();
+ ConstantPool cp = method.getConstantPool();
+ String methodName = ((ConstantUtf8) cp.getConstant(nameIndex)).getBytes();
+ String linkedName = methodName;
+ String callerName = methodName;
+ while (linkedName.matches("(lambda\\$)+null(\\$\\d+)+")) {
+ callerName = dynamicCallers.get(callerName);
+ linkedName = linkedName.replace("null", callerName);
+ }
+ cp.setConstant(nameIndex, new ConstantUtf8(linkedName));
+ }
+
+ private BootstrapMethod[] getBootstrapMethods(JavaClass jc) {
+ for (Attribute attribute : jc.getAttributes()) {
+ if (attribute instanceof BootstrapMethods) {
+ return ((BootstrapMethods) attribute).getBootstrapMethods();
+ }
+ }
+ return new BootstrapMethod[]{};
+ }
+}
diff --git a/src/main/java/gr/gousiosg/javacg/stat/JCallGraph.java b/src/main/java/gr/gousiosg/javacg/stat/JCallGraph.java
index 4d19408d..cb40dc96 100644
--- a/src/main/java/gr/gousiosg/javacg/stat/JCallGraph.java
+++ b/src/main/java/gr/gousiosg/javacg/stat/JCallGraph.java
@@ -28,48 +28,63 @@
package gr.gousiosg.javacg.stat;
-import java.io.File;
-import java.io.IOException;
-import java.util.Enumeration;
+import java.io.*;
+import java.util.*;
+import java.util.function.Function;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
import org.apache.bcel.classfile.ClassParser;
/**
* Constructs a callgraph out of a JAR archive. Can combine multiple archives
* into a single call graph.
- *
+ *
* @author Georgios Gousios
- *
*/
public class JCallGraph {
public static void main(String[] args) {
- ClassParser cp;
+
+ Function getClassVisitor =
+ (ClassParser cp) -> {
+ try {
+ return new ClassVisitor(cp.parse());
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ };
+
try {
for (String arg : args) {
File f = new File(arg);
-
+
if (!f.exists()) {
System.err.println("Jar file " + arg + " does not exist");
}
-
- JarFile jar = new JarFile(f);
- Enumeration entries = jar.entries();
- while (entries.hasMoreElements()) {
- JarEntry entry = entries.nextElement();
- if (entry.isDirectory())
- continue;
+ try (JarFile jar = new JarFile(f)) {
+ Stream entries = enumerationAsStream(jar.entries());
- if (!entry.getName().endsWith(".class"))
- continue;
+ String methodCalls = entries.
+ flatMap(e -> {
+ if (e.isDirectory() || !e.getName().endsWith(".class"))
+ return (new ArrayList()).stream();
- cp = new ClassParser(arg,entry.getName());
- ClassVisitor visitor = new ClassVisitor(cp.parse());
- visitor.start();
+ ClassParser cp = new ClassParser(arg, e.getName());
+ return getClassVisitor.apply(cp).start().methodCalls().stream();
+ }).
+ map(s -> s + "\n").
+ reduce(new StringBuilder(),
+ StringBuilder::append,
+ StringBuilder::append).toString();
+
+ BufferedWriter log = new BufferedWriter(new OutputStreamWriter(System.out));
+ log.write(methodCalls);
+ log.close();
}
}
} catch (IOException e) {
@@ -77,4 +92,19 @@ public static void main(String[] args) {
e.printStackTrace();
}
}
+
+ public static Stream enumerationAsStream(Enumeration e) {
+ return StreamSupport.stream(
+ Spliterators.spliteratorUnknownSize(
+ new Iterator() {
+ public T next() {
+ return e.nextElement();
+ }
+
+ public boolean hasNext() {
+ return e.hasMoreElements();
+ }
+ },
+ Spliterator.ORDERED), false);
+ }
}
diff --git a/src/main/java/gr/gousiosg/javacg/stat/MethodVisitor.java b/src/main/java/gr/gousiosg/javacg/stat/MethodVisitor.java
index 9f233ccd..21100420 100644
--- a/src/main/java/gr/gousiosg/javacg/stat/MethodVisitor.java
+++ b/src/main/java/gr/gousiosg/javacg/stat/MethodVisitor.java
@@ -29,18 +29,11 @@
package gr.gousiosg.javacg.stat;
import org.apache.bcel.classfile.JavaClass;
-import org.apache.bcel.generic.ConstantPoolGen;
-import org.apache.bcel.generic.ConstantPushInstruction;
-import org.apache.bcel.generic.EmptyVisitor;
-import org.apache.bcel.generic.INVOKEINTERFACE;
-import org.apache.bcel.generic.INVOKESPECIAL;
-import org.apache.bcel.generic.INVOKESTATIC;
-import org.apache.bcel.generic.INVOKEVIRTUAL;
-import org.apache.bcel.generic.Instruction;
-import org.apache.bcel.generic.InstructionConstants;
-import org.apache.bcel.generic.InstructionHandle;
-import org.apache.bcel.generic.MethodGen;
-import org.apache.bcel.generic.ReturnInstruction;
+import org.apache.bcel.generic.*;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
/**
* The simplest of method visitors, prints any invoked method
@@ -54,18 +47,31 @@ public class MethodVisitor extends EmptyVisitor {
private MethodGen mg;
private ConstantPoolGen cp;
private String format;
+ private List methodCalls = new ArrayList<>();
public MethodVisitor(MethodGen m, JavaClass jc) {
visitedClass = jc;
mg = m;
cp = mg.getConstantPool();
- format = "M:" + visitedClass.getClassName() + ":" + mg.getName()
- + " " + "(%s)%s:%s";
+ format = "M:" + visitedClass.getClassName() + ":" + mg.getName() + "(" + argumentList(mg.getArgumentTypes()) + ")"
+ + " " + "(%s)%s:%s(%s)";
}
- public void start() {
+ private String argumentList(Type[] arguments) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < arguments.length; i++) {
+ if (i != 0) {
+ sb.append(",");
+ }
+ sb.append(arguments[i].toString());
+ }
+ return sb.toString();
+ }
+
+ public List start() {
if (mg.isAbstract() || mg.isNative())
- return;
+ return Collections.emptyList();
+
for (InstructionHandle ih = mg.getInstructionList().getStart();
ih != null; ih = ih.getNext()) {
Instruction i = ih.getInstruction();
@@ -73,33 +79,39 @@ public void start() {
if (!visitInstruction(i))
i.accept(this);
}
+ return methodCalls;
}
private boolean visitInstruction(Instruction i) {
short opcode = i.getOpcode();
-
- return ((InstructionConstants.INSTRUCTIONS[opcode] != null)
+ return ((InstructionConst.getInstruction(opcode) != null)
&& !(i instanceof ConstantPushInstruction)
&& !(i instanceof ReturnInstruction));
}
@Override
public void visitINVOKEVIRTUAL(INVOKEVIRTUAL i) {
- System.out.println(String.format(format,"M",i.getReferenceType(cp),i.getMethodName(cp)));
+ methodCalls.add(String.format(format,"M",i.getReferenceType(cp),i.getMethodName(cp),argumentList(i.getArgumentTypes(cp))));
}
@Override
public void visitINVOKEINTERFACE(INVOKEINTERFACE i) {
- System.out.println(String.format(format,"I",i.getReferenceType(cp),i.getMethodName(cp)));
+ methodCalls.add(String.format(format,"I",i.getReferenceType(cp),i.getMethodName(cp),argumentList(i.getArgumentTypes(cp))));
}
@Override
public void visitINVOKESPECIAL(INVOKESPECIAL i) {
- System.out.println(String.format(format,"O",i.getReferenceType(cp),i.getMethodName(cp)));
+ methodCalls.add(String.format(format,"O",i.getReferenceType(cp),i.getMethodName(cp),argumentList(i.getArgumentTypes(cp))));
}
@Override
public void visitINVOKESTATIC(INVOKESTATIC i) {
- System.out.println(String.format(format,"S",i.getReferenceType(cp),i.getMethodName(cp)));
+ methodCalls.add(String.format(format,"S",i.getReferenceType(cp),i.getMethodName(cp),argumentList(i.getArgumentTypes(cp))));
+ }
+
+ @Override
+ public void visitINVOKEDYNAMIC(INVOKEDYNAMIC i) {
+ methodCalls.add(String.format(format,"D",i.getType(cp),i.getMethodName(cp),
+ argumentList(i.getArgumentTypes(cp))));
}
}
diff --git a/src/test/java/gr/gousiosg/javacg/JARBuilder.java b/src/test/java/gr/gousiosg/javacg/JARBuilder.java
new file mode 100644
index 00000000..327c55b1
--- /dev/null
+++ b/src/test/java/gr/gousiosg/javacg/JARBuilder.java
@@ -0,0 +1,117 @@
+package gr.gousiosg.javacg;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.net.URI;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.jar.Attributes;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
+
+import javax.tools.Diagnostic;
+import javax.tools.DiagnosticCollector;
+import javax.tools.JavaCompiler;
+import javax.tools.JavaCompiler.CompilationTask;
+import javax.tools.JavaFileObject;
+import javax.tools.SimpleJavaFileObject;
+import javax.tools.StandardJavaFileManager;
+import javax.tools.StandardLocation;
+import javax.tools.ToolProvider;
+
+public class JARBuilder {
+ private static final String TEMP_DIR = System.getProperty("java.io.tmpdir");
+
+ private final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
+ private final DiagnosticCollector diagnostics = new DiagnosticCollector();
+ private final StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null);
+ private final LinkedList compilationUnits = new LinkedList<>();
+ private final Collection classFiles = new LinkedList<>();
+
+ public JARBuilder() throws IOException {
+ fileManager.setLocation(StandardLocation.CLASS_OUTPUT, Arrays.asList(new File(TEMP_DIR)));
+ }
+
+ public void add(String className, String classCode) throws IOException {
+ compilationUnits.add(createJavaFile(className, classCode));
+ classFiles.add(new File(TEMP_DIR, className + ".class"));
+ }
+
+ public File build() throws FileNotFoundException, IOException {
+ CompilationTask task = compiler.getTask(null, fileManager, diagnostics, null, null, compilationUnits);
+ boolean success = task.call();
+ if (!success) {
+ displayDiagnostic(diagnostics);
+ throw new RuntimeException("Cannot compile classes for the JAR");
+ }
+
+ File file = File.createTempFile("test", ".jar");
+ JarOutputStream jar = new JarOutputStream(new FileOutputStream(file), createManifest());
+ for (File classFile : classFiles) {
+ add(classFile, jar);
+ }
+ jar.close();
+ return file;
+ }
+
+ private void displayDiagnostic(DiagnosticCollector diagnostics) {
+ for (Diagnostic> diagnostic : diagnostics.getDiagnostics()) {
+ JavaSourceFromString sourceClass = (JavaSourceFromString) diagnostic.getSource();
+ System.err.println("-----");
+ System.err.println("Source: " + sourceClass.getName());
+ System.err.println("Message: " + diagnostic.getMessage(null));
+ System.err.println("Position: " + diagnostic.getPosition());
+ System.err.println(diagnostic.getKind() + " " + diagnostic.getCode());
+ }
+ }
+
+ private JavaFileObject createJavaFile(String className, String classCode) throws IOException {
+ StringWriter writer = new StringWriter();
+ writer.append(classCode);
+ writer.close();
+ JavaFileObject file = new JavaSourceFromString(className, writer.toString());
+ return file;
+ }
+
+ private Manifest createManifest() {
+ Manifest manifest = new Manifest();
+ manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
+ return manifest;
+ }
+
+ private void add(File classFile, JarOutputStream jar) throws IOException {
+ JarEntry entry = new JarEntry(classFile.getPath().replace("\\", "/"));
+ jar.putNextEntry(entry);
+ try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(classFile))) {
+ byte[] buffer = new byte[1024];
+ while (true) {
+ int count = in.read(buffer);
+ if (count == -1)
+ break;
+ jar.write(buffer, 0, count);
+ }
+ jar.closeEntry();
+ }
+ }
+}
+
+class JavaSourceFromString extends SimpleJavaFileObject {
+ final String code;
+
+ JavaSourceFromString(String name, String code) throws IOException {
+ super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);
+ this.code = code;
+ }
+
+ @Override
+ public CharSequence getCharContent(boolean ignoreEncodingErrors) {
+ return code;
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/gr/gousiosg/javacg/RunCucumberTest.java b/src/test/java/gr/gousiosg/javacg/RunCucumberTest.java
new file mode 100644
index 00000000..e99e65aa
--- /dev/null
+++ b/src/test/java/gr/gousiosg/javacg/RunCucumberTest.java
@@ -0,0 +1,10 @@
+package gr.gousiosg.javacg;
+
+import cucumber.api.CucumberOptions;
+import cucumber.api.junit.Cucumber;
+import org.junit.runner.RunWith;
+
+@RunWith(Cucumber.class)
+@CucumberOptions(plugin = { "pretty" })
+public class RunCucumberTest {
+}
\ No newline at end of file
diff --git a/src/test/java/gr/gousiosg/javacg/StepDefinitions.java b/src/test/java/gr/gousiosg/javacg/StepDefinitions.java
new file mode 100644
index 00000000..281c1c1d
--- /dev/null
+++ b/src/test/java/gr/gousiosg/javacg/StepDefinitions.java
@@ -0,0 +1,48 @@
+package gr.gousiosg.javacg;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintStream;
+
+import cucumber.api.java.en.Given;
+import cucumber.api.java.en.Then;
+import cucumber.api.java.en.When;
+import gr.gousiosg.javacg.stat.JCallGraph;
+
+public class StepDefinitions {
+ private final JARBuilder jarBuilder;
+ private String result;
+
+ public StepDefinitions() throws IOException {
+ jarBuilder = new JARBuilder();
+ }
+
+ @Given("^I have the class \"([^\"]*)\" with code:$")
+ public void i_have_the_class_with_code(String className, String classCode) throws Exception {
+ jarBuilder.add(className, classCode);
+ }
+
+ @When("^I run the analyze$")
+ public void i_analyze_it() throws Exception {
+ File jarFile = jarBuilder.build();
+
+ PrintStream oldOut = System.out;
+ ByteArrayOutputStream resultBuffer = new ByteArrayOutputStream();
+ System.setOut(new PrintStream(resultBuffer));
+ JCallGraph.main(new String[] { jarFile.getPath() });
+ System.setOut(oldOut);
+
+ result = resultBuffer.toString();
+ }
+
+ @Then("^the result should contain:$")
+ public void the_result_should_contain(String line) throws Exception {
+ if (result.contains(line)) {
+ // OK
+ } else {
+ System.err.println(result);
+ throw new RuntimeException("Cannot found: " + line);
+ }
+ }
+}
diff --git a/src/test/resources/gr/gousiosg/javacg/lambda.feature b/src/test/resources/gr/gousiosg/javacg/lambda.feature
new file mode 100644
index 00000000..c651e953
--- /dev/null
+++ b/src/test/resources/gr/gousiosg/javacg/lambda.feature
@@ -0,0 +1,77 @@
+#Author: matthieu.vergne@gmail.com
+Feature: Lambda
+ I want to identify all lambdas within the analyzed code.
+
+ Background:
+ # Introduce the lambda we will use
+ Given I have the class "Runner" with code:
+ """
+ @FunctionalInterface
+ public interface Runner {
+ public void run();
+ }
+ """
+
+ Scenario: Retrieve lambda in method
+ Given I have the class "LambdaTest" with code:
+ """
+ public class LambdaTest {
+ public void methodA() {
+ Runner r = () -> methodB();
+ r.run();
+ }
+
+ public void methodB() {}
+ }
+ """
+ When I run the analyze
+ # Creation of r in methodA
+ Then the result should contain:
+ """
+ M:LambdaTest:methodA() (D)Runner:run(LambdaTest)
+ """
+ # Call of methodB in r
+ And the result should contain:
+ """
+ M:LambdaTest:lambda$methodA$0() (M)LambdaTest:methodB()
+ """
+
+ Scenario: Retrieve nested lambdas
+ Given I have the class "NestedLambdaTest" with code:
+ """
+ public class NestedLambdaTest {
+ public void methodA() {
+ Runner r = () -> {
+ Runner r2 = () -> {
+ Runner r3 = () -> methodB();
+ r3.run();
+ };
+ r2.run();
+ };
+ r.run();
+ }
+
+ public void methodB() {}
+ }
+ """
+ When I run the analyze
+ # Creation of r in methodA
+ Then the result should contain:
+ """
+ M:NestedLambdaTest:methodA() (D)Runner:run(NestedLambdaTest)
+ """
+ # Creation of r2 in r
+ And the result should contain:
+ """
+ M:NestedLambdaTest:lambda$methodA$2() (D)Runner:run(NestedLambdaTest)
+ """
+ # Creation of r3 in r2
+ And the result should contain:
+ """
+ M:NestedLambdaTest:lambda$lambda$methodA$2$1() (D)Runner:run(NestedLambdaTest)
+ """
+ # Call of methodB in r3
+ And the result should contain:
+ """
+ M:NestedLambdaTest:lambda$lambda$lambda$methodA$2$1$0() (M)NestedLambdaTest:methodB()
+ """
diff --git a/src/test/resources/gr/gousiosg/javacg/native.feature b/src/test/resources/gr/gousiosg/javacg/native.feature
new file mode 100644
index 00000000..877a69c9
--- /dev/null
+++ b/src/test/resources/gr/gousiosg/javacg/native.feature
@@ -0,0 +1,20 @@
+#Author: matthieu.vergne@gmail.com
+Feature: Native
+ I want to identify all native methods within the analyzed code.
+
+ Scenario: Retrieve native method call
+ Given I have the class "NativeTest" with code:
+ """
+ public class NativeTest {
+ public void methodA() {
+ methodB();
+ }
+
+ public native void methodB();
+ }
+ """
+ When I run the analyze
+ Then the result should contain:
+ """
+ M:NativeTest:methodA() (M)NativeTest:methodB()
+ """
\ No newline at end of file