Published on by

I have recently added another 2 GB of RAM to be able to perform some testing for a friend of mine, and I installed Windows XP Professional x64 Edition to be able to utilize all 4 GB of RAM from his test application.

After finishing hard work I tried to relax a bit by revisiting one of my all-time favorite games — the original Half-Life. Alas... it wouldn't run!

Uh-oh... someone has math problems!

Back in 1998 when the game was written it was unimaginable to have more than 2 GB of RAM in a personal computer. You can clearly see from the message above that the 16MB of physical memory was enough to play this fantastic game. Ah, those were the days... but I digress.

We are on a mission to find out why it doesn't work, and how to fix it, right?

Since the game throws up a fancy message box (well sort of), and not a system default message box or a dialog, I knew that searching for the text in the executable wouldn't work. I used a tool StraceNT to trace system API calls in order to find out which function the game uses to check for available physical memory.

Of course, I heavily suspected that it uses GlobalMemoryStatus but it didn't hurt to check and it turned out that I was right. Finding all references to GlobalMemoryStatus import in the hl.exe wasn't hard. The first one I looked at was the right one:

.text:00413036	call	ds:GlobalMemoryStatus
.text:0041303C	mov	eax, [esp + 0CCh + Buffer.dwTotalPhys]
.text:00413040	cmp	eax, 0F00000h
.text:00413045	mov	dwBytes, eax
.text:0041304A	jge	short loc_413074

So what exactly is wrong here? There are actually two separate problems at work. First one can be attributed to Microsoft — the MSDN article about GlobalMemoryStatus says:

On Intel x86 computers with more than 2 GB and less than 4 GB of memory, the GlobalMemoryStatus function will always return 2 GB in the dwTotalPhys member of the MEMORYSTATUS structure. Similarly, if the total available memory is between 2 and 4 GB, the dwAvailPhys member of the MEMORYSTATUS structure will be rounded down to 2 GB. If the executable is linked using the /LARGEADDRESSAWARE linker option, then the GlobalMemoryStatus function will return the correct amount of physical memory in both members.

However, practice almost always differs from the theory, so I wrote a small test application to verify GlobalMemoryStatus API behavior:

#include <stdio.h>
#include <windows.h>

int main(int argc, char *argv[])
{
	MEMORYSTATUS	ms;

	RtlZeroMemory(&ms, sizeof(MEMORYSTATUS));
	ms.dwLength = sizeof(MEMORYSTATUS);
	GlobalMemoryStatus(&ms);
	printf("%08lX %ld %lu\n", ms.dwTotalPhys, ms.dwTotalPhys, ms.dwTotalPhys);

	return 0;
}

I expected to see numbers clamped to 2 GB (2,147,483,647 bytes precisely) but what I saw was:

c:\test>test.exe
FFE2B000 -1921024 4293046272

In other words, the operating system was "correctly" reporting 4,094 MB of physical memory even though I haven't specified /LARGEADDRESSAWARE switch when I compiled the test application. I even checked the hl.exe header and the LARGEADDRESSAWARE flag wasn't set there either.

Ok, so we found out that the Microsoft is the one responsible for the compatibility nightmare but what about Valve programmers? Are they to be left off the hook?

Of course not! The field dwTotalPhys in MEMORYSTATUS structure is of type SIZE_T which maps to unsigned long — they used signed comparison when comparing if there is enough memory available and thus made the program sensitive to this sudden and most likely unintended GlobalMemoryStatus behavior change.

To cut the long story short, there are four conditional jumps in hl.exe which need to be changed from signed to unsigned comparison in order to make the game work again. For the Half-Life patch 1.1.1.0 they are at the following offsets:

0001304A: 7D 73
0001307C: 9D 93
00013092: 7D 73
000130A1: 7E 76

First column is the file offset, second column is the old value, third column is the new value. So, fire up your hex editor, change those bytes and enjoy being able to play the game again!

UPDATE (2008-01-27 00:57):

I have just noticed that Half-Life: Blue-Shift (bshift.exe version 1.0.1.6) exhibits the same problem so I have created a patch for that one too:

000156AA: 7D 73
000156DC: 9D 93
000156F2: 7D 73
00015701: 7E 76

UPDATE (2008-01-27 01:17):

I have noticed that there are newer game updates than the 1.1.1.0 I was using so I downloaded, installed and tested updates 1.1.1.1 and 1.1.1.2 — they do not address this issue.