Why Castrol Honda Superbike crashes on (most) modern systems

A friend cleaned up and gave me a copy of a game I hadn’t heard of before: Castrol Honda Superbike World ChampionsA motorbike racing game for PC, released in 1998 by Interactive Entertainment Ltd. and Midas Interactive Entertainment.

jewelry case drawing

Looking at the age of the game (and looking at the system requirements) it’s clear that the game comes from the difficult era of early 3D-accelerated PC gaming. For reference, my copy of the game helpfully asks to install DirectX 5.

Before Windows was known for cramming AI and account requirements into every single corner of the system, no matter how unnecessary, it was known for its excellent backward compatibility with older software. Generally, unless there are actual bugs (and sometimes even despite them), Windows does its best to keep older applications running correctly.

However, trying my luck and trying to run it on my Windows 7 machine resulted in either getting stuck on a black screen, or crashing, randomly:

Crash dialog from Windows 7: bike.exe has stopped working

Let’s go back in time and see how far we needed to go to get it running: Installing and running it on my Windows 98 and Windows XP machines was equally as unexpected, and the game works fine, including 3D acceleration. Spectacular 1024x768x16:

in game screencap

debugging the issue #

Debugging is more fun than playing, so let’s get started! ,

I pulled the installation directory over to my main machine and ran Detect It Easy to see what we could learn about the executable:

Screenshot of Detect It Easy loaded with bike.exe

Linker: Microsoft Linker(5.10) And Compiler: Microsoft Visual C/C++(...)[libcmtd] Here are the interesting bits. Notice how it is libcmtdNo libcmtBinary is linked against static debug The version of the VC5 runtime. The debug runtime does a lot of extra checking and logging that can help later.

Let’s attach a debugger and see what’s going on. Given that the game crashes very early on (before the credits intro screen), I expected to see something right away. In cases where the game got stuck in a loop, they were actually stuck in some Windows API call stack.

Anyway, the cases in which it crashed gave a clear starting point:

stack trace from debugger

Game seems to be stuck after call to DirectInput DirectInputCreateEx Celebration. At this point I began to do some static analysis of the actions leading up to this call. While doing this I noticed that the game has quite extensive logging, anything from game initialization to memory allocation.

If you are interested in all logs, here are the configuration settings to enable them all:

  • In Config.datChange ErrorLog, FileLog, MallocLog From off To on,
    • These are “normal” log files produced by the game.
    • ErrorLog production of error.logWhich is a normal log file.
    • FileLog production of files.logTracking all opened files and their access modes.
    • MallocLog production of malloc.logTracking all memory allocations and freezes. The developers also kept details for each allotment site!
  • Set an environment variable called errorfile For any file name (not path). The game will write the log to that file in the game directory.
    • You may also need to create a blank space *.c File in game directory.
    • Just gives a little extra logging.

Bonus: Add a setting called windowed=true In the configuration file to force windowed mode; Works correctly only in 16-bit mode (distorted graphics in true color).

After enabling all logging, I ran the game a few more times, and noticed that the final log messages were coming through. error.log Before the accident these were:

0> Instance : Mouse
0> Product : Mouse
1> Instance : Keyboard
1> Product : Keyboard
2> Instance : Gaming Mouse G502
2> Product : Gaming Mouse G502
3> Instance : Gaming Mouse G502
3> Product : Gaming Mouse G502
4> Instance : Gaming Mouse G502
4> Product : Gaming Mouse G502
5> Instance : Gaming Mouse G502
5> Product : Gaming Mouse G502
6> Instance : USB Keyboard
6> Product : USB Keyboard
7> Instance : USB Keyboard
7> Product : USB Keyboard
8> Instance : LED Controller
8> Product : LED Controller

Great, the game is counting input devices – uh, why is there a “LED controller” device? The motherboard of my Windows 7 machine has a built-in LED controller to check. Maybe the detection is not working properly, and the game is trying to use it as an input device?

After disabling the LED controller in Device Manager, the game consistently started fine! so far so good. Of course, I wanted to know what exactly was going wrong, so let’s see where these messages are printed.

Side Quest: CD Investigation #

If I forgot to insert the game disc the game closed without any notification. A quick trace revealed that GibbonPosture setting in f1.cfg Used to indicate the disk drive from which the game was installed. It seems the only check path is redist\dsetup.dll Exists on disk. Copying the redist folder to the installation directory and changing the settings GibbonPosture=.\ Seems to be working perfectly fine. ,

bug #

to find Instance : And Product : Log messages in binary were quite simple. They are only referenced in one function, which is a DIEnumDevicesCallback callback function that is provided IDirectInput::EnumDevices (Microsoft has only left documentation for the DX8 version of EnumDevices online, but it’s close enough).

This is the pseudocode and relevant data structure of the call and callback:

struct DinputDeviceData
{
  char instance_name[128];
  char product_name[128];
  DWORD dwDevType;
  GUID guid;
};

// ...

BOOL __stdcall dinput_enumdevices_callback(LPCDIDEVICEINSTANCEA lpDevice, LPVOID pvRef)
{
    int index = g_dinput_device_index;
    g_direct_input_devices[index].guid = lpDevice->guidInstance;
    strcpy(g_direct_input_devices[index].instance_name, lpDevice->tszInstanceName);
    strcpy(g_direct_input_devices[index].product_name, lpDevice->tszProductName);
    g_direct_input_devices[index].dwDevType = lpDevice->dwDevType;

    log_line("%d> Instance : %s\n", index, lpDevice->tszInstanceName);
    log_line("%d> Product : %s\n", index, lpDevice->tszProductName);

    if ( LOBYTE(g_direct_input_devices[index].dwDevType) == DIDEVTYPE_JOYSTICK )
    {
        int joystick_index = g_joystick_index;
        g_joystick_info[joystick_index].dinput_device_index = index;
        g_joystick_info[joystick_index].field_4 = 0;
        g_joystick_info[joystick_index].field_8 = 0;
        g_joystick_info[joystick_index].field_38 = 0;
        g_joystick_info[joystick_index].field_1 = 0;
        g_joystick_index = joystick_index + 1;
    }
    g_dinput_device_index = index + 1;

    return DIENUM_CONTINUE;
}

// ...

g_dinput_create_hresult = DirectInputCreateA(hInstance, 0x500u, &g_dinput_instance, 0);
g_dinput_device_index = 0;
g_joystick_index = 0;
g_dinput_instance->lpVtbl->EnumDevices(
    g_dinput_instance, 0, dinput_enumdevices_callback, 0, DIEDFL_ATTACHEDONLY);

Therefore, for each enumerated device, the game stores some general information about it in a global array g_direct_input_devicesThen, if the device has a joystick (typically, a game controller), it connects it to the g_joystick_info,

Can you guess the bug yet? 🙂 If not, here is the declaration of global arrays:

DinputDeviceData g_direct_input_devices[8];
// ...
JoystickInfo g_joystick_info[8];

There is only room for eight DirectInput devices in the array! 8> Instance : LED Controller The ninth was, overwriting a lot of other important data in the process, including the timer handle and the actual DirectInput instance pointer.

But it gets worse: the game only uses DirectInput for game controllers. Copying device information lpDevice Completely useless for other types of devices. just getting by DIDEVTYPE_JOYSTICK The check up will have the bug hidden for basically all setups, as you have to connect more than 8 game controllers to write the game out of range.

Actually, an even simpler solution would have been: EnumDevices allows to pass DIDEVTYPE As a filter:

g_dinput_instance->lpVtbl->EnumDevices(
    g_dinput_instance, DIDEVTYPE_JOYSTICK, dinput_enumdevices_callback, 0, DIEDFL_ATTACHEDONLY);
                    // ^^^^^^^^^^^^^^^^^^

This will cause DirectInput to call callbacks only for game controllers. Without it, all devices, be they keyboards, mice, or indeed any HID devices, are enumerated. (I checked the DirectX 5 SDK docs, and there it mentions HID device support.) This includes the vendor-defined devices of my mouse and its emulated keyboard (for macros), and of course the motherboard’s LED controller.

The moral of the story? Always check your limits, kids! You never know if some strange person comes over and plugs a dozen game controllers into your PC. ,

Solution #

On GitHub I’ve put up a minimal patch in the form of a classic DLL shim. with provided dinput.dll In the game directory, the game will load that instead of the system one. DirectInput has only one relevant exported function that we need to demonstrate: DirectInputCreateAThe rest of the API is implemented through COM interfaces, thanks to which we can modify the corresponding vtables as needed,

I have applied two improvements to the shim:

  1. inject DIDEVTYPE_JOYSTICK filter calls EnumDevices To return joystick/game controller only.
  2. Cancel calculation when 8 joysticks are found.

For fun, I’ve also tried reducing the size of the shim DLL – the final binary weighs 2 KiB.

These are the proper settings I changed:

  • compile with opt-level = "z" To optimize for minimum size. (Although the code is so low-level that it’s effectively the same opt-level = 3,
  • #[no_std] To avoid linking the Rust standard library.
  • codegen-units = 1 And lto = true To enable whole-program customization.
  • panic = "immediate-abort" To remove all unnecessary panic handling codes; An unwrap will immediately abort the process.

And these are the cursed:

  • /NODEFAULTLIB Not linking to any MSVC runtime library; add your own minimum DllMain,
  • /FORCE:UNRESOLVED ignore missing symbols for _aullrem, _aulldivAnd _fltusedWe’re not using any of these, but the LLVM target still insists on adding them,
  • /FILEALIGN:512 To force the linker to use the minimum supported PE section alignment in the file.
  • /MERGE:.rdata=.text Merges the read-only data section into the code section.
  • Prevent zero-initialization of system directory paths using MaybeUninit,
  • storing globals in static mut Just like the original game. Since this solution is specific to this game, I can make these “global” assumptions here :^)
    • Make sure all globals are zero-initialized .data The section in binary is 0 bytes.
  • .unwrap_unchecked() To avoid any extra branches where they are not needed.
  • /DEBUG:NONE To not generate and store debug information .pdb Path in binary.

Also, I switched rust-lld.exe As linker, because it is fine with the setting /SUBSYSTEM:WINDOWS,4.0" And /OSVERSION:4.0 Without complaining. 🙂 Since there is no linked runtime code, the resulting binary should work on any 32-bit Windows version, even without Rust9x. I have tested it on Windows 7 and Windows 98 SE.

Feel free to get the compiled DLL from the release page.



Leave a Comment