From 8d88cd17d668256ed4ff4d578e3bfe9d13b4ce98 Mon Sep 17 00:00:00 2001 From: EXtremeExploit Date: Mon, 13 May 2024 03:16:10 -0300 Subject: [PATCH] Auto Splitters: Add optional cache for memory mappings --- docs/auto-splitters.md | 42 +++++++++++++++++++++++++++++++ src/auto-splitter.c | 19 ++++++++++++++ src/auto-splitter.h | 1 + src/memory.c | 56 +++++++++++++++++++++++++++++++----------- src/process.c | 48 +++++++++++++++++++++++++++--------- src/process.h | 9 +++++++ 6 files changed, 150 insertions(+), 25 deletions(-) diff --git a/docs/auto-splitters.md b/docs/auto-splitters.md index 2d164c7..063c69a 100644 --- a/docs/auto-splitters.md +++ b/docs/auto-splitters.md @@ -285,3 +285,45 @@ end * A Pointer Path is a list of Offsets + a Base Address. The auto splitter reads the value at the base address and interprets the value as yet another address. It adds the first offset to this address and reads the value of the calculated address. It does this over and over until there are no more offsets. At that point, it has found the value it was searching for. This resembles the way objects are stored in memory. Every object has a clearly defined layout where each variable has a consistent offset within the object, so you basically follow these variables from object to object. * Cheat Engine is a tool that allows you to easily find Addresses and Pointer Paths for those Addresses, so you don't need to debug the game to figure out the structure of the memory. + +## getPID +* Returns the current PID + +# Experimental stuff +## `mapsCacheCycles` +* When a readAddress that uses a memory map the biggest bottleneck is reading every line of `/proc/pid/maps` and checking if that line is the corresponding module. This option allows you to set for how many cycles the cache of that file should be used. The cache is global so it gets reset every x number of cycles. + * `0` (default): Disabled completely + * `1`: Enabled for the current cycle + * `2`: Enabled for the current cycle and the next one + * `3`: Enabled for the current cycle and the 2 next ones + * You get the idea + +### Performance +* Every uncached map finding takes around 1ms (depends a lot on your ram and cpu) +* Every cached map finding takes around 100us + +* Mainly useful for lots of readAddresses and the game has uncapped game state update rate, where literally every millisecond matters + +### Example +```lua +function startup() + refreshRate = 60; + mapsCacheCycles = 1; +end + +-- Assume all this readAddresses are different, +-- Instead of taking near 10ms it will instead take 1-2ms, because only this cycle is cached and the first readAddress is a cache miss, if the mapsCacheCycles were higher than 1 then a cycle could take less than half a millisecond +function state() + current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); + current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); + current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); + current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); + current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); + current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); + current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); + current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); + current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); + current.isLoading = readAddress("bool", "UnityPlayer.dll", 0x019B4878, 0xD0, 0x8, 0x60, 0xA0, 0x18, 0xA0); +end + +``` diff --git a/src/auto-splitter.c b/src/auto-splitter.c index 09ef175..da7de36 100644 --- a/src/auto-splitter.c +++ b/src/auto-splitter.c @@ -19,6 +19,8 @@ char auto_splitter_file[PATH_MAX]; int refresh_rate = 60; +int maps_cache_cycles = 0; // 0=off, 1=current cycle, +1=multiple cycles +int maps_cache_cycles_value = 0; // same as `maps_cache_cycles` but this one represents the current value rather than the reference from the script atomic_bool auto_splitter_enabled = true; atomic_bool call_start = false; atomic_bool call_split = false; @@ -246,6 +248,14 @@ void startup(lua_State* L) refresh_rate = lua_tointeger(L, -1); } lua_pop(L, 1); // Remove 'refreshRate' from the stack + + lua_getglobal(L, "mapsCacheCycles"); + if (lua_isnumber(L, -1)) + { + maps_cache_cycles = lua_tointeger(L, -1); + maps_cache_cycles_value = maps_cache_cycles; + } + lua_pop(L, 1); // Remove 'mapsCacheCycles' from the stack } void state(lua_State* L) @@ -414,9 +424,18 @@ void run_auto_splitter() reset(L); } + // Clear the memory maps cache if needed + maps_cache_cycles_value--; + if (maps_cache_cycles_value < 1) { + p_maps_cache_size = 0; // We dont need to "empty" the list as the elements after index 0 are considered invalid + maps_cache_cycles_value = maps_cache_cycles; + // printf("Cleared maps cache\n"); + } + struct timespec clock_end; clock_gettime(CLOCK_MONOTONIC, &clock_end); long long duration = (clock_end.tv_sec - clock_start.tv_sec) * 1000000 + (clock_end.tv_nsec - clock_start.tv_nsec) / 1000; + // printf("duration: %llu\n", duration); if (duration < rate) { usleep(rate - duration); diff --git a/src/auto-splitter.h b/src/auto-splitter.h index 6b21fe2..d43c962 100644 --- a/src/auto-splitter.h +++ b/src/auto-splitter.h @@ -10,6 +10,7 @@ extern atomic_bool call_split; extern atomic_bool toggle_loading; extern atomic_bool call_reset; extern char auto_splitter_file[PATH_MAX]; +extern int maps_cache_cycles_value; void check_directories(); void run_auto_splitter(); diff --git a/src/memory.c b/src/memory.c index 939cfad..e5a36ad 100644 --- a/src/memory.c +++ b/src/memory.c @@ -4,6 +4,7 @@ #include #include #include +#include #include @@ -14,7 +15,7 @@ bool memory_error; extern last_process process; #define READ_MEMORY_FUNCTION(value_type) \ - value_type read_memory_##value_type(uint64_t mem_address) \ + value_type read_memory_##value_type(uint64_t mem_address, int32_t* err) \ { \ value_type value; \ \ @@ -29,6 +30,7 @@ extern last_process process; ssize_t mem_n_read = process_vm_readv(process.pid, &mem_local, 1, &mem_remote, 1, 0); \ if (mem_n_read == -1) \ { \ + *err = (int32_t)errno; \ memory_error = true; \ } \ else if (mem_n_read != (ssize_t)mem_remote.iov_len) \ @@ -83,6 +85,23 @@ char* read_memory_string(uint64_t mem_address, int buffer_size) return buffer; } +/* + Prints the according error to stdout + True if the error was printed + False if the error is unknown +*/ +bool handle_memory_error(uint32_t err) { + if (err == 0) return false; + switch (err) { + case EFAULT: printf("EFAULT: Invalid memory space/address\n"); break; + case EINVAL: printf("EINVAL: An error ocurred while reading memory\n"); break; + case ENOMEM: printf("ENOMEM: Please get more memory\n"); break; + case EPERM: printf("EPERM: Permission denied\n"); break; + case ESRCH: printf("ESRCH: No process with specified PID exists\n"); break; + } + return true; +} + int read_address(lua_State* L) { memory_error = false; @@ -106,15 +125,19 @@ int read_address(lua_State* L) i = 4; } + int error = 0; + for (; i <= lua_gettop(L); i++) { if (address <= UINT32_MAX) { - address = read_memory_uint32_t((uint64_t)address); + address = read_memory_uint32_t((uint64_t)address, &error); + if (memory_error) break; } else { - address = read_memory_uint64_t(address); + address = read_memory_uint64_t(address, &error); + if (memory_error) break; } address += lua_tointeger(L, i); } @@ -122,62 +145,66 @@ int read_address(lua_State* L) if (strcmp(value_type, "sbyte") == 0) { - int8_t value = read_memory_int8_t(address); + int8_t value = read_memory_int8_t(address, &error); lua_pushinteger(L, (int)value); } else if (strcmp(value_type, "byte") == 0) { - uint8_t value = read_memory_uint8_t(address); + uint8_t value = read_memory_uint8_t(address, &error); lua_pushinteger(L, (int)value); } else if (strcmp(value_type, "short") == 0) { - short value = read_memory_int16_t(address); + short value = read_memory_int16_t(address, &error); lua_pushinteger(L, (int)value); } else if (strcmp(value_type, "ushort") == 0) { - unsigned short value = read_memory_uint16_t(address); + unsigned short value = read_memory_uint16_t(address, &error); lua_pushinteger(L, (int)value); } else if (strcmp(value_type, "int") == 0) { - int value = read_memory_int32_t(address); + int value = read_memory_int32_t(address, &error); lua_pushinteger(L, value); } else if (strcmp(value_type, "uint") == 0) { - unsigned int value = read_memory_uint32_t(address); + unsigned int value = read_memory_uint32_t(address, &error); lua_pushinteger(L, (int)value); } else if (strcmp(value_type, "long") == 0) { - long value = read_memory_int64_t(address); + long value = read_memory_int64_t(address, &error); lua_pushinteger(L, (int)value); } else if (strcmp(value_type, "ulong") == 0) { - unsigned long value = read_memory_uint64_t(address); + unsigned long value = read_memory_uint64_t(address, &error); lua_pushinteger(L, (int)value); } else if (strcmp(value_type, "float") == 0) { - float value = read_memory_float(address); + float value = read_memory_float(address, &error); lua_pushnumber(L, (double)value); } else if (strcmp(value_type, "double") == 0) { - double value = read_memory_double(address); + double value = read_memory_double(address, &error); lua_pushnumber(L, value); } else if (strcmp(value_type, "bool") == 0) { - bool value = read_memory_bool(address); + bool value = read_memory_bool(address, &error); lua_pushboolean(L, value ? 1 : 0); } else if (strstr(value_type, "string") != NULL) { int buffer_size = atoi(value_type + 6); + if (buffer_size < 2) { + printf("Invalid string size, please read documentation"); + exit(1); + } char* value = read_memory_string(address, buffer_size); lua_pushstring(L, value != NULL ? value : ""); free(value); @@ -192,6 +219,7 @@ int read_address(lua_State* L) if (memory_error) { lua_pushinteger(L, -1); + handle_memory_error(error); } return 1; diff --git a/src/process.c b/src/process.c index 239d293..811cdff 100644 --- a/src/process.c +++ b/src/process.c @@ -1,3 +1,4 @@ +#include #include #include #include @@ -11,6 +12,9 @@ #include "auto-splitter.h" struct last_process process; +#define MAPS_CACHE_MAX_SIZE 32 +ProcessMap p_maps_cache[MAPS_CACHE_MAX_SIZE]; +uint32_t p_maps_cache_size = 0; void execute_command(const char* command, char* output) { @@ -38,6 +42,13 @@ uintptr_t find_base_address(const char* module) { const char* module_to_grep = module == 0 ? process.name : module; + for (int32_t i = 0; i < p_maps_cache_size; i++) { + const char* name = p_maps_cache[i].name; + if (strstr(name, module_to_grep) == NULL) { + return p_maps_cache[i].start; + } + } + char path[22]; // 22 is the maximum length the path can be (strlen("/proc/4294967296/maps")) snprintf(path, sizeof(path), "/proc/%d/maps", process.pid); @@ -45,21 +56,20 @@ uintptr_t find_base_address(const char* module) FILE *f = fopen(path, "r"); if (f) { - char current_line[1024]; + char current_line[PATH_MAX + 100]; while (fgets(current_line, sizeof(current_line), f) != NULL) { if (strstr(current_line, module_to_grep) == NULL) continue; fclose(f); - size_t dash_pos = strcspn(current_line, "-"); - - if (dash_pos != strlen(current_line)) { - char first_number[32]; - strncpy(first_number, current_line, dash_pos); - first_number[dash_pos] = '\0'; - uintptr_t addr = strtoull(first_number, NULL, 16); - return addr; - break; + uintptr_t addr_start = strtoull(current_line, NULL, 16); + if (maps_cache_cycles_value != 0 && p_maps_cache_size < MAPS_CACHE_MAX_SIZE) { + ProcessMap map; + if (parseMapsLine(current_line, &map)) { + p_maps_cache[p_maps_cache_size] = map; + p_maps_cache_size++; + } } + return addr_start; } fclose(f); } @@ -69,7 +79,7 @@ uintptr_t find_base_address(const char* module) void stock_process_id(const char* pid_command) { - char pid_output[4096]; + char pid_output[PATH_MAX + 100]; pid_output[0] = '\0'; while (atomic_load(&auto_splitter_enabled)) @@ -118,3 +128,19 @@ int process_exists() int result = kill(process.pid, 0); return result == 0; } + +bool parseMapsLine(char* line,ProcessMap *map) { + size_t end; + char mode[8]; + unsigned long offset; + unsigned int major_id, minor_id, node_id; + + // Thank you kernel source code + int sscanf_res = sscanf(line, "%lx-%lx %7s %lx %u:%u %u %s", &map->start, + &end, mode, &offset, &major_id, + &minor_id, &node_id, map->name); + if (!sscanf_res) + return false; + + return true; +} diff --git a/src/process.h b/src/process.h index d8a93f5..2e87db4 100644 --- a/src/process.h +++ b/src/process.h @@ -1,6 +1,8 @@ #ifndef __PROCESS_H__ #define __PROCESS_H__ +#include +#include #include #include @@ -14,9 +16,16 @@ struct last_process }; typedef struct last_process last_process; +typedef struct ProcessMap { + uint64_t start; + char name[PATH_MAX]; +} ProcessMap; +extern uint32_t p_maps_cache_size; + uintptr_t find_base_address(const char* module); int process_exists(); int find_process_id(lua_State* L); int getPid(lua_State* L); +bool parseMapsLine(char* line, ProcessMap *map); #endif /* __PROCESS_H__ */