DOS INT 23h: Control-C Handling, Vector Ownership, and Safe Cleanup
Understand when DOS invokes INT 23h, how process termination restores vectors, and how to design cancellation without unsafe DOS reentrancy.
Interrupt 23h is the DOS Control-C/Control-Break handling vector. It is not a hardware IRQ that an application should invoke directly, nor is it a modern asynchronous signal with a portable callback contract. DOS transfers control to the active handler when its input/output and Break-checking rules detect an interrupt request. The handler may belong to the current process, an earlier resident program, or the command interpreter, depending on vector ownership and process history.
For application authors, the safest default is to let the installed DOS behavior handle cancellation. If the program must intercept Control-C, treat the handler as a constrained control-transfer path: preserve the exact prior vector, install through DOS’s supported vector API, ensure the handler code remains resident for the time it is referenced, minimize work inside the handler, and restore ownership before termination when appropriate. Never assume it is safe to perform arbitrary file I/O, allocate memory, display text, or call an application routine that expects an ordinary stack frame from inside the handler.
The available archival MS-DOS reference describes the interface and register/return behavior for historical MS-DOS versions. FreeDOS documents its own extended Break-checking behavior and implements a compatible kernel API, but exact delivery points depend on the kernel, standard-input redirection, console driver, and BREAK setting. Test those combinations on the target rather than treating the historical reference as a complete specification for every DOS clone.
What INT 23h represents
In the traditional DOS environment, the interrupt vector table entry for 23h contains the far address of the Control-C handler. The MS-DOS process startup machinery stores the current vector in the program segment prefix (PSP) at offsets 0Eh through 11h; when a process terminates through DOS, its saved vector is restored as part of cleanup. This supports process-local replacement: a foreground application can set a handler, run, and return to the previous owner when it exits.
The 23h handler is a DOS-level cancellation path, not a direct keyboard interrupt. The keyboard driver or console input layer recognizes the key sequence; DOS decides when to call the vector. Historical documentation says it can be called when Control-C is detected during character I/O and, when Break checking is enabled, during most other DOS function calls. FreeDOS’s BREAK documentation describes its extended check policy around console I/O requests and, when enabled, checks more broadly. This explains why pressing Control-C during a compute-only loop may not be noticed until the program makes another DOS call, depending on runtime details.
Input redirection changes delivery semantics. A Control-C byte read from an ordinary keyboard path may be checked differently from the same byte coming from a redirected file. The historical reference calls out Break-state requirements for redirected input. Do not write a program that assumes every 03h byte either always invokes the vector or always returns as data. If the byte is application data, choose an input service and filtering policy intentionally; if it is cancellation, test both console and redirected cases.
Install and restore vectors with ownership discipline
DOS provides INT 21h function 35h to get a vector and function 25h to set one. The archival reference specifically advises reading and saving the existing vector before changing it and restoring that vector before program termination. Do not write the interrupt vector table directly: DOS’s own API is the documented method and gives the operating system a chance to maintain expected state.
At a high level, a foreground application follows this lifecycle:
- Query vector
23hwithAH=35h,AL=23h; preserve the returnedES:BXfar address in storage that remains valid. - Set the handler with
AH=25h,AL=23h, andDS:DXpointing to the application’s handler code. - Run only while that handler’s code and state remain resident and valid.
- Restore the previously saved address with
AH=25h,AL=23hbefore a normal return, unless DOS process termination is intentionally relied upon to restore the PSP-saved vector.
This is a control-flow sketch, not a drop-in assembler program. Register preservation, memory model, segment ownership, nesting, and termination mechanism must be checked in the actual 16-bit toolchain. A handler installed by a transient process cannot safely point into memory that has been freed or overwritten. For a terminate-and-stay-resident utility, the retained memory size must include every instruction and datum reachable from the handler, not just the entry label.
Nested owners require special care. If a program installs a handler and another component installs its own afterward, restoring an old vector unconditionally can overwrite the newer component’s ownership. A robust uninstall path should first query the current vector and verify that it still points to the handler being removed. If another handler has taken ownership, do not silently replace it. Chaining or cooperative removal requires a defined contract; DOS does not provide a general-purpose dynamic event subscription manager.
The handler return path is not a normal function return
The historical MS-DOS reference documents several distinct ways a handler can relinquish control. An IRET with the original register state resumes DOS’s interrupted function, which then proceeds and eventually returns normally to the application. A far return uses the carry flag as a DOS-specific termination/continuation decision in the documented interface. A handler may instead transfer control to an application’s own recovery path and retain control while it resolves the event.
These are specialized historical semantics, not interchangeable epilogues. IRET consumes an interrupt frame; RETF consumes a far-call-style frame. Returning with the wrong instruction corrupts the stack and may crash or misdirect control. Compiler-generated C functions generally use a normal near or far return and are not automatically valid interrupt handlers. Use an assembler entry point with a verified calling/return convention, or a compiler’s documented interrupt-handler facility only if it explicitly supports the DOS convention you need.
The meaning of register state and carry behavior is tied to the particular DOS implementation contract. The MS-DOS Encyclopedia describes the handler receiving registers as they were at the interrupted DOS call and says they must be preserved if returning with IRET. That historical behavior should not be generalized to every DOS-compatible kernel without checking its source or documentation. The conservative design is to use the handler only to record a minimal cancellation flag or transfer control through a deliberately designed assembly path, then test the result on each supported runtime.
Keep the handler short and avoid reentrancy hazards
The tempting handler is one that prints “cancelled,” closes files, frees memory, and returns to a shell. Those actions are exactly where a low-level handler can become unsafe. The original DOS call may still be active. Calling another DOS service from that path can reenter kernel code, contend with internal state, or recurse into the same cancellation behavior. Even where a specific DOS version documents that DOS calls are allowed, that statement does not prove that every service, redirector, device driver, or FreeDOS-compatible environment is reentrant in the current state.
A safer architecture divides work into two phases. The handler performs the smallest action required to communicate cancellation, such as setting a flag in memory whose lifetime and atomicity are understood, or transferring control to a dedicated cleanup path under documented semantics. The mainline code observes that event at a safe point and performs ordinary cleanup: close handles, flush or roll back outputs as appropriate, restore terminal or device state, release allocations, and exit with a meaningful return code.
Even the flag approach needs careful 16-bit reasoning. A byte flag is typically written atomically on 8086-class processors, but compiler caching, segment assumptions, interrupt nesting, and optimization still matter. Declare the object in a segment reachable by the handler, ensure the code reads it from the same storage location, and use assembly/compiler constructs that prevent the value from being optimized away. Avoid handler-side heap allocation, string formatting, disk I/O, or long loops.
If the application is in a long computation and DOS cannot call the handler until a DOS service is reached, add explicit cancellation checkpoints that are safe for the application. Do not poll the keyboard by calling DOS from inside the handler; keep polling decisions in the main program and select a documented input service with the intended semantics. The correct balance depends on whether cancellation latency, portability, or lowest memory footprint is the main requirement.
Process cleanup, TSRs, and other vector owners
For an ordinary foreground program, DOS process termination restores the saved Interrupt 23h address from the PSP according to the documented MS-DOS lifecycle. This is one reason to use documented process exit services rather than jumping directly to a shell or freeing the program’s memory manually while its vector is still installed. Before normal termination, explicit restoration can make ownership clearer, but it must not clobber a later owner’s vector.
A TSR has a different lifecycle because its handler remains installed after the installing process returns. The resident portion must include the handler, all state it accesses, and any chained-handler data. Installation must leave the vector pointing to resident memory, and an uninstall mechanism must validate the current chain before removing or reconnecting nodes. TSRs can conflict when two programs assume they own the same vector or when one removes an earlier vector without accounting for a later handler. These are memory-lifetime and ownership problems, not merely interrupt syntax problems.
Do not confuse Interrupt 23h with Interrupt 24h, which is the critical-error handler for conditions such as device or disk errors. The cleanup requirements and return protocol differ. Similarly, INT 21h function 33h controls the DOS Break-checking flag in the historical API; that global setting can affect when Control-C checks occur and should not be changed casually. If a program changes a process-wide or system-wide flag, restore the previous state rather than assuming the user’s initial setting.
A verification matrix for cancellation behavior
Test the handler lifecycle on the actual DOS kernel and command interpreter. Record the kernel build, emulator or hardware, console driver, shell, Break setting, and whether standard input is redirected. Use a test program that writes a trace to memory and emits it only after leaving the handler path; handler-side disk logging can hide or create reentrancy faults.
At minimum, verify these scenarios:
| Scenario | What to observe |
|---|---|
| Control-C at an interactive prompt or line-input call | Whether the active vector runs and whether input is echoed or consumed. |
| Control-Break during a DOS I/O operation | Delivery point and resulting process state. |
| Break OFF versus ON | Whether checking occurs only at the documented console-I/O boundary or more broadly. |
Redirected input containing byte 03h |
Whether the byte is delivered as input data or invokes cancellation. |
| Handler returns through its selected path | Registers, stack, carry state, and subsequent DOS call result. |
| Process exits normally after installation | Whether the previous vector is restored and the program’s memory is released safely. |
| A second vector owner installs later | Whether cleanup avoids overwriting another component’s handler. |
Also test the negative case: run a tight CPU loop that makes no DOS calls and determine whether cancellation is delayed. If low cancellation latency is required, design a deliberate polling strategy rather than assuming the DOS handler is a preemptive signal. Do not deploy the handler change until the exact return path has been tested in every supported runtime.
Related:
- FreeDOS INT 24h: Handling DOS Critical I/O Errors Safely
- TSR Programs: How DOS Ran Background Tasks Without Multitasking
Sources:
- The MS-DOS Encyclopedia, Section V: System Calls, historical reference for INT 23h, the PSP vector save/restore lifecycle, INT 21h functions 25h and 35h, and Control-C behavior. Its system-call entries describe MS-DOS through version 3.2.
- FreeDOS BREAK command documentation, FreeDOS-specific description of Control-C/Control-Break checks and extended checking.
- FreeDOS kernel source repository, primary source for the current FreeDOS kernel’s interrupt and process behavior; consult the exact target revision when relying on implementation details.