Debug assertions in game binaries for blizzard and blizzard-branched studios are more common than you might expect. At least at ArenaNet, it was studio philosophy to ship with most debug assertions enabled so we could gather crash reports in the field to fix bugs. We only exempted assertions where the checks would cause performance issues (i.e. in a renderer or audio mixer). Most of our engineering culture was inherited from Blizzard and I think that particular rule was too.
Cool insight to hear. Apparently for the original Diablo, the usual release builds actually didn't contain debug assertions. I guess it must've become a part of their culture sometime after the release of the original Diablo?
I definitely believe you, though. I am pretty much positive that I've seen Warcraft III's engine crashing on debug assertions before when playing with the scenario editing tools.
I wouldn't be surprised if Diablo development is why the policy was in place. One of ANet's founders did a lot of work trying to help ship Diablo and it sounded like it had some serious engineering and quality culture issues (iirc Diablo was originally being developed by another studio that Blizzard acquired, but I might be misremembering)
To give further background, the Devilution team has primarily relied on these resources:
1. The Japanese Playstation port with debug symbols contained in `DIABPSX.SYM`. (see [1]).
Example debug info of the Cathedral dungeon generation algorithm:
// address: 0x801259D0
// line start: 612
// line end: 624
void DRLG_L1Floor__Fv() {
// register: 19
register int i;
// register: 20
register int j;
// register: 3
register long rv;
}
2. The debug release of the PE executable, which contained assert strings (see [2]).
Example assert string:
"plr[myplr].InvGrid[i] <= plr[myplr]._pNumInv"
3. The Rich header of the PE executable, which details the exact version of the original compilers and linkers used to build `Diablo.exe` (see [3,4]).
Example information recovered from the Rich header of `Diablo.exe`:
Id Build Count Name Description
0 0 155 Unknown [---] Number of imported functions (old)
1 0 229 Import0 [---] Number of imported functions
6 1668 1 Cvtres500 [RES] VS97 (5.0) SP3 cvtres 5.00.1668
2 7303 29 Linker510 [IMP] VS97 (5.0) SP3 link 5.10.7303
3 7303 1 Cvtomf510 VS97 (5.0) SP3 cvtomf 5.10.7303
4 8447 2 Linker600 [LNK] VC++ 6.0 SP3,SP4,SP5,SP6 link 6.00.8447
48 9044 72 Utc12_2_C [---] VC++ 6.0 SP5 Processor Pack
19 9049 12 Linker512 Microsoft LINK 5.12.9049
4. Discovery of the original set of compiler flags used to build `Diablo.exe` (see [5]).
Primarily "/O1" was used, but there are also peculiarities such as the use of both Microsoft Visual Studio 6 and Microsoft Visual Code 5 for linking the game.
5. The heartfelt dedication of a team of people. GalaXyHaXz did the initial heavy lifting and succeeded in the tremendous task of getting the decompiled source code of Diablo 1 compiling with the original toolchain. Later on she released the project open source and a community of open source collaborators formed. Most of us have never met in real life prior to joining the project, which stands to show that there is strength in online collaboration that transcend both culture and borders.
6. The Beta release and the Alpha4 release of Diablo 1 has also proved invaluable resources for cross-validation as the compiler optimization level was not set to release mode for these binaries.
Interestingly, in the process a number of bugs in the original implementation of Diablo 1 were discovered. These have been documented in the source code of Devilution with `// BUGFIX: foo` comments, and have also been detailed in [6].
To track the progress of the project, the "Binary identical functions" milestone has been used in tandem with an assembly diffing tool developed in Rust (see [7,8]).
Anecdotally, it was an incredible moment when we first managed to run the cross-platform port of Diablo 1 (DevilutionX, see [9]) natively on Linux and succeeded in playing a multiplayer game connecting our computers in Korea and Denmark. It is equally thrilling to see the modding and porting community picking up the torch and already succeeding in porting Diablo 1 to Nintendo Switch!
The main reason for conducting this bit of software archeology is to preserve the classic title that is Diablo 1, for generations to come. And to revive it for modern hardware platforms and make it more mod-friendly in the age of open source software.
Happy coding!
- The Devilution Team
P.S. the project README explicitly states that to play the game, you still need to have access to the original game assets released on the Diablo 1 CD. To acquire a legal copy, please refer to https://www.gog.com/game/diablo
P.P.S. for the verification process, there have been proposals that are both ambitious at a level of PhD research (see [10]) and that made us feel warm and fuzzy <3 In the end, many of the techniques outlined were discussed mostly on a design level, some were included as Proof of Concepts, but most of the work in reverse engineering Diablo 1 was from tender labour of a team that care for Diablo 1 the way you would your firstborn child.
Super noob question but i'm trying to wrap my head around how one could figure out the source code just based on things like debug info and assert strings? I watched the video and am staring at your examples and I just don't understand how you go from those to actual source code.
In the disassembly we can see a bunch of fine grained operations but the meaning behind them is opaque. For example, we see two array access operations but its not clear what they do. They might look like this:
mov al, [array + ebx]
Considering the assert statement from point 2:
"plr[myplr].InvGrid[i] <= plr[myplr]._pNumInv"
From this we can see what the variables were named in the source code. Assuming "plr" = player and "InvGrid" = Inventory grid, we can deduce one the array access operations is to get the current player and another is for getting an item from the inventory grid.
This is such a huge thing. If you are trying to reverse engineer something, spend time looking for other versions or test releases that might be out there. I've had a lot of success with this method.