mirror of
https://github.com/ran-j/PS2Recomp.git
synced 2026-09-26 08:51:05 -04:00
93e221feaa
* 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
653 lines
24 KiB
Java
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()));
|
|
}
|
|
}
|