Files
PS2Recomp/ps2xRecomp/tools/ghidra/ExportPS2Functions.java
T
Ranieri 93e221feaa Feature/runtime ecosystem refactor (#107)
* feat: remove memory and pad from stub section

* feat: add support for resume entry targets in CodeGenerator (this allow jumps in address outside function)
feat: refactor entry point discovery one more try to reduce big generated file

* feat: optmizations for release build

* feat: remove unused  file

* feat: added some test cases for code gen

* feat: added log macro and remove win specific code

* feat: refactor runtime folder structure
feat: added reset sound driver RPC state and compatibility layout
feat: rename and added new test
feat: update RPC calls to use defined constants
feat: added more PSMC(16, 32)
feat: change cd read to try find the asset ignoring case sensitive
fix: fix some render problems
feat: add logs on pad
feat: added more RPC handles

* feat: added game override for code veronica

* feat: apply vita patch

* feat: flags to disable build

* feat: fix merges
feat: break a lot of tests

* feat: better throw error on empty cd path
feat: remove recompiler unusde function
feat: apply missing patch

* feat: gamedp is now part of lib
feat: missing file

* feat: small cleanup

* feat: missing vita changes

* feat: fix more merge

* feat: fix tests

* feat: last missing feature

* feat: added missing import

* feat: rename test local functions

* feat: init syscall on ps2 list

* feat: added DMA helpers

* feat: faster builds
feat: more implement for darkcloud

* feat: back missing file

* feat: added missing includes

* feat: remove test

* feat: missing include

* feat: read register funtion

* feat: build  fix

* feat: force  exit on detach thread
2026-04-04 23:09:56 -03:00

653 lines
24 KiB
Java

// Exports PS2Recomp TOML config (+ optional CSV) from Ghidra
// @category PS2Recomp
import ghidra.app.script.GhidraScript;
import ghidra.program.model.address.Address;
import ghidra.program.model.address.AddressSet;
import ghidra.program.model.address.AddressSetView;
import ghidra.program.model.listing.Instruction;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.FunctionIterator;
import ghidra.program.model.listing.FunctionManager;
import ghidra.program.model.listing.InstructionIterator;
import ghidra.program.model.mem.MemoryBlock;
import ghidra.program.model.symbol.Reference;
import ghidra.program.model.symbol.ReferenceIterator;
import ghidra.program.model.symbol.RefType;
import ghidra.program.model.symbol.Symbol;
import ghidra.program.model.symbol.SymbolIterator;
import ghidra.program.model.symbol.SymbolType;
import java.io.File;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
public class ExportPS2Functions extends GhidraScript {
private static final Set<String> SYSTEM_FUNCTION_NAMES = new HashSet<>(Arrays.asList(
"entry", "_start", "_init", "_fini",
"abort", "exit", "_exit",
"_profiler_start", "_profiler_stop",
"__main", "__do_global_ctors", "__do_global_dtors",
"_GLOBAL__sub_I_", "_GLOBAL__sub_D_",
"__ctor_list", "__dtor_list", "_edata", "_end",
"etext", "__exidx_start", "__exidx_end",
"_ftext", "__bss_start", "__bss_start__",
"__bss_end__", "__end__", "_stack", "_dso_handle"
));
private static final Set<String> DO_NOT_SKIP_OR_STUB = new HashSet<>(Arrays.asList(
"entry",
"_start",
"_init",
"topThread",
"cmd_sem_init"
));
private static final Set<String> KNOWN_LOCAL_HELPER_NAMES = new HashSet<>(Arrays.asList(
"memcpy2",
"_memcpy2"
));
private static final Set<String> PS2_API_PREFIXES = new HashSet<>(Arrays.asList(
"sce", "sif", "gs", "dma", "iop", "vif", "spu", "mc", "libc"
));
private static final Set<String> KNOWN_STDLIB_NAMES = new HashSet<>(Arrays.asList(
"printf", "sprintf", "snprintf", "fprintf", "vprintf", "vfprintf", "vsprintf", "vsnprintf",
"puts", "putchar", "getchar", "gets", "fgets", "fputs", "scanf", "fscanf", "sscanf",
"sprint", "sbprintf",
"malloc", "free", "calloc", "realloc", "aligned_alloc", "posix_memalign",
"memcpy", "memset", "memmove", "memcmp", "memchr", "bcopy", "bzero",
"strcpy", "strncpy", "strcat", "strncat", "strcmp", "strncmp", "strlen", "strstr",
"strchr", "strrchr", "strdup", "strtok", "strtok_r", "strerror",
"fopen", "fclose", "fread", "fwrite", "fseek", "ftell", "rewind", "fflush",
"fgetc", "feof", "ferror", "clearerr", "fileno", "tmpfile", "remove", "rename",
"open", "close", "read", "write", "lseek", "stat", "fstat",
"atoi", "atol", "atoll", "atof", "strtol", "strtoul", "strtoll", "strtoull", "strtod", "strtof",
"rand", "srand", "random", "srandom", "drand48", "sqrt", "pow", "exp", "log", "log10",
"sin", "cos", "tan", "asin", "acos", "atan", "atan2", "sinh", "cosh", "tanh",
"floor", "ceil", "fabs", "fmod", "frexp", "ldexp", "modf",
"time", "ctime", "clock", "difftime", "mktime", "localtime", "gmtime", "asctime", "strftime",
"gettimeofday", "nanosleep", "usleep",
"atexit", "system", "getpid", "fork", "waitpid",
"qsort", "bsearch", "abs", "div", "labs", "ldiv", "llabs", "lldiv",
"isalnum", "isalpha", "isdigit", "islower", "isupper", "isspace", "tolower", "toupper",
"setjmp", "longjmp", "getenv", "setenv", "unsetenv",
"perror", "fputc", "getc", "ungetc", "freopen", "setvbuf", "setbuf",
"strnlen", "strspn", "strcspn", "strcasecmp", "strncasecmp"
));
private static final Pattern C_LIB_PATTERN = Pattern.compile(
"^_*(mem|str|time|f?printf|f?scanf|malloc|free|calloc|realloc|atoi|itoa|rand|srand|abort|exit|atexit|getenv|system|bsearch|qsort|abs|labs|div|ldiv|mblen|mbtowc|wctomb|mbstowcs|wcstombs).*"
);
private static final Pattern KERNEL_RUNTIME_NAME_PATTERN = Pattern.compile(
"^(?:"
+ "(?:Create|Delete|Start|ExitDelete|Exit|Terminate|Suspend|Resume|Sleep|Wakeup|CancelWakeup|Change|Rotate|Release|Setup|Register|Query|Get|Set|Refer|Poll|Wait|Signal|Enable|Disable|Flush|Reset|Add|Init)"
+ "(?:Thread|Sema|EventFlag|Alarm|Intc|IntcHandler2|Dmac|DmacHandler2|OsdConfigParam|MemorySize|VSyncFlag|Heap|TLS|Status|Cache|Syscall|TLB|TLBEntry|GsCrt)"
+ "|EndOfHeap"
+ "|GsGetIMR|GsPutIMR"
+ "|Deci2Call"
+ "|Sif[A-Za-z0-9_]+"
+ "|i(?:SignalSema|PollSema|ReferSemaStatus|SetEventFlag|ClearEventFlag|PollEventFlag|ReferEventFlagStatus|WakeupThread|CancelWakeupThread|ReleaseWaitThread|SetAlarm|CancelAlarm|FlushCache|sceSifSetDma|sceSifSetDChain)"
+ ")$"
);
private static final class FunctionRecord {
String name;
long start;
long endExclusive;
long size;
boolean syntheticEntry = false;
}
private enum ClassificationKind {
STUB,
SKIP,
NONE
}
private static final class ClassificationResult {
final ClassificationKind kind;
final String name;
ClassificationResult(ClassificationKind kind, String name) {
this.kind = kind;
this.name = name;
}
}
private static String hex(long value) {
return String.format("0x%08X", value & 0xFFFFFFFFL);
}
private static String tomlString(String value) {
if (value == null) {
return "\"\"";
}
return "\"" + value.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
}
// Ghidra on Windows can return executable paths like "/D:/path/to/elf".
// TOML expects "D:/path/to/elf" for this field.
private static String normalizeWindowsDrivePath(String value) {
if (value == null || value.length() < 4) {
return value;
}
if (value.charAt(0) == '/' &&
Character.isLetter(value.charAt(1)) &&
value.charAt(2) == ':' &&
(value.charAt(3) == '/' || value.charAt(3) == '\\')) {
return value.substring(1);
}
return value;
}
private static String normalizeOptionalLeadingUnderscore(String value) {
if (value == null || value.isEmpty()) {
return "";
}
return value.startsWith("_") && value.length() > 1 ? value.substring(1) : value;
}
private static boolean hasReliableSymbolName(String name) {
if (name == null || name.isEmpty()) {
return false;
}
if (name.startsWith("sub_") || name.startsWith("FUN_") || name.startsWith("func_") ||
name.startsWith("entry_") || name.startsWith("function_") || name.startsWith("LAB_")) {
return false;
}
boolean hasAlpha = false;
boolean allHexOrPrefix = true;
for (int i = 0; i < name.length(); ++i) {
char c = name.charAt(i);
if (Character.isAlphabetic(c)) {
hasAlpha = true;
}
if (!(Character.digit(c, 16) >= 0 || c == 'x' || c == 'X' || c == '_')) {
allHexOrPrefix = false;
}
}
if (!hasAlpha) {
return false;
}
if ((name.startsWith("0x") || name.startsWith("0X")) && allHexOrPrefix) {
return false;
}
return true;
}
private static boolean hasPs2ApiPrefix(String name) {
if (name == null || name.isEmpty()) {
return false;
}
String base = normalizeOptionalLeadingUnderscore(name).toLowerCase();
for (String prefix : PS2_API_PREFIXES) {
if (base.startsWith(prefix)) {
return true;
}
}
return false;
}
private static boolean isSystemSymbolNameForHeuristics(String name) {
if (!hasReliableSymbolName(name)) {
return false;
}
return SYSTEM_FUNCTION_NAMES.contains(name) || name.startsWith("__") || name.startsWith(".");
}
private static boolean matchesWithOptionalLeadingUnderscoreAlias(String candidate, Set<String> names) {
if (candidate == null || candidate.isEmpty() || names == null || names.isEmpty()) {
return false;
}
if (names.contains(candidate)) {
return true;
}
String normalized = normalizeOptionalLeadingUnderscore(candidate);
if (!normalized.equals(candidate) && names.contains(normalized)) {
return true;
}
if (!candidate.startsWith("_") && names.contains("_" + candidate)) {
return true;
}
return false;
}
private static boolean isLibraryFunctionName(String name) {
if (name == null || name.isEmpty() || !hasReliableSymbolName(name)) {
return false;
}
if (KNOWN_LOCAL_HELPER_NAMES.contains(name)) {
return false;
}
String normalized = normalizeOptionalLeadingUnderscore(name);
if (KNOWN_LOCAL_HELPER_NAMES.contains(normalized)) {
return false;
}
if (KERNEL_RUNTIME_NAME_PATTERN.matcher(normalized).matches()) {
return true;
}
if (matchesWithOptionalLeadingUnderscoreAlias(normalized, KNOWN_STDLIB_NAMES)) {
return true;
}
if (hasPs2ApiPrefix(normalized)) {
return true;
}
return C_LIB_PATTERN.matcher(normalized).matches();
}
private static ClassificationResult classifyFunction(Function function) {
if (function == null) {
return new ClassificationResult(ClassificationKind.NONE, "");
}
String name = function.getName();
if (name == null || name.isEmpty() || DO_NOT_SKIP_OR_STUB.contains(name)) {
return new ClassificationResult(ClassificationKind.NONE, name == null ? "" : name);
}
if (function.isThunk()) {
if (isLibraryFunctionName(name)) {
return new ClassificationResult(ClassificationKind.STUB, name);
}
Function target = function.getThunkedFunction(true);
if (target != null) {
String targetName = target.getName();
if (isLibraryFunctionName(targetName)) {
return new ClassificationResult(ClassificationKind.STUB, targetName);
}
}
if (isSystemSymbolNameForHeuristics(name)) {
return new ClassificationResult(ClassificationKind.SKIP, name);
}
return new ClassificationResult(ClassificationKind.NONE, name);
}
if (isLibraryFunctionName(name)) {
return new ClassificationResult(ClassificationKind.STUB, name);
}
if (isSystemSymbolNameForHeuristics(name)) {
return new ClassificationResult(ClassificationKind.SKIP, name);
}
return new ClassificationResult(ClassificationKind.NONE, name);
}
private static String makeSelector(String name, long start, boolean includeAddress) {
if (includeAddress) {
return name + "@" + hex(start);
}
return name;
}
private static List<String> collectFunctionSelectors(
Set<String> names,
List<FunctionRecord> records,
boolean includeAddress
) {
List<FunctionRecord> ordered = new ArrayList<>(records);
ordered.sort(Comparator.comparingLong(r -> r.start));
List<String> selectors = new ArrayList<>();
Set<String> seenSelectors = new LinkedHashSet<>();
Set<String> coveredNames = new HashSet<>();
for (FunctionRecord record : ordered) {
if (record.name == null || !names.contains(record.name)) {
continue;
}
coveredNames.add(record.name);
String selector = makeSelector(record.name, record.start, includeAddress);
if (seenSelectors.add(selector)) {
selectors.add(selector);
}
}
if (includeAddress) {
List<String> unresolved = new ArrayList<>();
for (String name : names) {
if (!coveredNames.contains(name)) {
unresolved.add(name);
}
}
Collections.sort(unresolved);
for (String name : unresolved) {
System.out.println("Warning: unresolved selector name without address, omitting from TOML: " + name);
}
} else {
Collections.sort(selectors);
}
return selectors;
}
private boolean isExecutableAddress(Address address) {
if (address == null) {
return false;
}
MemoryBlock block = currentProgram.getMemory().getBlock(address);
return block != null && block.isExecute();
}
private boolean hasCallableLabelReference(Address address) {
if (address == null) {
return false;
}
ReferenceIterator refs = currentProgram.getReferenceManager().getReferencesTo(address);
while (refs.hasNext()) {
Reference ref = refs.next();
if (ref == null) {
continue;
}
RefType type = ref.getReferenceType();
if (type != null && type.isCall()) {
return true;
}
Address from = ref.getFromAddress();
if (from == null) {
continue;
}
MemoryBlock fromBlock = currentProgram.getMemory().getBlock(from);
if (fromBlock == null || !fromBlock.isExecute()) {
continue; // lets ignore DATA/non-code refs
}
}
return false;
}
private static String makeAnonymousEntryName(long start) {
return String.format("entry_%08x", start & 0xFFFFFFFFL);
}
private List<FunctionRecord> collectExecutableLabelRecords(List<FunctionRecord> functionRecords) {
List<FunctionRecord> labelRecords = new ArrayList<>();
Set<Long> existingStarts = new HashSet<>();
for (FunctionRecord record : functionRecords) {
existingStarts.add(record.start);
}
SymbolIterator symbols = currentProgram.getSymbolTable().getSymbolIterator(true);
while (symbols.hasNext() && !monitor.isCancelled()) {
Symbol symbol = symbols.next();
if (symbol == null || !symbol.isPrimary()) {
continue;
}
if (symbol.getSymbolType() == SymbolType.FUNCTION) {
continue;
}
Address address = symbol.getAddress();
if (!isExecutableAddress(address)) {
continue;
}
long start = address.getOffset();
if (existingStarts.contains(start)) {
continue;
}
Instruction instruction = currentProgram.getListing().getInstructionAt(address);
if (instruction == null) {
continue;
}
if (!hasCallableLabelReference(address)) {
continue;
}
FunctionRecord record = new FunctionRecord();
record.name = symbol.getName();
record.start = start;
record.syntheticEntry = true;
labelRecords.add(record);
existingStarts.add(start);
}
AddressSet executableAddresses = new AddressSet();
for (MemoryBlock block : currentProgram.getMemory().getBlocks()) {
if (block != null && block.isExecute()) {
executableAddresses.addRange(block.getStart(), block.getEnd());
}
}
InstructionIterator instructions = currentProgram.getListing().getInstructions(executableAddresses, true);
while (instructions.hasNext() && !monitor.isCancelled()) {
Instruction instruction = instructions.next();
if (instruction == null) {
continue;
}
Address address = instruction.getAddress();
long start = address.getOffset();
if (existingStarts.contains(start)) {
continue;
}
if (!hasCallableLabelReference(address)) {
continue;
}
FunctionRecord record = new FunctionRecord();
record.name = makeAnonymousEntryName(start);
record.start = start;
record.syntheticEntry = true;
labelRecords.add(record);
existingStarts.add(start);
}
if (labelRecords.isEmpty()) {
return labelRecords;
}
List<Long> boundaries = new ArrayList<>();
for (FunctionRecord record : functionRecords) {
boundaries.add(record.start);
}
for (FunctionRecord record : labelRecords) {
boundaries.add(record.start);
}
Collections.sort(boundaries);
functionRecords.sort(Comparator.comparingLong(r -> r.start));
for (FunctionRecord record : labelRecords) {
long endExclusive = 0L;
for (FunctionRecord functionRecord : functionRecords) {
if (record.start > functionRecord.start && record.start < functionRecord.endExclusive) {
endExclusive = functionRecord.endExclusive;
break;
}
}
if (endExclusive == 0L) {
Address startAddress = currentProgram.getAddressFactory().getDefaultAddressSpace().getAddress(record.start);
MemoryBlock block = currentProgram.getMemory().getBlock(startAddress);
if (block != null) {
endExclusive = block.getEnd().getOffset() + 1L;
}
}
for (Long boundary : boundaries) {
if (boundary > record.start && (endExclusive == 0L || boundary < endExclusive)) {
endExclusive = boundary;
break;
}
}
if (endExclusive <= record.start) {
endExclusive = record.start + 4L;
}
record.endExclusive = endExclusive;
record.size = record.endExclusive - record.start;
}
labelRecords.sort(Comparator.comparingLong(r -> r.start));
return labelRecords;
}
@Override
public void run() throws Exception {
File tomlFile = askFile("Choose output TOML config file", "Save");
if (tomlFile == null) {
return;
}
boolean exportCsv = askYesNo("Export CSV", "Also export compatibility CSV function map?");
File csvFile = null;
csvFile = askFile("Choose output CSV file", "Save");
if (csvFile == null) {
return;
}
FunctionManager fm = currentProgram.getFunctionManager();
FunctionIterator it = fm.getFunctions(true);
List<FunctionRecord> functionRecords = new ArrayList<>();
Set<String> stubNames = new LinkedHashSet<>();
Set<String> skipNames = new LinkedHashSet<>();
int uncategorizedCount = 0;
while (it.hasNext() && !monitor.isCancelled()) {
Function func = it.next();
AddressSetView body = func.getBody();
if (body == null || body.getNumAddresses() == 0) {
continue;
}
FunctionRecord record = new FunctionRecord();
record.name = func.getName();
record.start = func.getEntryPoint().getOffset();
record.endExclusive = body.getMaxAddress().getOffset() + 1L;
record.size = body.getNumAddresses();
functionRecords.add(record);
ClassificationResult classification = classifyFunction(func);
if (classification.kind == ClassificationKind.STUB) {
stubNames.add(classification.name);
} else if (classification.kind == ClassificationKind.SKIP) {
skipNames.add(classification.name);
} else {
uncategorizedCount++;
}
}
final int functionCount = functionRecords.size();
List<FunctionRecord> labelRecords = collectExecutableLabelRecords(functionRecords);
List<FunctionRecord> exportRecords = new ArrayList<>(functionRecords);
exportRecords.addAll(labelRecords);
exportRecords.sort(Comparator.comparingLong(r -> r.start));
List<String> stubSelectors = collectFunctionSelectors(stubNames, exportRecords, true);
List<String> skipSelectors = collectFunctionSelectors(skipNames, exportRecords, true);
try (PrintWriter writer = new PrintWriter(csvFile)) {
writer.println("Name,Start,End,Size");
for (FunctionRecord record : exportRecords) {
writer.printf("%s,0x%08X,0x%08X,%d%n",
record.name,
record.start,
record.endExclusive,
record.size
);
}
}
String programPath = currentProgram.getExecutablePath();
if (programPath == null) {
programPath = "";
}
programPath = normalizeWindowsDrivePath(programPath);
File outputDir = tomlFile.getParentFile() == null ? new File("output") : new File(tomlFile.getParentFile(), "output");
String ghidraCsvPath = csvFile.getAbsolutePath();
try (PrintWriter writer = new PrintWriter(tomlFile)) {
writer.println("# Auto-generated by ExportPS2Functions.java");
writer.println("#");
writer.println("# Classification policy (aligned with analyzer intent):");
writer.println("# - library/runtime names -> [general].stubs");
writer.println("# - system names -> [general].skip");
writer.println("# - others are left for recompilation");
writer.println();
writer.println("[general]");
writer.println("input = " + tomlString(programPath));
writer.println("output = " + tomlString(outputDir.getAbsolutePath()));
writer.println("ghidra_output = " + tomlString(ghidraCsvPath));
writer.println("single_file_output = false");
writer.println("patch_syscalls = false");
writer.println("patch_cop0 = true");
writer.println("patch_cache = true");
writer.println("stubs = [");
for (String selector : stubSelectors) {
writer.println(" " + tomlString(selector) + ",");
}
writer.println("]");
writer.println("skip = [");
for (String selector : skipSelectors) {
writer.println(" " + tomlString(selector) + ",");
}
writer.println("]");
writer.println();
writer.println("[ghidra_export]");
writer.println("function_count = " + functionCount);
writer.println("code_label_count = " + labelRecords.size());
writer.println("csv_record_count = " + exportRecords.size());
writer.println("stub_count = " + stubSelectors.size());
writer.println("skip_count = " + skipSelectors.size());
writer.println("uncategorized_count = " + uncategorizedCount);
writer.println("runtime_call_name_count = 0");
writer.println("runtime_call_source = \"regex_only\"");
}
println(String.format("Exported %d functions and %d executable labels to %s", functionCount, labelRecords.size(), csvFile.getAbsolutePath()));
println("Using regex-only runtime/library classification (no ps2_call_list.h).");
println(String.format("Exported TOML config to %s", tomlFile.getAbsolutePath()));
}
}