Painting of floppy disks, compact discs, and an old computer monitor with its circuitry exposed, generated by DALLE-2

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:

  1. Edit our source code using whatever text editor we want (e.g. VS Code)
  2. Leverage DOSBox to automatically do the following:
    1. Mount the source code in MS-DOS
    2. Compile the source code in Turbo C
    3. Capture the output logs and check for errors
  3. 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 as DOS.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) to O:(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:

  1. Download this VGA color palette tester to your ./src folder : main.c
  2. Test if everything works: ./build.sh && ./run.sh
A screenshot of the compiled test program successfully running in DOSBox, with rows of VGA colours on screen.

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
}