Everything a DOS program asks of the operating system goes through one interrupt. At boot DOS points vector 21h at its own dispatcher; a program puts a function number in AH and the arguments in other registers and executes INT 21h. The dispatcher looks AH up in a table, runs the service and comes back with IRET. Because the call goes through the vector table, a program never needs to know where DOS sits in memory, and the same .COM file runs under every DOS that has the functions it uses.

The functions grew with the versions. DOS 1 had 00h–2Eh, modelled on CP/M: console input and output, and files through file control blocks (FCBs). DOS 2 added the part programs still use, borrowed from Unix: file handles, subdirectories, memory allocation, loading programs (2Fh–57h). Later versions went on to 6Ch, and Windows 95 put long file names under 71h. A typical program needs a dozen of them.

Calling a service

AH always holds the function number. Some functions also take a subfunction or a mode in AL (INT 21h AH=3Dh the access mode, INT 21h AH=44h a whole family of device calls). The other registers have roles that recur from one service to the next:

RegisterUsual role
AHthe function number
ALa subfunction or mode; a character or a status coming back
DS:DXa buffer or a name: a $-terminated string for 09h, an ASCIIZ file name (ending in a 0 byte) for the file services
BXa file handle
CXa byte count, or file attributes
ES:BX, ES:DIa far pointer handed back, or a second name
CFon return from the DOS 2 services: 0 success, 1 error

Here are three output services printing parts of one line: a character from DL, a string up to a "$" from DS:DX, and a counted block of bytes sent to handle 1.

prints: DOS says hello
MOV DL, "D"
MOV AH, 02h ; 02h: one character, from DL
INT 21h
MOV DX, OFFSET PART1
MOV AH, 09h ; 09h: a string up to "$", from DS:DX
INT 21h
MOV DX, OFFSET PART2
MOV CX, 13 ; " says hello", CR, LF
MOV BX, 1 ; handle 1: standard output
MOV AH, 40h ; 40h: CX bytes from DS:DX to a handle
INT 21h
PART1 DB "OS$"
PART2 DB " says hello", 13, 10

DOS keeps every register it does not return a result in, with one exception that bites: AL. The output services return with AL changed, many others leave something in AX. Load AH again before every call rather than counting on it to survive; it costs two bytes. The flags come back changed too: CF carries the result of the handle-era services, and the others leave it undefined.

Errors: CF and AX

The services introduced with DOS 2 all report failure the same way: CF = 1 and an error code in AX. On success CF = 0 and AX holds the result, a handle or a byte count. So the instruction right after INT 21h is usually JC to an error path, or JNC past one. The older services have no such convention: the FCB functions and a few others, such as setting the date, return AL = FFh for failure, and the console services cannot fail at all.

AXMeaning
01hinvalid function number
02hfile not found
03hpath not found
04htoo many open files: no handle left
05haccess denied (a read-only file, a directory, a full root directory)
06hinvalid handle
08hnot enough memory
0Fhinvalid drive
12hno more files (find first / find next)
prints: open failed, error 02h
MOV AX, 3D00h ; DOS: open a file, AL = 0: for reading
MOV DX, OFFSET FNAME1 ; DS:DX = the name, ending in a 0 byte
INT 21h
JNC OPENED1 ; CF = 0: AX = a handle
PUSH AX ; CF = 1: AX = the error code
MOV DX, OFFSET FAIL1
MOV AH, 09h ; DOS: print the string at DS:DX
INT 21h
POP AX
CALL HEX2 ; 02: file not found
MOV DL, "h"
MOV AH, 02h ; DOS: print the character in DL
INT 21h
JMP DONE1
OPENED1:
MOV BX, AX ; it exists after all: close it
MOV AH, 3Eh
INT 21h
MOV DX, OFFSET OK1
MOV AH, 09h ; DOS: print the string at DS:DX
INT 21h
DONE1:
FNAME1 DB "NOSUCH.XYZ", 0
FAIL1 DB "open failed, error $"
OK1 DB "opened", 13, 10, "$"
HEX2 PROC NEAR ; print AL as two hex digits
PUSH AX
MOV CL, 4
SHR AL, CL ; the high digit first
CALL NIB1
POP AX
AND AL, 0Fh ; then the low one
CALL NIB1
RET
HEX2 ENDP
NIB1 PROC NEAR ; print AL (0-15) as one hex digit
ADD AL, "0"
CMP AL, "9"
JBE NIB2
ADD AL, 7 ; 10-15 become "A"-"F"
NIB2:
MOV DL, AL
MOV AH, 02h
INT 21h
RET
NIB1 ENDP

DOS 3 added function 59h, which returns an extended error code with a class, a suggested action and the place where it happened, for a program that wants to say more than "error 5".

Standard handles and redirection

HandleNameNormally
0standard inputCON: the keyboard
1standard outputCON: the screen
2standard errorCON: the screen, never redirected by COMMAND.COM
3standard auxiliaryAUX: the first serial port
4standard printerPRN: the first parallel port

Every program starts with these five handles open. The console services are not wired to the keyboard and the screen: since DOS 2 they read handle 0 and write handle 1, the same handles INT 21h AH=3Fh and INT 21h AH=40h use. When the user types PROG < IN.TXT > OUT.TXT, COMMAND.COM opens the two files as handles 0 and 1 before it starts the program, and the program's 01h, 02h, 09h and 0Ah calls read and write the files without knowing. A message that must reach the screen whatever happens goes to handle 2; output that must stay on the screen and nowhere else goes through the BIOS (INT 10h) or straight into video memory.

In the workbench the Terminal shows what DOS writes to the CON device: the output of 02h, 06h, 09h and 40h to handles 1 and 2, and the echo of 01h and 0Ah. BIOS output appears only on the DOS screen.

Ctrl+C and Ctrl+Break

DOS lets the user stop a program with Ctrl+C. The console services watch for it: when the key read is 03h, or, in MS-DOS, when a Ctrl+C is found waiting in the keyboard buffer during console output, DOS prints ^C and executes INT 23h. The handler COMMAND.COM provides ends the program, so for a program that has not hooked INT 23h a Ctrl+C means an immediate exit, its open handles closed. Ctrl+Break works one level lower: the BIOS keyboard handler sees it and calls INT 1Bh, DOS's INT 1Bh handler sets a flag, and the next service that checks treats it as a Ctrl+C.

ServicesCtrl+C
01h–05h, 08h–0Chchecked
06h, 07hnever checked: Ctrl+C comes back as the character 03h
all the otherschecked only after BREAK ON (INT 21h AH=33h with AL = 01h, DL = 1)

A program that must not be stopped halfway, one that has hooked a vector or changed the video mode, installs its own INT 23h handler with INT 21h AH=25h. If the handler returns with IRET, DOS carries on with the interrupted service as if nothing had happened; if it returns with a far return and CF = 1, DOS ends the program. DOS puts vector 23h back from the PSP when the program exits, so this one need not be restored by hand. MS-DOS also acts on Ctrl+S (pause the output) and Ctrl+P (copy the console to the printer) at the same points.

DOSBox-X arrives at much the same behaviour by another route. Its keyboard handler notes a Ctrl+C or Ctrl+Break keypress, and the next write to CON or the next service that checks ends the program. A 03h that a program puts into the buffer itself, with INT 16h AH=05h, is noticed only when 01h, 08h or 0Ah read it, which is why the examples in this chapter can feed Ctrl+C to INT 21h AH=07h safely.

The services by family

ServicesWhat for
INT 21h AH=01h, INT 21h AH=02h, INT 21h AH=06h, INT 21h AH=07h, INT 21h AH=08h, INT 21h AH=09h, INT 21h AH=0Ah, INT 21h AH=0Bhthe console: keys in, characters and strings out
INT 21h AH=3Ch, INT 21h AH=3Dh, INT 21h AH=3Fh, INT 21h AH=40h, INT 21h AH=42h, INT 21h AH=3Ehfiles through handles: create, open, read, write, seek, close
INT 21h AH=41h, INT 21h AH=43h, INT 21h AH=56h, INT 21h AH=57h, INT 21h AH=5Bhmanaging files: delete, attributes, rename, time stamp, create new
INT 21h AH=1Ah, INT 21h AH=4Eh, INT 21h AH=4Fhsearching a directory with wildcards
INT 21h AH=39h, INT 21h AH=3Ah, INT 21h AH=3Bh, INT 21h AH=47h, INT 21h AH=0Eh, INT 21h AH=19h, INT 21h AH=36hdirectories and drives
INT 21h AH=44h, INT 21h AH=45hdevices and handles: IOCTL, duplicating a handle
INT 21h AH=48h, INT 21h AH=49h, INT 21h AH=4Ahmemory blocks
INT 21h AH=4Bh, INT 21h AH=4Ch, INT 21h AH=4Dh, INT 21h AH=31h, INT 21h AH=62hprograms: run a child, exit, return codes, stay resident, the PSP
INT 21h AH=25h, INT 21h AH=35h, INT 21h AH=2Ah, INT 21h AH=2Ch, INT 21h AH=2Dh, INT 21h AH=30hthe system: interrupt vectors, date and time, the DOS version
0Fh–24h, 27h–29hthe DOS 1 file services through FCBs: replaced by the handles

Pitfalls

DOS is not re-entrant: it keeps one set of internal stacks and variables, so code that runs on a hardware interrupt (a timer or keyboard handler) must not call INT 21h, because the interrupt may have arrived while DOS itself was running. Resident programs that need DOS from an interrupt watch the InDOS flag (function 34h) and the idle interrupt INT 28h. Every program ends with INT 21h AH=4Ch (the guide's skeleton adds MOV AX, 4C00h / INT 21h after each example); running off the end of the code executes whatever bytes follow.

In DOSBox-X the services are not x86 code. Vector 21h points at a few bytes holding a callback instruction that hands control to the emulator, which runs the service in C++ and returns; stepping into INT 21h in a debugger shows that stub and nothing more. One difference to know about: when INT 21h AH=0Ah reads redirected input and the file ends, DOSBox-X does not return to the program; it shuts the emulator down.