We ❤️ Open Source
A community education resource
Tiny programming with FreeDOS: Just a kernel, shell, editor, and compiler
DOScember spotlight: How to set up a minimal FreeDOS environment and program without frameworks.
Read the entire DOScember series and boot into vintage computing.
Programming should be fun. I got “into” programming years ago because writing my own programs on the Apple II was something I enjoyed. I continued with programming even though my undergraduate degree was physics, not computer science. I’ve always enjoyed writing programs to do something useful.
These days, writing a new program doesn’t take a lot of work. There are so many libraries and frameworks and infrastructure systems that programming can sometimes seem like rote work, more like connecting things than building things.
If you’re like me and want to get back to your roots of “programming for fun,” then I encourage you to set up a “retrocomputing” challenge: can you write useful programs starting with just a kernel, command shell, editor, and compiler?
Read more: 26 commands to get started with FreeDOS
A tiny programming environment
Let’s start by installing FreeDOS on a tiny disk image. Most Linux distributions should include QEMU, although you may have to install it as a standard package. For example, Fedora Linux users can use this command to install QEMU using the DNF package manager system:
$ sudo dnf install qemu
We don’t need much space to set up a tiny programming environment. The qemu-img command lets us create disk images for use with QEMU, such as this command to set up a 5 MB virtual disk image that we can install FreeDOS on:
$ qemu-img create tiny.img 5M
Now we need to install a version of FreeDOS on this disk image. FreeDOS is a modern DOS that includes a ton of tools, compilers, editors, games, and other programs. But we don’t need all of that; instead of installing a full version of FreeDOS, we’ll make it the smallest version possible and include only a few tools that we need to write our own programs.
The latest official version of FreeDOS is FreeDOS 1.4, which is provided in different “flavors,” including USB and CD-ROM images. I downloaded the LiveCD (FD14-LiveCD.zip) and the BonusCD (FD14-BonusCD.zip). Unzipping these files on my Linux system gives me the CD-ROM images: the LiveCD is FD14LIVE.iso and the BonusCD is FD14BNS.iso.
Read more: How I run virtual machines with QEMU
Set up a QEMU virtual machine using these two CD-ROM images, plus the tiny disk image. QEMU uses the -cdrom option to set up a CD-ROM image using IDE bus 1 unit 0, which means you can only use this option to connect one CD-ROM image at a time. To use both CD-ROM images at once, we can use QEMU’s -drive option and specify the IDE bus and unit values for each. This command sets the tiny disk image to IDE bus 0 unit 0, and the LiveCD and BonusCD images to IDE bus 1 units 0 and 1:
$ qemu-system-i386 -drive bus=0,unit=1,media=disk,file=tiny.img -drive bus=1,unit=0,media=cdrom,file=FD14LIVE.iso -drive bus=1,unit=1,media=cdrom,file=FD14BNS.iso -boot order=d

Screen capture by Jim Hall
Boot the LiveCD into “Live environment mode,” which is a working “minimal” version of FreeDOS. We can use the command line to partition the disk and install the few packages we need.

Screen capture by Jim Hall
To set up the hard disk so we can install FreeDOS on it, use the FDISK program. The /AUTO option will define a new partition that fills the entire disk; we can see the result with the /INFO option:
D:\>fdisk 1 /AUTO
D:\>fdisk 1 /INFO
Current fixed disk drive: 1 10080 sectors, geometry 10/016/63
Partition Status Mbytes System Usage Start CHS End CHS
C: 1 1 A 4 FAT-12 100% 0/001/01 9/015/63
Largest continuous free space for primary partition = 0 MBytes
Like any DOS, FreeDOS needs to reboot to re-read the partition information on the tiny disk. Use the reboot option to reboot the system back into the “Live environment mode.” At the prompt, create a DOS filesystem on the new partition with the FORMAT command. The /S option will also transfer the “system” files: the kernel and the command shell program.
D:\>format C: /S
WARNING: ALL DATA ON NON-REMOVABLE DISK
DRIVE C: WILL BE LOST! PLEASE CONFIRM!
Proceed with Format (YES/NO)?YES
Disk size: 4988 kbytes, FAT12. ***
Please enter volume label (max. 11 chars): TINY
Safe QuickFormatting (trying to save UnFormat data)
Cluster stats: 34 used, 0 bad, 2 items, 35 last.
Saving UNFORMAT information...
Mirror map is 384 bytes long, 37 sectors mirrored.
Preparing FAT area...
100 percent completed.
Safe QuickFormat complete.
Running SYS in a shell: sys C:
System transferred.
5,128,704 bytes total disk space (disk size)
5,107,712 bytes available on disk (free clusters)
4,096 bytes in each allocation unit.
1,247 allocation units on disk.
Volume Serial Number is 2B40-17F9
Now the “C:” drive is bootable with just less then 5 MB of space:
D:\>dir C:
Volume in drive C is TINY
Volume Serial Number is 2B40-17F9
Directory of C:\
COMMAND COM 87,772 04/02/2025 10:22a
KERNEL SYS 46,256 04/02/2025 10:22a
2 file(s) 134,028 bytes
0 dir(s) 4,968,448 bytes free
To compile programs with this tiny system, we need an editor and a compiler. The most basic editor is Edlin, which is a “line” editor similar to ed on Linux. Edlin is available on the Live environment system, so we can just copy it to the “C:” drive in a new DOS directory:
D:\>mkdir C:\DOS
D:\>copy \freedos\bin\EDLIN.EXE C:\DOS
\freedos\bin\EDLIN.EXE =>> C:\DOS\EDLIN.EXE
FreeDOS includes several C compilers, such as the Open Watcom C compiler and the IA-16 version of the GCC compiler. But the smallest C compiler is BCC, which is quite old and supports most of the C89 or “ANSI C” language standards. This package is available from the BonusCD, which is the second CD-ROM image that we connected to the QEMU virtual machine. That makes it the “E:” drive.
D:\>dir E:\packages\devel /w
Volume in drive E is FD14-BONUS0
Directory of E:\PACKAGES\DEVEL
[.] [..] BCC.ZIP BWBASIC.ZIP CPP2CCMT.ZIP
DOJS.ZIP EUPHORIA.ZIP FASM.ZIP FBC.ZIP FBC_HELP.ZIP
FDISRC.ZIP FPC.ZIP I16BUDOC.ZIP I16BUTIL.ZIP I16ELKLC.ZIP
I16ENV.ZIP I16GCC.ZIP I16GCDOC.ZIP I16LBI86.ZIP I16NEWLI.ZIP
I16NLELK.ZIP I16SRC.ZIP INDEX.DE INDEX.FR INDEX.GZ
INDEX.LST INDEX.SV INDEX.TR INSIGHT.ZIP JWASM.ZIP
KITTENC.ZIP LDEBUG.ZIP LUA.ZIP MSA.ZIP NASM.ZIP
PERL.ZIP REGINA.ZIP RUNTIME.ZIP SQLITE.ZIP SUPPLS.ZIP
TINYASM.ZIP UPX.ZIP WATCOMC.ZIP WATCOMF.ZIP
42 file(s) 297,429,858 bytes
2 dir(s) 0 bytes free
We can use the UNZIP command to extract this package, but only the components from the DEVEL path in the package.
D:\>unzip E:\packages\devel\BCC.ZIP DEVEL/* -d C:\
As a final step, let’s also create new startup files on the tiny disk. The FreeDOS kernel looks first for FDCONFIG.SYS then CONFIG.SYS, so let’s use Edlin to make a new FDCONFIG.SYS file:
D:\>edlin C:\fdconfig.sys
C:\fdconfig.sys: New file.
*a
: SHELL=C:\\command.com /P
: .
*w
C:\fdconfig.sys: 1 line written
*q
Really quit (Y/N)? y
Edlin recognizes the backslash character (\) to insert special codes into a source file. To insert a literal backslash, we needed to use \\ which Edlin will turn into a single backslash when it saves the file.
This FDCONFIG.SYS file only defines the shell as C:\COMMAND.COM, and lets FreeDOS use defaults for other configuration values. The /P option tells COMMAND.COM that it is a “permanent” shell; this also means the shell reads the AUTOEXEC.BAT file to set the shell’s environment, such as the list of directories where it might look for programs: C:\DOS and C:\DEVEL\BCC\BIN. Let’s use Edlin to create the startup file with a few variables and command aliases:
D:\>edlin C:\autoexec.bat
C:\autoexec.bat: New file.
*a
: @ECHO OFF
: PATH C:\\dos;C:\\devel\\bcc\\bin
: SET TEMP=C:\\DOS
: SET DIRCMD=/O:GNE
: alias ls=DIR /W /B /L
: .
*w
C:\autoexec.bat: 5 lines written
*q
Really quit (Y/N)? y
The DIRCMD variable sets options that will always get used with the DIR command, such as /O to set the output order; using /O:GNE means directories will be grouped first (G), and files will be sorted by name (N) then extension (E). The ls alias is just a special way to run the DIR command with a few extra options that make it more like the Linux ls command. The /W option lists files in a “wide” format, /B uses “bare” output without the disk summary, and /L uses lowercase.
Finally, type the shutdown command to stop the virtual machine so we can use the new disk in a new QEMU instance.
Programming for fun
Let’s boot the new disk in a virtual machine so we can write a few programs. The disk image contains a working version of FreeDOS, although in a tiny size. Since this minimal install only includes the kernel, command shell, editor, and compiler—but not other tools like a memory manager—there’s no point in giving the virtual machine a lot of memory. The bare minimum will do. QEMU allows you to specify the memory size with the -m option; by default this is in megabytes, and -m 1 is the smallest we can make it:
$ qemu-system-i386 -m 1 -hda tiny.img

Screen capture by Jim Hall
This tiny programming environment is all we need to write new tools. First, let’s recognize that the FreeDOS command shell provides a number of built-in commands, called “Internal” commands:
C:\>?
Internal commands available:
ALIAS BEEP BREAK CALL CD CHDIR CDD CHCP
CLS COPY CTTY DATE DEL DIR DIRS DOSKEY
ECHO ERASE EXIT FOR GOTO HISTORY IF LFNFOR
LH LOADHIGH LOADFIX MEMORY MD MKDIR PATH PAUSE
PROMPT PUSHD POPD RD REM REN RENAME RMDIR
SET SHIFT TIME TITLE TRUENAME TYPE VER VERIFY
VOL ? WHICH
For example, DEL will delete files (like rm on Linux), COPY will copy files, and so on. For more features, we need to write our own programs, which we might store in a directory called SRC:
C:\>mkdir src
C:\>cd src
Let’s start by writing a simple “Hello world” test program, to make sure that the compiler is working correctly. Use Edlin to create a new hello.c file:
C:\SRC>edlin hello.c
hello.c: New file.
*a
: #include <stdio.h>
: int main()
: {
: puts("Hello world");
: return 0;
: }
: .
*w
hello.c: 6 lines written
*q
Really quit (Y/N)? y
When working with a line editor like Edlin, I find it helps not to add too many unnecessary blank lines. This file is written very sparsely, without any blank lines at all. But it’s still a valid program, which we can demonstrate by compiling it. The BCC compiler is basically a translator from C into Assembly, which is why it generates old-style DOS .COM programs. Specify the output program name with the -o option, like this:
C:\SRC>bcc -o hello.com hello.c
C:\SRC>dir hello.*
Volume in drive C is TINY
Volume Serial Number is 2B40-17F9
Directory of C:\SRC
HELLO C 75 12-15-25 6:51p
HELLO COM 1,596 12-15-25 6:52p
2 file(s) 1,671 bytes
0 dir(s) 3,715,072 bytes free
Running the program prints the friendly “Hello world” message:
C:\SRC>hello
Hello world
Read more: How to write your first FreeDOS program
Let’s write something more complicated, like a version of the Linux seq command. For this version, we’ll only support number arguments like this:
seq LAST
seq FIRST LAST
seq FIRST STEP LAST
Without any arguments, the program will assume counting from 1 to 10, by 1.
C:\SRC>edlin seq.c
seq.c: New file.
*a
: #include <stdio.h>
: #include <stdlib.h>
: int main(int argc, char **argv)
: {
: int num=1, last=10, step=1;
: if (argc==2) {
: last=atoi(argv[1]);
: }
: else if (argc==3) {
: num=atoi(argv[1]);
: last=atoi(argv[2]);
: }
: else if (argc==4) {
: num=atoi(argv[1]);
: step=atoi(argv[2]);
: last=atoi(argv[3]);
: }
: else if (argc>4) {
: puts("too many options");
: return 1;
: }
:
: while (num<=last) {
: printf("%d\n", num);
: num+=step;
: }
:
: return 0;
: }
: .
Oops, I forgot to use a double backslash on my printf statement, which means Edlin will try to interpret n as a special character—which it isn’t, so nothing will get inserted. But I can retype that line with a few Edlin commands. Use the l command in Edlin to list the ten most recent lines, then enter the line number you want to retype. Just be sure to enter the double backslash:
*l
18: else if (argc>4) {
19: puts("too many options");
20: return 1;
21: }
22:
23: while (num<=last) {
24: printf("%d", num);
25: num+=step;
26: }
27:
28: return 0;
29:*}
*24
24:* printf("%d", num);
24: printf("%d\\n", num);
*w
seq.c: 29 lines written
*q
Really quit (Y/N)? y
This program specifies the main program’s argument list using C89 or “ANSI C” style. To compile it, we need to add the -ansi option to the BCC command line:
C:\SRC>bcc -ansi -o seq.com seq.c
C:\SRC>dir seq.*
Volume in drive C is TINY
Volume Serial Number is 2B40-17F9
Directory of C:\SRC
SEQ C 498 12-15-25 6:58p
SEQ COM 4,044 12-15-25 6:59p
2 file(s) 4,542 bytes
0 dir(s) 3,710,976 bytes free
And with a few test cases, we can see the program works as expected:
C:\SRC>seq
1
2
3
4
5
6
7
8
9
10
C:\SRC>seq 6 8
6
7
8
C:\SRC>seq 10 5 20
10
15
20
However, this program only counts “up” from the first to the last number. Be careful if you specify range values that would produce infinite output; for example, 1 -1 10 to count from 1 to 10, stepping by -1, will print an endless list of negative numbers.
Another useful program counts words in a text file. For example, the Linux wc command counts not just words, but lines and characters. We can write our own version of this command in this tiny environment:
C:\SRC>edlin wc.c
wc.c: New file.
*a
: #include <stdio.h>
: #include <ctype.h>
: int main()
: {
: unsigned long words=0, chars=0, lines=0;
: int c, last=0;
: while ((c=getchar()) != EOF) {
: chars++;
: if (c=='\\n') { lines++; }
: if (isspace(c) && !isspace(last)) { words++; }
: }
: printf("%ld %ld %ld\n", lines, words, chars);
: return 0;
: }
: .
*w
wc.c: 14 lines written
*q
Really quit (Y/N)? y
If we compile this program and give it a list of numbers from the seq command, we can see how it counts lines, words, and characters:
C:\SRC>bcc -o wc.com wc.c
C:\SRC>seq | wc
10 10 21
That’s correct! Without any options, the seq command generates a list from 1 to 10, which should be 10 lines and 10 words. If you count the letters and include an invisible “new line” character at the end of each line, that’s 21 characters.
This simple program only reads files from “standard input.” That means if you want to examine a file, you need to use < on the command line to “read from” a file. Let’s test this by writing a 2-line file with Edlin, then using wc to count the words and lines:
C:\SRC>edlin file
file: New file.
*a
: this is a test file
: that has two lines in it.
: .
*w
file: 2 lines written
*q
Really quit (Y/N)? y
C:\SRC>wc < file
2 11 46
That’s correct too! We know the file has 2 lines: one with 5 words and another with 6 words, or 11 words total. And including the extra “new line” at the end of each line, that’s 46 characters in the file.
Make programming fun
With a simple editor and capable C compiler, the programs you make are limited only by your imagination (and patience when editing with Edlin). For example, you might try writing a simple desktop calculator or a Markdown-like processor or other interesting programs. With more effort, and some creativity, you can make any program you like, including tools, utilities, and games.
Make programming fun by going back to the basics. Install a tiny version of FreeDOS in your own virtual machine and see what you can create.
Read the entire DOScember series and boot into vintage computing.
More from We Love Open Source
- 26 commands to get started with FreeDOS
- Turn off acceleration for an authentic retrocomputing experience
- How to write your first FreeDOS program
- How I run virtual machines with QEMU
- Why FreeDOS is a modern DOS
The opinions expressed on this website are those of each author, not of the author's employer or All Things Open/We Love Open Source.