├── DOOM.EXE ├── DOOM1.WAD ├── DOOMD.EXE ├── DOOMDUPX.EXE ├── DOOMUPX.EXE ├── DOOMW.EXE ├── DOOMWUPX.exe ├── README.md └── smash.py /DOOM.EXE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nneonneo/universal-doom/fcf22d9773c62835724f2d25c40933ef051f6600/DOOM.EXE -------------------------------------------------------------------------------- /DOOM1.WAD: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nneonneo/universal-doom/fcf22d9773c62835724f2d25c40933ef051f6600/DOOM1.WAD -------------------------------------------------------------------------------- /DOOMD.EXE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nneonneo/universal-doom/fcf22d9773c62835724f2d25c40933ef051f6600/DOOMD.EXE -------------------------------------------------------------------------------- /DOOMDUPX.EXE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nneonneo/universal-doom/fcf22d9773c62835724f2d25c40933ef051f6600/DOOMDUPX.EXE -------------------------------------------------------------------------------- /DOOMUPX.EXE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nneonneo/universal-doom/fcf22d9773c62835724f2d25c40933ef051f6600/DOOMUPX.EXE -------------------------------------------------------------------------------- /DOOMW.EXE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nneonneo/universal-doom/fcf22d9773c62835724f2d25c40933ef051f6600/DOOMW.EXE -------------------------------------------------------------------------------- /DOOMWUPX.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nneonneo/universal-doom/fcf22d9773c62835724f2d25c40933ef051f6600/DOOMWUPX.exe -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A single .exe binary which runs DOOM on DOS 6, Windows 95 and Windows 10 (and probably everything in between). 2 | 3 | ## Quickstart 4 | 5 | Run `DOOM.EXE` on your OS of choice, making sure that a suitable .WAD file is present. `DOOM1.WAD`, in this repo, is the shareware demo. 6 | 7 | ## Introduction 8 | 9 | DOS and Windows .exe files both start with a common header (the DOS "MZ" header), which might suggest that you can run programs for one OS on the other one. However, this is complicated by a few facts: DOS support was dropped in Windows a long time ago (DOS binaries don't load on Windows 10, for example), and DOS programs use a completely different execution environment. 10 | 11 | This repo builds a single .exe file with a "polyglot" MZ header which allows it to load one program (the original DOOM) when running inside DOS, and a different program (a custom static build of Chocolate DOOM) when running on Windows. 12 | 13 | ## The DOS binary 14 | 15 | The DOS binary, `DOOMD.EXE`, is exactly the original DOOM for DOS, derived from the shareware version that was widely distributed. `DOOMDUPX.EXE` is the same binary, but packed with UPX to make it smaller. 16 | 17 | ## The Windows binary 18 | 19 | The Windows binary, `DOOMW.EXE`, is a custom build of Chocolate DOOM, a source port of DOOM which hews very closely to the original. Chocolate DOOM is based on libSDL 1.2; this custom build integrates it statically rather than linking to libSDL dynamically. This binary is stripped. `DOOMWUPX.EXE` is the same binary, but packed with UPX to make it smaller. 20 | 21 | ### Build instructions 22 | 23 | 0. `apt install mingw-w64 build-essential` on an Ubuntu 18.04 machine. 24 | 1. Download and unpack the source code for Chocolate DOOM 2.2.1, `SDL` 1.2.15, `SDL_mixer` 1.2.7, and `SDL_net` 1.2.8. 25 | 2. `cd SDL-1.2.15; ./configure --enable-shared --enable-static --disable-directx --host=i686-w64-mingw32; make; sudo make install` 26 | 2. `cd SDL_mixer-1.2.7; ./configure --enable-shared --enable-static --host=i686-w64-mingw32; make; sudo make install` 27 | 3. `sudo mv /usr/local/lib/libSDL_mixer.* /usr/local/cross-tools/i386-mingw32/lib/; sudo mv /usr/local/include/SDL/SDL_mixer.h /usr/local/cross-tools/i386-mingw32/include/SDL/` (I could probably also have just set prefix correctly) 28 | 4. Apply this patch to `SDL_net` to eliminate the dependency on `iphlpapi.dll`: 29 | 30 | ``` 31 | diff -ur a/SDL_net-1.2.8/SDLnet.c SDL_net-1.2.8/SDLnet.c 32 | --- a/SDL_net-1.2.8/SDLnet.c 2012-01-15 16:20:10.000000000 +0000 33 | +++ b/SDL_net-1.2.8/SDLnet.c 2020-11-13 18:49:24.602850922 +0000 34 | @@ -212,6 +212,7 @@ 35 | return 0; 36 | } 37 | 38 | +#if 0 39 | if ((dwRetVal = GetAdaptersInfo(pAdapterInfo, &ulOutBufLen)) == ERROR_BUFFER_OVERFLOW) { 40 | pAdapterInfo = (IP_ADAPTER_INFO *) SDL_realloc(pAdapterInfo, ulOutBufLen); 41 | if (pAdapterInfo == NULL) { 42 | @@ -219,6 +220,9 @@ 43 | } 44 | dwRetVal = GetAdaptersInfo(pAdapterInfo, &ulOutBufLen); 45 | } 46 | +#else 47 | + return 0; 48 | +#endif 49 | 50 | if (dwRetVal == NO_ERROR) { 51 | for (pAdapter = pAdapterInfo; pAdapter; pAdapter = pAdapter->Next) { 52 | ``` 53 | 5. `cd SDL_net-1.2.8; ./configure --enable-shared --enable-static --host=i686-w64-mingw32; make; sudo make install` 54 | 6. `sudo rm /usr/local/cross-tools/i386-mingw32/lib/*.dll.a`. This eliminates the dynamic libraries to force static linking of SDL. 55 | 7. `cd chocolate-doom-2.2.1; ./configure --host=i686-w64-mingw32 --build=i386-linux LIBS="-lgdi32 -lwinmm -lwsock32"; make` 56 | 8. `i686-w64-mingw32-strip src/chocolate-doom.exe` 57 | 58 | Voila, a statically built version of Chocolate DOOM which runs on everything from Windows 95 to Windows 10 (might need `msvcrt.dll` on old versions of Windows 95). 59 | 60 | ## The merged binary 61 | 62 | `smash.py` merges the two binaries together to produce the merged binary `DOOM.EXE`. What it basically does is extend the size of the DOS header to make room for a PE header, appends the entire Windows binary to the DOS binary, then inserts the PE header into the slack space and rewrites the section offsets appropriately. On DOS, the `next_offset` in the header is ignored: DOS/4GW uses the executable size in the second and third WORDs of the DOS header to find the next binary to load. On Windows, the entire DOS header is ignored except for the `next_offset`, which points to the PE header and thus loads the Windows binary. 63 | 64 | `DOOMUPX.EXE`, similarly, is generated by merging together the two UPX'd binaries `DOOMDUPX.EXE` and `DOOMWUPX.EXE`. It's quite a bit smaller, but the use of UPX (plus the unusual file structure) seems to cause a lot of virus scanners to flag it as suspicious. It's the original version that was [posted to Twitter](https://twitter.com/nneonneo/status/1327373913588785153). 65 | 66 | ## Acknowledgements 67 | 68 | Thanks [@angealbertini](https://twitter.com/angealbertini/status/1327138949408624642) ([angea](https://github.com/angea)) for this neat challenge, and for making a [lovely picture](https://twitter.com/angealbertini/status/1737121148368810169) dissecting the file structure. 69 | -------------------------------------------------------------------------------- /smash.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | ''' Pack a DOS binary and a Windows binary into a single "universal" .exe file. 3 | 4 | The DOS binary should be using the DOS/4GW extender. ''' 5 | 6 | from struct import pack_into, unpack_from 7 | 8 | # Previous attempt: place the Windows EXE (plus all headers) after the entire DOS executable. 9 | # Windows hates this because it doesn't like it if next_offset is too large. 10 | # Workaround: patch the DOS binary header to include enough space for the PE header. 11 | 12 | dos = bytearray(open('DOOMD.EXE', 'rb').read()) 13 | win = bytearray(open('DOOMW.EXE', 'rb').read()) 14 | 15 | msg = b'Universal DOOM (DOS/WIN) by @nneonneo, 2020-11-29\0' 16 | msg = msg.ljust(len(msg) + (-len(msg)) % 4, b'\0') 17 | 18 | next_offset, = unpack_from(' 64: 67 | total_header_size, = unpack_from('