├── .gitignore ├── README.md ├── assembly-dyn.xml ├── assembly-st.xml ├── pom.xml ├── process_trace.rb └── src ├── main └── java │ └── gr │ └── gousiosg │ └── javacg │ ├── dyn │ ├── Instrumenter.java │ ├── MethodStack.java │ └── Pair.java │ └── stat │ ├── ClassVisitor.java │ ├── DynamicCallManager.java │ ├── JCallGraph.java │ └── MethodVisitor.java └── test ├── java └── gr │ └── gousiosg │ └── javacg │ ├── JARBuilder.java │ ├── RunCucumberTest.java │ └── StepDefinitions.java └── resources └── gr └── gousiosg └── javacg ├── lambda.feature └── native.feature /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | nbproject/private/ 3 | *.out 4 | *.swp 5 | *.DS_Store 6 | *.lock 7 | *~ 8 | target/ 9 | bin/ 10 | .classpath 11 | .project 12 | .settings/ 13 | 14 | # intellij 15 | .idea/ 16 | *.iml 17 | 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | java-callgraph: Java Call Graph Utilities 2 | ========================================= 3 | 4 | A suite of programs for generating static and dynamic call graphs in Java. 5 | 6 | * javacg-static: Reads classes from a jar file, walks down the method bodies and 7 | prints a table of caller-caller relationships. 8 | * javacg-dynamic: Runs as a [Java agent](http://download.oracle.com/javase/6/docs/api/index.html?java/lang/instrument/package-summary.html) and instruments 9 | the methods of a user-defined set of classes in order to track their invocations. 10 | At JVM exit, prints a table of caller-callee relationships, along with a number 11 | of calls 12 | 13 | #### Compile 14 | 15 | The java-callgraph package is build with maven. Install maven and do: 16 | 17 | ``` 18 | mvn install 19 | ``` 20 | 21 | This will produce a `target` directory with the following three jars: 22 | - javacg-0.1-SNAPSHOT.jar: This is the standard maven packaged jar with static and dynamic call graph generator classes 23 | - `javacg-0.1-SNAPSHOT-static.jar`: This is an executable jar which includes the static call graph generator 24 | - `javacg-0.1-SNAPSHOT-dycg-agent.jar`: This is an executable jar which includes the dynamic call graph generator 25 | 26 | #### Run 27 | 28 | Instructions for running the callgraph generators 29 | 30 | ##### Static 31 | 32 | `javacg-static` accepts as arguments the jars to analyze. 33 | 34 | ``` 35 | java -jar javacg-0.1-SNAPSHOT-static.jar lib1.jar lib2.jar... 36 | ``` 37 | 38 | `javacg-static` produces combined output in the following format: 39 | 40 | ###### For methods 41 | 42 | ``` 43 | M:class1:(arg_types) (typeofcall)class2:(arg_types) 44 | ``` 45 | 46 | The line means that `method1` of `class1` called `method2` of `class2`. 47 | The type of call can have one of the following values (refer to 48 | the [JVM specification](http://java.sun.com/docs/books/jvms/second_edition/html/Instructions2.doc6.html) 49 | for the meaning of the calls): 50 | 51 | * `M` for `invokevirtual` calls 52 | * `I` for `invokeinterface` calls 53 | * `O` for `invokespecial` calls 54 | * `S` for `invokestatic` calls 55 | * `D` for `invokedynamic` calls 56 | 57 | For `invokedynamic` calls, it is not possible to infer the argument types. 58 | 59 | ###### For classes 60 | 61 | ``` 62 | C:class1 class2 63 | ``` 64 | 65 | This means that some method(s) in `class1` called some method(s) in `class2`. 66 | 67 | ##### Dynamic 68 | 69 | `javacg-dynamic` uses 70 | [javassist](http://www.csg.is.titech.ac.jp/~chiba/javassist/) to insert probes 71 | at method entry and exit points. To be able to analyze a class `javassist` must 72 | resolve all dependent classes at instrumentation time. To do so, it reads 73 | classes from the JVM's boot classloader. By default, the JVM sets the boot 74 | classpath to use Java's default classpath implementation (`rt.jar` on 75 | Win/Linux, `classes.jar` on the Mac). The boot classpath can be extended using 76 | the `-Xbootclasspath` option, which works the same as the traditional 77 | `-classpath` option. It is advisable for `javacg-dynamic` to work as expected, 78 | to set the boot classpath to the same, or an appropriate subset, entries as the 79 | normal application classpath. 80 | 81 | Moreover, since instrumenting all methods will produce huge callgraphs which 82 | are not necessarily helpful (e.g. it will include Java's default classpath 83 | entries), `javacg-dynamic` includes support for restricting the set of classes 84 | to be instrumented through include and exclude statements. The options are 85 | appended to the `-javaagent` argument and has the following format 86 | 87 | ``` 88 | -javaagent:javacg-dycg-agent.jar="incl=mylib.*,mylib2.*,java.nio.*;excl=java.nio.charset.*" 89 | ``` 90 | 91 | The example above will instrument all classes under the the `mylib`, `mylib2` and 92 | `java.nio` namespaces, except those that fall under the `java.nio.charset` namespace. 93 | 94 | ``` 95 | java 96 | -Xbootclasspath:/System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Classes/classes.jar:mylib.jar 97 | -javaagent:javacg-0.1-SNAPSHOT-dycg-agent.jar="incl=mylib.*;" 98 | -classpath mylib.jar mylib.Mainclass 99 | ``` 100 | 101 | `javacg-dynamic` produces two kinds of output. On the standard output, it 102 | writes method call pairs as shown below: 103 | 104 | ``` 105 | class1:method1 class2:method2 numcalls 106 | ``` 107 | 108 | It also produces a file named `calltrace.txt` in which it writes the entry 109 | and exit timestamps for methods, thereby turning `javacg-dynamic` into 110 | a poor man's profiler. The format is the following: 111 | 112 | ``` 113 | <>[stack_depth][thread_id]fqdn.class:method=timestamp_nanos 114 | ``` 115 | 116 | The output line starts with a `<` or `>` depending on whether it is a method 117 | entry or exit. It then writes the stack depth, thread id and the class and 118 | method name, followed by a timestamp. The provided `process_trace.rb` 119 | script processes the callgraph output to generate total time per method 120 | information. 121 | 122 | #### Examples 123 | 124 | The following examples instrument the 125 | [Dacapo benchmark suite](http://dacapobench.org/) to produce dynamic call graphs. 126 | The Dacapo benchmarks come in a single big jar archive that contains all dependency 127 | libraries. To build the boot class path required for the javacg-dyn program, 128 | extract the `dacapo.jar` to a directory: all the required libraries can be found 129 | in the `jar` directory. 130 | 131 | Running the batik Dacapo benchmark: 132 | 133 | ``` 134 | java -Xbootclasspath:/System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Classes/classes.jar:jar/batik-all.jar:jar/xml-apis-ext.jar -javaagent:target/javacg-0.1-SNAPSHOT-dycg-agent.jar="incl=org.apache.batik.*,org.w3c.*;" -jar dacapo-9.12-bach.jar batik -s small |tail -n 10 135 | ``` 136 |
137 | 138 | ``` 139 | [...] 140 | org.apache.batik.dom.AbstractParentNode:appendChild org.apache.batik.dom.AbstractParentNode:fireDOMNodeInsertedEvent 6270
141 | org.apache.batik.dom.AbstractParentNode:fireDOMNodeInsertedEvent org.apache.batik.dom.AbstractDocument:getEventsEnabled 6280
142 | org.apache.batik.dom.AbstractParentNode:checkAndRemove org.apache.batik.dom.AbstractNode:getOwnerDocument 6280
143 | org.apache.batik.dom.util.DoublyIndexedTable:put org.apache.batik.dom.util.DoublyIndexedTable$Entry:DoublyIndexedTable$Entry 6682
144 | org.apache.batik.dom.util.DoublyIndexedTable:put org.apache.batik.dom.util.DoublyIndexedTable:hashCode 6693
145 | org.apache.batik.dom.AbstractElement:invalidateElementsByTagName org.apache.batik.dom.AbstractElement:getNodeType 7198
146 | org.apache.batik.dom.AbstractElement:invalidateElementsByTagName org.apache.batik.dom.AbstractDocument:getElementsByTagName 14396
147 | org.apache.batik.dom.AbstractElement:invalidateElementsByTagName org.apache.batik.dom.AbstractDocument:getElementsByTagNameNS 28792
148 | ``` 149 | 150 | Running the lucene Dacapo benchmark: 151 | 152 | ``` 153 | java -Xbootclasspath:/System/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Classes/classes.jar:jar/lucene-core-2.4.jar:jar/luindex.jar -javaagent:target/javacg-0.1-SNAPSHOT-dycg-agent.jar="incl=org.apache.lucene.*;" -jar dacapo-9.12-bach.jar luindex -s small |tail -n 10 154 | ``` 155 |

156 | 157 | ``` 158 | [...] 159 | org.apache.lucene.analysis.Token:setTermBuffer org.apache.lucene.analysis.Token:growTermBuffer 43449
160 | org.apache.lucene.analysis.CharArraySet:getSlot org.apache.lucene.analysis.CharArraySet:getHashCode 43472
161 | org.apache.lucene.analysis.CharArraySet:getSlot org.apache.lucene.analysis.CharArraySet:equals 46107
162 | org.apache.lucene.index.FreqProxTermsWriter:appendPostings org.apache.lucene.store.IndexOutput:writeVInt 46507
163 | org.apache.lucene.store.IndexInput:readVInt org.apache.lucene.index.ByteSliceReader:readByte 63927
164 | org.apache.lucene.index.TermsHashPerField:writeVInt org.apache.lucene.index.TermsHashPerField:writeByte 63927
165 | org.apache.lucene.store.IndexOutput:writeVInt org.apache.lucene.store.BufferedIndexOutput:writeByte 94239
166 | org.apache.lucene.index.TermsHashPerField:quickSort org.apache.lucene.index.TermsHashPerField:comparePostings 107343
167 | org.apache.lucene.analysis.Token:termBuffer org.apache.lucene.analysis.Token:initTermBuffer 162115
168 | org.apache.lucene.analysis.Token:termLength org.apache.lucene.analysis.Token:initTermBuffer 205554
169 | ``` 170 | 171 | #### Known Restrictions 172 | 173 | * The static call graph generator does not account for methods invoked via 174 | reflection. 175 | * The dynamic call graph generator will not work reliably (or at all) for 176 | multithreaded programs 177 | * The dynamic call graph generator does not handle exceptions very well, so some 178 | methods might appear as having never returned 179 | 180 | #### Author 181 | 182 | Georgios Gousios 183 | 184 | #### License 185 | 186 | [2-clause BSD](http://www.opensource.org/licenses/bsd-license.php) 187 | -------------------------------------------------------------------------------- /assembly-dyn.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | dycg-agent 7 | 8 | jar 9 | 10 | false 11 | 12 | 13 | 14 | true 15 | 16 | javassist:javassist 17 | 18 | provided 19 | 20 | 21 | 22 | 23 | 24 | gr/gousiosg/javacg/dyn/*.class 25 | 26 | target/classes 27 | / 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /assembly-st.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | static 7 | 8 | jar 9 | 10 | false 11 | 12 | 13 | 14 | true 15 | 16 | org.apache.bcel:bcel 17 | 18 | provided 19 | 20 | 21 | 22 | 23 | 24 | gr/gousiosg/javacg/stat/*.class 25 | 26 | target/classes 27 | / 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | gr.gousiosg 6 | javacg 7 | 0.1-SNAPSHOT 8 | jar 9 | 10 | javacg 11 | 12 | 13 | UTF-8 14 | 15 | 16 | 17 | 18 | org.apache.bcel 19 | bcel 20 | 6.2 21 | provided 22 | 23 | 24 | javassist 25 | javassist 26 | 3.12.1.GA 27 | provided 28 | 29 | 30 | io.cucumber 31 | cucumber-java 32 | 2.3.1 33 | test 34 | 35 | 36 | io.cucumber 37 | cucumber-junit 38 | 2.3.1 39 | test 40 | 41 | 42 | junit 43 | junit 44 | 4.12 45 | test 46 | 47 | 48 | 49 | 50 | 51 | org.apache.maven.plugins 52 | maven-assembly-plugin 53 | 3.0.0 54 | 55 | 56 | assembly-dyn.xml 57 | assembly-st.xml 58 | 59 | 60 | 61 | true 62 | gr.gousiosg.javacg.dyn.Instrumenter 63 | gr.gousiosg.javacg.stat.JCallGraph 64 | 65 | 66 | 67 | 68 | 69 | make-assembly 70 | package 71 | 72 | single 73 | 74 | 75 | 76 | 77 | 78 | maven-compiler-plugin 79 | 3.6.1 80 | 81 | false 82 | 1.8 83 | 1.8 84 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /process_trace.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # 3 | # Reads a call trace, formatted as 4 | # 5 | # [<>][stack_depth][thread]called_method=time_nanosecs 6 | # 7 | # and produces the total time spent in each method. 8 | # Lines that start with > denote method entry 9 | # < denote method exit 10 | # (c) 2011 - Georgios Gousios 11 | # 12 | 13 | require 'rubygems' 14 | 15 | if ARGV.size <= 0 then 16 | print "usage: timings.rb timings file" 17 | end 18 | 19 | results = Hash.new 20 | mstack = Array.new 21 | tstack = Array.new 22 | total_time_at_depth = Hash.new 23 | lines_read = 0 24 | d = 0 25 | 26 | File.open(ARGV[0]).each do |line| 27 | type, depth, tid, method, time = line.scan(/([<>])\[(\d+)\]\[(\d+)\](.*)=(\d+)/)[0] 28 | lines_read += 1 29 | depth = depth.to_i 30 | time = time.to_i 31 | 32 | case type 33 | when '<': 34 | # Can only return from inner or same depth level frame 35 | if not [d, d - 1].include? depth then 36 | print "Return from stack depth: ", d, 37 | " to depth:", depth, ", line ", 38 | lines_read, "\n" 39 | return 40 | end 41 | m = mstack.pop 42 | t = tstack.pop 43 | 44 | # Sanity check 45 | if not m == method then 46 | print "Method ", method, " not found at depth ", 47 | depth, " line ", lines_read, "\n" 48 | return 49 | end 50 | 51 | # Total time for a method is the time to execute it minus the time to 52 | # invoke all called methods 53 | method_time = time - t 54 | results[method] = results.fetch(method, 0) + method_time - 55 | total_time_at_depth.fetch(depth + 1, 0) 56 | 57 | # Sanity check 58 | if method_time - total_time_at_depth.fetch(depth + 1, 0) < 0 then 59 | print "Time required for method #{method} (#{method_time}) less 60 | than time for required for inner frame 61 | (#{total_time_at_depth.fetch(depth + 1, 0)})! ", 62 | "Line ", lines_read, "\n" 63 | return 64 | end 65 | 66 | # Total for a frame is the total time of its first level decendant 67 | # plus the time required to execute all methods at the same depth 68 | total_time_at_depth[depth] = method_time - 69 | total_time_at_depth.fetch(depth + 1, 0) + 70 | total_time_at_depth.fetch(depth, 0) 71 | # Inner stack frame time was used already at this depth, 72 | # delete to avoid reusing 73 | total_time_at_depth.delete(depth + 1) 74 | 75 | # Reset total time for depth 0 after it has been used 76 | # since the statement above never runs at this depth 77 | if depth == 0: 78 | total_time_at_depth.delete(depth) 79 | end 80 | when '>': 81 | # Can only go into an inner or same depth level frame 82 | if not [d, d + 1].include? depth then 83 | print "Jump from stack depth: #{d} to depth: #{depth}, 84 | line #{lines_read} \n" 85 | return 86 | end 87 | mstack.push method 88 | tstack.push time 89 | else print "Cannot parse line #{line}" 90 | end 91 | d = depth 92 | end 93 | 94 | # Print results sorted 95 | results.sort{|a,b| b[1]<=>a[1]}.each {|x| 96 | print "#{x[0]} #{x[1]}\n" 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/gr/gousiosg/javacg/dyn/Instrumenter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 - Georgios Gousios 3 | * 4 | * Redistribution and use in source and binary forms, with or without 5 | * modification, are permitted provided that the following conditions are 6 | * met: 7 | * 8 | * * Redistributions of source code must retain the above copyright 9 | * notice, this list of conditions and the following disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above 12 | * copyright notice, this list of conditions and the following 13 | * disclaimer in the documentation and/or other materials provided 14 | * with the distribution. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 20 | * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | */ 28 | 29 | package gr.gousiosg.javacg.dyn; 30 | 31 | import java.io.ByteArrayInputStream; 32 | import java.io.IOException; 33 | import java.lang.instrument.ClassFileTransformer; 34 | import java.lang.instrument.Instrumentation; 35 | import java.util.ArrayList; 36 | import java.util.List; 37 | import java.util.regex.Matcher; 38 | import java.util.regex.Pattern; 39 | import java.util.regex.PatternSyntaxException; 40 | 41 | import javassist.CannotCompileException; 42 | import javassist.ClassPool; 43 | import javassist.CtBehavior; 44 | import javassist.CtClass; 45 | import javassist.NotFoundException; 46 | 47 | public class Instrumenter implements ClassFileTransformer { 48 | 49 | static List pkgIncl = new ArrayList<>(); 50 | static List pkgExcl = new ArrayList<>(); 51 | 52 | public static void premain(String argument, Instrumentation instrumentation) { 53 | 54 | // incl=com.foo.*,gr.bar.foo;excl=com.bar.foo.* 55 | 56 | if (argument == null) { 57 | err("Missing configuration argument"); 58 | return; 59 | } 60 | 61 | err("Argument is: " + argument); 62 | 63 | String[] tokens = argument.split(";"); 64 | 65 | if (tokens.length < 1) { 66 | err("Missing delimiter ;"); 67 | return; 68 | } 69 | 70 | for (String token : tokens) { 71 | String[] args = token.split("="); 72 | if (args.length < 2) { 73 | err("Missing argument delimiter =:" + token); 74 | return; 75 | } 76 | 77 | String argtype = args[0]; 78 | 79 | if (!argtype.equals("incl") && !argtype.equals("excl")) { 80 | err("Wrong argument: " + argtype); 81 | return; 82 | } 83 | 84 | String[] patterns = args[1].split(","); 85 | 86 | for (String pattern : patterns) { 87 | Pattern p = null; 88 | err("Compiling " + argtype + " pattern:" + pattern + "$"); 89 | try { 90 | p = Pattern.compile(pattern + "$"); 91 | } catch (PatternSyntaxException pse) { 92 | err("pattern: " + pattern + " not valid, ignoring"); 93 | } 94 | if (argtype.equals("incl")) 95 | pkgIncl.add(p); 96 | else 97 | pkgExcl.add(p); 98 | } 99 | } 100 | 101 | instrumentation.addTransformer(new Instrumenter()); 102 | } 103 | 104 | public byte[] transform(ClassLoader loader, String className, Class clazz, 105 | java.security.ProtectionDomain domain, byte[] bytes) { 106 | boolean enhanceClass = false; 107 | 108 | String name = className.replace("/", "."); 109 | 110 | for (Pattern p : pkgIncl) { 111 | Matcher m = p.matcher(name); 112 | if (m.matches()) { 113 | enhanceClass = true; 114 | break; 115 | } 116 | } 117 | 118 | for (Pattern p : pkgExcl) { 119 | Matcher m = p.matcher(name); 120 | if (m.matches()) { 121 | err("Skipping class: " + name); 122 | enhanceClass = false; 123 | break; 124 | } 125 | } 126 | 127 | if (enhanceClass) { 128 | return enhanceClass(className, bytes); 129 | } else { 130 | return bytes; 131 | } 132 | } 133 | 134 | private byte[] enhanceClass(String name, byte[] b) { 135 | ClassPool pool = ClassPool.getDefault(); 136 | CtClass clazz = null; 137 | try { 138 | clazz = pool.makeClass(new ByteArrayInputStream(b)); 139 | if (!clazz.isInterface()) { 140 | err("Enhancing class: " + name); 141 | CtBehavior[] methods = clazz.getDeclaredBehaviors(); 142 | for (int i = 0; i < methods.length; i++) { 143 | if (!methods[i].isEmpty()) { 144 | enhanceMethod(methods[i], clazz.getName()); 145 | } 146 | } 147 | b = clazz.toBytecode(); 148 | } 149 | } catch (CannotCompileException e) { 150 | e.printStackTrace(); 151 | err("Cannot compile: " + e.getMessage()); 152 | } catch (NotFoundException e) { 153 | e.printStackTrace(); 154 | err("Cannot find: " + e.getMessage()); 155 | } catch (IOException e) { 156 | err("Error writing: " + e.getMessage()); 157 | } finally { 158 | if (clazz != null) { 159 | clazz.detach(); 160 | } 161 | } 162 | return b; 163 | } 164 | 165 | private void enhanceMethod(CtBehavior method, String className) 166 | throws NotFoundException, CannotCompileException { 167 | String name = className.substring(className.lastIndexOf('.') + 1, className.length()); 168 | String methodName = method.getName(); 169 | 170 | if (method.getName().equals(name)) 171 | methodName = ""; 172 | 173 | method.insertBefore("gr.gousiosg.javacg.dyn.MethodStack.push(\"" + className 174 | + ":" + methodName + "\");"); 175 | method.insertAfter("gr.gousiosg.javacg.dyn.MethodStack.pop();"); 176 | } 177 | 178 | private static void err(String msg) { 179 | //System.err.println("[JAVACG-DYN] " + msg); 180 | } 181 | } -------------------------------------------------------------------------------- /src/main/java/gr/gousiosg/javacg/dyn/MethodStack.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 - Georgios Gousios 3 | * 4 | * Redistribution and use in source and binary forms, with or without 5 | * modification, are permitted provided that the following conditions are 6 | * met: 7 | * 8 | * * Redistributions of source code must retain the above copyright 9 | * notice, this list of conditions and the following disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above 12 | * copyright notice, this list of conditions and the following 13 | * disclaimer in the documentation and/or other materials provided 14 | * with the distribution. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 20 | * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | */ 28 | 29 | package gr.gousiosg.javacg.dyn; 30 | 31 | import java.io.File; 32 | import java.io.FileWriter; 33 | import java.io.IOException; 34 | import java.util.ArrayList; 35 | import java.util.Collections; 36 | import java.util.HashMap; 37 | import java.util.List; 38 | import java.util.Map; 39 | import java.util.Stack; 40 | 41 | public class MethodStack { 42 | 43 | private static Stack stack = new Stack<>(); 44 | private static Map, Integer> callgraph = new HashMap<>(); 45 | static FileWriter fw; 46 | static StringBuffer sb; 47 | static long threadid = -1L; 48 | 49 | static { 50 | Runtime.getRuntime().addShutdownHook(new Thread() { 51 | public void run() { 52 | try { 53 | fw.close(); 54 | } catch (IOException e) { 55 | e.printStackTrace(); 56 | } 57 | //Sort by number of calls 58 | List> keys = new ArrayList<>(); 59 | keys.addAll(callgraph.keySet()); 60 | Collections.sort(keys, (o1, o2) -> { 61 | Integer v1 = callgraph.get(o1); 62 | Integer v2 = callgraph.get(o2); 63 | return v1.compareTo(v2); 64 | }); 65 | 66 | for (Pair key : keys) { 67 | System.out.println(key + " " + callgraph.get(key)); 68 | } 69 | } 70 | }); 71 | File log = new File("calltrace.txt"); 72 | try { 73 | fw = new FileWriter(log); 74 | } catch (Exception e) { 75 | e.printStackTrace(); 76 | } 77 | sb = new StringBuffer(); 78 | } 79 | 80 | public static void push(String callname) throws IOException { 81 | if (threadid == -1) 82 | threadid = Thread.currentThread().getId(); 83 | 84 | if (Thread.currentThread().getId() != threadid) 85 | return; 86 | 87 | if (!stack.isEmpty()) { 88 | Pair p = new Pair<>(stack.peek(), callname); 89 | if (callgraph.containsKey(p)) 90 | callgraph.put(p, callgraph.get(p) + 1); 91 | else 92 | callgraph.put(p, 1); 93 | } 94 | sb.setLength(0); 95 | sb.append(">[").append(stack.size()).append("]"); 96 | sb.append("[").append(Thread.currentThread().getId()).append("]"); 97 | sb.append(callname).append("=").append(System.nanoTime()).append("\n"); 98 | fw.write(sb.toString()); 99 | stack.push(callname); 100 | } 101 | 102 | public static void pop() throws IOException { 103 | if (threadid == -1) 104 | threadid = Thread.currentThread().getId(); 105 | 106 | if (Thread.currentThread().getId() != threadid) 107 | return; 108 | 109 | String returnFrom = stack.pop(); 110 | sb.setLength(0); 111 | sb.append("<[").append(stack.size()).append("]"); 112 | sb.append("[").append(Thread.currentThread().getId()).append("]"); 113 | sb.append(returnFrom).append("=").append(System.nanoTime()).append("\n"); 114 | fw.write(sb.toString()); 115 | } 116 | } -------------------------------------------------------------------------------- /src/main/java/gr/gousiosg/javacg/dyn/Pair.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 - Georgios Gousios 3 | * 4 | * Redistribution and use in source and binary forms, with or without 5 | * modification, are permitted provided that the following conditions are 6 | * met: 7 | * 8 | * * Redistributions of source code must retain the above copyright 9 | * notice, this list of conditions and the following disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above 12 | * copyright notice, this list of conditions and the following 13 | * disclaimer in the documentation and/or other materials provided 14 | * with the distribution. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 20 | * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | */ 28 | 29 | package gr.gousiosg.javacg.dyn; 30 | 31 | public class Pair { 32 | 33 | public A first; 34 | public B second; 35 | 36 | public Pair(A first, B second) { 37 | this.first = first; 38 | this.second = second; 39 | } 40 | 41 | @Override 42 | public String toString() { 43 | StringBuffer b = new StringBuffer(first.toString()); 44 | b.append(" "); 45 | b.append(second); 46 | return b.toString(); 47 | } 48 | 49 | @Override 50 | public boolean equals(Object obj) { 51 | if (obj == null) 52 | return false; 53 | if (obj == this) 54 | return true; 55 | if (obj.getClass() != getClass()) 56 | return false; 57 | 58 | Pair p = (Pair)obj; 59 | 60 | return first.equals(p.first) && second.equals(p.second); 61 | } 62 | 63 | @Override 64 | public int hashCode() { 65 | int hash = 7; 66 | hash = 31 * hash + (null == first ? 0 : first.hashCode()); 67 | hash = 31 * hash + (null == second ? 0 : second.hashCode()); 68 | return hash; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/gr/gousiosg/javacg/stat/ClassVisitor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 - Georgios Gousios 3 | * 4 | * Redistribution and use in source and binary forms, with or without 5 | * modification, are permitted provided that the following conditions are 6 | * met: 7 | * 8 | * * Redistributions of source code must retain the above copyright 9 | * notice, this list of conditions and the following disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above 12 | * copyright notice, this list of conditions and the following 13 | * disclaimer in the documentation and/or other materials provided 14 | * with the distribution. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 20 | * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | */ 28 | 29 | package gr.gousiosg.javacg.stat; 30 | 31 | import org.apache.bcel.classfile.Constant; 32 | import org.apache.bcel.classfile.ConstantPool; 33 | import org.apache.bcel.classfile.EmptyVisitor; 34 | import org.apache.bcel.classfile.JavaClass; 35 | import org.apache.bcel.classfile.Method; 36 | import org.apache.bcel.generic.ConstantPoolGen; 37 | import org.apache.bcel.generic.MethodGen; 38 | 39 | import java.util.ArrayList; 40 | import java.util.List; 41 | 42 | /** 43 | * The simplest of class visitors, invokes the method visitor class for each 44 | * method found. 45 | */ 46 | public class ClassVisitor extends EmptyVisitor { 47 | 48 | private JavaClass clazz; 49 | private ConstantPoolGen constants; 50 | private String classReferenceFormat; 51 | private final DynamicCallManager DCManager = new DynamicCallManager(); 52 | private List methodCalls = new ArrayList<>(); 53 | 54 | public ClassVisitor(JavaClass jc) { 55 | clazz = jc; 56 | constants = new ConstantPoolGen(clazz.getConstantPool()); 57 | classReferenceFormat = "C:" + clazz.getClassName() + " %s"; 58 | } 59 | 60 | public void visitJavaClass(JavaClass jc) { 61 | jc.getConstantPool().accept(this); 62 | Method[] methods = jc.getMethods(); 63 | for (int i = 0; i < methods.length; i++) { 64 | Method method = methods[i]; 65 | DCManager.retrieveCalls(method, jc); 66 | DCManager.linkCalls(method); 67 | method.accept(this); 68 | 69 | } 70 | } 71 | 72 | public void visitConstantPool(ConstantPool constantPool) { 73 | for (int i = 0; i < constantPool.getLength(); i++) { 74 | Constant constant = constantPool.getConstant(i); 75 | if (constant == null) 76 | continue; 77 | if (constant.getTag() == 7) { 78 | String referencedClass = 79 | constantPool.constantToString(constant); 80 | System.out.println(String.format(classReferenceFormat, referencedClass)); 81 | } 82 | } 83 | } 84 | 85 | public void visitMethod(Method method) { 86 | MethodGen mg = new MethodGen(method, clazz.getClassName(), constants); 87 | MethodVisitor visitor = new MethodVisitor(mg, clazz); 88 | methodCalls.addAll(visitor.start()); 89 | } 90 | 91 | public ClassVisitor start() { 92 | visitJavaClass(clazz); 93 | return this; 94 | } 95 | 96 | public List methodCalls() { 97 | return this.methodCalls; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/gr/gousiosg/javacg/stat/DynamicCallManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Written in 2018 by Matthieu Vergne 3 | * 4 | * To the extent possible under law, the author(s) have dedicated all 5 | * copyright and related and neighboring rights to this software to 6 | * the public domain worldwide. This software is distributed without 7 | * any warranty. 8 | * 9 | * You should have received a copy of the CC0 Public Domain Dedication 10 | * along with this software. If not, 11 | * see . 12 | */ 13 | 14 | package gr.gousiosg.javacg.stat; 15 | 16 | import java.util.HashMap; 17 | import java.util.Map; 18 | import java.util.regex.Matcher; 19 | import java.util.regex.Pattern; 20 | 21 | import org.apache.bcel.classfile.Attribute; 22 | import org.apache.bcel.classfile.BootstrapMethod; 23 | import org.apache.bcel.classfile.BootstrapMethods; 24 | import org.apache.bcel.classfile.ConstantCP; 25 | import org.apache.bcel.classfile.ConstantMethodHandle; 26 | import org.apache.bcel.classfile.ConstantNameAndType; 27 | import org.apache.bcel.classfile.ConstantPool; 28 | import org.apache.bcel.classfile.ConstantUtf8; 29 | import org.apache.bcel.classfile.JavaClass; 30 | import org.apache.bcel.classfile.Method; 31 | 32 | /** 33 | * {@link DynamicCallManager} provides facilities to retrieve information about 34 | * dynamic calls statically. 35 | *

36 | * Most of the time, call relationships are explicit, which allows to properly 37 | * build the call graph statically. But in the case of dynamic linking, i.e. 38 | * invokedynamic instructions, this relationship might be unknown 39 | * until the code is actually executed. Indeed, bootstrap methods are used to 40 | * dynamically link the code at first call. One can read details about the 41 | * invokedynamic 43 | * instruction to know more about this mechanism. 44 | *

45 | * Nested lambdas are particularly subject to such absence of concrete caller, 46 | * which lead us to produce method names like lambda$null$0, which 47 | * breaks the call graph. This information can however be retrieved statically 48 | * through the code of the bootstrap method called. 49 | *

50 | * In {@link #retrieveCalls(Method, JavaClass)}, we retrieve the (called, 51 | * caller) relationships by analyzing the code of the caller {@link Method}. 52 | * This information is then used in {@link #linkCalls(Method)} to rename the 53 | * called {@link Method} properly. 54 | * 55 | * @author Matthieu Vergne 56 | */ 57 | public class DynamicCallManager { 58 | private static final Pattern BOOTSTRAP_CALL_PATTERN = Pattern 59 | .compile("invokedynamic\t(\\d+):\\S+ \\S+ \\(\\d+\\)"); 60 | private static final int CALL_HANDLE_INDEX_ARGUMENT = 1; 61 | 62 | private final Map dynamicCallers = new HashMap<>(); 63 | 64 | /** 65 | * Retrieve dynamic call relationships based on the code of the provided 66 | * {@link Method}. 67 | * 68 | * @param method {@link Method} to analyze the code 69 | * @param jc {@link JavaClass} info, which contains the bootstrap methods 70 | * @see #linkCalls(Method) 71 | */ 72 | public void retrieveCalls(Method method, JavaClass jc) { 73 | if (method.isAbstract() || method.isNative()) { 74 | // No code to consider 75 | return; 76 | } 77 | ConstantPool cp = method.getConstantPool(); 78 | BootstrapMethod[] boots = getBootstrapMethods(jc); 79 | String code = method.getCode().toString(); 80 | Matcher matcher = BOOTSTRAP_CALL_PATTERN.matcher(code); 81 | while (matcher.find()) { 82 | int bootIndex = Integer.parseInt(matcher.group(1)); 83 | BootstrapMethod bootMethod = boots[bootIndex]; 84 | int calledIndex = bootMethod.getBootstrapArguments()[CALL_HANDLE_INDEX_ARGUMENT]; 85 | String calledName = getMethodNameFromHandleIndex(cp, calledIndex); 86 | String callerName = method.getName(); 87 | dynamicCallers.put(calledName, callerName); 88 | } 89 | } 90 | 91 | private String getMethodNameFromHandleIndex(ConstantPool cp, int callIndex) { 92 | ConstantMethodHandle handle = (ConstantMethodHandle) cp.getConstant(callIndex); 93 | ConstantCP ref = (ConstantCP) cp.getConstant(handle.getReferenceIndex()); 94 | ConstantNameAndType nameAndType = (ConstantNameAndType) cp.getConstant(ref.getNameAndTypeIndex()); 95 | return nameAndType.getName(cp); 96 | } 97 | 98 | /** 99 | * Link the {@link Method}'s name to its concrete caller if required. 100 | * 101 | * @param method {@link Method} to analyze 102 | * @see #retrieveCalls(Method, JavaClass) 103 | */ 104 | public void linkCalls(Method method) { 105 | int nameIndex = method.getNameIndex(); 106 | ConstantPool cp = method.getConstantPool(); 107 | String methodName = ((ConstantUtf8) cp.getConstant(nameIndex)).getBytes(); 108 | String linkedName = methodName; 109 | String callerName = methodName; 110 | while (linkedName.matches("(lambda\\$)+null(\\$\\d+)+")) { 111 | callerName = dynamicCallers.get(callerName); 112 | linkedName = linkedName.replace("null", callerName); 113 | } 114 | cp.setConstant(nameIndex, new ConstantUtf8(linkedName)); 115 | } 116 | 117 | private BootstrapMethod[] getBootstrapMethods(JavaClass jc) { 118 | for (Attribute attribute : jc.getAttributes()) { 119 | if (attribute instanceof BootstrapMethods) { 120 | return ((BootstrapMethods) attribute).getBootstrapMethods(); 121 | } 122 | } 123 | return new BootstrapMethod[]{}; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/main/java/gr/gousiosg/javacg/stat/JCallGraph.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 - Georgios Gousios 3 | * 4 | * Redistribution and use in source and binary forms, with or without 5 | * modification, are permitted provided that the following conditions are 6 | * met: 7 | * 8 | * * Redistributions of source code must retain the above copyright 9 | * notice, this list of conditions and the following disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above 12 | * copyright notice, this list of conditions and the following 13 | * disclaimer in the documentation and/or other materials provided 14 | * with the distribution. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 20 | * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | */ 28 | 29 | package gr.gousiosg.javacg.stat; 30 | 31 | import java.io.*; 32 | import java.util.*; 33 | import java.util.function.Function; 34 | import java.util.jar.JarEntry; 35 | import java.util.jar.JarFile; 36 | import java.util.stream.Stream; 37 | import java.util.stream.StreamSupport; 38 | 39 | import org.apache.bcel.classfile.ClassParser; 40 | 41 | /** 42 | * Constructs a callgraph out of a JAR archive. Can combine multiple archives 43 | * into a single call graph. 44 | * 45 | * @author Georgios Gousios 46 | */ 47 | public class JCallGraph { 48 | 49 | public static void main(String[] args) { 50 | 51 | Function getClassVisitor = 52 | (ClassParser cp) -> { 53 | try { 54 | return new ClassVisitor(cp.parse()); 55 | } catch (IOException e) { 56 | throw new UncheckedIOException(e); 57 | } 58 | }; 59 | 60 | try { 61 | for (String arg : args) { 62 | 63 | File f = new File(arg); 64 | 65 | if (!f.exists()) { 66 | System.err.println("Jar file " + arg + " does not exist"); 67 | } 68 | 69 | try (JarFile jar = new JarFile(f)) { 70 | Stream entries = enumerationAsStream(jar.entries()); 71 | 72 | String methodCalls = entries. 73 | flatMap(e -> { 74 | if (e.isDirectory() || !e.getName().endsWith(".class")) 75 | return (new ArrayList()).stream(); 76 | 77 | ClassParser cp = new ClassParser(arg, e.getName()); 78 | return getClassVisitor.apply(cp).start().methodCalls().stream(); 79 | }). 80 | map(s -> s + "\n"). 81 | reduce(new StringBuilder(), 82 | StringBuilder::append, 83 | StringBuilder::append).toString(); 84 | 85 | BufferedWriter log = new BufferedWriter(new OutputStreamWriter(System.out)); 86 | log.write(methodCalls); 87 | log.close(); 88 | } 89 | } 90 | } catch (IOException e) { 91 | System.err.println("Error while processing jar: " + e.getMessage()); 92 | e.printStackTrace(); 93 | } 94 | } 95 | 96 | public static Stream enumerationAsStream(Enumeration e) { 97 | return StreamSupport.stream( 98 | Spliterators.spliteratorUnknownSize( 99 | new Iterator() { 100 | public T next() { 101 | return e.nextElement(); 102 | } 103 | 104 | public boolean hasNext() { 105 | return e.hasMoreElements(); 106 | } 107 | }, 108 | Spliterator.ORDERED), false); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/gr/gousiosg/javacg/stat/MethodVisitor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 - Georgios Gousios 3 | * 4 | * Redistribution and use in source and binary forms, with or without 5 | * modification, are permitted provided that the following conditions are 6 | * met: 7 | * 8 | * * Redistributions of source code must retain the above copyright 9 | * notice, this list of conditions and the following disclaimer. 10 | * 11 | * * Redistributions in binary form must reproduce the above 12 | * copyright notice, this list of conditions and the following 13 | * disclaimer in the documentation and/or other materials provided 14 | * with the distribution. 15 | * 16 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 17 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 18 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 19 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 20 | * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 21 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 22 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 23 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 24 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | */ 28 | 29 | package gr.gousiosg.javacg.stat; 30 | 31 | import org.apache.bcel.classfile.JavaClass; 32 | import org.apache.bcel.generic.*; 33 | 34 | import java.util.ArrayList; 35 | import java.util.Collections; 36 | import java.util.List; 37 | 38 | /** 39 | * The simplest of method visitors, prints any invoked method 40 | * signature for all method invocations. 41 | * 42 | * Class copied with modifications from CJKM: http://www.spinellis.gr/sw/ckjm/ 43 | */ 44 | public class MethodVisitor extends EmptyVisitor { 45 | 46 | JavaClass visitedClass; 47 | private MethodGen mg; 48 | private ConstantPoolGen cp; 49 | private String format; 50 | private List methodCalls = new ArrayList<>(); 51 | 52 | public MethodVisitor(MethodGen m, JavaClass jc) { 53 | visitedClass = jc; 54 | mg = m; 55 | cp = mg.getConstantPool(); 56 | format = "M:" + visitedClass.getClassName() + ":" + mg.getName() + "(" + argumentList(mg.getArgumentTypes()) + ")" 57 | + " " + "(%s)%s:%s(%s)"; 58 | } 59 | 60 | private String argumentList(Type[] arguments) { 61 | StringBuilder sb = new StringBuilder(); 62 | for (int i = 0; i < arguments.length; i++) { 63 | if (i != 0) { 64 | sb.append(","); 65 | } 66 | sb.append(arguments[i].toString()); 67 | } 68 | return sb.toString(); 69 | } 70 | 71 | public List start() { 72 | if (mg.isAbstract() || mg.isNative()) 73 | return Collections.emptyList(); 74 | 75 | for (InstructionHandle ih = mg.getInstructionList().getStart(); 76 | ih != null; ih = ih.getNext()) { 77 | Instruction i = ih.getInstruction(); 78 | 79 | if (!visitInstruction(i)) 80 | i.accept(this); 81 | } 82 | return methodCalls; 83 | } 84 | 85 | private boolean visitInstruction(Instruction i) { 86 | short opcode = i.getOpcode(); 87 | return ((InstructionConst.getInstruction(opcode) != null) 88 | && !(i instanceof ConstantPushInstruction) 89 | && !(i instanceof ReturnInstruction)); 90 | } 91 | 92 | @Override 93 | public void visitINVOKEVIRTUAL(INVOKEVIRTUAL i) { 94 | methodCalls.add(String.format(format,"M",i.getReferenceType(cp),i.getMethodName(cp),argumentList(i.getArgumentTypes(cp)))); 95 | } 96 | 97 | @Override 98 | public void visitINVOKEINTERFACE(INVOKEINTERFACE i) { 99 | methodCalls.add(String.format(format,"I",i.getReferenceType(cp),i.getMethodName(cp),argumentList(i.getArgumentTypes(cp)))); 100 | } 101 | 102 | @Override 103 | public void visitINVOKESPECIAL(INVOKESPECIAL i) { 104 | methodCalls.add(String.format(format,"O",i.getReferenceType(cp),i.getMethodName(cp),argumentList(i.getArgumentTypes(cp)))); 105 | } 106 | 107 | @Override 108 | public void visitINVOKESTATIC(INVOKESTATIC i) { 109 | methodCalls.add(String.format(format,"S",i.getReferenceType(cp),i.getMethodName(cp),argumentList(i.getArgumentTypes(cp)))); 110 | } 111 | 112 | @Override 113 | public void visitINVOKEDYNAMIC(INVOKEDYNAMIC i) { 114 | methodCalls.add(String.format(format,"D",i.getType(cp),i.getMethodName(cp), 115 | argumentList(i.getArgumentTypes(cp)))); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/test/java/gr/gousiosg/javacg/JARBuilder.java: -------------------------------------------------------------------------------- 1 | package gr.gousiosg.javacg; 2 | 3 | import java.io.BufferedInputStream; 4 | import java.io.File; 5 | import java.io.FileInputStream; 6 | import java.io.FileNotFoundException; 7 | import java.io.FileOutputStream; 8 | import java.io.IOException; 9 | import java.io.StringWriter; 10 | import java.net.URI; 11 | import java.util.Arrays; 12 | import java.util.Collection; 13 | import java.util.LinkedList; 14 | import java.util.jar.Attributes; 15 | import java.util.jar.JarEntry; 16 | import java.util.jar.JarOutputStream; 17 | import java.util.jar.Manifest; 18 | 19 | import javax.tools.Diagnostic; 20 | import javax.tools.DiagnosticCollector; 21 | import javax.tools.JavaCompiler; 22 | import javax.tools.JavaCompiler.CompilationTask; 23 | import javax.tools.JavaFileObject; 24 | import javax.tools.SimpleJavaFileObject; 25 | import javax.tools.StandardJavaFileManager; 26 | import javax.tools.StandardLocation; 27 | import javax.tools.ToolProvider; 28 | 29 | public class JARBuilder { 30 | private static final String TEMP_DIR = System.getProperty("java.io.tmpdir"); 31 | 32 | private final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); 33 | private final DiagnosticCollector diagnostics = new DiagnosticCollector(); 34 | private final StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnostics, null, null); 35 | private final LinkedList compilationUnits = new LinkedList<>(); 36 | private final Collection classFiles = new LinkedList<>(); 37 | 38 | public JARBuilder() throws IOException { 39 | fileManager.setLocation(StandardLocation.CLASS_OUTPUT, Arrays.asList(new File(TEMP_DIR))); 40 | } 41 | 42 | public void add(String className, String classCode) throws IOException { 43 | compilationUnits.add(createJavaFile(className, classCode)); 44 | classFiles.add(new File(TEMP_DIR, className + ".class")); 45 | } 46 | 47 | public File build() throws FileNotFoundException, IOException { 48 | CompilationTask task = compiler.getTask(null, fileManager, diagnostics, null, null, compilationUnits); 49 | boolean success = task.call(); 50 | if (!success) { 51 | displayDiagnostic(diagnostics); 52 | throw new RuntimeException("Cannot compile classes for the JAR"); 53 | } 54 | 55 | File file = File.createTempFile("test", ".jar"); 56 | JarOutputStream jar = new JarOutputStream(new FileOutputStream(file), createManifest()); 57 | for (File classFile : classFiles) { 58 | add(classFile, jar); 59 | } 60 | jar.close(); 61 | return file; 62 | } 63 | 64 | private void displayDiagnostic(DiagnosticCollector diagnostics) { 65 | for (Diagnostic diagnostic : diagnostics.getDiagnostics()) { 66 | JavaSourceFromString sourceClass = (JavaSourceFromString) diagnostic.getSource(); 67 | System.err.println("-----"); 68 | System.err.println("Source: " + sourceClass.getName()); 69 | System.err.println("Message: " + diagnostic.getMessage(null)); 70 | System.err.println("Position: " + diagnostic.getPosition()); 71 | System.err.println(diagnostic.getKind() + " " + diagnostic.getCode()); 72 | } 73 | } 74 | 75 | private JavaFileObject createJavaFile(String className, String classCode) throws IOException { 76 | StringWriter writer = new StringWriter(); 77 | writer.append(classCode); 78 | writer.close(); 79 | JavaFileObject file = new JavaSourceFromString(className, writer.toString()); 80 | return file; 81 | } 82 | 83 | private Manifest createManifest() { 84 | Manifest manifest = new Manifest(); 85 | manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); 86 | return manifest; 87 | } 88 | 89 | private void add(File classFile, JarOutputStream jar) throws IOException { 90 | JarEntry entry = new JarEntry(classFile.getPath().replace("\\", "/")); 91 | jar.putNextEntry(entry); 92 | try (BufferedInputStream in = new BufferedInputStream(new FileInputStream(classFile))) { 93 | byte[] buffer = new byte[1024]; 94 | while (true) { 95 | int count = in.read(buffer); 96 | if (count == -1) 97 | break; 98 | jar.write(buffer, 0, count); 99 | } 100 | jar.closeEntry(); 101 | } 102 | } 103 | } 104 | 105 | class JavaSourceFromString extends SimpleJavaFileObject { 106 | final String code; 107 | 108 | JavaSourceFromString(String name, String code) throws IOException { 109 | super(URI.create("string:///" + name.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE); 110 | this.code = code; 111 | } 112 | 113 | @Override 114 | public CharSequence getCharContent(boolean ignoreEncodingErrors) { 115 | return code; 116 | } 117 | } -------------------------------------------------------------------------------- /src/test/java/gr/gousiosg/javacg/RunCucumberTest.java: -------------------------------------------------------------------------------- 1 | package gr.gousiosg.javacg; 2 | 3 | import cucumber.api.CucumberOptions; 4 | import cucumber.api.junit.Cucumber; 5 | import org.junit.runner.RunWith; 6 | 7 | @RunWith(Cucumber.class) 8 | @CucumberOptions(plugin = { "pretty" }) 9 | public class RunCucumberTest { 10 | } -------------------------------------------------------------------------------- /src/test/java/gr/gousiosg/javacg/StepDefinitions.java: -------------------------------------------------------------------------------- 1 | package gr.gousiosg.javacg; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.File; 5 | import java.io.IOException; 6 | import java.io.PrintStream; 7 | 8 | import cucumber.api.java.en.Given; 9 | import cucumber.api.java.en.Then; 10 | import cucumber.api.java.en.When; 11 | import gr.gousiosg.javacg.stat.JCallGraph; 12 | 13 | public class StepDefinitions { 14 | private final JARBuilder jarBuilder; 15 | private String result; 16 | 17 | public StepDefinitions() throws IOException { 18 | jarBuilder = new JARBuilder(); 19 | } 20 | 21 | @Given("^I have the class \"([^\"]*)\" with code:$") 22 | public void i_have_the_class_with_code(String className, String classCode) throws Exception { 23 | jarBuilder.add(className, classCode); 24 | } 25 | 26 | @When("^I run the analyze$") 27 | public void i_analyze_it() throws Exception { 28 | File jarFile = jarBuilder.build(); 29 | 30 | PrintStream oldOut = System.out; 31 | ByteArrayOutputStream resultBuffer = new ByteArrayOutputStream(); 32 | System.setOut(new PrintStream(resultBuffer)); 33 | JCallGraph.main(new String[] { jarFile.getPath() }); 34 | System.setOut(oldOut); 35 | 36 | result = resultBuffer.toString(); 37 | } 38 | 39 | @Then("^the result should contain:$") 40 | public void the_result_should_contain(String line) throws Exception { 41 | if (result.contains(line)) { 42 | // OK 43 | } else { 44 | System.err.println(result); 45 | throw new RuntimeException("Cannot found: " + line); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/test/resources/gr/gousiosg/javacg/lambda.feature: -------------------------------------------------------------------------------- 1 | #Author: matthieu.vergne@gmail.com 2 | Feature: Lambda 3 | I want to identify all lambdas within the analyzed code. 4 | 5 | Background: 6 | # Introduce the lambda we will use 7 | Given I have the class "Runner" with code: 8 | """ 9 | @FunctionalInterface 10 | public interface Runner { 11 | public void run(); 12 | } 13 | """ 14 | 15 | Scenario: Retrieve lambda in method 16 | Given I have the class "LambdaTest" with code: 17 | """ 18 | public class LambdaTest { 19 | public void methodA() { 20 | Runner r = () -> methodB(); 21 | r.run(); 22 | } 23 | 24 | public void methodB() {} 25 | } 26 | """ 27 | When I run the analyze 28 | # Creation of r in methodA 29 | Then the result should contain: 30 | """ 31 | M:LambdaTest:methodA() (D)Runner:run(LambdaTest) 32 | """ 33 | # Call of methodB in r 34 | And the result should contain: 35 | """ 36 | M:LambdaTest:lambda$methodA$0() (M)LambdaTest:methodB() 37 | """ 38 | 39 | Scenario: Retrieve nested lambdas 40 | Given I have the class "NestedLambdaTest" with code: 41 | """ 42 | public class NestedLambdaTest { 43 | public void methodA() { 44 | Runner r = () -> { 45 | Runner r2 = () -> { 46 | Runner r3 = () -> methodB(); 47 | r3.run(); 48 | }; 49 | r2.run(); 50 | }; 51 | r.run(); 52 | } 53 | 54 | public void methodB() {} 55 | } 56 | """ 57 | When I run the analyze 58 | # Creation of r in methodA 59 | Then the result should contain: 60 | """ 61 | M:NestedLambdaTest:methodA() (D)Runner:run(NestedLambdaTest) 62 | """ 63 | # Creation of r2 in r 64 | And the result should contain: 65 | """ 66 | M:NestedLambdaTest:lambda$methodA$2() (D)Runner:run(NestedLambdaTest) 67 | """ 68 | # Creation of r3 in r2 69 | And the result should contain: 70 | """ 71 | M:NestedLambdaTest:lambda$lambda$methodA$2$1() (D)Runner:run(NestedLambdaTest) 72 | """ 73 | # Call of methodB in r3 74 | And the result should contain: 75 | """ 76 | M:NestedLambdaTest:lambda$lambda$lambda$methodA$2$1$0() (M)NestedLambdaTest:methodB() 77 | """ 78 | -------------------------------------------------------------------------------- /src/test/resources/gr/gousiosg/javacg/native.feature: -------------------------------------------------------------------------------- 1 | #Author: matthieu.vergne@gmail.com 2 | Feature: Native 3 | I want to identify all native methods within the analyzed code. 4 | 5 | Scenario: Retrieve native method call 6 | Given I have the class "NativeTest" with code: 7 | """ 8 | public class NativeTest { 9 | public void methodA() { 10 | methodB(); 11 | } 12 | 13 | public native void methodB(); 14 | } 15 | """ 16 | When I run the analyze 17 | Then the result should contain: 18 | """ 19 | M:NativeTest:methodA() (M)NativeTest:methodB() 20 | """ --------------------------------------------------------------------------------