
How to Compile MS-DOS Programs from your Windows/Mac/Linux desktop
Overview
The setup for this is pretty robust, since we're going to sidestep the issues with trying to get 16-bit compilation working on a modern computer.
We'll be able to:
- Edit our source code using whatever text editor we want (e.g. VS Code)
- Leverage DOSBox to automatically do the following:
- Mount the source code in MS-DOS
- Compile the source code in Turbo C
- Capture the output logs and check for errors
- If compilation succeeds, then we can also run the program automatically.
Check out an example template at GitHub.
Part 1: Installation
Start with installing DOSBox. If it doesn't automatically get added to your $PATH
variable, then follow the instructions for your OS to do this.
You'll also need to download Turbo C (official link) and extract it to an accessible folder on your computer (e.g. ~/turboc
). For inline assembly code, you'll need to download Turbo Assembler 5.0 - check the linked template above for an example integration.
Part 2: Project Structure
Create the following files:
~/project ├── build.dosbox.conf ├── build.sh ├── run.sh ├── build | ├── .gitkeep # optional - this file just keeps the folder present when git cloning └── src ├── main.c
build.sh
This script will be used to invoke DOSBox for compiling our code.
#!/usr/bin/env bash set -e executable="./build/GAME.EXE" buildlog="./build/TCC.LOG" SDL_VIDEODRIVER=dummy dosbox -conf ./build.dosbox.conf -noconsole if [ ! -f "$buildlog" ] then echo 'Error: No build log found.' exit 4 fi cat "$buildlog" if grep --quiet 'error|Error|Undefined symbol' $buildlog then echo 'Error(s) during compilation.' exit 5 elif [ ! -f "$executable" ] then echo 'Error: No executable found.' exit 6 fi
On Linux and Windows, the SDL_VIDEODRIVER=dummy
and -noconsole
options intentionally hide the DOSBox window. (During the build step, we don't need to view the window, which only takes a few seconds at most.)
We scan the contents of ./build/TCC.LOG
to automatically check for compilation errors.
Later, we'll populate the build.dosbox.conf
file which is referenced here.
run.sh
#!/usr/bin/env bash dosbox ./build/GAME.EXE
This script is used for executing the compiled code. To automatically run after a successful build, the command ./build.sh && ./run.sh
can achieve this.
If your game requires specific DOSBox settings (e.g. CPU speed), then you can create a run.dosbox.conf
file and update your run.sh
:
#!/usr/bin/env bash dosbox -conf run.dosbox.conf ./build/GAME.EXE
build.dosbox.conf
[autoexec] MOUNT S: ./src MOUNT T: ~/turboc MOUNT O: ./build DEL O:TCC.LOG T: TCC -1 -ml -IT:INCLUDE -IS: -LT:LIB -eGAME.EXE -nO: S:*.c > O:TCC.LOG EXIT
With this DOSBox config, we mount the folders containing our code, the Turbo C compiler, and build output. We then compile the code using Turbo C, store the output on our hard drive, and exit.
A breakdown of these Turbo C arguments:
-1
- Target 80186/286 (Optional - targets 80386 when omitted)-ml
- Uses the Large memory model, which we need to access VGA video memory-IT:\INCLUDE
- Includes the Turbo C header files (such asDOS.H
)-IS:\
- Includes your own header files in./src
-LT:\LIB
- Includes Turbo C's own libraries-eGAME.EXE
- Sets the compiled executable name (must be a 8.3 filename)-nO:\
- Outputs the compiled file(s) toO:
(a.k.a../build
)S:\*.c
- Compile your C source files in./src
> O:\TCC.LOG
- Redirects the output to./build/TCC.LOG
Part 3: Usage
If all is in working order, you should be able to compile and run an MS-DOS game that uses VGA graphics:
- Download this VGA color palette tester to your
./src
folder :main.c
- Test if everything works:
./build.sh && ./run.sh

Congratulations, if you've made it through all of these steps!
Part 4: Caveats
Since Turbo C was released in the 80s, these are a few things to remember to align with the older C spec:
- Comments must be block-style comments (
/* ... */
) - Source files must use Windows line endings (CRLF)
- Variables must be declared at the start of scope blocks
4.1 - Inline Assembly
If you use inline assembly code, Turbo Assembler 5.0 won't support the asm { ... }
block syntax:
/* Doesn't work! */ void interrupt scan_keyboard(void) { asm { cli ... sti } }
Instead, use the asm
keyword on each line:
/* Works! */ void interrupt scan_keyboard(void) { asm cli ... asm sti }