VOGONS


First post, by Abandoned Witch

User metadata
Rank Newbie
Rank
Newbie

Abandoned Witch here. Apperently the official video game of Salt Lake 2002 Winter Olympics has problems when running on modern hardware (like Windows 7). First when installing it gives a not enough memory error, and second, after installing, it crashes to desktop after starting the game (no opening logos). Any solutions?
It did work on Vista but not on 7.

The Witch will never die!!! At least there is hope.
There's no one but you on my mind, searching for a perfect ending that we'll never find!

Reply 1 of 5, by pr1met1me

User metadata
Rank Newbie
Rank
Newbie

Hi!
I own an original, US copy of this game and while looking through my old CDs a few weeks ago, I wanted to take it for a spin, having played this game the last time many-many years ago.
When I tried installing and running the game now, I encountered exactly the same issues as you did.
As someone who's interested in old software, disassembly and reverse engineering, I've decided to embark on a bit of a journey and try to debug and find out exactly what causes these errors on modern systems. While I'm there, I figured I'd also make a post about it here for anyone who ever runs into these exact problems in the future and is interested about their root cause.

Here's the very short explanation of the two problems with their quick solutions, basically TL;DR:

  1. Not enough memory when installing:
    Due to the installer calling a very old Windows function, on systems where you have "too much" RAM (more than 4GiB), it thinks you have a negative amount of physical memory available, which is less than the minimum required 64MB, so the installer will abort.

    Solution:
    You can just copy the game files to your hard drive, without having to run the installer at all. The necessary directories are Movies/, Music/, SOUNDS/, and the necessary files are FILES.IMG, COMM.OGG and SaltLake2002.exe. If you have the US build of the game like I do, I recommend replacing the SafeDisc-protected SaltLake2002.exe with beha_r's or my fixed EXE so that you won't have to install the SafeDisc driver (which doesn't work on newer systems anyway and is a massive security hole) and you won't have to mount the disc to run the game.

    If you don't have the US build or insist on wanting to go through the installer procedure and allowing long-deprecated vulnerable drivers onto your system, you can just fix the installer script file (setup.inx) itself to not abort the installation for you.
    For this, I've attached the fixed setup.inx below - just copy all files from the disc to a local directory on your hard drive, replace setup.inx with the one I've provided and run the installer from there. The installer won't care that it's not running from a disc.
    You can also get my modified setup.inx from here:
    https://mega.nz/file/IghmiQbI#IvfeRlTsICPajkN … YT02M-6j2SfC81Q

  2. Crashing to desktop after game launch:
    When the game is checking how much usable video memory you have, it corrupts the program stack due to a bug that is only triggered if you have "too much" (more than what the game devs anticipated) VRAM. The corrupted memory leads to an unrecoverable crash.

    Solution:
    Just use a modern fixed EXE to run the game.
    beha_r's can be downloaded from any of these links:
    (edit by Dominus: removed warez site links)

    Mine can be downloaded from the attachments or from here:
    https://mega.nz/file/Y1ASAAbJ#jJPr19th3pICz7Y … oOkMk7k-GDUSHr0

That was the TL;DR. Here are the technical details for anyone interested:

Not enough memory when installing

When the game installer queries your system for how much physical RAM it has, it uses an ancient, long deprecated Windows API called GlobalMemoryStatus that basically cannot count above 4GiB due to storing the result in a 32-bit integer.

So if you have more RAM than 4GiB, this API will return a negative number due to integer overflow, as even Microsoft mentions in their API docs. In turn, when the installer script receives this negative number from Windows and then compares it to 64MB (as that is the minimum memory requirement for the game), it will find that this negative amount of memory is less than that and will abort installation with the "There is not enough memory available to install this application. Setup will now exit." error message.
Fj0GwOI.png

The installer script was coded in a way that it unfortunately doesn't recognize when it has received an obviously bogus negative number for the available RAM. The game developers most probably didn't expect that the game would be run on systems using more than 4GiB of memory. And even if they did - and could have used the more future-proof API (GlobalMemoryStatusEx) which at the time was already available - they most probably opted for this one instead, because the newer API is not supported on Windows 95 and 98.

The installer is instructed to do these checks via its compiled InstallShield script (aka. InstallScript) file, which can be found on the disc as a file called setup.inx.

Here's a snippet of how this installer script looks like - it has to be decompiled from the setup.inx file using an InstallShield script decompiler tool, I've used SID (sexy installshield decompiler by sn00pee):
44uqFig.png

You can find the code for the decompiled setup.inx file here:
https://paste.gg/p/anonymous/ffedbaae71b5427a … b22c72f55be2b62

function_0 is the entry point, this is where the prerequisite checks are done for the installer. On line 926 (or offset @00004D99:0007), there's this snippet:

@00004D99:0007   label_4d99:
@00004D9B:0021 GetSystemInfo(185, local_number1, local_string3);
@00004DAC:0009 local_number6 = (local_number1 < 64000);
@00004DBB:0004 if(local_number6) then // ref index: 1
@00004DC7:0021 function_234("ERROR_MEMORY");
@00004DDC:0006 local_string12 = LASTRESULT;
@00004DE6:0021 MessageBox(local_string12, -65534);
@00004DF4:0002 abort;
@00004DF8:000B endif;

GetSystemInfo being called with 185 as its first argument executes the following code:

@0000A495:0005   label_a495:
@0000A497:000D local_number5 = (local_number1 = 185);
@0000A4A6:0004 if(local_number5) then // ref index: 1
@0000A4B2:0021 function_190(local_number2);
@0000A4BB:0006 local_number3 = LASTRESULT;
@0000A4C5:0005 goto label_aa2d;
@0000A4CE:0005 endif;

Which in turn executes function_190 and returns its result.
function_190 is where the actual RAM querying happens using the outdated GlobalMemoryStatus API:

@0000B3B4:0009   label_b3b4:
@0000B3B6:0022 function NUMBER function_190(local_number1)
@0000B3B6 NUMBER local_number2;
@0000B3B6
@0000B3B6 OBJECT local_object1;
@0000B3B6 begin
@0000B3BF:001A local_number2 = &local_object1;
@0000B3C9:0020 GlobalMemoryStatus(local_number2); // dll: KERNEL32.dll
@0000B3D2:0035 local_object1.nTotalPhys;
@0000B3E6:0006 local_number2 = LASTRESULT;
@0000B3F0:0011 local_number1 = (local_number2 / 1024);
@0000B3FF:0027 // return coming
@0000B403:0023 return 0;
@0000B40C:0026 end; // checksum: 4d013b

The GlobalMemoryStatus call populates a struct (local_object1) with information about the machine's RAM. The nTotalPhys/dwTotalPhys struct member contains the total physical memory present in the machine in bytes. The code divides it by 1024 to get the total RAM of the machine in kibibytes and then returns that value.
Back to the code on line 926 (or offset @00004D99:0007), the total installed RAM in KiB gets stored inside the local_number1 variable here:

@00004D99:0007   label_4d99:
@00004D9B:0021 GetSystemInfo(185, local_number1, local_string3);

And then gets compared to 64 000 KiB (a little less than 64 MiB):

@00004DAC:0009      local_number6 = (local_number1 < 64000);

Since this comparison is done with signed numbers - and as already mentioned, GlobalMemoryStatus returns negative numbers if there are more than 4GiB of RAM - this if-statement will evaluate to true on modern machines with high RAM capacity. This will in turn show the message box with the "not enough memory available" message and abort the installation:

@00004DBB:0004      if(local_number6) then // ref index: 1
@00004DC7:0021 function_234("ERROR_MEMORY");
@00004DDC:0006 local_string12 = LASTRESULT;
@00004DE6:0021 MessageBox(local_string12, -65534);
@00004DF4:0002 abort;
@00004DF8:000B endif;

Fixing this can be done easily with SID. Changing the comparison into something that would be far less likely to happen on an actual (modern) PC is what I've opted for. The way I've patched it is to change the less-than operator to an equals operator:

@00004DAC:0009      local_number6 = (local_number1 = 64000);

This way the setup will only abort if someone has exactly 64000 KiB RAM available, which is very unlikely, as even if you were to have 64 megs of system memory installed, function_190 would most probably return around 65536 KiB, thus it would pass the check.
You can find my patched setup.inx file here in the attachments or at the link above in the TL;DR.

Crashing to desktop after game launch

Now this one is trickier.

The crash itself is of type EXCEPTION_ACCESS_VIOLATION (error code 0xC0000005), which means the program is trying to read or write memory at an invalid memory location. The crash logs also disclose what the address of the problematic instruction is: 0x00000000. This means the application expects CPU instructions to be at memory address 0x00000000, but since that is zero, it won't be able to execute any instructions from there and is going to just shut down itself.

Something on older machines definitely did prevent these crashes from happening, as I clearly remember playing this game on my old XP desktop PC without any issues.

Figuring this one out seemed harder, so I've utilized the help of VirtualBox to create a clean virtualized environment for researching what exactly causes the crash. What also helped tremendously is that I've noticed early on that beha_r's fixed EXE does not crash; the game can be played flawlessly when using it.

Initially I thought - like maybe yourself - that something about the OS version itself is the trigger for the crash. To confirm this assumption, I created a virtual machine with Windows XP SP3, and tried running the game in it. To my surprise, it still crashed!
Just to confirm it's not the DRM causing it, I've also downloaded an old crack for the game (by D°N $iMoN) and it crashed even with that one both in the VM and also on my modern Windows 10 machine.

I then started looking at different parameters I could change. After some experimenting, I found that the crash can be controlled with the amount of VRAM that is allocated for the virtual machine. When I allocated 128MiB, it was always crashing. With 64MiB, it pretty much never did.

The resolution the game is launched at also influences this. Lower resolutions need less VRAM to crash the game, while higher ones need more. Here's a little table that demonstrates the minimum necessary VRAM for the game to crash, based on the manual tests I've done in my VM:
- 640x480: 68MiB
- 800x600: 70MiB
- 1024x768: 74MiB
- 1152x864: 76MiB

Any value at or above these VRAM values at the given resolution will crash the game. So basically if you want to run the game at 1024x768, just set your VM's VRAM capacity to 73MiB, and you'll be able to run the game just fine.

And with that, two questions arise - how the hell does more available VRAM in a GPU cause a crash like that? And how did beha_r fix it?

Since beha_r's fix has been available for a while now, I started looking for answers to the second question first.
I contacted beha_r and he was kind enough to share a before and after EXE with me so that I could see exactly what he changed in the original executable.

Besides obviously patching out the function call responsible for the CD check, he also changed another function's stack. By reverting his CD check patch to only include the stack modification in the executable, I was able to confirm that this stack trick was indeed the one that actually fixed the crash.
The change he's done is just simply to increase the stack size by 4 bytes for the function at address 0x00531C30:

sub_531C30+0
prev.: mov eax, 108Ch
fixd.: mov eax, 1090h (+4)

sub_531C30+203 (531E33)
prev.: add esp, 108Ch
fixd.: add esp, 1090h (+4)

sub_531C30+214 (531E44)
prev.: add esp, 108Ch
fixd.: add esp, 1090h (+4)

So it seems more space is given to this function on the stack.
Since this function is clearly the main point of interest in the quest to figure out why the crash occurs, I've decided to go through the painstaking process of decompiling it. Fortunately it isn't that huge and it didn't take that long, but it still did take some time to properly name, annotate and explain everything.

It did pay off eventually, as I was able to make sense of everything in its logic, allowing me to present it to you here in its full glory:
https://paste.gg/p/anonymous/45427fd1d16a4b5d … 73313e29c24f899

You could even compile this yourself using the DirectX 8 SDK if you replace the this references with constants and provide valid IDirect3DDevice8 and HWND objects.

As you can see, this function is basically a VRAM capacity test. It returns the usable amount of video memory in MiB.
This leads us back to the first question - how does a large amount of VRAM crash the game here? Well, there's a bug in this function. See if you can spot it yourself.

Ready?

Okay, here goes:

The game developers made two mistakes here, and the combination of these two mistakes causes the crash.

The first mistake: apparently they never expected (or didn't care) that GPUs could go beyond 512 * 128K == 64MiB VRAM capacity. They relied on DirectX to eventually throw a D3DERR_OUTOFVIDEOMEMORY result when creating up to 512 surfaces and textures to properly calculate how much VRAM is available for them to use,
basically always expecting a failure, an eventual exhaustion of video memory.

They thought 64MiB would be plenty for a VRAM overload test as most consumer graphics cards at the time of development (2001) had around 16-32MiB VRAM. Even the game's system requirements mention a 32MiB GPU for optimal performance. Interestingly, higher-end cards that had 128MiB VRAM had already come out in 2002, so this bug could have popped up for some people already only mere months after release.

Funnily enough, the usable VRAM amount this function calculates doesn't even appear to influence anything; it does get saved, but doesn't affect gameplay or playability/game stability whatsoever, even if it's 0. In fact, beha_r's fix makes the game believe it has 0 MiB usable VRAM, yet it still works fine.

However the other mistake is a much more severe one: they messed up the bounds checks for BOTH arrays. The dummy texture and surface arrays were both defined as capable of holding 512 pointers each, however their respective loops overrun both buffers with one extra iteration when too much VRAM is available.
This is because the devs were supposed to use < instead of <= operators in the do..while loops:

do {
...
} while ( numOfCreatedSurfaces <= 512 && !createResult );

Arrays in C/C++ are zero-indexed, meaning that dummySurfaces' first item is dummySurfaces[0], and its last item is dummySurfaces[511], not dummySurfaces[512]; that one points beyond the end of the array. With each successful texture/surface creation, this index is incremented, however it gets incremented one additional time - so 513 times instead of the intended 512 - due to the less-or-equal (<=) bounds check.

For the dummyTextures array, this isn't too big of a deal, the item dummyTextures[512] - which actually points to the 513th element of the array, thus is outside of it - only points to the next array's first item in memory, dummySurfaces[0]. That is overwritten by its own loop anyway later on.

The problem is with dummySurfaces[512], as dummySurfaces is the last array in the function's stack. When this last array is overrun, beyond its very last item, the function's own return address on the stack is overwritten with a pointer to a blank surface.

Due to how the stack is structured, the return address goes after all the variables and buffers defined on the stack. It just so happens that the very last buffer defined on the stack is overrun, corrupting the return address that comes just right after it.

While this last +1 surface is NULL'ed out, the original return address (that would tell the function where to go back after it finished executing) is lost forever and has been replaced with zeroes. That's why when the function finishes, it thinks it was called from 0x00000000, so it tries to go there to continue execution, but as that is invalid memory, the game crashes with an EXCEPTION_ACCESS_VIOLATION.

(If you don't quite understand what all this return address stuff is about, watch this video, it explains it much better than I do here.)

The developers most probably only tested the game with GPUs having up to 64MiB video RAM. In that case, no matter what resolution they run the game at, it will always run out of video memory when creating the 512 textures and surfaces, thus not reaching the problematic 513th iteration.

Now, you might be wondering: how does this relate to the game resolution and why the exact amount of VRAM "needed" to crash the game increases with higher resolutions? Well, I was kinda stumped on this for a longer time than I'd like to admit, but the answer is painfully simple: the higher the resolution, the more VRAM Windows itself and any other GPU-utilizing applications use. The textures and surfaces always take up the same amount of VRAM; it's everything else (that the video memory is shared with) that needs more resources as the resolution is increased.

Going back to the code, we can see that the way this was fixed by beha_r is pretty straightforward and simple: he just created 4 bytes more space on the stack for that extra surface iteration to write into, making the last array be able to store 513 pointers instead of 512, sparing the function's return value from getting overwritten:

  DDSURFACEDESC2 surfaceDescriptor; // [esp+5Ch] [ebp-1080h] BYREF
IDirect3DTexture8 *dummyTextures[512]; // [esp+D8h] [ebp-1004h] BYREF
IDirectDrawSurface7 *dummySurfaces[513]; // [esp+8D8h] [ebp-804h] BYREF

The dummyTextures buffer is still overflown, but since it overflows into the dummySurfaces buffer, it's not an issue. And dummySurfaces does not get overrun anymore.
This can be done without any problems, as there are a few bytes of padding between functions on the stack, and taking these 4 bytes off of the padding doesn't cause any issues to the next function in memory.

In my opinion (and what most probably the devs intended as well), the best way to fix this would be to fix the do..while loops to not iterate over the buffer's bounds:

do {
...
} while ( numOfCreatedSurfaces < 512 && !createResult );

Or better yet, since the return value of this function isn't even used that much and the game runs just fine even with a return value of 0, the entire function call could just be patched out. It's only called from one place. On modern systems, there are surely more than 64MiB graphics memory available anyway.

Before:

.text:0052D863                 mov     ecx, esi
.text:0052D865 mov [esi+604h], edi
.text:0052D86B mov [esi+608h], edi
.text:0052D871 call sub_531C30 ; problematic VRAM-testing function call
.text:0052D876 cmp eax, edi
.text:0052D878 mov [esi+594h], eax

After:

.text:0052D863                 mov     ecx, esi
.text:0052D865 mov [esi+604h], edi
.text:0052D86B mov [esi+608h], edi
.text:0052D871 mov eax, 40h ; function call completely patched out, program is told it has 64 MiB VRAM to use
.text:0052D876 cmp eax, edi
.text:0052D878 mov [esi+594h], eax

I patched the game executable in this fashion and didn't run into any issues. You can find it attached below or at the link above.

One could argue that the devs made a third mistake: they could've just simply used DirectX's builtin VRAM-querying functions to get the available VRAM amount, such as GetAvailableTextureMem or GetAvailableVidMem instead of trying to cram a bunch of blank textures and surfaces into the VRAM on each game startup. My guess would be that they went down this route as they deemed this more reliable than those functions? Not really sure.

Maybe some of the original developers like Derek Pettigrew or Simon Morris could be asked if these guys are still around and someone were able to get a hold of them.

Either way, this was my analysis of the issue. The fixed EXE I've attached and linked to patches out the VRAM-check function call shown above entirely. Maybe sparing that extra few milliseconds is valuable to someone, but otherwise, the beha_r patch should work just perfectly fine for everyone else. =)

Not sure if anyone will ever even read this as Salt Lake 2002 is quite an obscure game and it's not even that good.
But if at least one person did read this and found it helpful/enjoyed it, it was worth the effort.
Huge shoutout to beha_r for his original fixed EXE and for his help, my analysis would've taken much longer without him!

Last edited by Dominus on 2023-05-16, 07:09. Edited 2 times in total.

Reply 2 of 5, by Repo Man11

User metadata
Rank l33t
Rank
l33t

Wow Pr1met1me, that's an impressive first post!

"We do these things not because they are easy, but because we thought they would be easy."

Reply 3 of 5, by BEEN_Nath_58

User metadata
Rank l33t
Rank
l33t

Came to read the problem, stopped in awe of the 2nd post

previously known as Discrete_BOB_058

Reply 4 of 5, by Dominus

User metadata
Rank DOSBox Moderator
Rank
DOSBox Moderator

Indeed an impressive first post.
I only edited out the links to two abandonware/warez sites as we try to keep Vogons from being entangled with these.

Windows 3.1x guide for DOSBox
60 seconds guide to DOSBox
DOSBox SVN snapshot for macOS (10.4-11.x ppc/intel 32/64bit) notarized for gatekeeper

Reply 5 of 5, by gerry

User metadata
Rank Oldbie
Rank
Oldbie

this level of dedication to getting a relatively forgotten older game (with "generally unfavorable reviews" no less) running on a newer OS is vogons gold! 😀