Attention: for an even better experience, use my new VSCode DOS development extension
Want to relive the 90ies and create little demos and games for DOS using C/C++ with "modern" tools? Then this project template is for you.
It's a turn key solution to setup a develpoment environment to create DOS demos/apps. consisting of:
- DJGPP, the GCC-based C/C++ compiler for DOS (DPMI/protected mode).
- GDB, the debugger used to debug the DOS demo/game.
- DOSBox-x, the bells and whistles DOS-emulator to develop and run the DOS demo/game in.
- CMake, to define what needs to be build how.
- Ninja, to execute the build defined by CMake.
- Visual Studio Code, to tie all the above together and provide an integrated development environment.
Install:
- Git: On Windows, install Git for Windows. You will need
Git Bash
to run a shell script later. - CMake: ensure
cmake
is in yourPATH
. - Ninja
- Windows: Put the ninja.exe file somewhere and make sure it's available on the command line via your system PATH.
- Linux, e.g. Debian/Ubuntu:
sudo apt install ninja-build
- macOS, through brew:
brew install ninja
- Visual Studio Code: ensure command line interface is installed and in your
PATH
.
Open a shell (Git Bash on Windows), then:
git clone https://github.com/badlogic/dos-dev-template
cd dos-dev-template
./download-tools.sh
The download-tools.sh
script will download everything you need to start developing for DOS.
Note: On Linux, you need to install the following packages using your distribution's package manager:
libncurses5 libfl-dev libslirp-dev libfluidsynth-dev
Once complete, open the dos-dev-template/
folder in Visual Studio Code.
The first time you start Visual Studio Code, you may be promoted to install the clangd language service. Make sure you click "Install!"
You may also be prompted to select a kit. Select djgpp
.
.vscode/ <- configuration files for VS Code & extensions
assets/ <- files used by the program
doc/ <- documentation and example programs
src/ <- source code goes here
main.c <- a little mode 13h demo app
tools/ <- contains GDB, DJGPP, DOSBOX after download
dosbox-x.conf <- DOSBox-x config enabling debugging via serial port
toolchain-djgpp.cmake <- CMake toolchain definition file for DJGPP
CMakeLists.txt <- CMake build definition.
download-tools.sh <- Script to download GDB, DJGPP, DOSBOX,
and Visual Studio Code extensions
Open the Run and debug
view as shown in the screenshot above, then start one of the two launch configurations:
debug target
: builds and launches the currently selected CMake launch target in DOSBox-x, and attaches the debugger.run target
: builds and launches the currently selected CMake launch target in DOSBox-x without attaching a debugger.
Make sure the Cmake Build Variant matches the launch configuration, e.g. set the
Debug
variant when launchingdebug target
, and theRelease
variant when launching therun target
. Otherwise, the debugger may not be able to attach (Release
variant when launchingdebug target
), or the program may hang, waiting for the debugger (Debug
variant when launchingrelease target
)
For your program to support debugging, you have to do 3 things:
- In one
.c
or.cpp
files, add the following code:#define GDB_IMPLEMENTATION #include "gdbstub.h"
- In your
main()
, callgdb_start()
. - In your main loop, call
gdb_checkpoint()
to give the debugger a chance to interrupt your program.
The debugger has a few limitations & gotchas:
- You can not set breakpoints while the program is running. Set breakpoints before launching a debugging session, or while the program is halted after hitting a breakpoint, stepping, or was interrupted.
- The debugger is quite a bit slower than what you may be used to. This is due to the use of the serial port emulation for communication.
- Always make sure to close the DOSBox-x window before launching a new debugging session.
- If the debugger appears to be stuck, either wait for it to timeout, or execute the
Debug: Stop
command from the command palette. - When you click the debugger's
Pause
button or pressF6
to pause the program, you will end up ingdb_checkpoint()
. - In general, the debugging support is one big hack, there may be dragons. Still better than
printf
-ing your way through life!
Easy, just open or create a new .h
, .c
, or .cpp
file in the src/
folder and type away. You will get full code completion, navigation and what young people call "lints". Newly created files will be added to the build automatically.
Your code is compiled with DJGPP, which means it will become a beautiful 32-bit protected mode application. Check out the resources below to get your DOS programming juices flowing:
- 256-Color VGA Programming in C, a 5 part tutorial on VGA programming, using DJGPP.
- The Art of Demomaking, a 16 part series explaining and demonstrating various demo effects, using DJGPP.
- Brennan's Guide to Inline Assembly, in case you want to speed up your app with some artisan, hand-crafted assembly.
While you can do a lot with just code, sometimes you need a good old file. Put the files for your program in the assets/
folder. The file will then be available to your program via the path assets/<your-file-name>
, which you can pass to e.g. fopen()
.
If you don't like graphical user interfaces, you can also do all of the above from the command line.
cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_TOOLCHAIN_FILE=tools/toolchain-djgpp.cmake -S . -B build -G Ninja
This configures your build to produce a debug binary. For a release binary, specify -DCMAKE_BUILD_TYPE=Release
. You generally run this command only when your build type changes.
Once configured, you can build the program as follows:
cmake --build build
This puts the executable file and assets in the build/
directory.
./tools/dosbox-x/dosbox-x -fastlaunch -exit -conf tools/dosbox-x.conf build/main.exe
Launches the executable in DOSBox-x. Make sure to build with -DCMAKE_BUILD_TYPE=Release
before launching.
./tools/dosbox-x/dosbox-x -fastlaunch -exit -conf tools/dosbox-x.conf build/main.exe
Launches the executable in DOSBox-x. Make sure to build with -DCMAKE_BUILD_TYPE=Debug
before launching.
In a second shell:
./tools/gdb/gdb
(gdb) file build/main.exe
(gdb) target remote localhost:5123
GDB will connect to the program running in DOSBox-x. See the GDB cheat sheet on how to use the command line driver of GDB.
Here are a few questions you may have, if you want to dig deeper into the entire setup to modify it to your liking.
It will download the following tools for your operating system to the tools/
folder:
- My fork of GDB 7.1a, which is specifically build to debug DOS programs compiled with DJGPP remotely, e.g. running in DOSBox-x.
- DJGPP binaries provided by Andrew Wu
- My fork of DOSBox-x, which fixes a few things required for debugging, such as serial port emulation through TCP/IP on macOS.
After downloading the tools, it will install these Visual Studio Code extensions necessary for C/C++ development:
- clangd, the superior C/C++ code completion, navigation, and insights language service.
- CMake Tools, for CMake support in VS Code
- C/C++ for Visual Studio Code, needed to do anything C/C++ in VS Code. Its less than great intellisense service is replaced by clangd though.
- Native Debug Extension, provides the high-level debugger capanble of using GDB.
I'm glad you ask! It's an unholy ball of duct tape consisting of:
- A GDB stub, implemented as a header-file only library. It implements the GDB remote protocol and is included in the program itself, which it will then control and "expose" to the debugger for inspection and modification. It's a heavily modified and extended version of Glenn Engel & Jonathan Brogdon GDB stub, so it can actually do all the things expected of a somewhat modern debugger. I would suggest not looking into that file. It was not build with love or care, just enough spite so it gave up and started working. Mostly.
- A custom, minimal build of GDB 7.1a, one of the last versions of GDB to support
COFF_GO32
executables as produced by DJGPP. - A modified version of DOSBox-x, which was necessary as serial port communication over TCP/IP was broken on macOS. My pull request was merged, so hopefully this template can switch over to the official DOSBox-x build eventually.
And here's how it works:
- DOSBox-x is configured to expose serial port 0 (COM1) to the outside world via TCP on port 5123 in nullmodem, non-handshake mode.
- The program is started in DOSBox-x and first calls
gdb_start()
. This function initializes the serial port to use the highest baud rate possible, then triggers a breakpoint viaint 3
. This in turn triggers a special signal handler implemented in the GDB stub which listens for incoming data from the serial port. - The debugger (GDB) is told to connect to a remote process via TCP at address localhost:5123. This establishes a connection to the signal handler that waits for data coming in on the serial port.
- GDB sends commands, like set a breakpoint, step, or continue, which the signal handler interprets and executes.
- As a special case, if the program is running and the debugger wants to interrupt it to set further breakpoints or get information, the GDB stub also hooks the timer interrupt to poll the serial port state. In case there's data waiting, it set an internal flag, which will prompt
gdb_checkpoint()
to trigger a software breakpoint viaint 3
, which in turn will invoke the signal handler.
In theory, all of this is very simple. In practice, it can fall apart in the most creative ways. The implementation was tested with the included GDB version, as well as the Native Debugger extension which sits on top the included GDB version.
I can make no guarantees it will work with other GDB versions or higher level debuggers as found in e.g. CLion, etc. It should. But it might not, as most debuggers don't stick to the protocol spec.
These are configuration files for the various extensions and VS Code itself to provide you with a passable out-of-the-box experience.
- cmake-kits.json tells the CMake Tools extension about the DJGPP toolchain, which is defined as a CMake toolchain file in tools/toolchain-djgpp.cmake
- launch.json defines the 2 launch configurations
debug target
andrun target
. They in turn depend on tasks defined in tasks.json - settings.json turns off the not great C/C++ extension intellisense and configures a few other minor things.