Merge remote-tracking branch 'origin/GP-7031_ryanmkurtz_PR-9311_itxsamad1_feature_export-function-info-v2'

This commit is contained in:
Ryan Kurtz
2026-07-01 05:48:43 -04:00
2 changed files with 415 additions and 31 deletions

View File

@@ -4,60 +4,438 @@
* 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.
*/
// 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.File;
import java.io.FileWriter;
import java.io.*;
import java.util.*;
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;
import util.CollectionUtils;
public class ExportFunctionInfoScript extends GhidraScript {
private static final String NAME = "name";
private static final String ENTRY = "entry";
@Override
public void run() throws Exception {
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)");
Gson gson = new GsonBuilder().setPrettyPrinting().create();
private final String displayName;
File outputFile = askFile("Please Select Output File", "Choose");
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();
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);
ExportFormat(String displayName) {
this.displayName = displayName;
}
jsonWriter.endArray();
jsonWriter.close();
@Override
public String toString() {
return displayName;
}
}
println("Wrote functions to " + outputFile);
@Override
public void run() throws Exception {
if (currentProgram == null) {
printerr("No current program.");
return;
}
List<ExportFormat> 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");
if (outputFile == null) {
printerr("No output file selected.");
return;
}
AddressSetView scope = currentSelection != null ? currentSelection : currentHighlight;
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());
}
private void exportSimpleJson(File outputFile, AddressSetView scope)
throws IOException, CancelledException {
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.checkCancelled();
Function f = iter.next();
if (scope != null && !scope.intersects(f.getBody())) {
continue;
}
JsonObject json = new JsonObject();
json.addProperty(NAME, f.getName());
json.addProperty(ENTRY, f.getEntryPoint().toString());
gson.toJson(json, jsonWriter);
}
jsonWriter.endArray();
}
}
private void exportMetricsJson(File outputFile, AddressSetView scope)
throws IOException, CancelledException {
Gson gson = new GsonBuilder().setPrettyPrinting().create();
FunctionManager fm = currentProgram.getFunctionManager();
CyclomaticComplexity complexityCalc = new CyclomaticComplexity();
JsonObject root = new JsonObject();
JsonObject programInfo = new JsonObject();
programInfo.addProperty("name", currentProgram.getName());
programInfo.addProperty("language", currentProgram.getLanguageID().getIdAsString());
root.add("program", programInfo);
JsonArray functions = new JsonArray();
FunctionIterator it = fm.getFunctions(true);
monitor.initialize(fm.getFunctionCount());
int exported = 0;
while (it.hasNext()) {
monitor.checkCancelled();
Function f = it.next();
AddressSetView body = f.getBody();
if (scope != null && !scope.intersects(body)) {
continue;
}
monitor.setMessage("Exporting metrics: " + f.getName());
monitor.incrementProgress(1);
functions.add(buildFunctionMetricsJson(f, body, complexityCalc));
exported++;
}
root.add("functions", functions);
JsonObject summary = new JsonObject();
summary.addProperty("exported_functions", exported);
summary.addProperty("total_functions", fm.getFunctionCount());
summary.addProperty("selection_applied", scope != null);
root.add("summary", summary);
try (FileWriter w = new FileWriter(outputFile)) {
gson.toJson(root, w);
}
}
private JsonObject buildFunctionMetricsJson(Function f, AddressSetView body,
CyclomaticComplexity complexityCalc) throws CancelledException {
Listing listing = currentProgram.getListing();
int instCount = (int) CollectionUtils.asStream(listing.getInstructions(body, true))
.count();
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 localsCount = 0;
if (!f.isExternal()) {
localsCount = f.getLocalVariables().length;
}
String thunkTarget = null;
if (f.isThunk()) {
Function tf = f.getThunkedFunction(true);
if (tf != null) {
thunkTarget = tf.getEntryPoint().toString();
}
}
Namespace ns = f.getParentNamespace();
DataType retType = f.getReturnType();
JsonObject json = new JsonObject();
json.addProperty("name", f.getName());
json.addProperty("entry", f.getEntryPoint().toString());
json.addProperty("size_bytes", body.getNumAddresses());
json.addProperty("address_ranges", body.getNumAddressRanges());
json.addProperty("instruction_count", instCount);
json.addProperty("cyclomatic_complexity", cplx);
json.addProperty("parameters", f.getParameterCount());
json.addProperty("locals", localsCount);
json.addProperty("basic_blocks", getBasicBlockCount(body));
json.addProperty("external", f.isExternal());
json.addProperty("thunk", f.isThunk());
if (thunkTarget == null) {
json.add("thunk_target", JsonNull.INSTANCE);
}
else {
json.addProperty("thunk_target", thunkTarget);
}
json.addProperty("variadic", f.hasVarArgs());
json.addProperty("inline", f.isInline());
json.addProperty("no_return", f.hasNoReturn());
json.addProperty("custom_storage", f.hasCustomVariableStorage());
json.addProperty("stack_purge_size", f.getStackPurgeSize());
json.addProperty("calling_convention", safeString(f.getCallingConventionName()));
json.addProperty("namespace", ns != null ? ns.getName(true) : "");
json.addProperty("return_type",
retType != null ? retType.getDisplayName() : "void");
json.addProperty("prototype", f.getPrototypeString(true, false));
json.addProperty("prototype_with_cc", f.getPrototypeString(true, true));
json.addProperty("callers", callers);
json.addProperty("callees", callees);
return json;
}
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))) {
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.checkCancelled();
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();
int instCount = (int) CollectionUtils.asStream(listing.getInstructions(body, true))
.count();
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 localsCount = 0;
if (!f.isExternal()) {
localsCount = f.getLocalVariables().length;
}
String thunkTarget = null;
if (f.isThunk()) {
Function tf = f.getThunkedFunction(true);
if (tf != null) {
thunkTarget = tf.getEntryPoint().toString();
}
}
Namespace ns = f.getParentNamespace();
String namespace = ns != null ? ns.getName(true) : "";
DataType retType = f.getReturnType();
String returnType = retType != null ? retType.getDisplayName() : "void";
String callConv = safeString(f.getCallingConventionName());
String proto = f.getPrototypeString(true, false);
String protoWithCc = f.getPrototypeString(true, true);
w.write(String.join(",", csv(f.getName()), csv(entry),
Integer.toString(body.getNumAddressRanges()), Integer.toString(instCount),
Integer.toString(cplx), Integer.toString(f.getParameterCount()),
Integer.toString(localsCount), Integer.toString(getBasicBlockCount(body)),
Boolean.toString(f.isExternal()), Boolean.toString(f.isThunk()),
csvOrEmpty(thunkTarget), Boolean.toString(f.hasVarArgs()),
Boolean.toString(f.isInline()), Boolean.toString(f.hasNoReturn()),
Boolean.toString(f.hasCustomVariableStorage()),
Integer.toString(f.getStackPurgeSize()), csv(callConv), csv(namespace),
csv(returnType), csv(proto), csv(protoWithCc),
Integer.toString(callers), Integer.toString(callees)));
w.write("\n");
}
}
}
private void exportCallGraphDot(File outputFile, AddressSetView scope)
throws IOException, CancelledException {
FunctionManager fm = currentProgram.getFunctionManager();
FunctionIterator it = fm.getFunctions(true);
Map<String, Integer> idByEntry = new HashMap<>();
Set<String> edges = new HashSet<>();
monitor.initialize(fm.getFunctionCount());
while (it.hasNext()) {
monitor.checkCancelled();
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();
assignFunctionId(idByEntry, fEntry);
for (Function callee : f.getCalledFunctions(monitor)) {
monitor.checkCancelled();
if (scope != null && !scope.intersects(callee.getBody())) {
continue;
}
String cEntry = callee.getEntryPoint().toString();
assignFunctionId(idByEntry, cEntry);
edges.add(fEntry + "->" + cEntry);
}
}
try (BufferedWriter w = new BufferedWriter(new FileWriter(outputFile))) {
w.write("digraph \"" + escapeDot(currentProgram.getName()) + "\" {\n");
w.write(" node [shape=box, fontsize=10];\n");
for (Map.Entry<String, Integer> e : idByEntry.entrySet()) {
String entry = e.getKey();
int id = e.getValue();
Function f =
fm.getFunctionAt(currentProgram.getAddressFactory().getAddress(entry));
String name = f != null ? f.getName() : entry;
String label = name + "\n" + entry;
w.write(" n" + id + " [label=\"" + escapeDot(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 int getBasicBlockCount(AddressSetView body) throws CancelledException {
SimpleBlockModel model = new SimpleBlockModel(currentProgram);
CodeBlockIterator blocks = model.getCodeBlocksContaining(body, monitor);
int basicBlocks = 0;
while (blocks.hasNext()) {
monitor.checkCancelled();
blocks.next();
basicBlocks++;
}
return basicBlocks;
}
private static void assignFunctionId(Map<String, Integer> idByEntry, String entry) {
if (!idByEntry.containsKey(entry)) {
idByEntry.put(entry, idByEntry.size() + 1);
}
}
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 escapeDot(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 '\n':
b.append("\\n");
break;
default:
b.append(c);
break;
}
}
return b.toString();
}
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);
}
}

View File

@@ -4,9 +4,9 @@
* 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.
@@ -15,6 +15,7 @@
*/
package ghidra.app.plugin.core.script;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
@@ -22,6 +23,8 @@ import java.io.*;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JDialog;
import org.junit.Before;
import org.junit.Test;
@@ -77,6 +80,9 @@ public class ExportFunctionInfoScriptTest extends AbstractGhidraHeadedIntegratio
ScriptTaskListener listener = env.runScript(script);
JDialog dialog = waitForJDialog("Choose Export Format");
pressButtonByText(dialog, "OK");
chooseFile(outputFile);
waitForScriptCompletion(listener, 20000);