Be the first to know and get exclusive access to offers by signing up for our mailing list(s).

Subscribe

We ❤️ Open Source

A community education resource

14 min read

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
FreeDOS QEMU live environment mode
FreeDOS booting up in a QEMU window
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.

QEMU terminal LiveCD with D prompt
FreeDOS showing a DOS prompt in a QEMU window
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
QEMU tiny command prompt
FreeDOS showing a DOS prompt in a QEMU window
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

About the Author

Jim Hall is an open source software advocate and developer, best known for usability testing in GNOME and as the founder + project coordinator of FreeDOS. At work, Jim is CEO of Hallmentum, an IT executive consulting company that provides hands-on IT Leadership training, workshops, and coaching.

Read Jim's Full Bio

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.

Want to contribute your open source content?

Contribute to We ❤️ Open Source

Help educate our community by contributing a blog post, tutorial, or how-to.

We're hosting two world-class events in 2026!

Join us for All Things AI, March 23-24 and for All Things Open, October 18-20.

Open Source Meetups

We host some of the most active open source meetups in the U.S. Get more info and RSVP to an upcoming event.