OpenLoco/src/openloco/interop/interop.cpp

366 lines
10 KiB
C++

#include <algorithm>
#include <cinttypes>
#include <cstring>
#include <stdexcept>
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#endif // _WIN32
#include "../Console.h"
#include "interop.hpp"
#pragma warning(disable : 4731) // frame pointer register 'ebp' modified by inline assembly code
#define PLATFORM_X86
#if defined(__GNUC__)
#ifdef __clang__
#define DISABLE_OPT __attribute__((noinline, optnone))
#else
#define DISABLE_OPT __attribute__((noinline, optimize("O0")))
#endif // __clang__
#else
#define DISABLE_OPT
#endif // defined(__GNUC__)
namespace openloco::interop
{
// This variable serves a purpose of identifying a crash if it has happened inside original code.
// When switching to original code, stack frame pointer is modified and prevents breakpad from providing stack trace.
volatile int32_t _originalAddress = 0;
#ifdef _ENABLE_CALL_BYVALUE_
static int32_t DISABLE_OPT callByVal(int32_t address, int32_t _eax, int32_t _ebx, int32_t _ecx, int32_t _edx, int32_t _esi, int32_t _edi, int32_t _ebp)
{
int32_t result = 0;
_originalAddress = address;
#if defined(PLATFORM_X86)
#ifdef _MSC_VER
// clang-format off
__asm {
push ebp
push address
mov eax, _eax
mov ebx, _ebx
mov ecx, _ecx
mov edx, _edx
mov esi, _esi
mov edi, _edi
mov ebp, _ebp
call[esp]
lahf
pop ebp
pop ebp
/* Load result with flags */
mov result, eax
}
// clang-format on
#else
// clang-format off
__asm__ volatile ("\
\n\
push %%ebx \n\
push %%ebp \n\
push %[address] \n\
mov %[eax], %%eax \n\
mov %[ebx], %%ebx \n\
mov %[ecx], %%ecx \n\
mov %[edx], %%edx \n\
mov %[esi], %%esi \n\
mov %[edi], %%edi \n\
mov %[ebp], %%ebp \n\
call *(%%esp) \n\
lahf \n\
add $4, %%esp \n\
pop %%ebp \n\
pop %%ebx \n\
/* Load result with flags */ \n\
mov %%eax, %[result] \n\
" : [address] "+m" (address), [eax] "+m" (_eax), [ebx] "+m" (_ebx), [ecx] "+m" (_ecx), [edx] "+m" (_edx), [esi] "+m" (_esi), [edi] "+m" (_edi), [ebp] "+m" (_ebp), [result] "+m" (result)
:
: "eax", "ecx", "edx", "esi", "edi", "memory"
);
// clang-format on
#endif
#endif // PLATFORM_X86
_originalAddress = 0;
// lahf only modifies ah, zero out the rest
return result & 0xFF00;
}
#endif
static int32_t DISABLE_OPT callByRef(int32_t address, int32_t* _eax, int32_t* _ebx, int32_t* _ecx, int32_t* _edx, int32_t* _esi, int32_t* _edi, int32_t* _ebp)
{
#ifdef _LOG_INTEROP_CALLS_
openloco::console::group("0x%x", address);
#endif
int32_t result = 0;
_originalAddress = address;
#if defined(PLATFORM_X86)
#ifdef _MSC_VER
// clang-format off
__asm {
// Store C's base pointer
push ebp
push ebx
// Store address to call
push address
// Set all registers to the input values
mov eax, [_eax]
mov eax, [eax]
mov ebx, [_ebx]
mov ebx, [ebx]
mov ecx, [_ecx]
mov ecx, [ecx]
mov edx, [_edx]
mov edx, [edx]
mov esi, [_esi]
mov esi, [esi]
mov edi, [_edi]
mov edi, [edi]
mov ebp, [_ebp]
mov ebp, [ebp]
// Call function
call[esp]
// Store output eax
push eax
push ebp
push ebx
mov ebp, [esp + 20]
mov ebx, [esp + 16]
// Get resulting ecx, edx, esi, edi registers
mov eax, [_edi]
mov[eax], edi
mov eax, [_esi]
mov[eax], esi
mov eax, [_edx]
mov[eax], edx
mov eax, [_ecx]
mov[eax], ecx
// Pop ebx reg into ecx
pop ecx
mov eax, [_ebx]
mov[eax], ecx
// Pop ebp reg into ecx
pop ecx
mov eax, [_ebp]
mov[eax], ecx
pop eax
// Get resulting eax register
mov ecx, [_eax]
mov[ecx], eax
// Save flags as return in eax
lahf
// Pop address
pop ebp
pop ebx
pop ebp
/* Load result with flags */
mov result, eax
}
// clang-format on
#else
// clang-format off
__asm__ volatile ("\
\n\
/* Store C's base pointer*/ \n\
push %%ebp \n\
push %%ebx \n\
\n\
/* Store %[address] to call*/ \n\
push %[address] \n\
\n\
/* Set all registers to the input values*/ \n\
mov %[_eax], %%eax \n\
mov (%%eax), %%eax \n\
mov %[_ebx], %%ebx \n\
mov (%%ebx), %%ebx \n\
mov %[_ecx], %%ecx \n\
mov (%%ecx), %%ecx \n\
mov %[_edx], %%edx \n\
mov (%%edx), %%edx \n\
mov %[_esi], %%esi \n\
mov (%%esi), %%esi \n\
mov %[_edi], %%edi \n\
mov (%%edi), %%edi \n\
mov %[_ebp], %%ebp \n\
mov (%%ebp), %%ebp \n\
\n\
/* Call function*/ \n\
call *(%%esp) \n\
\n\
/* Store output eax */ \n\
push %%eax \n\
push %%ebp \n\
push %%ebx \n\
mov 20(%%esp), %%ebp \n\
mov 16(%%esp), %%ebx \n\
/* Get resulting ecx, edx, esi, edi registers*/ \n\
mov %[_edi], %%eax \n\
mov %%edi, (%%eax) \n\
mov %[_esi], %%eax \n\
mov %%esi, (%%eax) \n\
mov %[_edx], %%eax \n\
mov %%edx, (%%eax) \n\
mov %[_ecx], %%eax \n\
mov %%ecx, (%%eax) \n\
/* Pop ebx reg into ecx*/ \n\
pop %%ecx\n\
mov %[_ebx], %%eax \n\
mov %%ecx, (%%eax) \n\
\n\
/* Pop ebp reg into ecx */\n\
pop %%ecx \n\
mov %[_ebp], %%eax \n\
mov %%ecx, (%%eax) \n\
\n\
pop %%eax \n\
/* Get resulting eax register*/ \n\
mov %[_eax], %%ecx \n\
mov %%eax, (%%ecx) \n\
\n\
/* Save flags as return in eax*/ \n\
lahf \n\
/* Pop address*/ \n\
pop %%ebp \n\
\n\
pop %%ebx \n\
pop %%ebp \n\
/* Load result with flags */ \n\
mov %%eax, %[result] \n\
" : [address] "+m" (address), [_eax] "+m" (_eax), [_ebx] "+m" (_ebx), [_ecx] "+m" (_ecx), [_edx] "+m" (_edx), [_esi] "+m" (_esi), [_edi] "+m" (_edi), [_ebp] "+m" (_ebp), [result] "+m" (result)
:
: "eax", "ecx", "edx", "esi", "edi", "memory"
);
// clang-format on
#endif
#endif // PLATFORM_X86
_originalAddress = 0;
#ifdef _LOG_INTEROP_CALLS_
openloco::console::groupEnd();
#endif
// lahf only modifies ah, zero out the rest
return result & 0xFF00;
}
#ifdef _ENABLE_CALL_BYVALUE_
static int32_t callByVal(int32_t address, const registers& registers)
{
return callByVal(
address,
registers.eax,
registers.ebx,
registers.ecx,
registers.edx,
registers.esi,
registers.edi,
registers.ebp);
}
#endif
int32_t call(int32_t address)
{
registers regs;
return call(address, regs);
}
int32_t call(int32_t address, registers& registers)
{
return callByRef(
address,
&registers.eax,
&registers.ebx,
&registers.ecx,
&registers.edx,
&registers.esi,
&registers.edi,
&registers.ebp);
}
void readMemory(uint32_t address, void* data, size_t size)
{
#ifdef _WIN32
if (!ReadProcessMemory(GetCurrentProcess(), (LPVOID)address, data, size, nullptr))
{
throw std::runtime_error("ReadProcessMemory failed");
}
#else
// We own the pages with PROT_WRITE | PROT_EXEC, we can simply just memcpy the data
std::memcpy(data, (void*)address, size);
#endif // _WIN32
}
void writeMemory(uint32_t address, const void* data, size_t size)
{
#ifdef _WIN32
if (!WriteProcessMemory(GetCurrentProcess(), (LPVOID)address, data, size, nullptr))
{
throw std::runtime_error("WriteProcessMemory failed");
}
#else
// We own the pages with PROT_WRITE | PROT_EXEC, we can simply just memcpy the data
std::memcpy((void*)address, data, size);
#endif // _WIN32
}
save_state::save_state(uintptr_t begin, uintptr_t end)
: begin(begin)
, end(end)
{
state.resize(end - begin);
readMemory(begin, state.data(), state.size());
}
void save_state::reset()
{
interop::writeMemory(begin, state.data(), state.size());
}
void save_state::logDiff(const save_state& lhs, const save_state& rhs)
{
// TODO should we allow different base addresses?
// if so then we need to do extra work for that.
auto length = std::min(lhs.state.size(), rhs.state.size());
for (size_t i = 0; i < length; i++)
{
auto left = lhs.state[i];
auto right = rhs.state[i];
if (left != right)
{
uint32_t addr = lhs.begin + i;
std::printf("0x%06" PRIX32 ": %02" PRIX8 " %02" PRIX8 "\n", addr, (uint8_t)left, (uint8_t)right);
}
}
}
bool operator==(const save_state& lhs, const save_state& rhs)
{
return std::equal(
lhs.getState().begin(),
lhs.getState().end(),
rhs.getState().begin(),
rhs.getState().end());
}
bool operator!=(const save_state& lhs, const save_state& rhs)
{
return !(lhs == rhs);
}
}