diff --git a/Ghidra/Features/Base/ghidra_scripts/ExportFunctionInfoScript.java b/Ghidra/Features/Base/ghidra_scripts/ExportFunctionInfoScript.java index 528786f7e94..f90fd6ec9c8 100644 --- a/Ghidra/Features/Base/ghidra_scripts/ExportFunctionInfoScript.java +++ b/Ghidra/Features/Base/ghidra_scripts/ExportFunctionInfoScript.java @@ -13,51 +13,507 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -// List function names and entry point addresses to a file in JSON format +// Export function information in various formats (JSON, CSV, DOT) //@category Functions +import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; import com.google.gson.*; import com.google.gson.stream.JsonWriter; import ghidra.app.script.GhidraScript; import ghidra.program.model.address.Address; +import ghidra.program.model.address.AddressSetView; +import ghidra.program.model.block.CodeBlockIterator; +import ghidra.program.model.block.SimpleBlockModel; +import ghidra.program.model.data.DataType; import ghidra.program.model.listing.*; +import ghidra.program.model.symbol.Namespace; +import ghidra.program.util.CyclomaticComplexity; +import ghidra.util.Msg; +import ghidra.util.exception.CancelledException; public class ExportFunctionInfoScript extends GhidraScript { private static final String NAME = "name"; private static final String ENTRY = "entry"; + private enum ExportFormat { + JSON_SIMPLE("JSON (Simple - name and entry only)"), + JSON_METRICS("JSON (Detailed metrics)"), + CSV_METRICS("CSV (Detailed metrics)"), + DOT_CALLGRAPH("DOT (Function call graph)"); + + private final String displayName; + + ExportFormat(String displayName) { + this.displayName = displayName; + } + + @Override + public String toString() { + return displayName; + } + } + @Override public void run() throws Exception { + if (currentProgram == null) { + printerr("No current program."); + return; + } - Gson gson = new GsonBuilder().setPrettyPrinting().create(); + // Ask user to choose export format + List formats = Arrays.asList(ExportFormat.values()); + ExportFormat selectedFormat = askChoice("Choose Export Format", + "Select the format for exporting function information:", formats, ExportFormat.JSON_SIMPLE); File outputFile = askFile("Please Select Output File", "Choose"); - JsonWriter jsonWriter = new JsonWriter(new FileWriter(outputFile)); - jsonWriter.beginArray(); + if (outputFile == null) { + printerr("No output file selected."); + return; + } + + // Get optional scope (selection or highlight) + AddressSetView scope = currentSelection != null ? currentSelection : currentHighlight; + + // Export based on selected format + switch (selectedFormat) { + case JSON_SIMPLE: + exportSimpleJson(outputFile, scope); + break; + case JSON_METRICS: + exportMetricsJson(outputFile, scope); + break; + case CSV_METRICS: + exportMetricsCsv(outputFile, scope); + break; + case DOT_CALLGRAPH: + exportCallGraphDot(outputFile, scope); + break; + } + + println("Wrote function information to: " + outputFile.getAbsolutePath()); + } + + // ========== SIMPLE JSON EXPORT ========== + private void exportSimpleJson(File outputFile, AddressSetView scope) throws Exception { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + + try (JsonWriter jsonWriter = new JsonWriter(new FileWriter(outputFile))) { + jsonWriter.beginArray(); + + Listing listing = currentProgram.getListing(); + FunctionIterator iter = listing.getFunctions(true); + while (iter.hasNext() && !monitor.isCancelled()) { + Function f = iter.next(); + + // Apply scope filtering if present + if (scope != null && !scope.intersects(f.getBody())) { + continue; + } + + String name = f.getName(); + Address entry = f.getEntryPoint(); + JsonObject json = new JsonObject(); + json.addProperty(NAME, name); + json.addProperty(ENTRY, entry.toString()); + + gson.toJson(json, jsonWriter); + } + + jsonWriter.endArray(); + } + } + + // ========== DETAILED JSON METRICS EXPORT ========== + private void exportMetricsJson(File outputFile, AddressSetView scope) + throws IOException, CancelledException { + FunctionManager fm = currentProgram.getFunctionManager(); Listing listing = currentProgram.getListing(); - FunctionIterator iter = listing.getFunctions(true); - while (iter.hasNext() && !monitor.isCancelled()) { - Function f = iter.next(); + CyclomaticComplexity complexityCalc = new CyclomaticComplexity(); + + try (BufferedWriter w = new BufferedWriter(new FileWriter(outputFile))) { + w.write("{\n"); + // Basic program info + w.write(" \"program\": {\n"); + w.write(" \"name\": \"" + escape(currentProgram.getName()) + "\",\n"); + w.write(" \"language\": \"" + + escape(currentProgram.getLanguageID().getIdAsString()) + "\"\n"); + w.write(" },\n"); + w.write(" \"functions\": [\n"); + + FunctionIterator it = fm.getFunctions(true); + boolean first = true; + monitor.initialize(fm.getFunctionCount()); + int exported = 0; + + while (it.hasNext() && !monitor.isCancelled()) { + Function f = it.next(); + AddressSetView body = f.getBody(); + if (scope != null && !scope.intersects(body)) { + continue; + } + + monitor.setMessage("Exporting metrics: " + f.getName()); + monitor.incrementProgress(1); + if (!first) { + w.write(",\n"); + } + first = false; + + String entry = f.getEntryPoint().toString(); + long size = body.getNumAddresses(); - String name = f.getName(); - Address entry = f.getEntryPoint(); + // Instruction count - use for-each style loop + int instCount = 0; + for (Instruction inst : listing.getInstructions(body, true)) { + instCount++; + } - JsonObject json = new JsonObject(); - json.addProperty(NAME, name); - json.addProperty(ENTRY, entry.toString()); + int callers = sizeSafe(f.getCallingFunctions(monitor)); + int callees = sizeSafe(f.getCalledFunctions(monitor)); - gson.toJson(json, jsonWriter); + int cplx = 0; + try { + cplx = complexityCalc.calculateCyclomaticComplexity(f, monitor); + } + catch (Exception e) { + Msg.warn(this, "Failed to compute complexity for " + f.getName(), e); + } + + int params = f.getParameterCount(); + + // Basic blocks + int basicBlocks = 0; + SimpleBlockModel model = new SimpleBlockModel(currentProgram); + CodeBlockIterator blocks = model.getCodeBlocksContaining(body, monitor); + while (blocks.hasNext() && !monitor.isCancelled()) { + blocks.next(); + basicBlocks++; + } + + // Locals count + int localsCount = 0; + try { + if (!f.isExternal()) { + localsCount = f.getLocalVariables().length; + } + } + catch (Exception ignore) { + // In some cases locals may not resolve; ignore + } + + // Signature and flags + boolean isExternal = f.isExternal(); + boolean isThunk = f.isThunk(); + boolean hasVarArgs = f.hasVarArgs(); + boolean isInline = f.isInline(); + boolean noReturn = f.hasNoReturn(); + boolean customStorage = f.hasCustomVariableStorage(); + int stackPurgeSize = f.getStackPurgeSize(); + String callConv = safeString(f.getCallingConventionName()); + Namespace ns = f.getParentNamespace(); + String namespace = ns != null ? ns.getName(true) : ""; + DataType retType = f.getReturnType(); + String returnType = retType != null ? retType.getDisplayName() : "void"; + String proto = f.getPrototypeString(true, false); + String protoWithCc = f.getPrototypeString(true, true); + String thunkTarget = null; + if (isThunk) { + Function tf = f.getThunkedFunction(true); + if (tf != null) { + thunkTarget = tf.getEntryPoint().toString(); + } + } + + w.write(" {\n"); + w.write(" \"name\": \"" + escape(f.getName()) + "\",\n"); + w.write(" \"entry\": \"" + escape(entry) + "\",\n"); + w.write(" \"size_bytes\": " + size + ",\n"); + w.write(" \"address_ranges\": " + body.getNumAddressRanges() + ",\n"); + w.write(" \"instruction_count\": " + instCount + ",\n"); + w.write(" \"cyclomatic_complexity\": " + cplx + ",\n"); + w.write(" \"parameters\": " + params + ",\n"); + w.write(" \"locals\": " + localsCount + ",\n"); + w.write(" \"basic_blocks\": " + basicBlocks + ",\n"); + w.write(" \"external\": " + isExternal + ",\n"); + w.write(" \"thunk\": " + isThunk + ",\n"); + w.write(" \"thunk_target\": " + + (thunkTarget == null ? "null" : ("\"" + escape(thunkTarget) + "\"")) + ",\n"); + w.write(" \"variadic\": " + hasVarArgs + ",\n"); + w.write(" \"inline\": " + isInline + ",\n"); + w.write(" \"no_return\": " + noReturn + ",\n"); + w.write(" \"custom_storage\": " + customStorage + ",\n"); + w.write(" \"stack_purge_size\": " + stackPurgeSize + ",\n"); + w.write(" \"calling_convention\": \"" + escape(callConv) + "\",\n"); + w.write(" \"namespace\": \"" + escape(namespace) + "\",\n"); + w.write(" \"return_type\": \"" + escape(returnType) + "\",\n"); + w.write(" \"prototype\": \"" + escape(proto) + "\",\n"); + w.write(" \"prototype_with_cc\": \"" + escape(protoWithCc) + "\",\n"); + w.write(" \"callers\": " + callers + ",\n"); + w.write(" \"callees\": " + callees + "\n"); + w.write(" }"); + exported++; + } + + w.write("\n ],\n"); + w.write(" \"summary\": {\n"); + w.write(" \"exported_functions\": " + exported + ",\n"); + w.write(" \"total_functions\": " + fm.getFunctionCount() + ",\n"); + w.write(" \"selection_applied\": " + Boolean.toString(scope != null) + "\n"); + w.write(" }\n"); + w.write("}\n"); } + } + + // ========== CSV METRICS EXPORT ========== + private void exportMetricsCsv(File outputFile, AddressSetView scope) + throws IOException, CancelledException { + FunctionManager fm = currentProgram.getFunctionManager(); + Listing listing = currentProgram.getListing(); + CyclomaticComplexity complexityCalc = new CyclomaticComplexity(); + + try (BufferedWriter w = new BufferedWriter(new FileWriter(outputFile))) { + // Header + w.write(String.join(",", "name", "entry", "address_ranges", "instruction_count", + "cyclomatic_complexity", "parameters", "locals", "basic_blocks", "external", + "thunk", "thunk_target", "variadic", "inline", "no_return", "custom_storage", + "stack_purge_size", "calling_convention", "namespace", "return_type", + "prototype", "prototype_with_cc", "callers", "callees")); + w.write("\n"); - jsonWriter.endArray(); - jsonWriter.close(); + FunctionIterator it = fm.getFunctions(true); + monitor.initialize(fm.getFunctionCount()); + + while (it.hasNext() && !monitor.isCancelled()) { + Function f = it.next(); + AddressSetView body = f.getBody(); + if (scope != null && !scope.intersects(body)) { + continue; + } + + monitor.setMessage("Exporting metrics: " + f.getName()); + monitor.incrementProgress(1); + + String entry = f.getEntryPoint().toString(); + + // Instruction count - use for-each style loop + int instCount = 0; + for (Instruction inst : listing.getInstructions(body, true)) { + instCount++; + } + + int callers = sizeSafe(f.getCallingFunctions(monitor)); + int callees = sizeSafe(f.getCalledFunctions(monitor)); + + int cplx = 0; + try { + cplx = complexityCalc.calculateCyclomaticComplexity(f, monitor); + } + catch (Exception e) { + Msg.warn(this, "Failed to compute complexity for " + f.getName(), e); + } + + int params = f.getParameterCount(); + + // Basic blocks + int basicBlocks = 0; + SimpleBlockModel model = new SimpleBlockModel(currentProgram); + CodeBlockIterator blocks = model.getCodeBlocksContaining(body, monitor); + while (blocks.hasNext() && !monitor.isCancelled()) { + blocks.next(); + basicBlocks++; + } + + int localsCount = 0; + try { + if (!f.isExternal()) { + localsCount = f.getLocalVariables().length; + } + } + catch (Exception ignore) { + } + + boolean isExternal = f.isExternal(); + boolean isThunk = f.isThunk(); + boolean hasVarArgs = f.hasVarArgs(); + boolean isInline = f.isInline(); + boolean noReturn = f.hasNoReturn(); + boolean customStorage = f.hasCustomVariableStorage(); + int stackPurgeSize = f.getStackPurgeSize(); + String callConv = safeString(f.getCallingConventionName()); + Namespace ns = f.getParentNamespace(); + String namespace = ns != null ? ns.getName(true) : ""; + DataType retType = f.getReturnType(); + String returnType = retType != null ? retType.getDisplayName() : "void"; + String proto = f.getPrototypeString(true, false); + String protoWithCc = f.getPrototypeString(true, true); + String thunkTarget = null; + if (isThunk) { + Function tf = f.getThunkedFunction(true); + if (tf != null) { + thunkTarget = tf.getEntryPoint().toString(); + } + } + + w.write(String.join(",", csv(f.getName()), csv(entry), + Integer.toString(body.getNumAddressRanges()), Integer.toString(instCount), + Integer.toString(cplx), Integer.toString(params), + Integer.toString(localsCount), Integer.toString(basicBlocks), + Boolean.toString(isExternal), Boolean.toString(isThunk), + csvOrEmpty(thunkTarget), Boolean.toString(hasVarArgs), + Boolean.toString(isInline), Boolean.toString(noReturn), + Boolean.toString(customStorage), Integer.toString(stackPurgeSize), + csv(callConv), csv(namespace), csv(returnType), csv(proto), + csv(protoWithCc), Integer.toString(callers), Integer.toString(callees))); + w.write("\n"); + } + } + } + + // ========== DOT CALL GRAPH EXPORT ========== + private void exportCallGraphDot(File outputFile, AddressSetView scope) throws IOException { + FunctionManager fm = currentProgram.getFunctionManager(); + FunctionIterator it = fm.getFunctions(true); + + Map idByEntry = new HashMap<>(); + Set edges = new HashSet<>(); + + monitor.initialize(fm.getFunctionCount()); + + while (it.hasNext() && !monitor.isCancelled()) { + Function f = it.next(); + if (scope != null && !scope.intersects(f.getBody())) { + continue; + } + monitor.setMessage("Indexing function: " + f.getName()); + monitor.incrementProgress(1); + + String fEntry = f.getEntryPoint().toString(); + if (!idByEntry.containsKey(fEntry)) { + idByEntry.put(fEntry, idByEntry.size() + 1); + } + + for (Function callee : f.getCalledFunctions(monitor)) { + if (scope != null && !scope.intersects(callee.getBody())) { + // If filtered by selection, only keep edges entirely in scope + continue; + } + String cEntry = callee.getEntryPoint().toString(); + if (!idByEntry.containsKey(cEntry)) { + idByEntry.put(cEntry, idByEntry.size() + 1); + } + edges.add(fEntry + "->" + cEntry); + } + } + + try (BufferedWriter w = new BufferedWriter(new FileWriter(outputFile))) { + w.write("digraph \"" + escape(currentProgram.getName()) + "\" {\n"); + w.write(" node [shape=box, fontsize=10];\n"); + + for (Map.Entry e : idByEntry.entrySet()) { + String entry = e.getKey(); + int id = e.getValue(); + // Label: function name + entry + Function f = + fm.getFunctionAt(currentProgram.getAddressFactory().getAddress(entry)); + String label = (f != null ? f.getName() : entry) + "\\n" + entry; + w.write(" n" + id + " [label=\"" + escape(label) + "\"];\n"); + } + + for (String edge : edges) { + String[] parts = edge.split("->", 2); + Integer sId = idByEntry.get(parts[0]); + Integer tId = idByEntry.get(parts[1]); + if (sId != null && tId != null) { + w.write(" n" + sId + " -> n" + tId + ";\n"); + } + } + + w.write("}\n"); + } + } + + // ========== HELPER METHODS ========== + private static int sizeSafe(Set s) { + return s == null ? 0 : s.size(); + } + + private static String safeString(String s) { + return s == null ? "" : s; + } + + private static String escape(String s) { + if (s == null) { + return ""; + } + StringBuilder b = new StringBuilder(s.length() + 16); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + switch (c) { + case '"': + b.append("\\\""); + break; + case '\\': + b.append("\\\\"); + break; + case '\b': + b.append("\\b"); + break; + case '\f': + b.append("\\f"); + break; + case '\n': + b.append("\\n"); + break; + case '\r': + b.append("\\r"); + break; + case '\t': + b.append("\\t"); + break; + default: + if (c < 0x20) { + appendUnicodeEscape(b, c); + } + else { + b.append(c); + } + break; + } + } + return b.toString(); + } + + private static void appendUnicodeEscape(StringBuilder b, char c) { + b.append("\\u"); + String hex = Integer.toHexString(c); + for (int i = hex.length(); i < 4; i++) { + b.append('0'); + } + b.append(hex); + } + + private static String csv(String s) { + if (s == null) { + return "\"\""; + } + String v = s.replace("\"", "\"\""); + return "\"" + v + "\""; + } - println("Wrote functions to " + outputFile); + private static String csvOrEmpty(String s) { + return s == null ? "" : csv(s); } }