The BIOS (Basic Input/Output System) in IBM PC compatible computers is a
powerful programming interface that makes it possible to write operating
systems for a wide variety of hardware without having to implement device
drivers. Nowadays BIOS is often misunderstood mostly because of UEFI's
marketing propaganda that associates BIOS with many limitations that don't
really even exist. The most common lies are:
- BIOS does not support over 2 TB hard drives
- BIOS prevents the operating system from doing 32-bit or 64-bit stuff
- With BIOS you cannot have more than four partitions/filesystems on a hard disk (wtf?)
- BIOS is slow because all BIOS calls are synchronous
All these claims can easily be proven false by reading the official
specifications of the original IBM PC, PS/2 and AT BIOS and its extensions
that were introduced later, for example the BIOS Enhanced Disk Drive
Specification from Phoenix Technologies Ltd.
In this text is shown how to do asynchronous I/O with BIOS using standard BIOS
calls int15,ah=90 and int15,ah=91. These calls were first implemented in IBM PC
AT BIOS and they are present in virtually all clone BIOSes. With these calls
it is possible to, for example, write to a hard disk when reading from a floppy
at the same time, or send network packets while reading a hard disk. Useful
stuff.
Sadly the calls int15,ah=90 and int15,ah=91 are very poorly documented in the
original IBM documentation. To understand how they work I had to read the BIOS
listings that are written in x86 assembly. Basically they work like this:
1. User application reads from a disk using BIOS int13,ah=02
2. BIOS prepares the DMA and sends all necessary commands to the disk
controller.
3. The disk read needs time to complete. BIOS calls int15,ah=90 immediately
after the commands are sent to the disk controller.
4. int15,ah=90 handler does its thing. Here a timeout can be implemented.
If the int15,ah=90 handler returns with carry flag set, BIOS assumes
there was an error, or in other words, a timeout happened.
5. When the disk read is ready, the BIOS's hardware interrupt handler calls
int15,ah=91. This tells the user program that the int13,ah=90 handler
can return.
6. After the int15,ah=90 handler has returned, the BIOS call int15,ah=02
is completed normally.
The default int13,ah=90 handler only returns with iret and carry flag clear.
To distinguish between different ongoing I/O requests the AL register is used
to tell the device type in int15,ah=90 and int15,ah=91 calls. The types are as
follows:
= 00H - Disk
= 01H - Diskette
= 02H - Keyboard
= 03H - Pointing device
= 80H - Network
(ES:BX) = Network control block (NCB)
= FCH - Fixed disk reset for Personal System/2 products only
= FDH - Diskette drive motor start
= FEH - Printer
Devices from 00H to 7FH are devices that can only do one operation at a time.
Devices 80H - BFH are re-entrant devices, and the pointer in ES:BX is used to
distinguish between different calls. Devices C0H-FFH are devices that don't
trigger hardware interrupts when the I/O is ready.
To do non-blocking I/O with BIOS calls the user program needs to hook to
interrupt 15h. Because BIOS int 15h has also many other functions and most of
them are meant to be called by the user program and not the BIOS itself, the
interrupt handler needs to be chained to the original interrupt handler.
An example code how to do it:
Code: Select all
interrupt_ready: db 0
save_sp: dw 0
save_ss: dw 0
user_ds: dw 0
int15h_handler_ proc far
pushf ; save flags
cmp ah, 0x91 ; is this int15,ah=91?
jne int15_handler_1 ; if not, jump
inc byte ptr cs:interrupt_ready ; increment the variable
jmp int15h_chain_intr ; jump to the original interrupt handler
int15_handler_1:
cmp ah, 0x90 ; is this int15,ah=90?
jne int15h_chain_intr ; if not, jump to the original interrupt handler
cmp al, 0x01 ; is this interrupt about disk drives?
jg int15h_chain_intr ; if not, jump to the original interrupt handler -
; in this example we are only interested about
; doing things while disk I/O is being done
mov word ptr cs:save_sp, sp ; save stack pointer
mov word ptr cs:save_ss, ss ; save stack segment
mov ss, word ptr cs:user_ds ; switch to user stack
mov sp, 0x100 ; example value for stack pointer
push ds ; save data segment
push cs ; switch to user data segment
pop ds ;
sti ; enable interrupts - this handler was called using INT instruction
; and the only way out of this loop is via a timeout or a hardware interrupt
int15h_loop:
; ... do things ... ; it's also recommended to implement a timeout here
test byte ptr interrupt_ready, 0xFF ; has the interrupt been triggered?
jz int15h_loop ; if not, jump back to the loop
pop ds ; restore data segment
mov ss, word ptr cs:save_ss ; restore stack segment
mov sp, word ptr cs:save_sp ; restore stack pointer
;; add sp, 2 ; only works in correctly implemented BIOSes:
;; push bp ; make sure carry flag is clear and exit via IRET
;; and word ptr [bp+6], not 0x0001 ; (or if a timeout happened, set carry flag)
;; pop bp ; but to be in the safe side, it's recommended to
;; iret ; just chain this handler to the original
int15h_chain_intr:
popf ; restore flags
db 0xEA ; far jump opcode
_bios_int15_handler: dd 0 ; a 32-bit pointer to the original handler
int15h_handler_ endp
- It should be possible to just return via iret (the commented-out rows in the
assembly code) but some BIOS implementations don't work that way. Instead
if the code just jumps to the original BIOS int15h handler, it should work
on every computer. In some BIOS implementations the int15,ah=90 handler
implements delays that are necessary for the controller to work properly.
In some BIOS implementations the default int15,ah=91 is needed or else the
BIOS never knows that the interrupt happened.
- In the original IBM BIOS the hardware interrupt routine marks the interrupt
triggered before calling int15,ah=91, and it is the correct way to implement
it, based on the specification. Not all BIOSes are correctly implemented.
- In the original IBM BIOS the int15,ah=90 handler is used only for calling
user code, and it is the correct way to implement it.
- Because some BIOS implementations are buggy, it is recommended to save and
restore every register, which also includes the flags
- The above example can only handle one I/O wait at a time and only works with
disk I/O. With serially reusable devices different I/O waits can be identified
with the device type code in AL register. With re-entrant devices the I/O waits
can be differentiated by the pointer in ES:BX. You can use this information to
make a better handler.