DOS INT 21h AH=0Ah: Buffered Line Input and Its Real Limits
Implement DOS buffered line input with INT 21h AH=0Ah, size its buffer correctly, handle redirection and EOF, and choose safer alternatives.
INT 21h function 0Ah is a classic DOS line-input service. Its appeal is that DOS handles character collection and editing into a caller-owned buffer. Its danger is that the interface name can be read too literally: the function is not a modern, length-returning readline API, does not report an ordinary end-of-file result for redirected files, and does not guarantee identical interactive behavior across every DOS-compatible kernel or console driver.
Use it when you specifically need the traditional DOS buffered-input behavior and have tested the target environment. For general file or pipe input, prefer handle-based reads through INT 21h function 3Fh, which returns a byte count and an error status. For an interactive application, define the maximum accepted line length, allocate the full structure, inspect the returned count, and treat Control-C/Break and redirected input as distinct cases.
The register convention is small: set AH=0Ah, point DS:DX at the input structure, and invoke INT 21h. The details live in the structure and in DOS’s console/standard-input behavior. The historical MS-DOS reference describes this interface for older MS-DOS releases; FreeDOS aims to implement MS-DOS-compatible services, but applications should validate details on the specific kernel, shell, and redirection path they support.
The buffer is a counted structure, not a C string
The buffer begins with two control bytes followed by character storage:
| Offset | Meaning on entry/return |
|---|---|
+0 |
Maximum input count, including the terminating carriage return, set by the caller. |
+1 |
Returned character count, excluding the carriage return. Initialize it to zero for clarity. |
+2 onward |
Input bytes; on a completed line the carriage return follows the counted data. |
The capacity byte is not the number of visible characters the application can accept. If it is 81, at most 80 ordinary characters plus a carriage return fit. The allocated object must include both header bytes plus all bytes named by the capacity field. That means a capacity of 81 requires 83 bytes of storage. The maximum capacity value documented by the MS-DOS reference is 255, which leaves 254 non-CR input characters at the upper boundary.
This distinction prevents a familiar off-by-one bug. If a program wants at most 80 characters, it must reserve 81 data bytes including carriage return and 2 header bytes. If it allocates only 82 total bytes while declaring 81, it has one byte too little. If it declares 80 and assumes 80 user characters, it has one fewer visible character than intended.
An illustrative MASM/TASM-style declaration and call is:
linebuf db 81, 0, 81 dup (0) ; 80 data characters plus CR, plus two header bytes
mov dx, OFFSET linebuf
mov ah, 0Ah
int 21h
; Returned count excludes CR. CX is now safe for exact-length output.
xor cx, cx
mov cl, [linebuf+1]
mov dx, OFFSET linebuf+2
mov bx, 1 ; standard output handle
mov ah, 40h ; write exactly CX bytes
int 21h
jc write_failed
The snippet assumes a 16-bit assembler using MASM/TASM syntax, that DS addresses the data segment containing linebuf, and that handle 1 is the intended standard output. The line-input call itself does not return a normal character count in a register; read byte linebuf+1. The sample uses handle-based output so it does not need to append a $ terminator or accidentally print beyond the entered bytes. A production program should also check the carry flag and returned byte count from function 40h, since a successful call can write fewer bytes than requested.
Do not treat the buffer as NUL-terminated. It is a counted byte sequence, and the carriage return is a terminator with DOS semantics, not C’s zero byte. Before passing the data to a routine that expects a C string, copy it into a separately sized destination and append NUL only after proving the destination has room. Before passing it to function 09h, which expects $ termination, likewise make a bounded copy and handle any embedded dollar sign deliberately.
Understand editing, echo, and saturation behavior
With an interactive console, function 0Ah provides the DOS line-editing behavior associated with the active console implementation. Characters are echoed to standard output as they are accepted. A program should not assume that the function is silent, that echo is directed to a particular physical screen, or that editing key details are identical between FreeDOS, MS-DOS, a DOS emulator, and a redirected stream. Test the exact environment when key editing or display formatting is part of the user interface contract.
When the buffer approaches its declared limit, the historical MS-DOS documentation says additional characters are ignored and a beep is sounded until carriage return is entered. That behavior is easy to miss in automated tests because the program may eventually return a seemingly valid line that is simply truncated to the maximum. A robust UI should make the accepted limit visible, validate the returned count, and decide what a full buffer means: reject the line, ask the operator to shorten it, or accept the maximum-length value. Never assume overflow will be signaled through carry or a separate status code.
The exact maximum is a product decision. For a fixed command token, a modest bound is safer than a large stack or static allocation. For a path or configuration value, base the limit on the representation actually consumed downstream, not merely on the screen width. DOS-era path limits, OEM code pages, and filesystem name rules may differ from the byte length that a modern editor displays. The service counts bytes, not Unicode grapheme clusters.
Redirection changes the meaning of “keyboard input”
For DOS versions 2.0 and later, the standard-input abstraction can be redirected. A program that uses 0Ah may therefore read from a file or device rather than a keyboard, despite the historical function name “Buffered Keyboard Input.” This makes it useful for some shell workflows, but the function’s editing and echo model is still a poor fit for robust file parsing.
The key operational limitation is end-of-file. The MS-DOS reference explicitly says redirected EOF is not detected by function 0Ah. A loop that repeatedly calls it while reading a redirected file can fail to terminate or process stale/empty data, depending on the target implementation. Do not build a reliable file-processing loop around this interface. Use function 3Fh on handle 0, check the carry flag for errors, and interpret a returned byte count of zero as end-of-file for a regular file or other handle that documents that behavior.
Redirection also separates input from echo. Function 0Ah echoes characters to standard output; if input is redirected from a file, those bytes may be copied to the console or to a separately redirected output stream. This can corrupt a machine-readable pipeline. For a command intended to filter or parse data, use handle-based input and explicitly control output instead of inheriting an interactive line editor’s echo policy.
Before relying on redirection, test at least these cases in the exact kernel and shell combination: a single short line with a final CR/LF, a line exactly at the declared limit, a line longer than the limit, an empty file, a file without a final newline, and a file containing Control-Z. Historical DOS text-file EOF conventions and individual shell behavior can affect what a program observes. Function 0Ah is not a portable parser for arbitrary byte streams.
Control-C and BREAK are part of the input contract
Control-C is not just another character when DOS performs a checked character-input operation. The MS-DOS reference describes calls to Interrupt 23h when Control-C is detected, with behavior depending on redirection and the Break setting. FreeDOS documents its own BREAK extended-checking policy. The precise path can vary with whether input is a console or redirected file, which input/output function is active, and the kernel configuration.
This matters if a program installs its own Interrupt 23h handler, masks or defers cancellation, or expects byte 03h to be returned as data. Do not infer from a test at the keyboard that Control-C will behave the same in a batch pipeline. If a control byte must be treated literally, select an API with the intended filtering semantics and test it under the exact BREAK state. If cancellation should abort a long operation, define where cleanup occurs and avoid leaving partially updated files or device state behind.
The handler itself is a separate system-level interface and should not be improvised as part of a line-input routine. In particular, do not assume a signal-like callback has modern process isolation, automatic stack safety, or reentrant DOS services. Keep the data returned from 0Ah separate from cancellation handling and consult the specific kernel’s documented compatibility behavior.
When to choose another function
Choose INT 21h function 3Fh when input can be a file, pipe, or device and correctness depends on observing the number of bytes read, errors, or EOF. It works with a caller-selected handle and buffer, making it suitable for bounded line assemblers: read chunks, retain bytes between reads, split on CR/LF according to the file format, and cap the maximum line length explicitly. This is more code than one 0Ah call, but it makes termination and truncation visible.
Use functions 07h or 08h only when you need single-character input and have intentionally selected their echo and Control-C filtering semantics. Use function 0Ch only when flushing type-ahead input is required; clearing a user’s pending input has a visible interaction cost and should be deliberate. There is no universally best DOS input service: the correct choice follows from whether the program is interactive, whether standard input may be redirected, whether echo is desired, and whether cancellation is data or control flow.
Acceptance checks for a real DOS target
Record the DOS kernel/version, command interpreter, emulator or hardware, BREAK state, and redirection path. Then verify the buffer invariants, not only that a prompt appears:
- A blank line returns count zero and places the carriage return at the first data byte.
- A line shorter than the limit returns the exact byte count, excluding CR.
- A line at the visible-character limit does not overwrite the next sentinel byte in a test build.
- A longer line is handled according to an explicit product policy; it is not silently accepted as complete input.
- Redirected input does not get mistaken for keyboard input, echoed output does not pollute a pipeline, and EOF cannot cause an infinite loop.
- Control-C behavior is tested once at the console and once with redirected input under both relevant Break states.
Use a guard byte before and after the declared buffer in a test build, then inspect them after the call. Run the same test under the actual DOS runtime you support. A modern host compiler cannot validate real-mode segment setup, DOS editing behavior, or the kernel’s redirection path. The format and call sequence are deterministic; the runtime contract still needs target testing.
Related:
- DOS BIOS Keyboard Input: INT 16h, Buffer Semantics, and Enhanced Keys
- DOS File Handles: Open, Read, Inherit, and Redirect I/O
Sources:
- The MS-DOS Encyclopedia, Section V: System Calls, historical reference for INT 21h functions 0Ah, 3Fh, 40h, and Control-C behavior. It documents MS-DOS through version 3.2 and is not a guarantee for every later compatible kernel.
- FreeDOS kernel source repository, primary implementation source for the FreeDOS kernel’s DOS-compatible interrupt services.
- FreeDOS BREAK command documentation, FreeDOS-specific behavior for Control-C/Control-Break checking.