diff options
| author | spicyjpeg <88942473+spicyjpeg@users.noreply.github.com> | 2021-08-17 11:37:03 +0000 |
|---|---|---|
| committer | spicyjpeg <88942473+spicyjpeg@users.noreply.github.com> | 2021-08-17 11:37:03 +0000 |
| commit | f2fc18f82dd7900465d6ab3ae2080726d5589d39 (patch) | |
| tree | 74bcf61c346cdc4f108b8295dd82eca421a0eb1d /libpsn00b | |
| parent | 89751f29467b359339a8c8b57c218cc3328bae43 (diff) | |
| download | psn00bsdk-f2fc18f82dd7900465d6ab3ae2080726d5589d39.tar.gz | |
Added dynamic linker API and example, updated README and changelog
Diffstat (limited to 'libpsn00b')
| -rw-r--r-- | libpsn00b/include/dlfcn.h | 176 | ||||
| -rw-r--r-- | libpsn00b/include/elf.h | 138 | ||||
| -rw-r--r-- | libpsn00b/psxetc/_dl_resolve_wrapper.s | 54 | ||||
| -rw-r--r-- | libpsn00b/psxetc/dl.c | 665 | ||||
| -rw-r--r-- | libpsn00b/psxetc/readme.txt | 16 |
5 files changed, 1042 insertions, 7 deletions
diff --git a/libpsn00b/include/dlfcn.h b/libpsn00b/include/dlfcn.h new file mode 100644 index 0000000..5fdd343 --- /dev/null +++ b/libpsn00b/include/dlfcn.h @@ -0,0 +1,176 @@ +/* + * PSn00bSDK dynamic linker + * (C) 2021 spicyjpeg - MPL licensed + */ + +#ifndef __DLFCN_H +#define __DLFCN_H + +#include <sys/types.h> +#include <elf.h> + +/* Helper macro for setting $t9 before calling a function */ + +#define DL_CALL(func, ...) { \ + __asm__ volatile("move $t9, %0;" :: "r"(func) : "$t9"); \ + func(__VA_ARGS__); \ +} + +/* Types */ + +#define RTLD_DEFAULT ((DLL *) 0) + +typedef enum _DL_ResolveMode { + RTLD_LAZY = 1, // Resolve functions when they are first called (default) + RTLD_NOW = 2 // Resolve all symbols immediately on load +} DL_ResolveMode; + +// Members of this struct should not be accessed directly in most cases, but +// they are intentionally exposed for easier expandability. +typedef struct _DLL { + void *ptr; + void *malloc_ptr; + size_t size; + const uint32_t *hash; + uint32_t *got; + Elf32_Sym *symtab; + const char *strtab; + uint16_t symbol_count; + uint16_t got_length; +} DLL; + +/* API */ + +#ifdef __cplusplus +extern "C" { +#endif + +// TODO: rewrite these javadoc comments into proper documentation + +/** + * @brief Reads the symbol table from the provided string buffer (which may or + * may not be null-terminated), parses it and stores the parsed entries into a + * private hash table; the buffer won't be further referenced and can be safely + * deallocated after parsing. Returns the number of entries successfully parsed + * or -1 if an error occurred. + * + * This function expects the string buffer to contain one more lines, each of + * which must follow this format: + * + * <SYMBOL_NAME> <T|R|D|B> <HEX_ADDRESS> <HEX_SIZE> [DEBUG_INFO...] + * + * The "nm" tool included in the GCC toolchain can be used to generate a map + * file in the appropriate format after building the executable, by using this + * command: + * + * mipsel-unknown-elf-nm -f posix -l -n executable.elf >executable.map + * + * @param ptr + * @param size + * @return -1 or number of entries parsed + */ +int32_t DL_ParseSymbolMap(const char *ptr, size_t size); +/** + * @brief File wrapper around DL_ParseSymbolMap(). Allocates a temporary buffer + * then loads the specified map file into it (using BIOS APIs) and calls + * DL_ParseSymbolMap() to parse it. The buffer is deallocated immediately after + * parsing. + * + * @param filename Must always contain device name, e.g. "cdrom:MODULE.DLL;1" + * @return -1 or number of entries parsed + */ +int32_t DL_LoadSymbolMap(const char *filename); +/** + * @brief Frees internal buffers containing the currently loaded symbol map. + * This is automatically done before loading a new symbol map so there is no + * need to call this function in most cases, however it can still be useful to + * free up space on the heap once the symbol map is no longer needed. + */ +void DL_UnloadSymbolMap(void); +/** + * @brief Queries the currently loaded symbol map for the symbol with the given + * name and returns a pointer to it, which can then be used to directly access + * the symbol. If the symbol can't be found, null is returned instead. + * + * @param name + * @return NULL or pointer to symbol (any type) + */ +void *DL_GetSymbolByName(const char *name); +/** + * @brief Sets a custom function to be called for resolving symbols in DLLs. + * The function will be given a pointer to the current DLL and the unresolved + * symbol's name, and should return the address of the symbol in the executable + * (the dynamic linker will lock up if it returns null). Passing null instead + * of a function resets the default behavior of calling DL_GetSymbolByName() to + * find the symbol in the currently loaded symbol map. + * + * @param callback NULL or pointer to callback function + */ +void DL_SetResolveCallback(void *(*callback)(DLL *, const char *)); + +/** + * @brief Initializes a buffer holding the contents of a dynamically-loaded + * library file (compiled with the dll.ld linker script and converted to a raw + * binary) *in-place*. A new DLL struct is allocated to store metadata but, + * unlike DL_ParseSymbolMap(), the DLL's actual code, data and tables are + * referenced directly from the provided buffer. The buffer must not be moved + * or deallocated, at least not before calling dlclose() on the DLL struct + * returned by this function. + * + * The third argument specifies when symbols in the DLL should be resolved. + * Setting it to RTLD_LAZY defers resolution of undefined functions to when + * they are first called, while RTLD_DEFAULT forces all symbols to be resolved + * immediately. If a custom resolver has been set via DL_SetResolveCallback(), + * it will be called for each symbol to resolve. + * + * @param ptr + * @param size + * @param mode RTLD_LAZY or RTLD_NOW + * @return NULL or pointer to a new DLL struct + */ +DLL *dlinit(void *ptr, size_t size, DL_ResolveMode mode); +/** + * @brief File wrapper around dlinit(). Allocates a new buffer, loads the + * specified file into it (using BIOS APIs) and calls dlinit() on that. When + * calling dlclose() on a DLL loaded from a file, the file buffer is + * automatically destroyed. + * + * @param filename Must always contain device name, e.g. "cdrom:MODULE.DLL;1" + * @param mode RTLD_LAZY or RTLD_NOW + * @return NULL or pointer to a new DLL struct + */ +DLL *dlopen(const char *filename, DL_ResolveMode mode); +/** + * @brief Destroys a loaded DLL by calling its global destructors and freeing + * the buffer it's loaded in. Any pointer passed to dlclose() should no longer + * be used after the call. If the DLL was initialized in-place using dlinit(), + * dlclose() will *NOT* free the buffer initially passed to dlinit(). + * + * @param dll + */ +void dlclose(DLL *dll); +/** + * @brief Returns a pointer to the DLL symbol with the given name, or null if + * it can't be found. If null or RTLD_DEFAULT is passed as first argument, the + * executable itself is searched instead using the symbol map (behaving the + * same as DL_GetSymbolByName()). + * + * @param dll DLL struct or RTLD_DEFAULT + * @param name + * @return NULL or pointer to symbol (any type) + */ +void *dlsym(DLL *dll, const char *name); +/** + * @brief Returns a string describing the last error that occurred, or null if + * no error has occurred since the last call to dlerror() (i.e. calling this + * also resets the internal error flags). + * + * @return NULL or pointer to const string + */ +const char *const dlerror(void); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/libpsn00b/include/elf.h b/libpsn00b/include/elf.h new file mode 100644 index 0000000..b4c4408 --- /dev/null +++ b/libpsn00b/include/elf.h @@ -0,0 +1,138 @@ +/* + * PSn00bSDK dynamic linker + * (C) 2021 spicyjpeg - MPL licensed + * + * This file is used internally by the dynamic linker to parse the "header" + * (.dynamic and .dynsym sections) of dynamically-linked libraries built with + * the provided linker script. Most of it is copied from the standard Unix + * elf.h header, with the only changes being removed typedefs and #defines + * converted to enums. + */ + +#ifndef __ELF_H +#define __ELF_H + +#include <sys/types.h> + +typedef enum { + DT_NULL = 0, /* Marks end of dynamic section */ + DT_NEEDED = 1, /* Name of needed library */ + DT_PLTRELSZ = 2, /* Size in bytes of PLT relocs */ + DT_PLTGOT = 3, /* Processor defined value */ + DT_HASH = 4, /* Address of symbol hash table */ + DT_STRTAB = 5, /* Address of string table */ + DT_SYMTAB = 6, /* Address of symbol table */ + DT_RELA = 7, /* Address of Rela relocs */ + DT_RELASZ = 8, /* Total size of Rela relocs */ + DT_RELAENT = 9, /* Size of one Rela reloc */ + DT_STRSZ = 10, /* Size of string table */ + DT_SYMENT = 11, /* Size of one symbol table entry */ + DT_INIT = 12, /* Address of init function */ + DT_FINI = 13, /* Address of termination function */ + DT_SONAME = 14, /* Name of shared object */ + DT_RPATH = 15, /* Library search path (deprecated) */ + DT_SYMBOLIC = 16, /* Start symbol search here */ + DT_REL = 17, /* Address of Rel relocs */ + DT_RELSZ = 18, /* Total size of Rel relocs */ + DT_RELENT = 19, /* Size of one Rel reloc */ + DT_PLTREL = 20, /* Type of reloc in PLT */ + DT_DEBUG = 21, /* For debugging; unspecified */ + DT_TEXTREL = 22, /* Reloc might modify .text */ + DT_JMPREL = 23, /* Address of PLT relocs */ + DT_BIND_NOW = 24, /* Process relocations of object */ + DT_INIT_ARRAY = 25, /* Array with addresses of init fct */ + DT_FINI_ARRAY = 26, /* Array with addresses of fini fct */ + DT_INIT_ARRAYSZ = 27, /* Size in bytes of DT_INIT_ARRAY */ + DT_FINI_ARRAYSZ = 28, /* Size in bytes of DT_FINI_ARRAY */ + DT_RUNPATH = 29, /* Library search path */ + DT_FLAGS = 30, /* Flags for the object being loaded */ + DT_ENCODING = 32, /* Start of encoded range */ + DT_PREINIT_ARRAY = 32, /* Array with addresses of preinit fct*/ + DT_PREINIT_ARRAYSZ = 33, /* size in bytes of DT_PREINIT_ARRAY */ + DT_SYMTAB_SHNDX = 34, /* Address of SYMTAB_SHNDX section */ + DT_NUM = 35, /* Number used */ + DT_LOOS = 0x6000000d, /* Start of OS-specific */ + DT_HIOS = 0x6ffff000, /* End of OS-specific */ + DT_LOPROC = 0x70000000, /* Start of processor-specific */ + DT_HIPROC = 0x7fffffff, /* End of processor-specific */ + + DT_MIPS_RLD_VERSION = 0x70000001, + DT_MIPS_FLAGS = 0x70000005, + DT_MIPS_BASE_ADDRESS = 0x70000006, + DT_MIPS_LOCAL_GOTNO = 0x7000000a, + DT_MIPS_SYMTABNO = 0x70000011, + DT_MIPS_UNREFEXTNO = 0x70000012, + DT_MIPS_GOTSYM = 0x70000013, + DT_MIPS_HIPAGENO = 0x70000014 +} Elf32_d_tag; + +typedef enum { + RHF_NONE = 0, /* No flags */ + RHF_QUICKSTART = 1, /* Use quickstart */ + RHF_NOTPOT = 2, /* Hash size not power of 2 */ + RHF_NO_LIBRARY_REPLACEMENT = 4 /* Ignore LD_LIBRARY_PATH */ +} Elf32_d_MIPS_FLAGS; + +typedef struct { + Elf32_d_tag d_tag; /* Dynamic entry type */ + union { + uint32_t d_val; /* Integer value */ + void *d_ptr; /* Address value */ + } d_un; +} Elf32_Dyn; + +typedef struct { + uint32_t st_name; /* Symbol name (string tbl index) */ + void *st_value; /* Symbol value */ + size_t st_size; /* Symbol size */ + uint8_t st_info; /* Symbol type and binding */ + uint8_t st_other; /* Symbol visibility */ + uint16_t st_shndx; /* Section index */ +} Elf32_Sym; + +#define ELF32_ST_BIND(val) ((Elf32_st_bind) (((uint8_t) (val)) >> 4)) +#define ELF32_ST_TYPE(val) ((Elf32_st_type) ((val) & 0xf)) +#define ELF32_ST_INFO(bind, type) ((((uint8_t) (bind)) << 4) + (((uint8_t) (type)) & 0xf)) + +typedef enum { + STB_LOCAL = 0, /* Local symbol */ + STB_GLOBAL = 1, /* Global symbol */ + STB_WEAK = 2, /* Weak symbol */ + STB_NUM = 3, /* Number of defined types. */ + STB_LOOS = 10, /* Start of OS-specific */ + STB_GNU_UNIQUE = 10, /* Unique symbol. */ + STB_HIOS = 12, /* End of OS-specific */ + STB_LOPROC = 13, /* Start of processor-specific */ + STB_HIPROC = 15 /* End of processor-specific */ +} Elf32_st_bind; + +typedef enum { + STT_NOTYPE = 0, /* Symbol type is unspecified */ + STT_OBJECT = 1, /* Symbol is a data object */ + STT_FUNC = 2, /* Symbol is a code object */ + STT_SECTION = 3, /* Symbol associated with a section */ + STT_FILE = 4, /* Symbol's name is file name */ + STT_COMMON = 5, /* Symbol is a common data object */ + STT_TLS = 6, /* Symbol is thread-local data object*/ + STT_NUM = 7, /* Number of defined types. */ + STT_LOOS = 10, /* Start of OS-specific */ + STT_GNU_IFUNC = 10, /* Symbol is indirect code object */ + STT_HIOS = 12, /* End of OS-specific */ + STT_LOPROC = 13, /* Start of processor-specific */ + STT_HIPROC = 15 /* End of processor-specific */ +} Elf32_st_type; + +// If you need to add more constants, you may use the following Python snippet +// to quickly convert #defines to enums: +/* +import re +t = """<paste #defines here>""" +t = re.sub( + r"(0x[0-9a-f]+|0b[01]+|[0-9]+)", + lambda m: f"= {m.group(1)},", + t.replace("#define ", "\t").replace("#define\t", "\t") +) +print("typedef enum {\n" + t + "\n} NAME;") +*/ + +#endif diff --git a/libpsn00b/psxetc/_dl_resolve_wrapper.s b/libpsn00b/psxetc/_dl_resolve_wrapper.s new file mode 100644 index 0000000..069ee84 --- /dev/null +++ b/libpsn00b/psxetc/_dl_resolve_wrapper.s @@ -0,0 +1,54 @@ +# PSn00bSDK dynamic linker +# (C) 2021 spicyjpeg - MPL licensed +# +# This function is called by the lazy loader stubs generated by GCC in the +# .plt/.MIPS.stubs section when attempting to call a GOT entry whose address +# hasn't yet been resolved. The generated stubs conform to the MIPS ABI and +# uses the following registers to pass the following parameters: +# - $t7 = address the resolved function should return to (i.e. $ra of the +# caller that triggered the stub) +# - $t8 = index of the function in the .dynsym symbol table +# - $t9 = _dl_resolve_wrapper itself's address + +.set noreorder +.section .text + +.global _dl_resolve_wrapper +.type _dl_resolve_wrapper, @function +_dl_resolve_wrapper: + # Push the registers we're going to use onto the stack. + addiu $sp, -16 + sw $a0, 0($sp) + sw $a1, 4($sp) + sw $v0, 8($sp) + sw $t7, 12($sp) # (will be restored directly to $ra) + + # Figure out where the DLL's struct is. dlinit() places a pointer to the + # struct in the second GOT entry, so it's just a matter of indexing the GOT + # using GP. Then call _dl_resolve_helper with the struct and $t8 as + # arguments, and store the return value into $t0. + lw $a0, -0x7fec($gp) # sizeof(uint32_t) - 0x7ff0 (see dll.ld) + move $a1, $t8 + + jal _dl_resolve_helper + addiu $sp, -8 # (branch delay) + addiu $sp, 8 + + move $t0, $v0 + + # Restore registers from the stack and tail-call the function at the + # address returned by the resolver. This will cause the resolved function + # to run and return directly to the code that called the stub. + lw $a0, 0($sp) + lw $a1, 4($sp) + lw $v0, 8($sp) + lw $ra, 12($sp) + addiu $sp, 16 + + jr $t0 + nop + +.global _dl_credits +.type _dl_credits, @object +_dl_credits: + .asciiz "psxetc runtime dynamic linker by spicyjpeg\n" diff --git a/libpsn00b/psxetc/dl.c b/libpsn00b/psxetc/dl.c new file mode 100644 index 0000000..1485183 --- /dev/null +++ b/libpsn00b/psxetc/dl.c @@ -0,0 +1,665 @@ +/* + * PSn00bSDK dynamic linker + * (C) 2021 spicyjpeg - MPL licensed + * + * The bulk of this code is MIPS-specific but not PS1-specific, so the whole + * dynamic linker could be ported to other MIPS platforms that do not have one + * in their OS/SDK (e.g. N64, PIC32 microcontrollers). Note that, despite the + * various ELF references, I did *not* implement a full ELF file parser: this + * implementation just expects all library files to begin with several metadata + * sections (.dynamic, .dynsym, .hash, .dynstr) laid out in a fixed order. Use + * the dll.ld linker script to generate compatible libraries. + * + * Most of the stuff implemented here, despite looking like black magic, is + * actually well-documented across several PDFs scattered around the internet: + * - http://www.sco.com/developers/devspecs/gabi41.pdf + * - http://math-atlas.sourceforge.net/devel/assembly/mipsabi32.pdf + * - http://flint.cs.yale.edu/cs422/doc/ELF_Format.pdf + * + * TODO: + * - Clean up duplicated code, especially hash-table-related loops + * - Fix bugs and improve allocation checks + * - Find a way to predict the smallest ELF hash table size for a certain set + * of entries + */ + +#include <sys/types.h> +#include <stdio.h> +#include <malloc.h> +#include <ctype.h> +#include <elf.h> +#include <dlfcn.h> +#include <string.h> +#include <psxapi.h> + +/* Compile options */ + +// Uncomment before building to enable debug logging (via printf()). +#define DEBUG + +// Comment before building to disable functions that rely on BIOS file APIs, +// i.e. DL_LoadSymbolMap() and dlopen(). +#define USE_FILE_API + +/* Private types */ + +typedef struct { + uint32_t hash; + void *ptr; +} MapEntry; + +typedef enum { + ERR_NONE, + ERR_FILE, + ERR_FILE_MALLOC, + ERR_FILE_READ, + ERR_NO_MAP, + ERR_MAP_MALLOC, + ERR_NO_SYMBOLS, + ERR_DLL_NULL, + ERR_DLL_MALLOC, + ERR_DLL_FORMAT, + ERR_NO_FILE_API, + ERR_MAP_SYMBOL, + ERR_DLL_SYMBOL +} ErrorCode; + +/* Data */ + +static const char *const DL_ERROR_MESSAGES[] = { + "Unable to find file", + "Unable to allocate buffer to load file into", + "Failed to read file", + "No symbol map has been loaded yet", + "Unable to allocate symbol map structures", + "No symbols found in symbol map", + "Unable to initialize DLL from null pointer", + "Unable to allocate DLL metadata structures", + "Unsupported DLL type or format", + "psxetc has been built without file support", + "Symbol not found in symbol map", + "Symbol not found in DLL" +}; + +static ErrorCode _error_code = ERR_NONE; +static uint32_t *_map_hash_table = 0; +static MapEntry *_map_entry_table = 0; + +// Accessed by _dl_resolve_helper, stores the pointer to the current resolver +// function. Can be changed using DL_SetResolveCallback(). +void *(*_dl_resolve_callback)(DLL *, const char *) = 0; + +/* Private utilities */ + +#ifdef DEBUG +#define _LOG(...) printf(__VA_ARGS__) +#define _ERROR(code, ret) { \ + _LOG("psxetc: ERROR! %s\n", DL_ERROR_MESSAGES[code - 1]); \ + _error_code = code; \ + return ret; \ +} +#else +#define _LOG(...) +#define _ERROR(code, ret) { \ + _error_code = code; \ + return ret; \ +} +#endif + +void _dl_resolve_wrapper(void); + +// Called by _dl_resolve_wrapper() (which is in turn called by GCC stubs) to +// resolve a function. +void *_dl_resolve_helper(DLL *dll, uint32_t index) { + Elf32_Sym *sym = &(dll->symtab[index]); + const char *_name = &(dll->strtab[sym->st_name]); + void *address; + + if (_dl_resolve_callback) + address = _dl_resolve_callback(dll, _name); + else + address = DL_GetSymbolByName(_name); + + if (!address) { + _LOG("psxetc: FATAL! Can't resolve %s, locking up\n", _name); + while (1) + __asm__ volatile("nop"); + } + + // Patch the GOT entry to "cache" the resolved address. This can probably + // be implemented in a faster way, but this thing is already too complex. + for (uint32_t i = 0; i < dll->got_length; i++) { + if (dll->got[2 + i] == (uint32_t) sym->st_value) { + dll->got[2 + i] = (uint32_t) address; + break; + } + } + + return address; +} + +// Implementation of the weird obscure hashing function used in the ELF .hash +// section. Not sure how collision-prone this is. +// https://en.wikipedia.org/wiki/PJW_hash_function +static uint32_t _elf_hash(const char *str) { + uint32_t value = 0; + + while (*str) { + value <<= 4; + value += (uint32_t) *(str++); + + uint32_t nibble = value & 0xf0000000; + if (nibble) + value ^= nibble >> 24; + + value &= ~nibble; + } + + return value; +} + +#ifdef USE_FILE_API +static uint8_t *_load_file(const char *filename, size_t *size_output) { + int32_t fd = open(filename, 1); + if (fd < 0) + _ERROR(ERR_FILE, 0); + + // Extract file size from the file's associated control block. + // https://problemkaputt.de/psx-spx.htm#biosmemorymap + FCB *fcb = (FCB *) *((FCB **) 0x80000140); + size_t size = fcb[fd].filesize; + + uint8_t *buffer = malloc(size); + if (!buffer) + _ERROR(ERR_FILE_MALLOC, 0); + + _LOG("psxetc: Loading %s (%d bytes)..", filename, size); + + for (uint32_t offset = 0; offset < size; ) { + int32_t length = read(fd, &(buffer[offset]), 0x800); + + if (length <= 0) { + close(fd); + free(buffer); + _ERROR(ERR_FILE_READ, 0); + } + + _LOG("."); + offset += length; + } + + close(fd); + _LOG(" done\n"); + + if (size_output) + *size_output = size; + return buffer; +} +#endif + +/* Symbol map loading/parsing API */ + +int32_t DL_ParseSymbolMap(const char *ptr, size_t size) { + DL_UnloadSymbolMap(); + + // Perform a quick scan over the entire map text and count the number of + // newlines. This allows us to (over)estimate the number of entries and + // allocate a sufficiently large hash/entry table. + uint32_t entries = 0; + for (uint32_t pos = 0; pos < size; pos++) { + if (ptr[pos] == '\n') + entries++; + } + + // TODO: find a way to calculate the optimal number of hash table "buckets" + // in order to minimize hash table size + uint32_t nbucket = entries; + _LOG("psxetc: Predicted %d entries, %d hash buckets\n", entries, nbucket); + + // Allocate an entry table to store parsed symbols in, and an associated + // hash table (same format as .hash section, with 8-byte header). + _map_hash_table = malloc(sizeof(uint32_t) * (2 + nbucket + entries)); + _map_entry_table = malloc(sizeof(MapEntry) * entries); + if (!_map_hash_table || !_map_entry_table) + _ERROR(ERR_MAP_MALLOC, -1); + + _map_hash_table[0] = nbucket; + _map_hash_table[1] = entries; + for (uint32_t i = 0; i < (nbucket + entries); i++) + _map_hash_table[2 + i] = 0xffffffff; + + // Go again through the symbol map and fill in the hash table. + uint32_t index = 0; + for (uint32_t pos = 0; (pos < size) && ptr[pos]; pos++) { + char name[64]; + char type_string[2]; + uint64_t address64; + size_t _size; + + // e.g. "main T ffffffff80000000 100 ...\n" + int32_t parsed = sscanf( + &(ptr[pos]), + "%63s %1s %Lx %x", + name, + type_string, + &address64, + &_size // Optional, unused (yet) + ); + + if (parsed >= 3) { + // Drop the upper 32 bits of the address (for some reason MIPS nm + // insists on printing 64-bit addresses... wtf) and normalize the + // type letter to upper case, then check if the entry is valid and + // non-null. + void *address = (void *) address64; + char _type = toupper(type_string[0]); + uint32_t hash = _elf_hash(name); + uint32_t hash_mod = hash % nbucket; + + if (address && ( + (_type == 'T') || // .text + (_type == 'R') || // .rodata + (_type == 'D') || // .data + (_type == 'B') // .bss + )) { + _LOG( + "psxetc: Map sym: %08x,%08x [%c %s]\n", + address, + _size, + _type, + name + ); + + MapEntry *entry = &(_map_entry_table[index]); + entry->hash = hash; + entry->ptr = address; + + // Append a reference to the entry to the hash table's chain + // for the current hash_mod. I can't explain this properly. + uint32_t *hash_entry = &(_map_hash_table[2 + hash_mod]); + while (*hash_entry != 0xffffffff) + hash_entry = &(_map_hash_table[2 + nbucket + *hash_entry]); + + *hash_entry = index; + index++; + } + } + + // Skip the line by advancing the pointer until a newline is found. The + // for loop will then skip the newline itself. + while ((pos < size) && (ptr[pos] != '\n')) + pos++; + } + + _LOG("psxetc: Parsed %d symbols from map\n", entries); + if (!entries) + _ERROR(ERR_NO_SYMBOLS, -1); + + return entries; +} + +int32_t DL_LoadSymbolMap(const char *filename) { +#ifdef USE_FILE_API + size_t size; + char *ptr = _load_file(filename, &size); + if (!ptr) + return -1; + + int32_t entries = DL_ParseSymbolMap(ptr, size); + free(ptr); + + return entries; +#else + _ERROR(ERR_NO_FILE_API, -1); +#endif +} + +void DL_UnloadSymbolMap(void) { + if (!_map_hash_table) + return; + + free(_map_hash_table); + free(_map_entry_table); + _map_hash_table = 0; +} + +void *DL_GetSymbolByName(const char *name) { + if (!_map_hash_table) + _ERROR(ERR_NO_MAP, 0); + + // https://docs.oracle.com/cd/E23824_01/html/819-0690/chapter6-48031.html + uint32_t nbucket = _map_hash_table[0]; + uint32_t hash = _elf_hash(name); + uint32_t hash_mod = hash % nbucket; + + // Go through the hash table's chain until the symbol hash matches the one + // calculated. + for (uint32_t i = _map_hash_table[2 + hash_mod]; i;) { + MapEntry *entry = &(_map_entry_table[i]); + + if (hash == entry->hash) { + _LOG("psxetc: Map lookup [%s = %08x]\n", name, entry->ptr); + return entry->ptr; + } + + i = _map_hash_table[2 + nbucket + i]; + } + + _ERROR(ERR_MAP_SYMBOL, 0); +} + +void DL_SetResolveCallback(void *(*callback)(DLL *, const char *)) { + _dl_resolve_callback = callback; +} + +/* Library loading and linking API */ + +DLL *dlinit(void *ptr, size_t size, DL_ResolveMode mode) { + if (!ptr) + _ERROR(ERR_DLL_NULL, 0); + + DLL *dll = malloc(sizeof(DLL)); + if (!dll) + _ERROR(ERR_DLL_MALLOC, 0); + + dll->ptr = ptr; + dll->malloc_ptr = 0; + dll->size = size; + _LOG("psxetc: Initializing DLL at %08x\n", ptr); + + // Interpret the key-value pairs in the .dynamic section to obtain info + // about all the other sections. The pairs are null-terminated, which makes + // parsing trivial. + uint32_t local_got_len = 0; + uint32_t first_got_sym = 0; + + for (Elf32_Dyn *dyn = (Elf32_Dyn *) ptr; dyn->d_tag; dyn++) { + _LOG("psxetc: .dynamic %08x=%08x ", dyn->d_tag, dyn->d_un.d_val); + + switch (dyn->d_tag) { + // Offset of .got section + case DT_PLTGOT: + _LOG("[PLTGOT]\n"); + + dll->got = (void *) (ptr + dyn->d_un.d_val); + break; + + // Offset of .hash section + case DT_HASH: + _LOG("[HASH]\n"); + + dll->hash = (void *) (ptr + dyn->d_un.d_val); + break; + + // Offset of .dynstr (NOT .strtab) section + case DT_STRTAB: + _LOG("[STRTAB]\n"); + + dll->strtab = (void *) (ptr + dyn->d_un.d_val); + break; + + // Offset of .dynsym (NOT .symtab) section + case DT_SYMTAB: + _LOG("[SYMTAB]\n"); + + dll->symtab = (void *) (ptr + dyn->d_un.d_val); + break; + + // Length of .dynstr section + //case DT_STRSZ: + //_LOG("[STRSZ]\n"); + //break; + + // Length of each .dynsym entry + case DT_SYMENT: + _LOG("[SYMENT]\n"); + + // Only 16-byte symbol table entries are supported. + if (dyn->d_un.d_val != sizeof(Elf32_Sym)) { + free(dll); + _ERROR(ERR_DLL_FORMAT, 0); + } + break; + + // MIPS ABI (?) version + case DT_MIPS_RLD_VERSION: + _LOG("[MIPS_RLD_VERSION]\n"); + + // Versions other than 1 are unsupported (do they even exist?). + if (dyn->d_un.d_val != 1) { + free(dll); + _ERROR(ERR_DLL_FORMAT, 0); + } + break; + + // DLL/ABI flags + case DT_MIPS_FLAGS: + _LOG("[MIPS_FLAGS]\n"); + + // Shortcut pointers (whatever they are) are not supported. + if (dyn->d_un.d_val & RHF_QUICKSTART) { + free(dll); + _ERROR(ERR_DLL_FORMAT, 0); + } + break; + + // Number of local (not to resolve) GOT entries + case DT_MIPS_LOCAL_GOTNO: + _LOG("[MIPS_LOCAL_GOTNO]\n"); + + local_got_len = dyn->d_un.d_val; + break; + + // Base address DLL was compiled for + case DT_MIPS_BASE_ADDRESS: + _LOG("[MIPS_BASE_ADDRESS]\n"); + + // Base addresses other than zero are not supported. It would + // be easy enough to support them, but why? + if (dyn->d_un.d_val) { + free(dll); + _ERROR(ERR_DLL_FORMAT, 0); + } + break; + + // Number of symbol table entries + case DT_MIPS_SYMTABNO: + _LOG("[MIPS_SYMTABNO]\n"); + + dll->symbol_count = dyn->d_un.d_val; + break; + + // Index of first unresolved symbol table entry + //case DT_MIPS_UNREFEXTNO: + //_LOG("[MIPS_UNREFEXTNO]\n"); + //break; + + // Index of first symbol table entry which has a matching GOT entry + case DT_MIPS_GOTSYM: + _LOG("[MIPS_GOTSYM]\n"); + + first_got_sym = dyn->d_un.d_val; + break; + + // Number of pages the GOT is split into (does not apply to PS1) + //case DT_MIPS_HIPAGENO: + //_LOG("[MIPS_HIPAGENO]\n"); + //break; + + default: + _LOG("[ignored]\n"); + } + } + + // Calculate the number of GOT entries (and symbols, if MIPS_SYMTABNO was + // not found in the .dynamic section). + //dll->symbol_count = \ + ((uint32_t) dll->hash - (uint32_t) dll->symtab) / sizeof(Elf32_Sym); + //dll->got_length = \ + ((uint32_t) ptr + size - (uint32_t) dll->got) / sizeof(uint32_t) - 2; + + dll->got_length = local_got_len + (dll->symbol_count - first_got_sym) - 2; + _LOG( + "psxetc: %d symbols, %d GOT entries\n", + dll->symbol_count, + dll->got_length + ); + + // Relocate the DLL by adding its base address to all pointers in the GOT + // except the first two, which are reserved. The first entry in particular + // is a pointer to the lazy resolver, called by auto-generated stubs when a + // function is first used. got[1] is normally unused, but here we'll set it + // to the DLL's metadata struct so we can look that up when resolving + // functions (see _dl_resolve_wrapper()). + dll->got[0] = (uint32_t) &_dl_resolve_wrapper; + dll->got[1] = (uint32_t) dll; + + for (uint32_t i = 0; i < dll->got_length; i++) + dll->got[2 + i] += (uint32_t) ptr; + + // Fix addresses in the symbol table. + // TODO: clean this shit up + uint32_t got_offset = first_got_sym; + + for (uint32_t i = 0; i < dll->symbol_count; i++) { + Elf32_Sym *sym = &(dll->symtab[i]); + const char *_name = &(dll->strtab[sym->st_name]); + + if (!sym->st_value) + continue; + + sym->st_value += (uint32_t) ptr; + _LOG( + "psxetc: DLL sym: %08x,%08x [%s]\n", + sym->st_value, + sym->st_size, + _name + ); + + // If RTLD_NOW was passed, resolve GOT entries ahead of time by + // cross-referencing them with the symbol table. + if (mode != RTLD_NOW) + continue; + + for (uint32_t j = got_offset; j < dll->got_length; j++) { + if (dll->got[2 + j] != (uint32_t) sym->st_value) + continue; + + got_offset = j; + + // If the symbol is undefined (st_shndx = 0) and is either a + // variable or a function, resolve it immediately. + // TODO: linking of external variables needs more testing + if (!(sym->st_shndx) && ( + ELF32_ST_TYPE(sym->st_info) == STT_OBJECT || + ELF32_ST_TYPE(sym->st_info) == STT_FUNC + )) { + dll->got[2 + j] = (uint32_t) _dl_resolve_callback(dll, _name); + + if (!dll->got[2 + j]) { + free(dll); + _ERROR(ERR_MAP_SYMBOL, 0); + } + } + + break; + } + } + + EnterCriticalSection(); + FlushCache(); + ExitCriticalSection(); + + // Call the DLL's global constructors. This is the same thing we'd do in + // _start() for regular executables, but we have to do it outside of the + // DLL as there's no _start() or even a defined entry point within the + // DLL itself. + const uint32_t *ctor_list = dlsym(dll, "__CTOR_LIST__"); + if (ctor_list) { + for (uint32_t i = ((uint32_t) ctor_list[0]); i >= 1; i--) { + void (*ctor)(void) = (void (*)(void)) ctor_list[i]; + DL_CALL(ctor); + } + } + + return dll; +} + +DLL *dlopen(const char *filename, DL_ResolveMode mode) { +#ifdef USE_FILE_API + size_t size; + char *ptr = _load_file(filename, &size); + if (!ptr) + return 0; + + DLL *dll = dlinit(ptr, size, mode); + if (dll) + dll->malloc_ptr = dll->ptr; + else + free(ptr); + + return dll; +#else + _ERROR(ERR_NO_FILE_API, 0); +#endif +} + +void dlclose(DLL *dll) { + if (dll == RTLD_DEFAULT) + return; + + if (dll->ptr) { + // Call the DLL's global destructors. + const uint32_t *dtor_list = dlsym(dll, "__DTOR_LIST__"); + if (dtor_list) { + for (uint32_t i = 0; i < ((uint32_t) dtor_list[0]); i++) { + void (*dtor)(void) = (void (*)(void)) dtor_list[i + 1]; + DL_CALL(dtor); + } + } + } + + // If the DLL is associated to a buffer allocated by dlopen(), free that + // buffer. + if (dll->malloc_ptr) + free(dll->malloc_ptr); + + free(dll); +} + +void *dlsym(DLL *dll, const char *name) { + if (dll == RTLD_DEFAULT) + return DL_GetSymbolByName(name); + //return _dl_resolve_callback(RTLD_DEFAULT, name); + + // https://docs.oracle.com/cd/E23824_01/html/819-0690/chapter6-48031.html + const uint32_t *hash_table = dll->hash; + uint32_t nbucket = hash_table[0]; + uint32_t hash_mod = _elf_hash(name) % nbucket; + + // Go through the hash table's chain until the symbol name matches the one + // provided. + for (uint32_t i = hash_table[2 + hash_mod]; i;) { + Elf32_Sym *sym = &(dll->symtab[i]); + const char *_name = &(dll->strtab[sym->st_name]); + + if (!strcmp(name, _name)) { + _LOG("psxetc: DLL lookup [%s = %08x]\n", name, sym->st_value); + return sym->st_value; + } + + i = hash_table[2 + nbucket + i]; + } + + _ERROR(ERR_DLL_SYMBOL, 0); +} + +const char *const dlerror(void) { + uint32_t last = _error_code; + _error_code = ERR_NONE; + + if (last) + return DL_ERROR_MESSAGES[last - 1]; + + return 0; +} diff --git a/libpsn00b/psxetc/readme.txt b/libpsn00b/psxetc/readme.txt index f2e7649..24645f7 100644 --- a/libpsn00b/psxetc/readme.txt +++ b/libpsn00b/psxetc/readme.txt @@ -1,18 +1,20 @@ PSX Misc library, part of PSn00bSDK -2019 Lameguy64 / Meido-Tek Productions +2021 Lameguy64 / Meido-Tek Productions Licensed under Mozilla Public License -Open source implementation of the ETC library. Doesn't really provide much -aside from some debug font stuff at the moment. - +Open source implementation of the ETC library. Currently provides the interrupt +and DMA callback dispatchers (used by other libraries) as well as the DL_* and +dl* functions for dynamic library loading (original, not present in the official +SDK but similar to the standard dlopen() API). Library developer(s): Lameguy64 - - + spicyjpeg + Library header(s): psxetc.h - + dlfcn.h + elf.h (used internally) |
