UserCaller is a small x86-only C++ library for calling and hooking functions that use non-standard register-based ABIs, especially the kinds of __usercall signatures often seen in reverse-engineered code.
It uses JIT-generated thunks to bridge between normal C++ __cdecl code and custom target conventions. The project currently targets 32-bit x86 only.
Normal C++ code expects ordinary ABI rules:
- integer and pointer arguments usually arrive on the stack
- scalar floating-point returns usually come back in
st0 - plain function pointers have a fixed calling convention
Reverse-engineered game and engine code often does something else:
- arguments in
eax,ecx,edx,esi,edi, orebx - scalar floating-point arguments in
xmmregisters - returns in
eax,edx:eax,st0, orxmm0 - caller-clean or callee-clean stack behavior depending on the target
UserCaller lets you describe that target ABI in templates and get back a normal callable C++ wrapper.
UserCaller has three main thunk directions:
-
uc::make<...>(target)Wraps one fixed target address in a normal C++ callable object. -
uc::make_invoker<...>()Builds one shared thunk for a signature and lets you pass the target address at call time. -
uc::make_callback<...>(callback_fn)Builds the reverse bridge so foreign code can call a normal C++ callback through a weird register ABI.
Internally, each of these emits x86 machine code with AsmJit and stores the result in executable memory.
uc::eax_arg<T>uc::ebx_arg<T>uc::ecx_arg<T>uc::edx_arg<T>uc::esi_arg<T>uc::edi_arg<T>uc::stack_arg<T>uc::xmm0_arg<T>throughuc::xmm7_arg<T>
uc::void_ret<void>uc::eax_ret<T>uc::edx_eax_ret<long long>uc::st0_ret<float>/uc::st0_ret<double>uc::xmm0_ret<float>/uc::xmm0_ret<double>
uc::cleanup::calleruc::cleanup::callee
- GPR register args (
eax/ebx/ecx/edx/esi/edi) must be 4 bytes or smaller. - Stack args may be up to 8 bytes.
edx:eaxreturn is supported for 64-bit integral or enum values.st0return is supported forfloatanddouble.xmmargs are supported for scalarfloatanddoubleonly.xmm0return is supported for scalarfloatanddoubleonly.
Not supported right now:
- x64
- structs/classes by value
__m128/ packed vector ABI support- aggregate returns
- duplicate use of the same non-stack argument register in one signature
auto fn = uc::make<
uc::eax_ret<int>,
uc::edx_arg<int>
>(target_address);
int result = fn(7);This means:
- the target returns
intineax - the target expects its argument in
edx - your C++ code still calls it like a normal function
auto invoker = uc::make_invoker<
uc::eax_ret<int>,
uc::edx_arg<int>
>();
int result = invoker(target_address, 7);This is useful when many functions share the same ABI shape.
using target_abi = uc::abi<
uc::eax_ret<int>,
uc::edx_arg<int>
>;
auto fn = target_abi::make(target_address);
auto invoker = target_abi::make_invoker();uc::abi<...> bundles:
ret_tinvoker_tcallback_fn_tfunction_tfunction_callee_tcallback_tcallback_callee_t
and exposes factory helpers for each.
uc::make_callback() builds a JIT stub that looks like the foreign ABI on the outside, but calls a normal C++ __cdecl function on the inside.
Example:
float __cdecl MyCallback(float value)
{
return value * 3.0f;
}
auto cb = uc::make_callback<
uc::xmm0_ret<float>,
uc::xmm1_arg<float>
>(&MyCallback);
void* foreign_fn_ptr = cb.raw();That stub:
- receives the incoming arg from
xmm1 - repackages it into a normal C++ stack call
- calls
MyCallback - converts the C++
st0float return intoxmm0 - returns to the foreign caller
The callback object owns the executable stub memory, so keep it alive as long as foreign code may call it.
UserCaller now supports scalar SSE bridging for x86:
xmm0-xmm7argumentsxmm0returnfloatanddoubleonly
This is important because the public C++ side is still ordinary x86 __cdecl, which does not naturally match a usercall-like SSE ABI.
When you call:
auto fn = uc::make<
uc::xmm0_ret<float>,
uc::xmm1_arg<float>
>(target);the generated thunk:
- receives the C++ argument from the stack
- loads it into
xmm1 - calls the target
- reads the target's
xmm0return - converts that return back to
st0 - returns to the normal C++ caller
When you build:
auto cb = uc::make_callback<
uc::xmm0_ret<float>,
uc::xmm1_arg<float>
>(&MyCallback);the generated callback stub:
- receives the foreign arg from
xmm1 - spills it onto the stack as a normal C++ argument
- calls the
__cdeclcallback - takes the callback's
st0return - moves that result into
xmm0 - returns to the foreign caller
The cleanup mode describes who removes stack-passed target arguments:
uc::make<...>()anduc::make_invoker<...>()default to caller-cleanuc::make_callee<...>()anduc::make_invoker_callee<...>()are for callee-clean targetsuc::make_callback_callee<...>()creates callback stubs that return with stack cleanup
For stack arguments, UserCaller emits the appropriate add esp, ... or ret N behavior based on that mode.
UserCaller also includes a simple helper for patching an existing E8 rel32 call instruction:
uc::patch_call(call_site, new_target);This:
- verifies the first byte is
0xE8 - temporarily changes page protection with
VirtualProtect - writes the new relative target
- restores protection
- flushes the instruction cache
It is intentionally small and only handles direct x86 call rel32 sites.
SafetyHook integration is intentionally kept out of the core header. It lives in:
usercaller/safetyhook_inline.hpp
To enable it, define:
#define UC_WITH_SAFETYHOOK 1before including the optional header, and make sure safetyhook.hpp is available on the include path.
SafetyHook is responsible for:
- patching the target function
- creating the trampoline
- enabling/disabling/resetting the inline hook
UserCaller is responsible for:
- converting between the target ABI and normal C++ callback ABI
- calling the trampoline through the correct usercall ABI
That means uc::inline_hook<Abi> does not use SafetyHook's call() helpers for the original function. Instead it does:
callback_ = Abi::make_callback(callback)invoker_ = Abi::make_invoker()hook_ = safetyhook::create_inline(target, callback_.raw())call_original(args...)callsinvoker_(hook_.trampoline().address(), args...)
This is the key point that makes SafetyHook usable with non-standard usercall layouts.
using target_abi = uc::abi<
uc::eax_ret<int>,
uc::edx_arg<int>
>;
static uc::inline_hook<target_abi> g_hook;
int __cdecl Hook_Target(int value)
{
int og = g_hook.call_original(value);
return og * 4;
}
void install()
{
g_hook.create(reinterpret_cast<void*>(target_addr), &Hook_Target);
}At a high level, each thunk:
- creates a stable x86 frame with
ebp - reads incoming C++ arguments from stack slots
- marshals them into the target ABI:
- pushes stack args right-to-left
- moves integer/pointer args into GPRs
- loads scalar FP args into
xmmregisters when requested
- calls either:
- a baked target address
- a runtime
targetparameter - a normal C++ callback function
- fixes up cleanup according to the descriptor
- bridges return values if needed:
eaxreturns naturallyedx:eaxreturns naturallyst0returns naturallyxmm0 -> st0for normal C++ callersst0 -> xmm0for foreign SSE callback callers
The executable code is stored in a detail::code_block, which frees the JIT allocation when the last owner goes away.
-
UserCaller/usercaller/usercaller.hppPublic API and ABI description types. -
UserCaller/usercaller/usercaller.cppJIT thunk generation and low-level patch helpers. -
UserCaller/usercaller/safetyhook_inline.hppOptional SafetyHook inline-hook wrapper. -
UserCaller/tests/test.cppExample targets and runtime validation coverage.
The solution contains a small console test program that exercises:
- GPR argument passing
- stack arguments
- caller-clean and callee-clean conventions
- 64-bit
edx:eaxreturns st0float/double returns- scalar
xmmfloat/double argument and return bridging - callback bridging
- optional SafetyHook inline hooking
Build the Win32 target and run the generated UserCaller.exe.
- Keep callback objects alive while native code may still call them.
- Use
abi<...>when you expect to create both callbacks and invokers for the same signature. - Use
make_invoker()for trampoline/original calls where only the target address changes. - Treat
xmmsupport as scalar-only for now. If you need__m128or larger vector ABI support, that should be added as a separate feature.