Skip to content

Make the x86_64 hook manager generator work on Linux - #285

Open
makadore wants to merge 22 commits into
alliedmodders:1.12-devfrom
makadore:gen64
Open

Make the x86_64 hook manager generator work on Linux#285
makadore wants to merge 22 commits into
alliedmodders:1.12-devfrom
makadore:gen64

Conversation

@makadore

Copy link
Copy Markdown

core/AMBuilder leaves sourcehook_hookmangen_x86_64.cpp out of the build on Linux, and the file would not compile there anyway: sh_asm_x86_64.h names a method xor, which outside of MSVC is an alternative token for ^. So on 64 bit Linux Metamod:Source ships with no hook manager generator at all, and anything that builds hook managers at runtime cannot work on a CS2 server there.

This is @rtldg's branch from #212, rebased onto current master with his commits intact, plus fixes for what still kept the generated code from working. He closed that PR himself. The work was close, and this is the rest of it.

What was wrong

Each of these was found by a failing case in the existing TestHookManGen, which is turned off for 64 bit with a // TODO: Fix for 64-bit.

  • GenerateHookFunc opened with a leftover breakpoint(). That one is only on the branch, not on master.
  • PushParameters ignored v_this on System V and restored the incoming argument registers instead, so a hook delegate ran with the hooked object as its this. It read a foreign object for its own fields and returned whatever that produced as its META_RES — in the test suite that came out as MRES_SUPERCEDE, so the original never ran. The MSVC path already did this correctly.
  • An object passed by value with a destructor or a copy constructor arrives by invisible reference, so every hook and the original shared the caller's object. They each get a copy now, destroyed after the call it was made for, which is what the x86 generator has always done. The end-of-function parameter destruction becomes MSVC only, since under the Itanium ABI the caller destroys arguments.
  • The three internal return objects were destroyed before DoReturn copied one of them out.
  • Returning an object by value was refused outright unless it was exactly 12 bytes, and the threshold for returning in memory was 64 bytes rather than two eightbytes.
  • CallConv_HasVafmt was accepted but nothing was ever formatted, so hooks were handed the raw format string. A va_list is now built over a register save area laid out the way the ABI describes, and vsnprintf runs once before any hook sees anything.
  • A reference to a float takes an integer register, not an SSE one.
  • x64GenContext leaked its two PassInfo arrays — Clear() is commented out in the destructor and nothing else released them — and both generators released those arrays with delete rather than delete[]. The second half of that applies to x86 as well, so it affects builds that ship today.
  • Which registers hold an aggregate returned by value depends on the class of each of its eightbytes, and a PassInfo carries sizes but not field types. Nothing was written to the SSE registers on the way out, so a post hook returning a value of the same type left its own in xmm0 and the caller got that instead of the function's. Both register files are loaded now: the integer ones follow SourceHook's copy of the value, the SSE ones carry the original's through.

Protos that would need an argument register the ABI does not have are declined rather than generated wrongly.

Checks

clang 18.1, x86_64, --enable-optimize 16/16
gcc 13.3, x86_64 16/16
clang 18.1, x86 16/16
valgrind, --smc-check=all-non-file --leak-check=full no errors, nothing left leaking from the generator
metamod for CS2, x86_64 Linux builds and links

TestHookManGen is around forty protos: empty ones and ones with five parameters, floats and doubles, PODs of 7 and 600 bytes by value and by reference, objects with constructors, destructors and assignment operators, returns of char, short, int, float, double and PODs of 1, 4, 8 and 13 bytes, objects with a destructor in the return, vafmt in every combination, high vtable indices — and for each of them the full matrix of pre and post hooks with ignore, supercede, override and recall, checking the exact sequence of constructor and destructor calls.

Building and running it again for 64 bit Linux needed one more thing: the generator's dependency on metamod is only there for a debug helper whose calls are commented out, so it moves under #if !defined(SOURCEHOOK_TESTS). The CI already runs test_sourcehook for linux-x86_64 when that directory exists, so this starts being covered without any change there.

Stack alignment was checked rather than argued about. Every generated function was dumped, disassembled and walked over its control flow graph, tracking rsp through pushes, pops and adjustments: 74 functions, 414 call instructions, every one of them with rsp 16 byte aligned and every ret with the alignment the ABI asks for. That covers the two boxes left unticked in the review on #212, GeneratePubFunc and CallSetupHookLoop — the second had already been dealt with on the branch.

Beyond the suite, this ran on a CS2 dedicated server with a plugin that takes metamod's own IHookManagerAutoGen and hooks through generated code: pre and post hooks on GameFrame reading a field through this, an object passed by value where the hook scribbles over its copy and the original still sees clean data, a vafmt function where the hook is handed the formatted string, an aggregate of two floats returned through a post hook, and a thousand MakeHookMan and ReleaseHookMan cycles repeated three times — the first run grows the page pool, the two after it move nothing.

Known limits

  • Windows x64 is untested here. Everything but the return value handling and the two delete[] fixes is under #if SH_COMP == SH_COMP_GCC, and the test stays off for MSVC.
  • A hook cannot replace an aggregate return value of the SSE class. That was already true, only silently; it is at least predictable now. Doing better needs field types in ProtoInfo, which seemed like your call rather than something to slip in here.
  • Aggregates of 9 to 16 bytes that mix integer and SSE eightbytes have the same problem.

@makadore
makadore changed the base branch from master to 1.12-dev August 27, 2026 08:09
Seven defects kept the generated code from working on 64-bit Linux, each
found by a failing case in the existing TestHookManGen suite:

- GenerateHookFunc opened with a leftover breakpoint().
- PushParameters ignored v_this on System V and restored the incoming
  argument registers instead, so a hook delegate was called with the
  hooked object as its this pointer.
- Objects passed by value with a destructor or copy constructor arrive by
  invisible reference, so every hook and the original shared the caller's
  object. Copy them per call and destroy the copies afterwards, the way
  the x86 generator does; scope the end-of-function parameter destruction
  to MSVC, since the Itanium ABI has the caller destroy arguments.
- The three internal return objects were destroyed before DoReturn copied
  one of them out.
- Returning an object by value was refused outright unless it was exactly
  12 bytes, and the memory threshold was 64 bytes rather than two
  eightbytes.
- Vafmt was accepted but never formatted, so hooks saw the raw format
  string. Build a va_list over the register save area and format once.
- A reference to a float takes an integer register, not an SSE one.

Protos that would need an argument register the ABI does not have are now
declined instead of generated wrongly.

The suite passes on x86_64 Linux with clang and gcc, and under valgrind.
Enabling it required scoping the generator's metamod dependency, which is
only there for a debug helper, out of test builds.
…ocated

x64GenContext leaked its two PassInfo arrays: the destructor has Clear()
commented out, and nothing else released them, so every generated hook
manager lost them. Freeing them there rather than restoring the Clear()
call keeps the generated code, which outlives the context, untouched.

Both generators also released the arrays with delete rather than delete[]
when rebuilding them.

valgrind over the test suite goes from 3,840 bytes definitely lost in 81
blocks to 144 in 7, all of which are in the tests themselves.
Which registers hold an aggregate returned by value depends on the class
of each of its eightbytes, and a PassInfo carries sizes but not field
types, so it cannot be worked out here. SourceHook's copy of the value is
read as INTEGER, which is right for most aggregates and wrong for one made
only of floats.

Keeping the registers the original returned in, and loading both files on
the way out, makes that case behave: the integer registers follow
SourceHook's copy, a hook's replacement for it included, while the SSE
ones carry the original through. An aggregate of the SSE class now comes
back intact instead of depending on nothing having touched xmm0 in the
meantime; replacing its value from a hook still does not work, which is
what already happened, only quietly.

Checked on a live CS2 server against a function returning two floats: the
value survives a hook that asks to override it, where before the override
was dropped and the result only happened to be right.
A post hook that returns a value of the same type leaves it in xmm0, which
is where a caller expecting an aggregate of floats looks. Without the
change that goes with this the caller gets the hook's value rather than
the function's, so the case is worth pinning down.

The hooks come off before the values are judged. A CHECK that returns
early would leave them installed, and SourceHook shutting down after the
generator is gone takes the process with it, which turns a readable
failure into a crash after the test has already found the fault.
@makadore

Copy link
Copy Markdown
Author

Verified this end to end on a live 64-bit HL2DM dedicated server, since the diff alone does not make the before-state obvious.

On 1.12-dev this is not a partially working path — it is absent. MetaFactory does not hand out IHookManagerAutoGen at all under __amd64__, and MakeHookMan is a bare return nullptr, so no plugin can obtain a runtime-generated hook manager on 64-bit Linux. With this branch the interface is available and produces working code.

What I ran: Metamod built from this branch (1247:43360b1) plus SourceMod 1.12.0.7251 x64 — 18 plugins, SDK Tools and SDK Hooks loaded, stable across map changes and plugin reloads.

For the generator itself I used a throwaway Metamod plugin that calls MakeHookMan for void() and int(int), installs both through AddHook, and then calls the methods. The pre-handlers ran the expected number of times, the original bodies ran, and the return value came back unchanged. The same plugin on a clean 1.12-dev build cannot even get the interface. Ten load/unload cycles with ReleaseHookMan reused the same page each time, with no drift or instability.

Two notes. HL2DM ships no 64-bit srcds launcher, so I used a small local one that dlopens bin/linux64/dedicated_srv.so and calls DedicatedMain. And the SourceHook suite passes 16 of 16 on gcc and clang x86_64 and gcc x86, though building it needs #283.

@makadore

Copy link
Copy Markdown
Author

The linux failure here is not from this branch. The pr-checks workflow builds the SourceHook tests with --sdks=, which on 1.12-dev splits into a list holding one empty string; that is then looked up as an SDK name, and configure dies with Missing hl2sdks: before any code from this PR is reached. I reproduced it on a clean 1.12-dev checkout with nothing from this branch applied. master already filters empty entries there — #286 is that same one-line change.

With #286 applied on top of this branch the job runs to completion: 16 of 16 on linux-x86 and 16 of 16 on linux-x86_64. On clean 1.12-dev plus #286 only linux-x86 gets built, since core/sourcehook/test/AMBuilder still skips x86_64 on Linux — removing that skip is what this PR does.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants