209 lines
11 KiB
Markdown
209 lines
11 KiB
Markdown
# 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`
|
|
```c
|
|
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:
|
|
```c
|
|
// 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<AIPlayerRequestStamp> @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<StarSystem> @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.
|