Carmageddon is a 1997 car game that I lost many hours of my younger life playing. The open world, do anything gameplay combined with fun vehicle physics was intoxicating.
Looking back now, it feels like it was fun for those same basic reasons that the GTA series is fun for players today.
For some reason, it still fascinates me and I like to reverse engineer how it works.
“The most effective debugging tool is still careful thought, coupled with judiciously placed print statements.” — Brian Kernighan, “Unix for Beginners” (1979)
When building software, developers need some way to test their code to make sure it is doing what it is expected. Today unit tests, CI/CD pipelines and automated integration checks are obvious, but still nothing beats printf
for simplicity.
Stainless Software, working on this game in the mid-nineties, was certainly not practicing CI/CD. They did know all about printf
though!
The Carmageddon executable contains many references to debug strings, easily visible in a disassembler.
However, of course, players shouldn’t see this debug output - they are only expected to be read by the developers. So the retail build doesn’t emit any debugging logs.
And yet…! The executable still contains those messages, so maybe we can re-enable them somehow.
For a start, its easy to trace references to these debug strings in a disassembler, and quickly you can find this function. Debug strings are always passed to it, so we guess this is the function that prints them out.
Unfortunately it’s not very helpful - just an empty function, following the cdecl
calling convention.
We can make a guess that the original function looked something like this. Without DEBUGGING_ENABLED
defined, the function would be compiled with an empty body.
void sub_461645(char *fmt, args...) {
#ifdef DEBUGGING_ENABLED
...
printf(fmt, args);
#endif
}
If we look hard enough, we can find traces of this debugging information.
In the dumped debugging symbols, we can see some suspicious-sounding functions[1].
void* OpenDiagnostics();
void* CloseDiagnostics();
void* dprintf();
sub_461645
is likely dprintf
. We will refer to it as dr_dprintf
from now on to avoid confusion with glibc dprintf.
The debug strings contain references to a “diagnostic file”, and a “DIAGNOST.TXT”
So now we can guess that the debug messages were written into a DIAGNOST.TXT
file. Unfortunately, we don’t know anything else about the file, as the retail executable doesn’t ever create it.
An obscure Carmageddon demo, originally included in a magazine cover CD, contained some leftover files.
And guess what, one of those files is a DIAGNOST.TXT
, last modified in March 1997! Thats three months prior to the retail release.
This is the contents of the file.
DIAGNOSTIC OUTPUT
Date/time: Mon Mar 24 16:32:33 1997
0.00: PDDoWeLeadAnAustereExistance (sic): LargestBlockAvail=4530176
0.00: PDNetObtainSystemUserName()
11.35: Trying to open smack file 'C:\DEMO\DATA\CUTSCENE\MIX_INTR.SMK'
12.36: Smack file opened OK
39.85: Sorry, but Carmageddon has run out of memory (kMem_flic_data/273828)
39.90: FATAL ERROR: Sorry, but Carmageddon has run out of memory (kMem_flic_data/273828)
It seems that on that morning in 1997, the game ran out of memory and exited after 40 seconds, leaving only a few lines of debug output, but its enough to see the format of the file in case we want to reproduce it faithfully.
The same demo release also includes various MEMDMP.TXT files, presumably used by the developers to debugging memory usage over time.
CARMDEMO.EXE
appears to have been compiled on 21 March 1997, almost a full month before the “official” Carmageddon demo was compiled (17 April 1997).
So we know a little more than we did before, and crucially we know that the original debug printing code is simply not present in the executable. If we want those messages logged, we have to add some code to do it.
Here’s our plan:
dr_dprintf
to redirect to our new codeA few functions up from dr_dprintf
, we find sub_4614F1
- a function that doesn’t appear to be called from anywhere. This is where we will add our own code.
Conceptually, our new code could be pretty simple. For now, I’m not bothering to replicate exactly the format of the DIAGNOST.TXT
example above.
if (!diagnostics_file) {
diagnostics_file = fopen("DIAGNOST.TXT", "w");
}
vsprintf(buffer, fmt, args);
fputs(diagnostics_file, buffer);
fputc(diagnostics_file, '\n');
fflush(diagnostics_file);
Translating that into x86 code that we can inject into an existing executable took an embarrasing amount of effort, but this is what I ended up with.
Obviously we can’t inject an assembly text file into the executable, we need to assemble it into binary machine code first.
cc -c -masm=intel -m32 dr_dprintf.s
produces dr_printf.o
- in our case, an ELF-format object file.
We can’t inject this directly either, because all we want to pull out is the raw machine code, not all the object file metadata.
objcopy --dump-section .text=dr_dprintf.o.raw dr_dprintf.o
does just that, and finally we have some injectable raw machine code.
Now we can add the JMP command to dr_dprintf
, and overwrite sub_4614F1
with our raw x86 code blob via a small python script.
SOUND
directory, and MIDI-related variables still exist in the executable.int gRandom_MIDI_tunes[3]
and int gRandom_Rockin_MIDI_tunes[3]
, to store our file name and open file pointer.After shall we say - “plenty” - of attempts, it actually works! It blows my mind a little still that code written and assembled on a Mac in 2022 can be jammed into a Windows 95 executable from 1997 and it works! 🤯
After patching my own CARM95.EXE
, I captured this video showing Carmageddon 1 on the left and a live view of the DIAGNOST.TXT
on the right.
The most interesting debug content relates to the AI opponent vehicles. They run through a state engine where they might be racing or actively attacking the player.
FollowCheatyPath()
for example.
So thats it! Now we can all see diagnostics messages that probably haven’t been seen since 1997, and were never intended to be seen by anyone except a handful of talented programmers and artists at Stainless Software on the Isle of Wight. ❤️
The full code including instructions on how to apply the patch or modify it are in the repo: https://github.com/jeff-1amstudios/carmageddon-debug-output
[1] https://github.com/jeff-1amstudios/carmageddon1-symbol-dump/…/errors.c#L20-L22