11 KiB
Ghidra Recon — Sword of the Stars (2006), "Sword of the Stars.exe"
ImageBase 0x00400000, 32-bit MSVC-2010 DX9. Program sots in /srv/re-lab/ghidra-projects.
All addresses below are Ghidra/virtual addresses (VA = ImageBase + RVA).
Method: headless GhidraScripts (ReVa MCP was down this run). reva-server stopped for runs, restarted after.
1. Battle-load / many-core lead: ForceSingleCore
Key strings
| String | Address |
|---|---|
ForceSingleCore |
0x00a36eec (key literal), config-tag blob at 0x00a36ee8 |
Limiting process affinity to CPU-%i... |
0x00a36880 |
SetProcessAffinityMask (data ref name) |
0x00ad8202 |
The affinity worker — FUN_0089ee70 @ 0x0089ee70
void FUN_0089ee70(void) { // core index arrives in ESI from caller
int unaff_ESI;
FUN_008ba1e0("Limiting process affinity to CPU-%i...", unaff_ESI + 1);
HANDLE hProcess = GetCurrentProcess();
BOOL ok = SetProcessAffinityMask(hProcess, 1 << ((byte)unaff_ESI & 0x1f)); // pin to ONE core
if (ok) FUN_008ba1e0(" OK.\n");
else FUN_008ba1e0(" FAILED!\n");
}
This is the whole mitigation: it pins the entire process to a single logical core via a
one-bit affinity mask. SetProcessAffinityMask is the ONLY affinity/topology API the
binary uses (see below).
Where the config is read & branches — FUN_008a0e50 @ 0x008a0e50
This is the application startup/init routine (also creates the window, D3D device, reads display.cfg/audio.cfg, spawns a thread). Relevant excerpt:
// ref site 0x008a0ee6
uVar3 = FUN_00898bc0(&DAT_00a36ee8, "ForceSingleCore", local_64c, local_630); // read config key
FUN_008e7160(uVar3);
...
iVar4 = FUN_008e7140(local_64c); // convert the read value -> int
if (0 < iVar4) { // <-- the many-core branch: any value > 0
FUN_0089ee70(); // pin process to a single core
}
FUN_00898bc0 = generic config/ini lookup (key -> value string); FUN_008e7140
extracts an integer from the parsed value. If the integer is > 0 the process is bound
to one core (the core index is the value carried in ESI into FUN_0089ee70, and the
"CPU-%i" log uses index+1).
Read on the many-core slowdown
The binary performs no runtime CPU-topology detection at all — there is no
GetSystemInfo, GetNativeSystemInfo, GetLogicalProcessorInformation(Ex),
GetActiveProcessorCount, GetProcessAffinityMask, or SetThreadAffinityMask import
anywhere. So there is no adaptive "if many cores do X else Y" branch in the code.
ForceSingleCore is a blunt, opt-in override read once at startup that forces the whole
process onto one core.
Implication for the battle-load bug: the slowdown is not gated by a core-count code path we can flip; it is emergent multi-core behavior (thread contention / lock convoy / false sharing / scheduler thrash, or the D3D9 driver's own worker threads) that gets worse as the OS spreads the process's threads across more cores. Pinning to one core removes the cross-core traffic and masks it. Next place to dig for the actual contention is the thread creation sites:
Threading entry points (CreateThread callers) — follow-ups
| Caller | Address | Note |
|---|---|---|
FUN_00902350 |
0x00902350 |
creates a thread at 0x0090242d |
FUN_008a0e50 |
0x008a0e50 |
startup routine, creates a thread at 0x008a14ef |
FUN_00736e30 |
0x00736e30 |
creates a thread at 0x00736e84 — likely a game/loader worker; prime suspect for the battle-load path |
SetProcessAffinityMask external thunk: EXTERNAL:00000052; CreateThread: EXTERNAL:00000019.
2. vftable dumps (primary vftable per class)
MSVC layout note: each class has a ..::vftable_meta_ptr label (0 methods — that slot
holds the RTTI Complete-Object-Locator pointer, sitting one word before the real table)
immediately followed by ..::vftable, the actual function-pointer array. Classes with
multiple vftable groups use multiple inheritance (one sub-table per polymorphic base).
Only the primary (largest / class-named) table is listed here; secondary tables are
small MI thunks (IStreamable etc.). Repeated addresses = shared/thunk stubs:
0x0080c5a0 and 0x005f8ac0 are stub/return-nullptr style thunks reused across many
slots; 0x00496e00 a shared pure/empty stub; 0x0085b290 a common getter.
Game::ClientPlayer — vftable @ 0x00a32b68, 8 methods
[0]0x00882630 [1]0x008f6630 [2]0x008188f0 [3]0x00818910
[4]0x0085b290 [5]0x008801d0 [6]0x0080c5a0 [7]0x0080c5a0
Game::AIPlayer — vftable @ 0x00a199c4, 11 methods
[0]0x004f7230 [1]0x005f8ac0 [2]0x005f8ac0 [3]0x00723ed0 [4]0x00692350 [5]0x00496e00
[6]0x00496e00 [7]0x00496e00 [8]0x00496e00 [9]0x00496e00 [10]0x006922e0
(secondary MI tables: VectorHelper @0x00a1a700, StreamableHelper @0x00a1a730, each 3 methods.)
Game::StarSystem — primary vftable @ 0x00a200e4, 8 methods
[0]0x00753160 [1]0x00725130 [2]0x00751b30 [3]0x00751b40
[4]0x00751b40 [5]0x005f8ac0 [6]0x0085b290 [7]0x004f41d0
(also a 3-method table @0x00a200d4; StreamableHelper @0x00a23cc4.)
Game::Planet — primary vftable @ 0x009ef144, 36 methods
[0]0x008a1860 [1]0x004e8960 [2]0x004e72b0 [3]0x0080c5a0 [4]0x004d75a0 [5]0x004d75d0
[6]0x004e16e0 [7]0x004d9cb0 [8]0x004e5e80 [9]0x0080c5a0 [10]0x005f8ac0 [11]0x00423710
[12]0x004229a0 [13]0x004f59c0 [14]0x005f8ac0 [15]0x004ddbf0 [16]0x0085b290 [17]0x00425770
[18]0x00425340 [19]0x00424b70 [20]0x004267f0 [21]0x00426810 [22]0x00426830 [23]0x00422bd0
[24]0x0080d1f0 [25]0x004242b0 [26]0x00496e00 [27]0x00496e00 [28]0x00496e00 [29]0x004d9c20
[30]0x0080c5a0 [31]0x0080c5a0 [32]0x005f8ac0 [33]0x0085b290 [34]0x00422b70 [35]0x005dbb90
(Planet also: Creator vftable @0x009ee234 (1); two 2-method tables @0x009ef12c/0x009ef138. DOPlanet is a separate class @0x00a28cfc, 7 methods.)
Game::ShipDesignDef — vftable @ 0x009fef64, 3 methods
[0]0x00827240 [1]0x00827390 [2]0x0057c250
(no deep hierarchy — RTTI shows 0 bases; the many Mars::StreamableHelper<ShipDesignDef::*>
and VectorHelper<...GunBank> tables are serialization helpers for its member containers.)
Game::Actor — primary vftable @ 0x009e2114, 36 methods
[0]0x008a1860 [1]0x00426d10 [2]0x004267d0 [3]0x0080c5a0 [4]0x00426c80 [5]0x00426c90
[6]0x0080c5a0 [7]0x005f8ac0 [8]0x0080c5a0 [9]0x0080c5a0 [10]0x005f8ac0 [11]0x00423710
[12]0x004229a0 [13]0x004f59c0 [14]0x00422af0 [15]0x004267e0 [16]0x0085b290 [17]0x00425770
[18]0x00425340 [19]0x00424b70 [20]0x004267f0 [21]0x00426810 [22]0x00426830 [23]0x00422bd0
[24]0x0080d1f0 [25]0x004242b0 [26]0x00496e00 [27]0x005dbbc0 [28]0x00496e00 [29]0x00422830
[30]0x0080c5a0 [31]0x0080c5a0 [32]0x005f8ac0 [33]0x0085b290 [34]0x00422b70 [35]0x005dbb90
(Actor also: Creator @0x009e1fa8 (1); two 2-method tables @0x009e20f8/0x009e2104. Interfaces IActorReference @0x009e604c (4), IActorEvents @0x009e90ac (2).) Note: Planet's primary table shares slots 11-13,16-25,30-35 verbatim with Actor's, and both are 36-wide — confirming Planet : Actor (Planet overrides slots 1-8,14,15,27-29).
Mars::EntityBase — primary vftable @ 0x00a36fd0, 10 methods
[0]0x008a1860 [1]0x008a1da0 [2]0x008a1c30 [3]0x0080c5a0 [4]0x0080c5a0
[5]0x0080c5a0 [6]0x0080c5a0 [7]0x005f8ac0 [8]0x0080c5a0 [9]0x0080c5a0
(Creator @0x00a36fb0 (1); two 2-method MI tables @0x00a36fb8/0x00a36fc4.)
Mars::PhysicalEntity — primary vftable @ 0x00a3710c, 12 methods
[0]0x008a1860 [1]0x00423340 [2]0x008a24d0 [3]0x0080c5a0 [4]0x0080c5a0 [5]0x0080c5a0
[6]0x0080c5a0 [7]0x005f8ac0 [8]0x0080c5a0 [9]0x0080c5a0 [10]0x005f8ac0 [11]0x004227a0
(Creator @0x00a36f50 (1); two 2-method MI tables @0x00a370f4/0x00a37100.) Note: slot [0]=0x008a1860 shared with EntityBase's slot [0] → PhysicalEntity : EntityBase (12 = 10 + 2 added slots [1],[2],[11] with 1 EntityBase slot overridden).
3. RTTI inheritance graph (from Base_Class_Array structures)
RTTI is fully populated — namespaces are NOT empty; the "known empty-namespace issue"
does not apply to this program. Chains read directly from each class's
RTTI_Class_Hierarchy_Descriptor -> RTTI_Base_Class_Array -> BaseClassDescriptors ->
TypeDescriptors. Listed most-derived → root; mdisp = byte offset of that base subobject.
ClientPlayer (3 bases): ClientPlayer → StrategyPlayer → Mars::NetworkObject.
AIPlayer (3): AIPlayer → AIObject → Mars::IStreamable.
StarSystem (5): StarSystem → StarMapNode; + Mars::NetworkObject(@0),
Mars::HandleObject(@12), Mars::IStreamable(@8).
ShipDesignDef (1): ShipDesignDef — no base classes (leaf/standalone data-def class).
Mars::EntityBase (7): EntityBase → IScheduledEvent →
ListDLI<IScheduledEvent,IScheduledEventScope>::Link(@4) → DLink(@4);
IScriptable(@16),NetworkObject(@20),HandleObject(@28).
Mars::PhysicalEntity (11): PhysicalEntity → EntityBase → IScheduledEvent →
ListDLI<...>::Link(@4) → DLink(@4); + IScriptable(@16), NetworkObject(@20),
HandleObject(@28), RigidBody(@44) → Body(@44), SceneObject::Owner(@212).
Game::Actor (13): Actor → TargetableEntity → Mars::PhysicalEntity →
Mars::EntityBase → IScheduledEvent → ListDLI<...>::Link(@4) → DLink(@4);
IScriptable(@16),NetworkObject(@20),HandleObject(@28),RigidBody(@44) →Body(@44),SceneObject::Owner(@212).
Game::Planet (14): Planet → Actor → TargetableEntity → PhysicalEntity →
EntityBase → IScheduledEvent → ListDLI<...>::Link(@4) → DLink(@4);
IScriptable(@16),NetworkObject(@20),HandleObject(@28),RigidBody(@44) →Body(@44),SceneObject::Owner(@212).
Consolidated single-inheritance spine (Mars physics/entity → Game gameplay):
Body → RigidBody (Mars physics; mixed in at offset 44)
DLink → ListDLI::Link (Mars intrusive list node; offset 4)
IScheduledEvent IScriptable NetworkObject HandleObject IStreamable (Mars interfaces)
\______________ all mixed into ______________/
Mars::EntityBase → Mars::PhysicalEntity → Game::TargetableEntity → Game::Actor → Game::Planet
StrategyPlayer → ... , ClientPlayer : StrategyPlayer, NetworkObject
AIObject → AIPlayer : AIObject, IStreamable
StarMapNode → StarSystem : StarMapNode (+NetworkObject/HandleObject/IStreamable)
Artifacts / reproduction
Scripts (pushed to CT111 /root/): CoreRecon.java, VtRecon.java, RttiHier.java.
Run pattern: systemctl stop reva-server; sleep 3; then
analyzeHeadless /srv/re-lab/ghidra-projects sots -process "Sword of the Stars.exe" -noanalysis -scriptPath /root -postScript <Script>.java; systemctl start reva-server after.