QEMU config for running DOS compilers

Last modified: Wed Dec 2 15:25:53 EST 2020

QEMU command line base

Using QEMU version 5.1.0 on 64-bit Linux.

QEMU="qemu-system-i386 -nodefaults -enable-kvm  Omit superfluous emulated devices.  Use hardware virtualization.
-audiodev alsa,id=alsa  Use ALSA audio backend (YMMV; pa = PulseAudio).
-machine type=pc,accel=kvm,pcspk-audiodev=alsa  Emulate i440FX + PIIX chipset and PC speaker.
-cpu host  Use the KVM processor with all supported host features.
-display gtk  Use GTK-based QEMU window.
-vga std -net none  Emulate "standard" VGA card.  No network card.
-rtc base=localtime  Calibrate emulated CMOS clock for correct results in DOS.
-serial stdio  Connect COM1 to stdin/stdout of the QEMU process.
-m 4G  Provide 4 GiB guest RAM.
-mem-prealloc -mem-path /hugepages/qemu"  Use 4 × 1 GiB hugepages mounted at /hugepages (optional).

The deprecated option -soundhw pcspk can replace both -audiodev alsa,id=alsa and pcspk-audiodev=alsa.

Creating images

To create a sparse image file for use as an emulated HDD of size 2 GiB:

qemu-img create -f raw C.img 2G

To boot from a floppy image (A.img) for the purpose of running FDISK or FORMAT on the empty HDD image (C.img):

${QEMU} \
  -drive file=C.img,format=raw,if=ide,media=disk,readonly=off,index=0,cache=unsafe \
  -drive file=A.img,format=raw,if=floppy,media=disk,readonly=off,index=0,snapshot=on \
  -boot a

(cache=unsafe means that changes made to the image by the guest will be saved, while snapshot=on means that they will be discarded.)

To prepare a bootable C: drive image:

  1. Create empty 2 GiB image.
  2. Boot Windows 98 SE floppy image, do FDISK.  (For compatibility reasons, I prefer to use W98SE FDISK for partitioning instead of FreeDOS or Linux fdisk.)
  3. Boot FreeDOS floppy image, do FORMAT /S /Q /U /V:DOS C:

To prepare a non-bootable empty scratch drive image, repeat these steps but end with FORMAT /Q /U /V:SCRATCH C:

File transfer between host and guest

The easiest way to get files in or out is just mount the image when the VM isn't running.

To mount C: from Linux in such a way that Long File Names (LFN) will be compatible with FreeDOS:

mount -t vfat -o uid=1000,gid=100,shortname=winnt,loop,offset=32256 C.img /mnt

The offset of 32256 is specific to how W98SE FDISK partitions the drive, with the file system starting at sector 63.  If Linux fdisk was used instead, the offset is 1048576.

Booting the VM


${QEMU} \
  -drive file=C.img,format=raw,if=ide,media=disk,readonly=off,index=0,cache=unsafe \
  -drive file=D.img,format=raw,if=ide,media=disk,readonly=off,index=1,snapshot=on \
  -boot c

To discard guest changes to C:, replace cache=unsafe with snapshot=on.

Basic DOS configuration (without UMBs)






On actual hardware, DOSLFNMS slows things to a crawl unless a disk cache like XHDD is used.  In QEMU, I found no benefit to the DOS cache:  things are slowed to a crawl regardless what you do.


Memory Type         Total      Used       Free
----------------  --------   --------   --------
Conventional          639K        36K       603K
Upper                   0K         0K         0K
Reserved              385K       385K         0K
Extended (XMS)   3,144,576K       158K 3,144,418K
----------------  --------   --------   --------
Total memory     3,145,600K       579K 3,145,021K

Total under 1 MB      639K        36K       603K

Memory accessible using Int 15h         0K (      0 bytes)
Largest executable program size       603K (617,360 bytes)
Available space in High Memory Area     8K (  8,055 bytes)
FreeDOS is resident in the high memory area.

Alternate DOS configuration (with UMBs)


This configuration is not as well tested since I think it is more complicated than necessary.

The DJGPP FAQ recommends using an expanded memory manager (EMM) like JEMM386, mainly to improve the management of extended memory:

Memory managers provide an API for allocating extended memory called VCPI (the Virtual Control Program Interface).  Using that API allows CWSDPMI to allocate only as much extended memory as is needed, leaving the rest for non-DJGPP programs, in case you invoke them from DJGPP programs.  In contrast, without a memory manager, CWSDPMI will allocate all of the available extended memory to itself, leaving none of it to non-DJGPP programs.  This consideration is especially important if you use some DJGPP program, like Bash or Emacs, as your primary system interface.

The special case of a DJGPP program invoking a non-DJGPP program that uses extended memory can happen as described in the last line, but I found no benefit to using a complicated EMM instead of a simple extended memory manager (XMM) if you are only using DJGPP and friends to build and run Unix software.  There's enough low memory without UMBs.

Manual examination of the upper memory area shows that 0xCA300 to 0xEA7FF (written as CA30-EA7F on the JEMM386 command line) is all zeros and thus probably unused by QEMU's emulation.  But automatic detection always excludes part of this range for whatever reason, so take care.  JEMM386 furthermore refuses to start any lower than 0xCB000 even if the SPLIT option is given.


DEVICE=C:\LOCAL\DRIVERS\XMGR.SYS /B /N24    ; Temporary "boot"





Memory Type         Total      Used       Free
----------------  --------   --------   --------
Conventional          736K        12K       724K
Upper                 124K        25K        99K
Reserved              164K       164K         0K
Extended (XMS)   3,144,576K       542K 3,144,034K
----------------  --------   --------   --------
Total memory     3,145,600K       743K 3,144,857K

Total under 1 MB      860K        37K       823K

Total Expanded (EMS)                8,576K (8,781,824 bytes)
Free Expanded (EMS)                 8,192K (8,388,608 bytes)

Memory accessible using Int 15h         0K (      0 bytes)
Largest executable program size       724K (741,312 bytes)
Largest free upper memory block        97K ( 98,960 bytes)
Available space in High Memory Area     8K (  8,055 bytes)
FreeDOS is resident in the high memory area.

Redirecting the console

The GTK-based QEMU window doesn't support copy/paste, doesn't respect X's XkbLayout, and doesn't have a scrollback buffer.  Unfortunately, the following two ways of bypassing it each have their own limitations.

Using emulated serial port

To redirect console input from and output to the host XTerm (stdin/stdout of the QEMU process), say CTTY COM1 at the C: prompt.

Plusses:  Supports copy/paste.  Respects XkbLayout.  XTerm's scrollback buffer and the script command can be used to capture logs.

Minuses:  Backspace and paste malfunction on first use (glitch).  Some programs in the DJGPP collection, notably the Bash shell and Emacs, override the redirection and switch back to the GTK window.  ^C kills QEMU.  Cannot log messages printed before the shell is started.

Using curses

Another approach is to replace -display gtk with -display curses in the QEMU command line.  The 25 lines of VGA text are then rendered in the center of the XTerm.  This window-in-a-window can be expanded to 50 lines with the DOS command MODE CON LINES=50.

Plusses:  Supports copy/paste.  Respects XkbLayout.  Bash and Emacs do the right thing.

Minuses:  The first character typed or pasted is eaten (glitch).  No scrollback.  Logs captured by the script command are full of escape sequences and thus unreadable.