BIOS INT 15h AH=87h: The Protected-Mode Block-Move Service
Understand the AT BIOS block-move descriptor table, 24-bit addresses, transfer limits, status flags, and why XMS or a DPMI host usually owns memory.
INT 15h, function AH=87h, is a historical AT-class BIOS service that moves a block of memory by briefly entering protected mode. It predates modern DOS memory managers and gives a real-mode caller a constrained way to copy data at physical addresses above the one-megabyte boundary. Its interface is unusually complex for a BIOS call because the caller supplies a Global Descriptor Table (GDT), and the transfer temporarily suppresses interrupts.
This is not a general allocator, a way to enter protected mode permanently, or a substitute for XMS or DPMI. The IBM BIOS interface reference documents support on AT, PC XT Model 286, and specified PS/2 products, while earlier PCjr/PC and several XT/Convertible systems report the function as unsupported. Other BIOSes may differ. Only use it when the exact machine’s firmware and operating contract are known.
Interface and transfer bound
The caller passes the word count in CX and a pointer to its prepared GDT in ES:SI. IBM documents a maximum count of 8000h words, which is 32,768 words or 65,536 bytes. The source and destination descriptors must each describe at least 2 * CX - 1 bytes. The segment base is encoded as a 24-bit physical address. A caller should calculate length and inclusive limit with wider arithmetic, reject zero or out-of-range counts according to its firmware contract, and ensure that both ranges lie in memory that is safe to access.
The transfer size’s maximum does not make every 64 KiB range safe. The old interface cannot discover whether an address contains usable RAM, a hole, an adapter aperture, or reserved firmware memory. Consult the machine’s memory map and avoid regions owned by DOS, a memory manager, a device, or firmware. A source or destination that crosses a physical boundary needs separate descriptors or multiple carefully bounded requests, if the target BIOS supports that plan.
Descriptor table layout
The IBM reference shows six eight-byte descriptors at the GDT location. Entry zero is the required null descriptor. The next descriptor describes the GDT itself and may be modified by the BIOS. The source and destination descriptors are supplied by the caller. The fifth descriptor gives the BIOS a protected-mode code segment, and the sixth supplies a temporary stack. The caller’s descriptor construction must match the reference exactly; a GDT copied from a 32-bit application is not interchangeable.
For source and destination data descriptors, IBM specifies a segment limit large enough for the request, a 24-bit base split across the descriptor’s base fields, a data access-rights byte of 93h, and a zero reserved word. 93h is a 286-era access-rights encoding for a present, ring-zero, writable data segment. Do not reuse the same descriptor field ordering from a flat protected-mode tutorial without checking the 286 format and the specific assembler’s structure packing.
Before the interrupt, the complete GDT must be resident, aligned and addressable through the segment:offset in ES:SI. The descriptor bases must identify the intended physical source and target. Since the BIOS may update some descriptor entries during the operation, place the table in memory it can modify and do not share it with unrelated code. Save the original bytes if the descriptor table must be reused later.
Call outline, not a drop-in implementation
The register-level call after a target-specific six-entry table has been fully built is short:
; Pseudocode: GDT layout and descriptors must follow the target BIOS manual.
; ES:SI -> the complete caller-owned block-move GDT
; CX -> number of 16-bit words, at most 8000h per IBM's reference
mov ah, 87h
int 15h
jc move_failed
; IBM documents AH=00h and ZF=1 on success for this interface.
This is only the call boundary, not an assembled program. A real implementation must build the table in the correct 286 descriptor format, validate physical ranges, preserve any state required by its DOS environment, and inspect the return status immediately. IBM lists AH=00h for success, 01h for RAM parity error, 02h for another exception interrupt error, and 03h for an A20 gate failure, with carry clear and zero set on success and the opposite flag results on errors. Check the target’s manual for differences and preserve AH before another BIOS or DOS call overwrites it.
Timing and interrupt consequences
The BIOS reference warns that interrupts are not allowed during the transfer and that a large move can cause lost interrupts. Therefore, chunking is not only a size question. An operation near the maximum can delay timer, keyboard, serial, diskette, or other interrupt-driven work. A program that invokes it during active disk or audio activity risks disrupting the system even when the copy’s source and destination are valid.
Keep the transfer short and isolated. Do not call it from an interrupt handler, a DOS critical-error callback, or while a driver is waiting on time-sensitive hardware. Do not rely on the BIOS to preserve a real-time deadline. If the data volume is large, an XMS move request, a DPMI host service, or an operating-system-specific API is normally more appropriate because the memory manager has the ownership information and execution model the raw BIOS call lacks.
Relationship to XMS, E820, and A20
AH=87h performs a bounded copy using BIOS descriptors. It does not allocate memory, reserve it, expose an XMS handle, or describe installed RAM. E820 is a firmware map interface, not an allocator, and an E820 range marked as RAM is not automatically safe for an application to overwrite. The XMS manager arbitrates extended-memory ownership and A20 behavior according to the XMS API. DPMI offers a separate protected-mode client model with its own memory descriptors and host responsibilities.
Do not toggle A20 directly as a shortcut before or after the move if an XMS manager or extender is active. A BIOS return code for an A20 gate problem should be treated as a failed move, not as permission to manipulate port 92h or the keyboard controller behind the memory manager. Record the returned status and use the documented provider for the environment.
Failure containment
An invalid GDT, wrong descriptor base, insufficient limit, inaccessible low-memory table, or bad memory range can fail catastrophically. A BIOS error status is useful, but it cannot guarantee that every clone validates every field before using it. Never test an uncertain descriptor against the running DOS installation’s kernel, interrupt table, video aperture, or valuable file cache.
Use a virtual machine snapshot or sacrificial hardware with a minimal boot profile. Begin with a tiny transfer between two known scratch regions and compare source and destination checksums. Then test a count boundary, a range ending at a segment limit, and each documented error path only in a harness designed to provoke it. Do not intentionally provoke RAM parity or A20 faults on a production machine.
Acceptance checklist
Before shipping a caller, verify the BIOS family and support status; GDT size and descriptor offsets; source and destination bases; byte count and inclusive limit; 24-bit address range; memory ownership; interrupts and time-sensitive devices; return carry, zero flag, and AH; and checksum integrity. Confirm that the data moved exactly once, no sentinel outside the requested range changed, and an error path does not treat a partial or failed transfer as success.
The most important design choice is usually to avoid AH=87h unless compatibility requires it. It is a narrow BIOS-assisted copy service with significant global timing and memory-ownership consequences. Prefer managed XMS or DPMI operations for normal DOS software, and reserve this call for a machine-specific legacy interface whose exact table and failure behavior can be validated.
Related:
- XMS HMA Ownership and A20 Control: Balance Every Enable and Release
- BIOS INT 15h E820: Reading the Physical Memory Map Before DOS
Sources: