From c853f996cdfcd2c61417e021ebc8f0346e4e3f2e Mon Sep 17 00:00:00 2001 From: Abdul Samad Date: Sat, 18 Oct 2025 01:11:13 +0500 Subject: [PATCH 1/3] Add scripts: export function metrics (JSON/CSV) and function call graph (DOT) --- .../ExportFunctionCallGraphToDot.java | 132 +++++++++ .../ExportFunctionMetricsToCsv.java | 196 +++++++++++++ .../ExportFunctionMetricsToJson.java | 265 ++++++++++++++++++ 3 files changed, 593 insertions(+) create mode 100644 Ghidra/Features/Base/ghidra_scripts/ExportFunctionCallGraphToDot.java create mode 100644 Ghidra/Features/Base/ghidra_scripts/ExportFunctionMetricsToCsv.java create mode 100644 Ghidra/Features/Base/ghidra_scripts/ExportFunctionMetricsToJson.java diff --git a/Ghidra/Features/Base/ghidra_scripts/ExportFunctionCallGraphToDot.java b/Ghidra/Features/Base/ghidra_scripts/ExportFunctionCallGraphToDot.java new file mode 100644 index 00000000000..6d510cc8584 --- /dev/null +++ b/Ghidra/Features/Base/ghidra_scripts/ExportFunctionCallGraphToDot.java @@ -0,0 +1,132 @@ +/* ### + * IP: GHIDRA + * REVIEWED: NO + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// Exports the program's function call graph to a DOT file. +//@category Graph + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import ghidra.app.script.GhidraScript; +import ghidra.program.model.address.AddressSetView; +import ghidra.program.model.listing.Function; +import ghidra.program.model.listing.FunctionIterator; +import ghidra.program.model.listing.FunctionManager; +import ghidra.program.model.listing.Program; + +public class ExportFunctionCallGraphToDot extends GhidraScript { + + @Override + protected void run() throws Exception { + if (currentProgram == null) { + printerr("No current program."); + return; + } + + File outFile = askFile("Choose output DOT file", "Save"); + if (outFile == null) { + printerr("No output file selected."); + return; + } + + AddressSetView scope = currentSelection != null ? currentSelection : currentHighlight; + writeCallGraphDot(currentProgram, outFile, scope); + println("Wrote function call graph to: " + outFile.getAbsolutePath()); + } + + private void writeCallGraphDot(Program program, File outFile, AddressSetView scope) + throws IOException { + FunctionManager fm = program.getFunctionManager(); + FunctionIterator it = fm.getFunctions(true); + + Map idByEntry = new HashMap<>(); + Set edges = new HashSet<>(); + int nextId = 1; + + 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(); + idByEntry.computeIfAbsent(fEntry, k -> nextId++); + + 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(); + idByEntry.computeIfAbsent(cEntry, k -> nextId++); + edges.add(fEntry + "->" + cEntry); + } + } + + try (BufferedWriter w = new BufferedWriter(new FileWriter(outFile))) { + String graphName = sanitizeId(program.getName()); + w.write("digraph \"" + escape(program.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(program.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"); + } + } + + private static String sanitizeId(String s) { + if (s == null || s.isEmpty()) return "G"; + StringBuilder b = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (Character.isLetterOrDigit(c) || c == '_') b.append(c); + } + if (b.length() == 0) b.append('G'); + return b.toString(); + } + + private static String escape(String s) { + if (s == null) return ""; + return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n"); + } +} + diff --git a/Ghidra/Features/Base/ghidra_scripts/ExportFunctionMetricsToCsv.java b/Ghidra/Features/Base/ghidra_scripts/ExportFunctionMetricsToCsv.java new file mode 100644 index 00000000000..f6b87ca208d --- /dev/null +++ b/Ghidra/Features/Base/ghidra_scripts/ExportFunctionMetricsToCsv.java @@ -0,0 +1,196 @@ +/* ### + * IP: GHIDRA + * REVIEWED: NO + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// Exports per-function metrics to a CSV file for the current program. +//@category Functions + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Iterator; +import java.util.Set; + +import ghidra.app.script.GhidraScript; +import ghidra.program.model.address.AddressSetView; +import ghidra.program.model.listing.Function; +import ghidra.program.model.listing.FunctionIterator; +import ghidra.program.model.listing.FunctionManager; +import ghidra.program.model.listing.Instruction; +import ghidra.program.model.listing.Listing; +import ghidra.program.model.listing.Program; +import ghidra.program.util.CyclomaticComplexity; +import ghidra.util.Msg; +import ghidra.program.model.block.SimpleBlockModel; +import ghidra.program.model.block.CodeBlockIterator; +import ghidra.util.exception.CancelledException; +import ghidra.program.model.data.DataType; +import ghidra.program.model.symbol.Namespace; + +public class ExportFunctionMetricsToCsv extends GhidraScript { + + @Override + protected void run() throws Exception { + if (currentProgram == null) { + printerr("No current program."); + return; + } + + File outFile = askFile("Choose output CSV file", "Save"); + if (outFile == null) { + printerr("No output file selected."); + return; + } + + AddressSetView scope = currentSelection != null ? currentSelection : currentHighlight; + writeFunctionMetricsCsv(currentProgram, outFile, scope); + println("Wrote function metrics to: " + outFile.getAbsolutePath()); + } + + private void writeFunctionMetricsCsv(Program program, File outFile, AddressSetView scope) + throws IOException { + FunctionManager fm = program.getFunctionManager(); + Listing listing = program.getListing(); + CyclomaticComplexity complexityCalc = new CyclomaticComplexity(); + + try (BufferedWriter w = new BufferedWriter(new FileWriter(outFile))) { + // 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"); + + 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 + int instCount = 0; + for (Iterator insIt = listing.getInstructions(body, true); insIt.hasNext();) { + insIt.next(); + 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; + try { + SimpleBlockModel model = new SimpleBlockModel(program); + CodeBlockIterator blocks = model.getCodeBlocksContaining(body, monitor); + while (blocks.hasNext() && !monitor.isCancelled()) { + blocks.next(); + basicBlocks++; + } + } + catch (CancelledException ce) { + // respect cancellation + } + + 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"); + } + } + } + + 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 csv(String s) { + if (s == null) { + return "\"\""; + } + String v = s.replace("\"", "\"\""); + return "\"" + v + "\""; + } + + private static String csvOrEmpty(String s) { + return s == null ? "" : csv(s); + } +} + diff --git a/Ghidra/Features/Base/ghidra_scripts/ExportFunctionMetricsToJson.java b/Ghidra/Features/Base/ghidra_scripts/ExportFunctionMetricsToJson.java new file mode 100644 index 00000000000..d27e5d3abc0 --- /dev/null +++ b/Ghidra/Features/Base/ghidra_scripts/ExportFunctionMetricsToJson.java @@ -0,0 +1,265 @@ +/* ### + * IP: GHIDRA + * REVIEWED: NO + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// Exports basic per-function metrics to a JSON file for the current program. +//@category Functions + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Iterator; +import java.util.Set; + +import ghidra.app.script.GhidraScript; +import ghidra.program.model.address.AddressSetView; +import ghidra.program.model.listing.Function; +import ghidra.program.model.listing.FunctionIterator; +import ghidra.program.model.listing.FunctionManager; +import ghidra.program.model.listing.Instruction; +import ghidra.program.model.listing.Listing; +import ghidra.program.model.listing.Program; +import ghidra.program.util.CyclomaticComplexity; +import ghidra.util.Msg; +import ghidra.program.model.block.SimpleBlockModel; +import ghidra.program.model.block.CodeBlockIterator; +import ghidra.util.exception.CancelledException; +import ghidra.program.model.data.DataType; +import ghidra.program.model.symbol.Namespace; + +public class ExportFunctionMetricsToJson extends GhidraScript { + + @Override + protected void run() throws Exception { + if (currentProgram == null) { + printerr("No current program."); + return; + } + + File outFile = askFile("Choose output JSON file", "Save"); + if (outFile == null) { + printerr("No output file selected."); + return; + } + + AddressSetView scope = currentSelection != null ? currentSelection : currentHighlight; + writeFunctionMetricsJson(currentProgram, outFile, scope); + println("Wrote function metrics to: " + outFile.getAbsolutePath()); + } + + private void writeFunctionMetricsJson(Program program, File outFile, AddressSetView scope) + throws IOException { + FunctionManager fm = program.getFunctionManager(); + Listing listing = program.getListing(); + CyclomaticComplexity complexityCalc = new CyclomaticComplexity(); + + try (BufferedWriter w = new BufferedWriter(new FileWriter(outFile))) { + w.write("{\n"); + // basic program info + w.write(" \"program\": {\n"); + w.write(" \"name\": \"" + escape(program.getName()) + "\",\n"); + w.write(" \"language\": \"" + escape(program.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(); + + // instruction count (iterate rather than rely on size for accuracy) + int instCount = 0; + for (Iterator insIt = listing.getInstructions(body, true); insIt.hasNext();) { + insIt.next(); + 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 (Simple block model over function body) + int basicBlocks = 0; + try { + SimpleBlockModel model = new SimpleBlockModel(program); + CodeBlockIterator blocks = model.getCodeBlocksContaining(body, monitor); + while (blocks.hasNext() && !monitor.isCancelled()) { + blocks.next(); + basicBlocks++; + } + } + catch (CancelledException ce) { + // respect cancellation; leave count as is + } + + // 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"); + } + } + + 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); + } +} From e63a1dda57d99e5033716019299f1c126d21753f Mon Sep 17 00:00:00 2001 From: Abdul Samad Date: Fri, 31 Oct 2025 15:54:17 +0500 Subject: [PATCH 2/3] Integrate function export scripts into ExportFunctionInfoScript - Consolidated ExportFunctionMetricsToJson, ExportFunctionMetricsToCsv, and ExportFunctionCallGraphToDot into ExportFunctionInfoScript.java - Added format selection using GhidraScript.askChoices() as requested in code review - Users can now choose between: JSON (Simple), JSON (Detailed metrics), CSV (Detailed metrics), and DOT (Function call graph) - All export formats support optional selection/highlight filtering - Maintains backward compatibility with existing simple JSON export functionality --- .../ExportFunctionCallGraphToDot.java | 132 ----- .../ExportFunctionInfoScript.java | 492 +++++++++++++++++- .../ExportFunctionMetricsToCsv.java | 196 ------- .../ExportFunctionMetricsToJson.java | 265 ---------- 4 files changed, 487 insertions(+), 598 deletions(-) delete mode 100644 Ghidra/Features/Base/ghidra_scripts/ExportFunctionCallGraphToDot.java delete mode 100644 Ghidra/Features/Base/ghidra_scripts/ExportFunctionMetricsToCsv.java delete mode 100644 Ghidra/Features/Base/ghidra_scripts/ExportFunctionMetricsToJson.java diff --git a/Ghidra/Features/Base/ghidra_scripts/ExportFunctionCallGraphToDot.java b/Ghidra/Features/Base/ghidra_scripts/ExportFunctionCallGraphToDot.java deleted file mode 100644 index 6d510cc8584..00000000000 --- a/Ghidra/Features/Base/ghidra_scripts/ExportFunctionCallGraphToDot.java +++ /dev/null @@ -1,132 +0,0 @@ -/* ### - * IP: GHIDRA - * REVIEWED: NO - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -// Exports the program's function call graph to a DOT file. -//@category Graph - -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import ghidra.app.script.GhidraScript; -import ghidra.program.model.address.AddressSetView; -import ghidra.program.model.listing.Function; -import ghidra.program.model.listing.FunctionIterator; -import ghidra.program.model.listing.FunctionManager; -import ghidra.program.model.listing.Program; - -public class ExportFunctionCallGraphToDot extends GhidraScript { - - @Override - protected void run() throws Exception { - if (currentProgram == null) { - printerr("No current program."); - return; - } - - File outFile = askFile("Choose output DOT file", "Save"); - if (outFile == null) { - printerr("No output file selected."); - return; - } - - AddressSetView scope = currentSelection != null ? currentSelection : currentHighlight; - writeCallGraphDot(currentProgram, outFile, scope); - println("Wrote function call graph to: " + outFile.getAbsolutePath()); - } - - private void writeCallGraphDot(Program program, File outFile, AddressSetView scope) - throws IOException { - FunctionManager fm = program.getFunctionManager(); - FunctionIterator it = fm.getFunctions(true); - - Map idByEntry = new HashMap<>(); - Set edges = new HashSet<>(); - int nextId = 1; - - 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(); - idByEntry.computeIfAbsent(fEntry, k -> nextId++); - - 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(); - idByEntry.computeIfAbsent(cEntry, k -> nextId++); - edges.add(fEntry + "->" + cEntry); - } - } - - try (BufferedWriter w = new BufferedWriter(new FileWriter(outFile))) { - String graphName = sanitizeId(program.getName()); - w.write("digraph \"" + escape(program.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(program.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"); - } - } - - private static String sanitizeId(String s) { - if (s == null || s.isEmpty()) return "G"; - StringBuilder b = new StringBuilder(); - for (int i = 0; i < s.length(); i++) { - char c = s.charAt(i); - if (Character.isLetterOrDigit(c) || c == '_') b.append(c); - } - if (b.length() == 0) b.append('G'); - return b.toString(); - } - - private static String escape(String s) { - if (s == null) return ""; - return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n"); - } -} - diff --git a/Ghidra/Features/Base/ghidra_scripts/ExportFunctionInfoScript.java b/Ghidra/Features/Base/ghidra_scripts/ExportFunctionInfoScript.java index 528786f7e94..5ac83cfd43a 100644 --- a/Ghidra/Features/Base/ghidra_scripts/ExportFunctionInfoScript.java +++ b/Ghidra/Features/Base/ghidra_scripts/ExportFunctionInfoScript.java @@ -13,31 +13,122 @@ * 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.Iterator; +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); + + // Determine file extension based on format + String extension; + switch (selectedFormat) { + case JSON_SIMPLE: + case JSON_METRICS: + extension = ".json"; + break; + case CSV_METRICS: + extension = ".csv"; + break; + case DOT_CALLGRAPH: + extension = ".dot"; + break; + default: + extension = ".txt"; + break; + } File outputFile = askFile("Please Select Output File", "Choose"); - JsonWriter jsonWriter = new JsonWriter(new FileWriter(outputFile)); + 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(); @@ -45,6 +136,11 @@ public void run() throws Exception { 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(); @@ -56,8 +152,394 @@ public void run() throws Exception { } jsonWriter.endArray(); - jsonWriter.close(); + } + } + + // ========== DETAILED JSON METRICS EXPORT ========== + private void exportMetricsJson(File outputFile, AddressSetView scope) throws IOException { + FunctionManager fm = currentProgram.getFunctionManager(); + Listing listing = currentProgram.getListing(); + 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(); + + // Instruction count + int instCount = 0; + for (Iterator insIt = listing.getInstructions(body, true); insIt.hasNext();) { + insIt.next(); + 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; + try { + SimpleBlockModel model = new SimpleBlockModel(currentProgram); + CodeBlockIterator blocks = model.getCodeBlocksContaining(body, monitor); + while (blocks.hasNext() && !monitor.isCancelled()) { + blocks.next(); + basicBlocks++; + } + } + catch (CancelledException ce) { + // Respect cancellation; leave count as is + } + + // 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 { + 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"); + + 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 + int instCount = 0; + for (Iterator insIt = listing.getInstructions(body, true); insIt.hasNext();) { + insIt.next(); + 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; + try { + SimpleBlockModel model = new SimpleBlockModel(currentProgram); + CodeBlockIterator blocks = model.getCodeBlocksContaining(body, monitor); + while (blocks.hasNext() && !monitor.isCancelled()) { + blocks.next(); + basicBlocks++; + } + } + catch (CancelledException ce) { + // Respect cancellation + } + + 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<>(); + int nextId = 1; + + 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(); + idByEntry.computeIfAbsent(fEntry, k -> nextId++); + + 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(); + idByEntry.computeIfAbsent(cEntry, k -> nextId++); + 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); } } diff --git a/Ghidra/Features/Base/ghidra_scripts/ExportFunctionMetricsToCsv.java b/Ghidra/Features/Base/ghidra_scripts/ExportFunctionMetricsToCsv.java deleted file mode 100644 index f6b87ca208d..00000000000 --- a/Ghidra/Features/Base/ghidra_scripts/ExportFunctionMetricsToCsv.java +++ /dev/null @@ -1,196 +0,0 @@ -/* ### - * IP: GHIDRA - * REVIEWED: NO - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -// Exports per-function metrics to a CSV file for the current program. -//@category Functions - -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.util.Iterator; -import java.util.Set; - -import ghidra.app.script.GhidraScript; -import ghidra.program.model.address.AddressSetView; -import ghidra.program.model.listing.Function; -import ghidra.program.model.listing.FunctionIterator; -import ghidra.program.model.listing.FunctionManager; -import ghidra.program.model.listing.Instruction; -import ghidra.program.model.listing.Listing; -import ghidra.program.model.listing.Program; -import ghidra.program.util.CyclomaticComplexity; -import ghidra.util.Msg; -import ghidra.program.model.block.SimpleBlockModel; -import ghidra.program.model.block.CodeBlockIterator; -import ghidra.util.exception.CancelledException; -import ghidra.program.model.data.DataType; -import ghidra.program.model.symbol.Namespace; - -public class ExportFunctionMetricsToCsv extends GhidraScript { - - @Override - protected void run() throws Exception { - if (currentProgram == null) { - printerr("No current program."); - return; - } - - File outFile = askFile("Choose output CSV file", "Save"); - if (outFile == null) { - printerr("No output file selected."); - return; - } - - AddressSetView scope = currentSelection != null ? currentSelection : currentHighlight; - writeFunctionMetricsCsv(currentProgram, outFile, scope); - println("Wrote function metrics to: " + outFile.getAbsolutePath()); - } - - private void writeFunctionMetricsCsv(Program program, File outFile, AddressSetView scope) - throws IOException { - FunctionManager fm = program.getFunctionManager(); - Listing listing = program.getListing(); - CyclomaticComplexity complexityCalc = new CyclomaticComplexity(); - - try (BufferedWriter w = new BufferedWriter(new FileWriter(outFile))) { - // 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"); - - 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 - int instCount = 0; - for (Iterator insIt = listing.getInstructions(body, true); insIt.hasNext();) { - insIt.next(); - 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; - try { - SimpleBlockModel model = new SimpleBlockModel(program); - CodeBlockIterator blocks = model.getCodeBlocksContaining(body, monitor); - while (blocks.hasNext() && !monitor.isCancelled()) { - blocks.next(); - basicBlocks++; - } - } - catch (CancelledException ce) { - // respect cancellation - } - - 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"); - } - } - } - - 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 csv(String s) { - if (s == null) { - return "\"\""; - } - String v = s.replace("\"", "\"\""); - return "\"" + v + "\""; - } - - private static String csvOrEmpty(String s) { - return s == null ? "" : csv(s); - } -} - diff --git a/Ghidra/Features/Base/ghidra_scripts/ExportFunctionMetricsToJson.java b/Ghidra/Features/Base/ghidra_scripts/ExportFunctionMetricsToJson.java deleted file mode 100644 index d27e5d3abc0..00000000000 --- a/Ghidra/Features/Base/ghidra_scripts/ExportFunctionMetricsToJson.java +++ /dev/null @@ -1,265 +0,0 @@ -/* ### - * IP: GHIDRA - * REVIEWED: NO - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -// Exports basic per-function metrics to a JSON file for the current program. -//@category Functions - -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.util.Iterator; -import java.util.Set; - -import ghidra.app.script.GhidraScript; -import ghidra.program.model.address.AddressSetView; -import ghidra.program.model.listing.Function; -import ghidra.program.model.listing.FunctionIterator; -import ghidra.program.model.listing.FunctionManager; -import ghidra.program.model.listing.Instruction; -import ghidra.program.model.listing.Listing; -import ghidra.program.model.listing.Program; -import ghidra.program.util.CyclomaticComplexity; -import ghidra.util.Msg; -import ghidra.program.model.block.SimpleBlockModel; -import ghidra.program.model.block.CodeBlockIterator; -import ghidra.util.exception.CancelledException; -import ghidra.program.model.data.DataType; -import ghidra.program.model.symbol.Namespace; - -public class ExportFunctionMetricsToJson extends GhidraScript { - - @Override - protected void run() throws Exception { - if (currentProgram == null) { - printerr("No current program."); - return; - } - - File outFile = askFile("Choose output JSON file", "Save"); - if (outFile == null) { - printerr("No output file selected."); - return; - } - - AddressSetView scope = currentSelection != null ? currentSelection : currentHighlight; - writeFunctionMetricsJson(currentProgram, outFile, scope); - println("Wrote function metrics to: " + outFile.getAbsolutePath()); - } - - private void writeFunctionMetricsJson(Program program, File outFile, AddressSetView scope) - throws IOException { - FunctionManager fm = program.getFunctionManager(); - Listing listing = program.getListing(); - CyclomaticComplexity complexityCalc = new CyclomaticComplexity(); - - try (BufferedWriter w = new BufferedWriter(new FileWriter(outFile))) { - w.write("{\n"); - // basic program info - w.write(" \"program\": {\n"); - w.write(" \"name\": \"" + escape(program.getName()) + "\",\n"); - w.write(" \"language\": \"" + escape(program.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(); - - // instruction count (iterate rather than rely on size for accuracy) - int instCount = 0; - for (Iterator insIt = listing.getInstructions(body, true); insIt.hasNext();) { - insIt.next(); - 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 (Simple block model over function body) - int basicBlocks = 0; - try { - SimpleBlockModel model = new SimpleBlockModel(program); - CodeBlockIterator blocks = model.getCodeBlocksContaining(body, monitor); - while (blocks.hasNext() && !monitor.isCancelled()) { - blocks.next(); - basicBlocks++; - } - } - catch (CancelledException ce) { - // respect cancellation; leave count as is - } - - // 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"); - } - } - - 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); - } -} From 1add1c2ae678177abfda8f40cd295abb2327082d Mon Sep 17 00:00:00 2001 From: Abdul Samad Date: Mon, 1 Dec 2025 20:49:22 +0500 Subject: [PATCH 3/3] Address code review feedback: fix lambda variable issue, remove unused variable, propagate CancelledException, use for-each loops --- .../ExportFunctionInfoScript.java | 142 +++++++----------- 1 file changed, 58 insertions(+), 84 deletions(-) diff --git a/Ghidra/Features/Base/ghidra_scripts/ExportFunctionInfoScript.java b/Ghidra/Features/Base/ghidra_scripts/ExportFunctionInfoScript.java index 5ac83cfd43a..f90fd6ec9c8 100644 --- a/Ghidra/Features/Base/ghidra_scripts/ExportFunctionInfoScript.java +++ b/Ghidra/Features/Base/ghidra_scripts/ExportFunctionInfoScript.java @@ -23,7 +23,6 @@ import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -78,24 +77,6 @@ public void run() throws Exception { ExportFormat selectedFormat = askChoice("Choose Export Format", "Select the format for exporting function information:", formats, ExportFormat.JSON_SIMPLE); - // Determine file extension based on format - String extension; - switch (selectedFormat) { - case JSON_SIMPLE: - case JSON_METRICS: - extension = ".json"; - break; - case CSV_METRICS: - extension = ".csv"; - break; - case DOT_CALLGRAPH: - extension = ".dot"; - break; - default: - extension = ".txt"; - break; - } - File outputFile = askFile("Please Select Output File", "Choose"); if (outputFile == null) { printerr("No output file selected."); @@ -129,34 +110,35 @@ private void exportSimpleJson(File outputFile, AddressSetView scope) throws Exce Gson gson = new GsonBuilder().setPrettyPrinting().create(); try (JsonWriter jsonWriter = new JsonWriter(new FileWriter(outputFile))) { - jsonWriter.beginArray(); + jsonWriter.beginArray(); - Listing listing = currentProgram.getListing(); - FunctionIterator iter = listing.getFunctions(true); - while (iter.hasNext() && !monitor.isCancelled()) { - Function f = iter.next(); + 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(); + String name = f.getName(); + Address entry = f.getEntryPoint(); - JsonObject json = new JsonObject(); - json.addProperty(NAME, name); - json.addProperty(ENTRY, entry.toString()); + JsonObject json = new JsonObject(); + json.addProperty(NAME, name); + json.addProperty(ENTRY, entry.toString()); - gson.toJson(json, jsonWriter); - } + gson.toJson(json, jsonWriter); + } - jsonWriter.endArray(); + jsonWriter.endArray(); } } // ========== DETAILED JSON METRICS EXPORT ========== - private void exportMetricsJson(File outputFile, AddressSetView scope) throws IOException { + private void exportMetricsJson(File outputFile, AddressSetView scope) + throws IOException, CancelledException { FunctionManager fm = currentProgram.getFunctionManager(); Listing listing = currentProgram.getListing(); CyclomaticComplexity complexityCalc = new CyclomaticComplexity(); @@ -166,7 +148,8 @@ private void exportMetricsJson(File outputFile, AddressSetView scope) throws IOE // Basic program info w.write(" \"program\": {\n"); w.write(" \"name\": \"" + escape(currentProgram.getName()) + "\",\n"); - w.write(" \"language\": \"" + escape(currentProgram.getLanguageID().getIdAsString()) + "\"\n"); + w.write(" \"language\": \"" + + escape(currentProgram.getLanguageID().getIdAsString()) + "\"\n"); w.write(" },\n"); w.write(" \"functions\": [\n"); @@ -192,10 +175,9 @@ private void exportMetricsJson(File outputFile, AddressSetView scope) throws IOE String entry = f.getEntryPoint().toString(); long size = body.getNumAddresses(); - // Instruction count + // Instruction count - use for-each style loop int instCount = 0; - for (Iterator insIt = listing.getInstructions(body, true); insIt.hasNext();) { - insIt.next(); + for (Instruction inst : listing.getInstructions(body, true)) { instCount++; } @@ -214,16 +196,11 @@ private void exportMetricsJson(File outputFile, AddressSetView scope) throws IOE // Basic blocks int basicBlocks = 0; - try { - SimpleBlockModel model = new SimpleBlockModel(currentProgram); - CodeBlockIterator blocks = model.getCodeBlocksContaining(body, monitor); - while (blocks.hasNext() && !monitor.isCancelled()) { - blocks.next(); - basicBlocks++; - } - } - catch (CancelledException ce) { - // Respect cancellation; leave count as is + SimpleBlockModel model = new SimpleBlockModel(currentProgram); + CodeBlockIterator blocks = model.getCodeBlocksContaining(body, monitor); + while (blocks.hasNext() && !monitor.isCancelled()) { + blocks.next(); + basicBlocks++; } // Locals count @@ -272,7 +249,8 @@ private void exportMetricsJson(File outputFile, AddressSetView scope) throws IOE 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(" \"thunk_target\": " + + (thunkTarget == null ? "null" : ("\"" + escape(thunkTarget) + "\"")) + ",\n"); w.write(" \"variadic\": " + hasVarArgs + ",\n"); w.write(" \"inline\": " + isInline + ",\n"); w.write(" \"no_return\": " + noReturn + ",\n"); @@ -300,19 +278,19 @@ private void exportMetricsJson(File outputFile, AddressSetView scope) throws IOE } // ========== CSV METRICS EXPORT ========== - private void exportMetricsCsv(File outputFile, AddressSetView scope) throws IOException { + 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(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"); FunctionIterator it = fm.getFunctions(true); @@ -330,10 +308,9 @@ private void exportMetricsCsv(File outputFile, AddressSetView scope) throws IOEx String entry = f.getEntryPoint().toString(); - // Instruction count + // Instruction count - use for-each style loop int instCount = 0; - for (Iterator insIt = listing.getInstructions(body, true); insIt.hasNext();) { - insIt.next(); + for (Instruction inst : listing.getInstructions(body, true)) { instCount++; } @@ -352,16 +329,11 @@ private void exportMetricsCsv(File outputFile, AddressSetView scope) throws IOEx // Basic blocks int basicBlocks = 0; - try { - SimpleBlockModel model = new SimpleBlockModel(currentProgram); - CodeBlockIterator blocks = model.getCodeBlocksContaining(body, monitor); - while (blocks.hasNext() && !monitor.isCancelled()) { - blocks.next(); - basicBlocks++; - } - } - catch (CancelledException ce) { - // Respect cancellation + SimpleBlockModel model = new SimpleBlockModel(currentProgram); + CodeBlockIterator blocks = model.getCodeBlocksContaining(body, monitor); + while (blocks.hasNext() && !monitor.isCancelled()) { + blocks.next(); + basicBlocks++; } int localsCount = 0; @@ -395,18 +367,16 @@ private void exportMetricsCsv(File outputFile, AddressSetView scope) throws IOEx } } - 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(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"); } } @@ -419,7 +389,6 @@ private void exportCallGraphDot(File outputFile, AddressSetView scope) throws IO Map idByEntry = new HashMap<>(); Set edges = new HashSet<>(); - int nextId = 1; monitor.initialize(fm.getFunctionCount()); @@ -432,7 +401,9 @@ private void exportCallGraphDot(File outputFile, AddressSetView scope) throws IO monitor.incrementProgress(1); String fEntry = f.getEntryPoint().toString(); - idByEntry.computeIfAbsent(fEntry, k -> nextId++); + if (!idByEntry.containsKey(fEntry)) { + idByEntry.put(fEntry, idByEntry.size() + 1); + } for (Function callee : f.getCalledFunctions(monitor)) { if (scope != null && !scope.intersects(callee.getBody())) { @@ -440,7 +411,9 @@ private void exportCallGraphDot(File outputFile, AddressSetView scope) throws IO continue; } String cEntry = callee.getEntryPoint().toString(); - idByEntry.computeIfAbsent(cEntry, k -> nextId++); + if (!idByEntry.containsKey(cEntry)) { + idByEntry.put(cEntry, idByEntry.size() + 1); + } edges.add(fEntry + "->" + cEntry); } } @@ -453,7 +426,8 @@ private void exportCallGraphDot(File outputFile, AddressSetView scope) throws IO String entry = e.getKey(); int id = e.getValue(); // Label: function name + entry - Function f = fm.getFunctionAt(currentProgram.getAddressFactory().getAddress(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"); }