Skip to main content

INT 3 vs INT 21h in 8086 Assembly: Breakpoint (CC) vs DOS Service Call

INT 3 is the one-byte CC debugger trap; INT 21h is the two-byte CD 21 gateway to DOS, where AH selects the service. Covers the opcode difference (NASM’s int 3 is not CC), the stack frame, DEBUG behaviour, the standard AH function table from Ralf Brown’s list, a runnable companion module and 14 viva questions.

You wrote int 3 expecting a breakpoint. Your debugger stopped, but on the wrong instruction, in the middle of an opcode, and the next mov never ran. That is not a debugger bug: in NASM, int 3 does not assemble to the one-byte breakpoint at all. This article runs the experiment, and uses it to explain the difference everybody asks about in viva and exam questions: INT 3 is a one-byte debugger trap (CC), INT 21h is the two-byte door into DOS (CD 21) where the function number in AH picks the service. What you get below: the opcode difference shown as real bytes, the stack frame both instructions build, what DEBUG does with CC versus CD 03, a full table of the standard AH functions of INT 21h taken from Ralf Brown’s interrupt list, a runnable companion module, and a 14-question viva section in accordions. Everything I ran is marked as run; everything I could not run (emu8086, Microsoft’s own DEBUG.EXE, a real 8086) is marked as documented, not executed.
TL;DR. INT 3 = opcode CC, one byte, reserved for breakpoints, no parameters, handled by a debugger (vector 3, at 0000:000C). INT 21h = opcode CD 21, two bytes, handled by DOS (vector 21h, at 0000:0084), needs a function number in AH and usually reports failure through the carry flag.
If you are…Start here
New to interruptsThe first three sections: what an interrupt is, what INT 21h does, what INT 3 is, and the comparison table
Preparing for viva or examsThe comparison table, the AH function table, then the Viva questions section at the end
Debugging real codeThe DEBUG section (CC versus CD 03) and the byte-patching section near the end
What was run. NASM 2.16.01, DOSBox 0.74-3 (headless, runs the .COM files), FreeDOS Debug 2.50 (a DEBUG.EXE-compatible debugger, MIT licence), Unicorn 2.1.4 for the byte-patching experiment, all in a Linux sandbox. The code and captured output are in the interrupts module of the 8086-Programs repository; 31 assertions in check.py pin every claim made about that output. Not run: emu8086, Microsoft DEBUG.EXE, MASM/TASM, real MS-DOS on real hardware, a real 8086. The AH function table is reference data from Ralf Brown’s Interrupt List, not something I executed function by function.

An interrupt is a numbered far call through a table at address zero

Start with the mechanism both instructions share. The first 1024 bytes of an 8086’s memory, 0000:0000 to 0000:03FF, hold the interrupt vector table: 256 entries of four bytes, each a far pointer (offset word, then segment word) to a handler. INT n looks at entry n, which lives at address n * 4, pushes the flags, the code segment and the return IP, clears the interrupt flag (and the trap flag), and jumps to the handler. IRET undoes it. So INT 3 goes through address 3 * 4 = 0000:000C, and INT 21h goes through 21h * 4 = 0000:0084. The instruction is the same mechanism; what differs is who put a handler in that slot and what the handler does.
Interrupt vector table0000:0000vector 00000:000Cvector 3 (INT 3)0000:0010vector 4…0000:0084vector 21h (INT 21h)0000:03FCvector FFhBreakpoint handlerowned by the debuggerDOS dispatcherreads AH, jumps to thematching serviceOn INT npush FLAGSpush CSpush IPIF = 0, TF = 0IRETpops IP, CS, FLAGSBoth instructions use this one mechanism. What differs is who owns the slot and what the handler does.
The diagram is the whole story of this article. The next two sections look at the two slots in turn. First, a program that installs its own vector 3 handler and records what the CPU did. The interesting lines are the ones that read the stack the CPU built (int3_handler.asm):
; The handler runs with IF=0 and TF=0 (the CPU cleared them when it took the interrupt).
handler:
    push bp
    mov bp, sp
    push ax
    ; stack now: [bp+0]=saved BP, [bp+2]=return IP, [bp+4]=return CS, [bp+6]=FLAGS
    mov ax, [bp+2]
    mov [ret_ip], ax
    mov ax, [bp+6]
    mov [flags_pushed], ax
    pushf
    pop ax                  ; FLAGS as the handler itself sees them
    mov [flags_inside], ax
    pop ax
    pop bp
    iret
Run under DOSBox, with one CC trap and one CD 03 trap (output: 04-int3-handler.txt):
CC    (INT3): return IP = trap address + 1, IF pushed = 1, IF in handler = 0, TF in handler = 0, IF after IRET = 1
CD 03 (INT 3): return IP = trap address + 2, IF pushed = 1, IF in handler = 0, TF in handler = 0, IF after IRET = 1
Three things are now measured rather than remembered. The interrupt flag was 1 when the interrupt was taken (IF pushed = 1), 0 inside the handler, and 1 again after IRET, because IRET pops the old flags. The return IP points one byte past the trap for CC and two bytes past it for CD 03. One caveat: the trap flag was already 0 when this program trapped, so these lines show TF is 0 in the handler but do not prove the CPU cleared it; that part is from Intel’s documentation, not my run.
Trap to remember. IF = 0 inside the handler means other maskable hardware interrupts are held off until the handler does IRET (or executes STI). A handler that loops forever without re-enabling interrupts freezes the keyboard. This is also why breakpoint handlers are kept short.
Going deeper

INT 21h is the front door to DOS, and AH picks the room

INT 21h is an ordinary software interrupt whose handler happens to be DOS. When DOS starts it fills vector 21h with its dispatcher. You put a function number in AH, the other arguments in other registers, execute INT 21h, and DOS jumps to the right service and returns the result in registers. Here is the smallest useful program (hello21.asm): print a string with AH=09h, then exit with return code 7 using AH=4Ch.
    mov ah, 09h             ; AH selects the DOS function: write '$'-terminated string
    mov dx, msg             ; DS:DX -> string (DS = CS in a .COM file)
    int 21h                 ; CD 21 -- the DOS entry point
    mov ax, 4C07h           ; AH=4Ch terminate, AL=7 return code (ERRORLEVEL)
    int 21h
msg db 'Hello from INT 21h, AH=09h', 0Dh, 0Ah, '$'
Output, captured under DOSBox, including a batch check that the return code reached the shell (if errorlevel 7 is true when ERRORLEVEL is 7 or more; fn_tour later reads back exactly 7 with AH=4Dh) (02-hello21.txt):
Hello from INT 21h, AH=09h
ERRORLEVEL is at least 7
Your programmov ah, 09hmov dx, msgint 21hDOS dispatcher(vector 21h)switch on AHAH=01h read char, echoAH=02h write charAH=09h write $-stringAH=3Dh open fileAH=4Ch exit with codeOnly the matching service runs. AH is the only thing that selects it, which is why forgetting to set AH is the classic bug.
The diagram shows the cost of the design: one interrupt vector, and the function number in AH does all the routing. That is why INT 21h with the wrong AH does something unrelated instead of failing loudly. It is also why the full function list is worth having in front of you; it is further down. How failure is reported. Most file and memory functions signal errors through the carry flag: CF = 0 means success, CF = 1 means failure and AX holds an error code. The companion program fn_tour triggers this on purpose by deleting a file and opening it again, and by asking DOS for a megabyte it cannot give (03-fn-tour.txt):
AH=48h ask for 1 MB (fails)  : CF=1 AX=0008 ; largest block (paragraphs) = 8E6C
...
AH=41h delete, reopen fails : CF=1 AX=0002
AX=0002 is DOS error 2, file not found; AX=0008 is error 8, insufficient memory, and in that second case DOS also returns the size of the largest block it could have given you in BX. The error code meanings come from Ralf Brown’s list (the list of codes returned by AH=59h); I verified those two by triggering them, not the whole table.
The two classic beginner mistakes. (1) Setting AH and then clobbering it before INT 21h, for example mov ax, 4C00h versus mov ah, 4Ch: the first also sets the return code to 0 in AL, the second leaves AL as whatever it was. (2) Forgetting the $ that ends an AH=09h string: DOS keeps printing memory until it finds a dollar sign somewhere.
Going deeper

INT 3 is a breakpoint, and its trick is that it is one byte

A debugger sets a breakpoint by overwriting one byte of your program with CC, running, and waiting for vector 3 to fire. It is one byte on purpose: Intel’s documentation says the one-byte form can replace the first byte of any instruction, including another one-byte instruction, without overwriting the instruction after it. Its two-byte sibling INT n (CD 03 for n = 3) has the same effect on the flow of control but cannot do that. The same documentation adds a detail most tutorials skip: CD 03 is “the normal 2-byte opcode for INT 3”, and Intel and Microsoft assemblers will not generate it from any mnemonic. NASM, it turns out, does. Here is the listing of one source file with five spellings (opcodes.asm, output 01-opcodes.lst):
     1                                  ; opcodes.asm -- which bytes does each spelling assemble to?  (read the listing, never run)
     2                                  bits 16
     3                                  org 0x100
     4 00000000 CC                          int3                    ; the one-byte breakpoint        -> CC
     5 00000001 CD03                        int 3                   ; "INT n" with n = 3 in NASM      -> CD 03
     6 00000003 CD03                        db 0CDh, 03h            ; the same two bytes, written out -> CD 03
     7 00000005 CD21                        int 21h                 ; DOS entry point                 -> CD 21
     8 00000007 CD10                        int 10h                 ; BIOS video                      -> CD 10
     9 00000009 CE                          into                    ; one-byte INTO                   -> CE
Read the middle column: int3 gives CC, but int 3 gives CD03 in NASM 2.16.01. The same listing gives CD21 for int 21h, so the “INT 3 is one byte, INT 21h is two” rule in most tutorials is really “int3/CC is one byte, every INT n is two”. Whether MASM, TASM, or emu8086’s built-in assembler emit CC for INT 3 I did not test; Intel’s note says the Intel and Microsoft assemblers will not generate CD 03 from any mnemonic, which implies INT 3 gives CC there; that is documentation, not something I ran.
original414141inc cx ; inc cx ; inc cxCC patchCC4141trap, restore 1 byte: all three INCs still runCD 03 patchCD03412nd INC overwritten; restoring 1 byte is not enoughA debugger saves one byte per breakpoint because CC is one byte. CD 03 needs two saved bytes and a different return-address fix.
This is exactly the experiment in the byte-patching section below, run on a real (emulated) CPU; the picture above is the point of it. First, the comparison table that belongs to every answer on this topic. It keeps the five rows from the old version of this article and adds the mechanics this article measured:
FeatureINT 3 (breakpoint)INT 21h (DOS services)
PurposeDebugging: stop and hand control to a debuggerAsk MS-DOS for a service: I/O, files, memory, exit
FunctionHalts execution and invokes the debuggerPerforms system-level operations chosen by AH
OpcodeCC, one byte (int3 in NASM)CD 21, two bytes
Vector address0000:000C (3 * 4)0000:0084 (21h * 4)
Extra parametersNoneFunction number in AH, other arguments in other registers
Who handles itA debugger, or a dummy handler if none is installedDOS
Return address pushedOne byte past the trap (measured: +1)Two bytes past the instruction (as every INT n; same +2 measured for CD 03)
Reports errors viaDoes not return a statusUsually CF and an error code in AX
Typical useInspect registers and memory at a chosen addressDisplay text, read input, open/read/write files, allocate memory, terminate
Why INT 3 and INT 21h are not alternatives. A common wrong answer is to treat them as two ways to “call the OS”. They are not. INT 21h is how your program asks DOS for something. INT 3 is how a debugger (or you, as a debugging aid) interrupts your own program. You can, and in the original examples of this article do, use both in the same program.
Going deeper

What happens when a program executes INT 3 depends on who is listening

The same CC byte behaves differently depending on what owns vector 3. I ran three of the environments and could not run the rest, and the table says which is which. Failure mode first: the NASM spelling int 3 versus a debugger. Two tiny programs, identical except for the breakpoint bytes: dbg_cc.asm uses int3 (CC), dbg_cd03.asm uses db 0CDh, 03h (what NASM’s int 3 emits). Each is loaded into FreeDOS Debug 2.50 (run inside DOSBox, driven by a script: u, g, r, t, r, g, q). With CC, the debugger stops cleanly at the next instruction and a single step executes mov ax, 2222h (06-debug-cc.txt):
-u 100 L9
072E:0100 B81111            MOV     AX,1111
072E:0103 CC                INT     3
072E:0104 B82222            MOV     AX,2222
072E:0107 B8004C            MOV     AX,4C00
-g
Unexpected breakpoint interrupt
AX=1111 BX=0000 CX=000C DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000
DS=072E ES=072E SS=072E CS=072E IP=0104 NV UP EI PL ZR NA PE NC
072E:0104 B82222            MOV     AX,2222
...
-t
AX=2222 BX=0000 CX=000C DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000
DS=072E ES=072E SS=072E CS=072E IP=0107 NV UP EI PL ZR NA PE NC
072E:0107 B8004C            MOV     AX,4C00
With CD 03, the debugger stops at IP=0104, one byte inside the two-byte INT 03, and disassembles the second half of the opcode as the start of a different instruction. The step that follows executes that garbage, lands at 0108, and AX is still 1111: the mov ax, 2222h never ran (07-debug-cd03.txt):
-u 100 L9
072E:0100 B81111            MOV     AX,1111
072E:0103 CD03              INT     03
072E:0105 B82222            MOV     AX,2222
072E:0108 B8004C            MOV     AX,4C00
-g
AX=1111 BX=0000 CX=000D DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000
DS=072E ES=072E SS=072E CS=072E IP=0104 NV UP EI PL ZR NA PE NC
072E:0104 03B82222          ADD     DI,[BX+SI+2222]                DS:2222=0000
...
-t
AX=1111 BX=0000 CX=000D DX=0000 SP=FFFE BP=0000 SI=0000 DI=0000
DS=072E ES=072E SS=072E CS=072E IP=0108 NV UP EI PL ZR NA PE NC
072E:0108 B8004C            MOV     AX,4C00
Fingerprint of the bug. Registers that look fine, then a nonsense disassembly such as ADD DI,[BX+SI+2222] exactly one byte after your INT 03. That is consistent with a debugger that treats every vector-3 trap as a one-byte CC and backs IP up by one (the return address was 0105); I did not read Debug’s source to confirm that is what it does. The fix is to write int3 (or db 0CCh) whenever you mean a breakpoint. I observed this with FreeDOS Debug; how Microsoft’s original DEBUG.EXE treats CD 03 I did not test.
What each environment does with INT 3, and whether I executed it:
EnvironmentWhat INT 3 (CC) doesStatus
DOSBox 0.74-3, no debugger, no handlerNothing visible: the program carries on (after INT3 (program survived))Run (output)
Your own handler installed with AH=25hHandler runs with IF=0, return IP = trap + 1, then IRET resumesRun (output)
FreeDOS Debug 2.50 (DEBUG.EXE clone), inside DOSBoxStops with Unexpected breakpoint interrupt and IP on the next instruction; execution can continueRun (output)
Microsoft DEBUG.EXEI expect behaviour like the FreeDOS clone, but I did not run itDocumented, not executed
emu8086Commercial Windows software I could not run in this Linux sandbox. Check your own copy: assemble, look at the listing bytes (CC or CD 03?), and stepDocumented, not executed
Real MS-DOS on real hardware, or a real 8086Not tested. The 8086 itself does the same push-and-jump through vector 3 (Intel documentation)Documented, not executed
Two limits on everything above: Unicorn, which I use in the byte-patching section, is a modern x86 core running in real mode, not an 8086, so I use it for opcode lengths and control flow only, never for timing or hardware quirks. And DOSBox is an emulator of a PC with DOS-compatible services, not MS-DOS itself. Going deeper

The console functions of INT 21h differ in three ways: echo, Ctrl-C checking, and buffering

The console functions every 8086 exam asks about differ in only three properties. The table is from Ralf Brown’s Interrupt List (the notes for each function); the fn_tour program runs 01h, 02h, 06h, 09h and 0Ah and the checks pin their output.
AHDoesInput / output registersEcho?Checks Ctrl-C?
01hRead one character from standard inputreturns AL = characteryesyes
02hWrite one characterDL = character; AL = last character output (undocumented per Brown)–yes
06hDirect console output (DL != FFh) or input (DL = FFh)DL = character, except FFh–no
07hDirect character input, no echoreturns AL = characternono
08hCharacter input, no echoreturns AL = characternoyes
09hWrite $-terminated stringDS:DX -> string; AL = 24h on return (undocumented per Brown)–yes
0AhBuffered input (a whole line)DS:DX -> buffer: byte 0 = max, byte 1 = count read (CR not counted), then charactersyesyes
0BhGet standard input statusreturns AL = 00h no character, FFh character ready–yes
The run: 03-fn-tour.txt was produced with standard input redirected from a file containing ZAnkur. AH=01h returned 5A (Z), and AH=0Ah read the rest of the line, counting 5 characters for Ankur:
AH=01h + AH=0Ah (stdin)     : echo -> Ankur

                              AH=01h got 0x5A ; AH=0Ah count = 5, text = Ankur
All of these read standard input and write standard output, which DOS 2 and later can redirect, as this very run did. That is why 06h/07h and their “direct” cousins matter less today than 3Fh and 40h (read and write with a file handle): the handle versions are the same console functions generalised to files and devices.

The INT 21h function table: every standard AH function, from Ralf Brown’s list

Below are the functions most programs use, with registers in and out, followed by an accordion with the complete list of standard DOS functions AH=00h to AH=6Ch. The source is Ralf Brown’s Interrupt List (the INT 21 section; cited per row). I include only functions that list marks as DOS functions with a version (for example DOS 2+), and leave out the ones it marks internal, ROM, OEM, or belonging to a specific vendor’s network or extender. The right-hand column says whether the function was executed in this article’s harness. Note the version limit: a function listed as DOS 2+ does not exist on DOS 1.x; programs that must run there use the AH=00h–2Eh range.
AHDOSFunctionIn / outNotes (from Brown)Run here
00h1+Terminate programCS = PSP segmentnever returns; Brown notes Microsoft recommends 4Ch for DOS 2+
01h1+Read char, with echo-> ALchecks Ctrl-Cyes
02h1+Write charDL = charAL = last char outputyes
06h1+Direct console I/ODL = char (not FFh)does not check Ctrl-Cyes
09h1+Write $ stringDS:DX -> stringAL = 24h on returnyes
0Ah1+Buffered inputDS:DX -> bufferbyte 1 = count readyes
25h1+Set interrupt vectorAL = vector, DS:DX = handlerpreferred over writing the table directlyyes
2Ah1+Get date-> CX year, DH month, DL dayAL = day of week (DOS 1.10+)range-checked
2Ch1+Get time-> CH hour, CL min, DH sec, DL 1/100 sresolution is usually ~5/100 srange-checked
30h2+Get DOS version-> AL major, AH minorDOSBox reported 5.0yes
31h2+Terminate and stay residentAL = code, DX = paragraphs keptnever returns
35h2+Get interrupt vectorAL = vector -> ES:BXyes
39h/3Ah2+Make / remove directoryDS:DX -> ASCIZ pathCF and AX on erroryes
3Ch2+Create or truncate fileCX = attributes, DS:DX -> name -> AX handleCF on erroryes
3Dh2+Open fileAL = mode, DS:DX -> name -> AX handleCF on erroryes
3Eh2+Close fileBX = handleflushes pending writesyes
3Fh2+Read file or deviceBX = handle, CX = bytes, DS:DX -> buffer -> AX bytes readAX = 0 at EOFyes
40h2+Write file or deviceBX = handle, CX = bytes, DS:DX -> data -> AX writtenCX = 0 truncatesyes
41h2+Delete fileDS:DX -> ASCIZ nameCF and AX on erroryes
42h2+Seek (LSEEK)AL = origin (0 start, 1 current, 2 end), BX = handle, CX:DX = offset -> DX:AXyes
47h2+Get current directoryDL = drive, DS:SI -> 64-byte bufferno drive letter, no leading backslash
48h2+Allocate memoryBX = paragraphs -> AX segmenton failure BX = largest blockyes
49h2+Free memoryES = segmentyes
4Ah2+Resize memory blockBX = paragraphs, ES = blocka .COM starts owning all memoryyes
4Bh2+EXEC: load and run a programAL = 0, DS:DX -> name, ES:BX -> parameter blockCF on erroryes
4Ch2+Exit with return codeAL = return codenever returnsyes
4Dh2+Get child’s return code-> AL code, AH termination typereadable onceyes
4Eh/4Fh2+Find first / next fileCX = attr mask, DS:DX -> specfills the DTA; DTA+1Ah = size, DTA+1Eh = name4Eh yes
56h2+Rename fileDS:DX -> old, ES:DI -> newcan move within the same volumeyes
62h3+Get current PSP segment-> BX
Two notes on that table. The Run here column means the function was executed under DOSBox, which is a DOS-compatible emulator: it shows that the register conventions in my assembly are right, not that every DOS version behaves the same. Brown’s notes are full of per-version differences and bugs; the most useful one for learners is the 4Ah note that a .COM program is handed all of memory, so it must shrink itself before it can 48h-allocate anything, which is why fn_tour does that first.
Reference: all 101 standard INT 21h AH values from 00h to 6Ch that Brown lists as DOS functions Each AH links to its page in the Ralf Brown Interrupt List (CMU/ctyme mirror). Excluded on purpose: entries Brown marks internal (for example 51h, 52h), ROM, OEM (F8h–FFh on DOS 2.11-2.13), and vendor-specific ranges above 6Ch (Windows 95 long file names at 71h, Novell NetWare at B4h–F3h, DOS extenders, viruses). Brown files some functions (43h, 44h, 57h, 59h, 5Eh, 5Fh, 63h, 66h, 6Ch, 2Eh) under their AL subfunction numbers; I folded each into one row and link the first subfunction page. Function names are Brown’s; I only changed the letter case.
AHDOS versionFunction (Brown’s name; mnemonics kept upper-case)Run here
00h1+Terminate program
01h1+Read character from standard input, with echoyes
02h1+Write character to standard outputyes
03h1+Read character from STDAUX
04h1+Write character to STDAUX
05h1+Write character to printer
06h1+Direct console outputyes
07h1+Direct character input, without echo
08h1+Character input without echo
09h1+Write string to standard outputyes
0Ah1+Buffered inputyes
0Bh1+Get STDIN status
0Ch1+Flush buffer and read standard input
0Dh1+Disk reset
0Eh1+Select default drive
0Fh1+Open file using FCB
10h1+Close file using FCB
11h1+Find first matching file using FCB
12h1+Find next matching file using FCB
13h1+Delete file using FCB
14h1+Sequential read from FCB file
15h1+Sequential write to FCB file
16h1+Create or truncate file using FCB
17h1+Rename file using FCB
18h1+Null function for CP/M compatibility
19h1+Get current default driveyes
1Ah1+Set disk transfer area address
1Bh1+Get allocation information for default drive
1Ch1+Get allocation information for specific drive
1Dh1+Null function for CP/M compatibility
1Eh1+Null function for CP/M compatibility
1Fh1+Get drive parameter block for default drive
20h1+Null function for CP/M compatibility
21h1+Read random record from FCB file
22h1+Write random record to FCB file
23h1+Get file size for FCB
24h1+Set random record number for FCB
25h1+Set interrupt vectoryes
26h1+Create new program segment prefix
27h1+Random block read from FCB file
28h1+Random block write to FCB file
29h1+Parse filename into FCB
2Ah1+Get system dateyes
2Bh1+Set system date
2Ch1+Get system timeyes
2Dh1+Set system time
2Eh1+Set verify flag
2Fh2+Get disk transfer area address
30h2+Get DOS versionyes
31h2+Terminate and stay resident
32h2+Get DOS drive parameter block for specific drive
33h2+Extended break checking
34h2+Get address of INDOS flag
35h2+Get interrupt vectoryes
36h2+Get free disk space
37h2.x and 3.3+ onlyAVAILDEV – specify \dev\ prefix use
38h2+Get country-specific information
39h2+MKDIR – create subdirectoryyes
3Ah2+RMDIR – remove subdirectoryyes
3Bh2+CHDIR – set current directory
3Ch2+CREAT – create or truncate fileyes
3Dh2+OPEN – open existing fileyes
3Eh2+CLOSE – close fileyes
3Fh2+READ – read from file or deviceyes
40h2+WRITE – write to file or deviceyes
41h2+UNLINK – delete fileyes
42h2+LSEEK – set current file positionyes
43h2+Get / set file attributes (AL=00h get, 01h set)
44h2+IOCTL – device control (AL=00h-11h subfunctions)
45h2+DUP – duplicate file handle
46h2+DUP2, FORCEDUP – force duplicate file handle
47h2+CWD – get current directory
48h2+Allocate memoryyes
49h2+Free memoryyes
4Ah2+Resize memory blockyes
4Bh2+EXEC – load and/or execute programyes
4Ch2+EXIT – terminate with return codeyes
4Dh2+Get return code (ERRORLEVEL)yes
4Eh2+FINDFIRST – find first matching fileyes
4Fh2+FINDNEXT – find next matching file
54h2+Get verify flag
56h2+RENAME – rename fileyes
57h2+Get / set file’s last-written date and time (AL=00h/01h)
58h2.11+ / 5+Get or set memory allocation strategy / Get or set UMB link state
59h3.0+Get extended error information (BX=0000h)
5Ah3.0+Create temporary file
5Bh3.0+Create new file
5Ch3.0+FLOCK – record locking
5Eh3.1+ networkMachine name and printer setup (AL=00h-05h)
5Fh3.1+ networkNetwork redirection (AL=00h-08h)
60h3.0+TRUENAME – canonicalize filename or path
61h3.0+Unused (reserved for network use)
62h3.0+Get current PSP address
63h2.25 / 3.2+Double-byte character set lead-byte table (AL=00h-02h)
65h3.3+ / 4.0+Get extended country information / Country-dependent character capitalization
66h3.3+Get / set global code page table (AL=01h/02h)
67h3.3+Set handle count
68h3.3+FFLUSH – commit file
6Ah4.0+Commit file
6Bh5+Null function
6Ch4.0+Extended open / create
Remember what this table is: a catalogue, not a test. The functions marked yes were executed by fn_tour under DOSBox; the rest are taken from the list without running them.
Going deeper

A debugger saves one byte, and that is why CD 03 breaks the IP-minus-one rule

Here is the experiment behind the diagram above, on a real instruction stream rather than a picture. A software breakpoint needs three steps: save the original byte at the address, write CC there, and on the trap put the byte back and back IP up by one, so the original instruction runs. The Unicorn script (bp_experiment.py) implements a miniature debugger that does exactly that for the routine in bp_target.asm: three one-byte inc cx, so the correct answer is CX = 3.
def on_int(uc, intno, _):
    ip = uc.reg_read(UC_X86_REG_IP)        # return address (first byte after the trapping instruction)
    events.append((intno, ip))
    if intno == 3:
        bp = ip - 1                        # "back up one byte" -- only right for CC
        if bp == SITE:
            uc.mem_write(SITE, saved)      # restore the original byte, re-run it
        uc.reg_write(UC_X86_REG_IP, bp)
Patching the first inc with the one-byte CC works: the trap hands back IP one byte past the site, the debugger backs up by one, restores the byte, and CX ends at 3. Patching the same site with the two-byte CD 03 makes the debugger do the same arithmetic on the wrong return address and lose the first instruction and the second’s first byte (08-bp-experiment.txt):
CC       patch=CC     trap: vector=3 return IP=7C03 (site=7C02, +1)  debugger restores IP=7C02
         after resume: CX=3 (expected 3), final IP=7C05
CD 03    patch=CD03   trap: vector=3 return IP=7C04 (site=7C02, +2)  debugger restores IP=7C03
         after resume: CX=0 (expected 3), final IP=7C64

RESULT: PASS
CX = 0 means the first inc never ran and the restored flow started mid-instruction; the final IP is wherever the garbage ran off to before my 50-instruction limit stopped it, so the exact value is an artefact of the experiment. The shape matches what FreeDOS Debug did with CD 03 in the DEBUG section: stop one byte too early, disassemble the wrong thing, carry on at the wrong place. One limit on the experiment: the interrupt hook in Unicorn hands me the return IP directly and I did not use it to inspect the stack, so this experiment is about IP arithmetic only; the stack-frame and IF measurements come from the DOSBox handler run earlier.
Why the return-address difference is the whole trick (advanced notes, partly documented-only) Protected mode and virtual-8086 mode. Intel’s manual lists differences between the one-byte forms (INT3, INTO, INT1) and INT n: in virtual-8086 mode the normal IOPL checks do not apply to the one-byte forms, and VME interrupt redirection does not apply to them, so they are always handled by a protected-mode handler. Those differences do not apply to CD 03. None of that matters on a real 8086 or in DOSBox’s real-mode emulation, and I did not run it: it is from Intel’s documentation. Single stepping is a different vector. The trap flag makes the CPU raise vector 1 (INT 1, single step) after each instruction; Brown’s list names vector 1 as “CPU-generated – SINGLE STEP” and vector 4 as “INTO detected overflow”. A debugger therefore owns vectors 1 and 3 at once: INT 1 to walk instruction by instruction, INT 3 to run at full speed until a chosen address. The t command in the DEBUG transcripts above is the single-step path; g ran until the breakpoint path fired. Who restores the vector. int3_handler.asm saves the old vector with AH=35h before installing its own with AH=25h and restores it with AH=25h before exiting. A handler that forgets this leaves a pointer into freed memory in vector 3, and the next INT 3 from anything jumps into whatever DOS loads there next.
Going deeper

The two original examples, kept and explained

The first version of this article showed two MASM-style programs. They are kept here because people link to them. I did not assemble them with MASM or TASM, but they use nothing except AH=09h and AH=4Ch, which hello21 and fn_tour exercise in NASM form. Example 1 adds an int 3 after the first print:
Original Example 1: INT 3 after a DOS print (MASM syntax, kept from the first version) Under a debugger, execution stops at the int 3; the expected console output was Debugging with INT 3h. Not assembled or run with MASM here. The equivalent int3 placement was run under DOSBox with a handler (the handler section) and under FreeDOS Debug (the DEBUG section). If no debugger is present, this program carries on: under DOSBox 0.74-3 (see 05-int3-default.txt) the program survived an unhandled int3, but I did not test real DOS.
.model small
.stack 100h
.data
    msg db 'Debugging with INT 3h', 0Dh, 0Ah, '$'
.code
main proc
    mov ax, @data  ; Load data segment
    mov ds, ax

    mov dx, offset msg
    mov ah, 09h    ; DOS function to print string
    int 21h        ; Call DOS interrupt to display message

    int 3          ; Trigger breakpoint interrupt

    mov ah, 4Ch    ; DOS function to terminate program
    int 21h        ; Call DOS interrupt
main endp
end main
Original Example 2: INT 21h print and exit (MASM syntax, kept from the first version) Expected console output: Hello, World!. The program uses AH=09h and AH=4Ch; the NASM equivalent (hello21.asm) was run, output 02-hello21.txt.
.model small
.stack 100h
.data
    msg db 'Hello, World!', 0Dh, 0Ah, '$'
.code
main proc
    mov ax, @data  ; Load data segment
    mov ds, ax

    mov dx, offset msg
    mov ah, 09h    ; DOS function to print string
    int 21h        ; Call DOS interrupt to display message

    mov ah, 4Ch    ; DOS function to terminate program
    int 21h        ; Call DOS interrupt
main endp
end main
One correction to the first version, in the spirit of honesty: its bullet list put the function names inside literal <strong> tags that rendered as visible markup in the code text, and its note that INT 3h “requires no additional parameters” is true of the instruction but hides that it needs a listener, a debugger or handler, to be useful. The table above replaces both.

Viva questions and answers

Fourteen questions, each answered from what was measured or cited above. Click a question to open the answer.
1. What is the difference between INT 3 and INT 21h? INT 3 is the breakpoint interrupt, opcode CC, used by debuggers; no parameters. INT 21h is the DOS service interrupt, opcode CD 21; the function number goes in AH. One asks a debugger to take over; the other asks DOS to do work.
2. How many bytes is each instruction, and what are the opcodes? INT 3 as the breakpoint is one byte, CC. INT 21h is two bytes, CD 21: opcode CD (INT n) followed by the vector number. Every INT n is CD n. Caveat measured here: NASM’s spelling int 3 assembles to CD 03, not CC; write int3 to get the one-byte form.
3. Why is INT 3 a one-byte instruction? So a debugger can replace the first byte of any instruction with it, including one-byte instructions, without touching the next instruction. That is Intel’s stated reason for the one-byte form.
4. What does the CPU do when it executes an INT instruction? It pushes FLAGS, then CS, then IP (the return address); clears the interrupt flag (and trap flag); reads the far pointer at vector * 4 from the table at 0000:0000; and jumps there. IRET pops IP, CS, FLAGS. Measured here: IF was 1 when pushed, 0 in the handler, 1 after IRET.
5. Where are the vectors for INT 3 and INT 21h stored? INT 3: 3 * 4 = 0000:000C. INT 21h: 21h * 4 = 0000:0084. Each entry is offset word then segment word.
6. How does a program tell DOS which service it wants? By putting the function number in AH before INT 21h, with other parameters in other registers (for example DS:DX for a string). AH=09h prints a $-terminated string; AH=4Ch exits with the return code in AL.
7. How does INT 21h report an error? Most file and memory functions set the carry flag and put an error code in AX; CF = 0 means success. Measured here: reopening a deleted file gave CF=1 AX=0002 (file not found); asking for 1 MB gave CF=1 AX=0008 (insufficient memory) with the largest block in BX.
8. What is the difference between AH=01h, 07h and 08h? All read one character into AL. 01h echoes it and checks Ctrl-C; 08h does not echo but checks Ctrl-C; 07h does not echo and does not check Ctrl-C. (Ralf Brown’s list, notes for each function.)
9. What is the difference between AH=09h and AH=02h? 09h writes a whole $-terminated string from DS:DX; 02h writes the single character in DL. Forgetting the $ makes 09h print until it finds one.
10. Why use AH=4Ch instead of AH=00h to end a program? 4Ch (DOS 2+) takes a return code in AL, which a batch file sees as ERRORLEVEL (measured here: mov ax, 4C07h made if errorlevel 7 true in the DOSBox batch check, and AH=4Dh read back exactly 7 after an AH=4Bh child exit). 00h sets the return code to 0, and Brown’s list notes Microsoft recommends 4Ch for DOS 2 and later.
11. Why does my breakpoint stop at the wrong place in DEBUG? Probably because the two-byte CD 03 is in the program, not the one-byte CC. In FreeDOS Debug, CD 03 stopped one byte inside the instruction and disassembled garbage. NASM’s int 3 emits CD 03; use int3. (Microsoft’s DEBUG.EXE not tested.)
12. Does INT 21h work without DOS? No. Vector 21h is just a slot in the table; DOS fills it at start-up. Without DOS (or your own handler) there is no dispatcher behind it. This follows from the interrupt-table mechanism at the top; I did not run a no-DOS test.
13. Which vectors does a debugger need, and why? Vector 1 (single step, raised after each instruction while the trap flag is set) and vector 3 (breakpoint). Brown’s list names vector 1 as “CPU-generated – SINGLE STEP” and vector 3 as “CPU-generated – BREAKPOINT”.
14. How do you install your own interrupt handler under DOS? AH=35h (vector in AL) returns the old handler in ES:BX so you can save it; AH=25h (AL = vector, DS:DX = new handler) installs yours; restore the old one before exiting. int3_handler.asm does exactly this.
More viva practice across the 8086 topics: Top 60 8086 Viva Questions and Answers (2026).
Should you still learn INT 21h? For exams and for understanding how operating systems expose services, yes: the pattern of number in a register, trap, result in registers, error in the carry flag is the ancestor of modern system-call conventions. For writing new software, no: DOS is gone, and a modern debugger sets breakpoints for you. One opinion, labelled as such: learn AH=09h, AH=4Ch, AH=01h, AH=0Ah, AH=3Ch–3Fh, AH=40h and AH=25h/35h, and treat the rest of the table as a reference you look up.
Understanding these two interrupts is what separates “I can copy the exam program” from “I can debug it when it misbehaves”: INT 21h is how your program asks for things, INT 3 is how you stop it to look. Do you have a question the table does not answer, or an emulator where INT 3 behaves differently from what is reported here? Put it in the comments.

Further reading

No Comments yet!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.