Skip to content

Auto Splitters: Add optional cache for memory mappings #45

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions docs/auto-splitters.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

```
19 changes: 19 additions & 0 deletions src/auto-splitter.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions src/auto-splitter.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
56 changes: 42 additions & 14 deletions src/memory.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <stdlib.h>
#include <string.h>
#include <sys/uio.h>
#include <errno.h>

#include <luajit.h>

Expand All @@ -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; \
\
Expand All @@ -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) \
Expand Down Expand Up @@ -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;
Expand All @@ -106,78 +125,86 @@ 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);
}


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);
Expand All @@ -192,6 +219,7 @@ int read_address(lua_State* L)
if (memory_error)
{
lua_pushinteger(L, -1);
handle_memory_error(error);
}

return 1;
Expand Down
48 changes: 37 additions & 11 deletions src/process.c
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#include <linux/limits.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
Expand All @@ -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)
{
Expand Down Expand Up @@ -38,28 +42,34 @@ 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);

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);
}
Expand All @@ -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))
Expand Down Expand Up @@ -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;
}
9 changes: 9 additions & 0 deletions src/process.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#ifndef __PROCESS_H__
#define __PROCESS_H__

#include <linux/limits.h>
#include <stdbool.h>
#include <stdint.h>

#include <luajit.h>
Expand All @@ -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__ */