// Export startup-oriented function metadata from the current Ghidra program. //@category RT3 import ghidra.app.script.GhidraScript; import ghidra.program.model.address.Address; import ghidra.program.model.listing.Function; import ghidra.program.model.listing.Instruction; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; public class ExportStartupFunctions extends GhidraScript { private static class QueueEntry { final Function function; final int depth; final String parentAddress; final String callSite; QueueEntry(Function function, int depth, String parentAddress, String callSite) { this.function = function; this.depth = depth; this.parentAddress = parentAddress; this.callSite = callSite; } } private static class RootSpec { final String name; final String address; RootSpec(String name, String address) { this.name = name; this.address = address; } } private static class FunctionRow { String rootName; String rootAddress; String address; int depth; String name; long sizeBytes; String callingConvention; String signatureSource; String signature; String parentAddress; String callSite; } private static class EdgeRow { String rootName; String rootAddress; String parentAddress; String childAddress; String callSite; } private static class CallTarget { final String callSite; final Function callee; CallTarget(String callSite, Function callee) { this.callSite = callSite; this.callee = callee; } } @Override protected void run() throws Exception { String[] args = getScriptArgs(); if (args.length < 1) { throw new RuntimeException( "usage: ExportStartupFunctions.java [max-depth] [root-name:address ...]"); } String outputDir = args[0]; int maxDepth = args.length > 1 ? Integer.parseInt(args[1]) : 2; List roots = parseRoots(args); File output = new File(outputDir); output.mkdirs(); List rows = new ArrayList(); List edges = new ArrayList(); for (RootSpec root : roots) { collectRoot(rows, edges, root, maxDepth); } Collections.sort(rows, Comparator .comparing((FunctionRow row) -> row.rootName) .thenComparing(row -> row.rootAddress) .thenComparingInt(row -> row.depth) .thenComparing(row -> row.address)); Collections.sort(edges, Comparator .comparing((EdgeRow row) -> row.rootName) .thenComparing(row -> row.rootAddress) .thenComparing(row -> row.parentAddress) .thenComparing(row -> row.callSite) .thenComparing(row -> row.childAddress)); writeCsv(new File(output, "ghidra-startup-functions.csv"), rows); writeMarkdown(new File(output, "startup-call-chain.md"), rows, edges, roots, maxDepth); } private void collectRoot( List rows, List edges, RootSpec root, int maxDepth) { Function rootFunction = getInternalFunction(toAddr(root.address)); if (rootFunction == null) { throw new IllegalArgumentException("no function found at " + root.address); } ArrayDeque queue = new ArrayDeque(); Set seen = new HashSet(); queue.add(new QueueEntry(rootFunction, 0, "", "")); while (!queue.isEmpty()) { QueueEntry item = queue.removeFirst(); Function function = item.function; String address = asAddress(function.getEntryPoint()); if (seen.contains(address)) { continue; } seen.add(address); FunctionRow row = new FunctionRow(); row.rootName = root.name; row.rootAddress = root.address; row.address = address; row.depth = item.depth; row.name = function.getName(); row.sizeBytes = function.getBody().getNumAddresses(); row.callingConvention = safeString(function.getCallingConventionName(), "unknown"); row.signatureSource = safeSignatureSource(function); row.signature = safeSignature(function); row.parentAddress = item.parentAddress; row.callSite = item.callSite; rows.add(row); if (!item.parentAddress.isEmpty()) { EdgeRow edge = new EdgeRow(); edge.rootName = root.name; edge.rootAddress = root.address; edge.parentAddress = item.parentAddress; edge.childAddress = address; edge.callSite = item.callSite; edges.add(edge); } if (item.depth >= maxDepth) { continue; } for (CallTarget callTarget : iterInternalCalls(function)) { queue.add(new QueueEntry( callTarget.callee, item.depth + 1, address, callTarget.callSite)); } } } private List parseRoots(String[] args) { List roots = new ArrayList(); if (args.length <= 2) { roots.add(new RootSpec("entry", "0x005a313b")); return roots; } for (int index = 2; index < args.length; index++) { String token = args[index]; int separator = token.indexOf(':'); if (separator <= 0 || separator == token.length() - 1) { throw new IllegalArgumentException("root spec must be name:address: " + token); } roots.add(new RootSpec(token.substring(0, separator), token.substring(separator + 1))); } return roots; } private Function getInternalFunction(Address address) { Function function = getFunctionAt(address); if (function == null) { function = getFunctionContaining(address); } if (function != null && function.isExternal()) { return null; } return function; } private List iterInternalCalls(Function function) { List result = new ArrayList(); Set seenCalls = new HashSet(); var instructions = currentProgram.getListing().getInstructions(function.getBody(), true); while (instructions.hasNext() && !monitor.isCancelled()) { Instruction instruction = instructions.next(); if (!instruction.getFlowType().isCall()) { continue; } Address[] flows = instruction.getFlows(); for (Address flow : flows) { Function callee = getInternalFunction(flow); if (callee == null) { continue; } String key = asAddress(instruction.getAddress()) + "->" + asAddress(callee.getEntryPoint()); if (seenCalls.contains(key)) { continue; } seenCalls.add(key); result.add(new CallTarget(asAddress(instruction.getAddress()), callee)); } } return result; } private void writeCsv(File path, List rows) throws IOException { BufferedWriter writer = new BufferedWriter(new FileWriter(path)); try { writer.write( "root_name,root_address,address,depth,name,size_bytes,calling_convention,signature_source,signature,parent_address,call_site\n"); for (FunctionRow row : rows) { writer.write(csv(row.rootName)); writer.write(","); writer.write(csv(row.rootAddress)); writer.write(","); writer.write(csv(row.address)); writer.write(","); writer.write(csv(Integer.toString(row.depth))); writer.write(","); writer.write(csv(row.name)); writer.write(","); writer.write(csv(Long.toString(row.sizeBytes))); writer.write(","); writer.write(csv(row.callingConvention)); writer.write(","); writer.write(csv(row.signatureSource)); writer.write(","); writer.write(csv(row.signature)); writer.write(","); writer.write(csv(row.parentAddress)); writer.write(","); writer.write(csv(row.callSite)); writer.write("\n"); } } finally { writer.close(); } } private void writeMarkdown( File path, List rows, List edges, List roots, int maxDepth) throws IOException { Map byAddress = new HashMap(); Map> children = new HashMap>(); for (FunctionRow row : rows) { byAddress.put(keyedAddress(row.rootName, row.rootAddress, row.address), row); } for (EdgeRow edge : edges) { children.computeIfAbsent( keyedAddress(edge.rootName, edge.rootAddress, edge.parentAddress), ignored -> new ArrayList()).add(edge); } for (List childList : children.values()) { Collections.sort(childList, Comparator.comparing(edge -> edge.childAddress)); } BufferedWriter writer = new BufferedWriter(new FileWriter(path)); try { writer.write("# Startup Call Chain\n\n"); writer.write("- Depth limit: `" + maxDepth + "`\n"); writer.write("- Internal call targets only; imported APIs are intentionally excluded.\n\n"); for (RootSpec root : roots) { FunctionRow row = byAddress.get(keyedAddress(root.name, root.address, root.address)); if (row == null) { continue; } writer.write("## `" + root.name + "` root `" + row.address + "` `" + row.name + "`\n\n"); writer.write("- `" + row.address + "` `" + row.name + "`\n"); HashSet visited = new HashSet(); visited.add(keyedAddress(root.name, root.address, row.address)); emitChildren(writer, root.name, root.address, row.address, 1, children, byAddress, visited); writer.write("\n"); } } finally { writer.close(); } } private void emitChildren( BufferedWriter writer, String rootName, String rootAddress, String address, int indent, Map> children, Map byAddress, Set visited) throws IOException { List childRows = children.get(keyedAddress(rootName, rootAddress, address)); if (childRows == null) { return; } for (EdgeRow edge : childRows) { String childKey = keyedAddress(edge.rootName, edge.rootAddress, edge.childAddress); FunctionRow child = byAddress.get(childKey); if (child == null || visited.contains(childKey)) { continue; } visited.add(childKey); writer.write(" ".repeat(indent)); writer.write("- `" + child.address + "` `" + child.name + "` via `" + edge.callSite + "`\n"); emitChildren( writer, edge.rootName, edge.rootAddress, child.address, indent + 1, children, byAddress, visited); } } private String keyedAddress(String rootName, String rootAddress, String address) { return rootName + "|" + rootAddress + "|" + address; } private String safeSignature(Function function) { try { return function.getPrototypeString(true, false); } catch (Exception ignored) { return ""; } } private String safeSignatureSource(Function function) { try { return function.getSignatureSource().toString(); } catch (Exception ignored) { return ""; } } private String asAddress(Address address) { return "0x" + address.toString(); } private String safeString(String value, String fallback) { if (value == null || value.isEmpty()) { return fallback; } return value; } private String csv(String value) { if (value == null) { value = ""; } return "\"" + value.replace("\"", "\"\"") + "\""; } }