From 48f3465613a3f7e03e7b623f76ee82bba37fdae8 Mon Sep 17 00:00:00 2001 From: Francisco Javier Trujillo Mata Date: Sat, 31 Oct 2026 16:06:34 +0100 Subject: [PATCH] ps2client: Add native C EE/IOP exception symbolizer Annotate PS2Link exception logs with addr2line results using a shell-free C subprocess runner. Include C parser tests, documentation, and make all/install integration for macOS/Linux. No Python runtime or source is required. --- Makefile | 23 +++++- README.md | 19 +++++ tests/test_symbolize.c | 51 ++++++++++++ tools/ps2symbolize.c | 171 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 261 insertions(+), 3 deletions(-) create mode 100644 tests/test_symbolize.c create mode 100644 tools/ps2symbolize.c diff --git a/Makefile b/Makefile index 24a8726..0e8a08f 100644 --- a/Makefile +++ b/Makefile @@ -29,19 +29,28 @@ clean: $(MAKE) -f Makefile.mingw32 clean else - all: bin/fsclient bin/ps2client + all: bin/fsclient bin/ps2client bin/ps2symbolize clean: - rm -f obj/*.o bin/*client* + rm -f obj/*.o bin/*client* bin/ps2symbolize bin/test_symbolize + endif + + ifneq ($(OS),Windows_NT) + install: bin/ps2symbolize endif install: bin/fsclient bin/ps2client strip bin/*client* @mkdir -p $(DESTDIR)$(PREFIX)/bin cp bin/*client* $(DESTDIR)$(PREFIX)/bin + ifneq ($(OS),Windows_NT) + strip bin/ps2symbolize + cp bin/ps2symbolize $(DESTDIR)$(PREFIX)/bin + endif - check: bin/ps2client bin/smoke bin/fake_ps2link bin/crc32 + check: bin/ps2client bin/smoke bin/fake_ps2link bin/crc32 bin/ps2symbolize bin/test_symbolize bin/smoke $(CURDIR)/bin/ps2client + bin/test_symbolize #################### ## CLIENT MODULES ## @@ -90,3 +99,11 @@ bin/crc32: tests/crc32.c @mkdir -p bin $(CC) $(CFLAGS) $< -o $@ + + bin/ps2symbolize: tools/ps2symbolize.c + @mkdir -p bin + $(CC) $(CFLAGS) $< -o $@ + + bin/test_symbolize: tests/test_symbolize.c tools/ps2symbolize.c + @mkdir -p bin + $(CC) $(CFLAGS) $< -o $@ diff --git a/README.md b/README.md index 2f3c740..d08c220 100644 --- a/README.md +++ b/README.md @@ -171,6 +171,25 @@ old one: do not ping while another client is serving a running program. Commands that load host files (`execee`, `execiop`, ...) do the same check before they send the command, instead of waiting a fixed time. +### SYMBOLIZING EXCEPTION LOGS + +The optional `bin/ps2symbolize` annotates ps2link's EE/IOP exception logs +with function names and source locations from matching **unstripped** ELF files. +It does not change ps2link or require a new wire protocol. For example: + +```sh +ps2client console | bin/ps2symbolize --ee game.elf +bin/ps2symbolize --ee game.elf crash.log +bin/ps2symbolize --iop mydriver=driver.elf crash.log +``` + +The C tool uses the PS2DEV `mips64r5900el-ps2-elf-addr2line` and +`mipsel-none-elf-addr2line` tools found on `PATH`; override them with +`--ee-tool` or `--iop-tool`. For IOP modules, the file must have debug symbols +and the logged **relative EPC** must match its ELF section addresses. Keep the +ELFs from the exact build being debugged. Unresolved locations are left as +original log text. + ### TESTS `make check` builds and runs plain-C portable smoke tests (including invalid diff --git a/tests/test_symbolize.c b/tests/test_symbolize.c new file mode 100644 index 0000000..47f4a21 --- /dev/null +++ b/tests/test_symbolize.c @@ -0,0 +1,51 @@ +/* Unit tests for EE/IOP exception log parsing. */ +#define PS2_SYMBOLIZE_NO_MAIN +#include "../tools/ps2symbolize.c" + +static uint32_t last_address; +static const char *last_elf; +static int calls; +static int fake_resolver(const char *tool, const char *elf, uint32_t address, + char *output, size_t capacity, void *context) +{ + (void)tool; (void)context; + last_elf = elf; last_address = address; calls++; + snprintf(output, capacity, "updateSprites at renderer.c:284"); + return 1; +} + +static void test_symbolizer(void) +{ + struct symbolizer config = {0}; + FILE *output = tmpfile(); + char text[1024]; + size_t length; + if (!output) { perror("tmpfile"); exit(1); } + config.ee_tool = "ee-addr2line"; + config.iop_tool = "iop-addr2line"; + config.ee_elf = "game.elf"; + config.module_count = 1; + strcpy(config.modules[0].name, "mydriver"); + config.modules[0].elf = "driver.elf"; + annotate_line(&config, "EE Exception handler: Address load exception\n", + output, fake_resolver, NULL); + annotate_line(&config, "Cause 00000010 EPC 00123456\n", + output, fake_resolver, NULL); + if (calls != 1 || last_address != 0x123456 || + strcmp(last_elf, "game.elf")) exit(1); + annotate_line(&config, "IOP Exception handler: Address store\n", + output, fake_resolver, NULL); + annotate_line(&config, "Module Name \"mydriver\" Relative EPC 00000064\n", + output, fake_resolver, NULL); + if (calls != 2 || last_address != 100 || + strcmp(last_elf, "driver.elf")) exit(1); + rewind(output); + length = fread(text, 1, sizeof(text) - 1, output); + text[length] = 0; + if (!strstr(text, "[EE symbol]") || !strstr(text, "[IOP mydriver symbol]")) + exit(1); + fclose(output); + puts("PASS EE/IOP symbolization, original log preservation"); +} + +int main(void) { test_symbolizer(); return 0; } diff --git a/tools/ps2symbolize.c b/tools/ps2symbolize.c new file mode 100644 index 0000000..efa3317 --- /dev/null +++ b/tools/ps2symbolize.c @@ -0,0 +1,171 @@ +/* Annotate ps2link EE/IOP exception logs with matching ELF debug symbols. */ +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern char **environ; + +struct module_map { char name[128]; const char *elf; }; +struct symbolizer { + const char *ee_elf; + const char *ee_tool; + const char *iop_tool; + struct module_map modules[64]; + size_t module_count; + int exception; /* 0 none, 1 EE, 2 IOP */ + int errors; +}; + +typedef int (*resolve_fn)(const char *, const char *, uint32_t, + char *, size_t, void *); + +static int equals_ignore_case(const char *a, const char *b) +{ + while (*a && *b) { + if (tolower((unsigned char)*a) != tolower((unsigned char)*b)) return 0; + a++; b++; + } + return !*a && !*b; +} + +/* addr2line is invoked directly, without a shell or string interpolation. */ +#ifndef PS2_SYMBOLIZE_NO_MAIN +static int resolve_symbol(const char *tool, const char *elf, uint32_t address, + char *output, size_t capacity, void *unused) +{ + int pipefd[2], status; + pid_t pid; + FILE *stream; + char pc[16]; + char *const argv[] = {(char *)tool, "-f", "-C", "-p", "-e", + (char *)elf, pc, NULL}; + posix_spawn_file_actions_t actions; + (void)unused; + snprintf(pc, sizeof(pc), "0x%08x", address); + if (pipe(pipefd) < 0) return -1; + if (posix_spawn_file_actions_init(&actions) != 0) { + close(pipefd[0]); close(pipefd[1]); return -1; + } + posix_spawn_file_actions_adddup2(&actions, pipefd[1], STDOUT_FILENO); + posix_spawn_file_actions_addclose(&actions, pipefd[0]); + posix_spawn_file_actions_addclose(&actions, pipefd[1]); + status = posix_spawnp(&pid, tool, &actions, NULL, argv, environ); + posix_spawn_file_actions_destroy(&actions); + close(pipefd[1]); + if (status != 0) { close(pipefd[0]); return -1; } + stream = fdopen(pipefd[0], "r"); + if (!stream) { close(pipefd[0]); waitpid(pid, &status, 0); return -1; } + if (!fgets(output, (int)capacity, stream)) output[0] = '\0'; + fclose(stream); + if (waitpid(pid, &status, 0) < 0 || !WIFEXITED(status) || WEXITSTATUS(status)) + return -1; + output[strcspn(output, "\r\n")] = '\0'; + if (!output[0] || strncmp(output, "??", 2) == 0) return 0; + return 1; +} +#endif + +static void annotate_line(struct symbolizer *config, const char *line, + FILE *out, resolve_fn resolve, void *context) +{ + const char *elf = NULL, *tool = NULL, *epc; + uint32_t pc; + char name[128] = "", symbol[1024]; + int value; + + fputs(line, out); + if (strstr(line, "EE Exception handler:") != NULL) config->exception = 1; + if (strstr(line, "IOP Exception handler:") != NULL) config->exception = 2; + if (config->exception == 1 && config->ee_elf) { + epc = strstr(line, "EPC "); + if (!epc || sscanf(epc, "EPC %x", &pc) != 1) return; + elf = config->ee_elf; + tool = config->ee_tool; + config->exception = 0; + } else if (config->exception == 2) { + size_t i; + epc = strstr(line, "Module Name \""); + if (!epc || sscanf(epc, "Module Name \"%127[^\"]\" Relative EPC %x", + name, &pc) != 2) return; + for (i = 0; i < config->module_count; i++) { + if (equals_ignore_case(name, config->modules[i].name)) { + elf = config->modules[i].elf; + break; + } + } + tool = config->iop_tool; + config->exception = 0; + } else { + return; + } + if (!elf) return; + value = resolve(tool, elf, pc, symbol, sizeof(symbol), context); + if (value > 0) { + if (name[0]) fprintf(out, " [IOP %s symbol] %s\n", name, symbol); + else fprintf(out, " [EE symbol] %s\n", symbol); + } else if (value < 0) { + fprintf(stderr, "ps2symbolize: cannot execute %s for %s\n", tool, elf); + config->errors++; + } +} + +#ifndef PS2_SYMBOLIZE_NO_MAIN +static void usage(FILE *out) +{ + fputs("Usage: ps2symbolize [--ee ELF] [--iop MODULE=ELF]... " + "[--ee-tool PATH] [--iop-tool PATH] [LOG|-]\n", out); +} + +int main(int argc, char **argv) +{ + struct symbolizer config = {0}; + FILE *input = stdin; + char line[8192]; + const char *log_path = NULL; + int i; + config.ee_tool = "mips64r5900el-ps2-elf-addr2line"; + config.iop_tool = "mipsel-none-elf-addr2line"; + setvbuf(stdout, NULL, _IOLBF, 0); + for (i = 1; i < argc; i++) { + if ((!strcmp(argv[i], "--ee") || !strcmp(argv[i], "--iop") || + !strcmp(argv[i], "--ee-tool") || !strcmp(argv[i], "--iop-tool")) && + i + 1 >= argc) { usage(stderr); return 2; } + if (!strcmp(argv[i], "--ee")) config.ee_elf = argv[++i]; + else if (!strcmp(argv[i], "--ee-tool")) config.ee_tool = argv[++i]; + else if (!strcmp(argv[i], "--iop-tool")) config.iop_tool = argv[++i]; + else if (!strcmp(argv[i], "--iop")) { + const char *pair = argv[++i]; + const char *sep = strchr(pair, '='); + size_t length = sep ? (size_t)(sep - pair) : 0; + struct module_map *entry; + if (!length || length >= sizeof(config.modules[0].name) || + !sep[1] || config.module_count == 64) { usage(stderr); return 2; } + entry = &config.modules[config.module_count++]; + memcpy(entry->name, pair, length); + entry->name[length] = '\0'; + entry->elf = sep + 1; + } else if (!strcmp(argv[i], "--help")) { usage(stdout); return 0; } + else if (!log_path && (argv[i][0] != '-' || !strcmp(argv[i], "-"))) { + log_path = argv[i]; + } else { usage(stderr); return 2; } + } + if (!config.ee_elf && !config.module_count) { usage(stderr); return 2; } + if (log_path && strcmp(log_path, "-")) { + input = fopen(log_path, "r"); + if (!input) { perror(log_path); return 1; } + } + while (fgets(line, sizeof(line), input)) { + annotate_line(&config, line, stdout, resolve_symbol, NULL); + } + if (input != stdin) fclose(input); + return config.errors ? 1 : 0; +} +#endif