INT 15h C0h: Read the IBM BIOS System Descriptor Safely
Query an AT or PS/2 BIOS system descriptor through INT 15h C0h, validate the far pointer and declared length, and interpret feature bits cautiously.
The IBM BIOS INT 15h function AH=C0h returns a pointer to a ROM-resident system descriptor on BIOS versions that implement the service. That descriptor can report a model byte, submodel byte, BIOS revision, and a feature-information byte. It is useful for compatibility diagnostics, but it is neither a universal PC hardware inventory nor an API available on every original IBM-compatible machine. A DOS program must check the function’s return status before following the far pointer.
This article is about the contract documented by IBM for later AT, XT, and Personal System/2 BIOS generations. Clone firmware can implement only part of the service or return vendor-specific values. Do not hard-code one BIOS ROM address, and do not treat the model byte as a unique motherboard identity.
Call the service and check the carry flag
Invoke INT 15h with AH=C0h. On the supported BIOS generations described in IBM’s interface reference, success returns CF=0, AH=00h, and ES:BX pointing to the system descriptor in ROM. Older PC, PCjr, XT, and early AT BIOS revisions can report the function as unsupported; the returned status is not a valid descriptor pointer. The BIOS reference gives different error values for different system families, which is why checking only whether ES:BX is nonzero is insufficient.
At the assembly boundary, preserve the registers your caller needs and inspect the flags immediately after the BIOS interrupt. A language runtime’s inline interrupt wrapper may expose carry differently, so verify its documented conventions. The conceptual control flow is:
set AH = C0h
invoke INT 15h
if CF is set or AH is nonzero:
report unavailable or failed
else:
validate ES:BX before reading memory
Do not retry by reading a guessed location such as F000:E6F5h. IBM documentation identifies ROM addresses for particular BIOS builds, but implementations can relocate code and descriptor data. The function pointer is the documented interface; a fixed address is an implementation detail.
Parse only the structure the BIOS says exists
The descriptor begins with a word that gives the descriptor length; IBM specifies a minimum length of eight bytes for the documented structure. Following bytes contain model, submodel, BIOS revision, a feature-information byte, and reserved or future-defined data. Code should read the declared length first, verify that the pointer and at least the minimum documented bytes are accessible, and then access only fields covered by that length.
A defensive parser should treat the returned pointer as a far address and avoid arithmetic that wraps a 16-bit offset or crosses an inaccessible segment boundary. In real mode the processor forms a physical address from segment and offset, but the BIOS may return a ROM segment or a memory-mapped address with platform-specific behavior. A protected-mode extender may need a supported mapping; a raw segment:offset pointer is not automatically usable from every memory model.
Use the length field to maintain forward compatibility. If the descriptor is longer than the minimum, ignore bytes whose definitions your program does not understand. If it is shorter than the minimum, reject it rather than indexing past its end. Cap the length to a small, documented upper bound before copying or logging it; a malformed or emulated BIOS return should not be allowed to trigger unbounded reads.
Interpret feature bits as BIOS claims
The feature byte describes capabilities or configuration known to that BIOS. In IBM’s documented layout, bits can indicate the presence of a slave interrupt controller, RTC, Extended BIOS Data Area, and whether fixed-disk BIOS uses DMA channel 3; other fields are reserved or architecture-specific. A set bit is not an exhaustive probe of installed hardware, and a clear bit can mean “not present” or “usage cannot be determined,” depending on the field definition.
Do not use a model or feature byte as the only basis for a dangerous hardware operation. Firmware can be cloned, patched, shadowed, or incomplete. If a program needs to know whether a specific DOS interface is available, probe that interface through its own documented function and check its return status. If it needs memory size, use the appropriate BIOS memory map service rather than inferring it from an old model code.
The BIOS equipment word from INT 11h is a different interface with a different set of reported equipment flags. It may be useful alongside the system descriptor, but neither makes the other redundant. A diagnostic can display both values and label their provenance, while avoiding a false claim that either one identifies every attached device.
Why the function is not a general plug-and-play inventory
The system descriptor describes BIOS-level system characteristics, not a dynamically enumerated list of expansion cards. It does not expose every PCI device, resource assignment, ACPI object, or DOS driver. On MCA systems, the BIOS may indicate architecture-related facts; that still does not replace the appropriate adapter or POS interface. On later machines, use a hardware enumeration method suited to the actual bus and execution environment.
Likewise, finding an RTC bit does not prove the clock battery is healthy or that an alarm interrupt is enabled. A DMA-related bit describes a BIOS use, not a free channel allocation. A slave PIC bit says a controller is present, not that every IRQ is available. Treat each field as a firmware capability claim and consult the subsystem documentation before taking ownership.
Compatibility strategy for FreeDOS tools
A FreeDOS utility should gracefully fall back when INT 15h C0h is absent. Continue with generic diagnostics, clearly label system identification as unavailable, and avoid inventing a model name from an unrecognized byte. Keep raw hexadecimal fields in logs so a later firmware-specific decoder can add support without changing the original evidence.
When adding model names, maintain a table sourced to the IBM or OEM interface documentation and preserve unknown codes. Do not map only the low byte of a machine ID unless that is the field the source defines. If a field is reserved, display it as reserved rather than deriving undocumented meaning from a few test machines.
A compatibility test matrix should include an early BIOS where the call fails, a supported AT/PS/2 BIOS where it succeeds, and at least one clone or emulator. Verify both carry-flag handling and length bounds. Mock a returned pointer with a truncated descriptor in unit tests for the parser, even if actual firmware never returns one in normal operation.
Diagnostic output and acceptance checks
For useful, reproducible output, record BIOS vendor/version where available, call status, returned far pointer, declared length, and decoded fields with their raw values. Report “BIOS reports RTC present” rather than “RTC definitely works.” Keep detection results separate from hardware tests such as reading the clock or enumerating a PCI bus.
Before shipping, verify the unsupported-service path, the minimum-length path, a longer descriptor with unknown trailing bytes, and a protected-mode configuration where the memory pointer cannot be dereferenced directly. Make sure the program neither hangs nor crashes when the BIOS reports an error. If it reads only a prefix of the structure, document exactly which fields it uses.
INT 15h C0h is valuable precisely when its limits stay visible. It supplies a compact firmware description through a defined call and length field. Checked status, bounded parsing, cautious interpretation, and a clean fallback make it a useful diagnostic input without turning it into a fictional universal hardware database.
Related:
- BIOS INT 11h and 12h: Read Legacy Equipment and Conventional Memory
- Reading Legacy PnP BIOS Device Nodes from a DOS Utility
Sources: