Non-blocking I/O using standard BIOS calls

Discussions on more advanced topics such as monolithic vs micro-kernels, transactional memory models, and paging vs segmentation should go here. Use this forum to expand and improve the wiki!
Post Reply
Smoppi
Posts: 3
Joined: Mon Oct 17, 2022 4:44 am
Libera.chat IRC: Sompi

Non-blocking I/O using standard BIOS calls

Post by Smoppi »

Non-blocking I/O using standard BIOS calls


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
Notes:
- 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.
nullplan
Member
Member
Posts: 1790
Joined: Wed Aug 30, 2017 8:24 am

Re: Non-blocking I/O using standard BIOS calls

Post by nullplan »

I am implementing a pure 64-bit OS. The biggest problem I would have with using BIOS calls for anything is that I would have to drop to 16-bit mode. That means
  1. Allocating a page in the low 1MB
  2. Identity mapping that page. Since in my OS, the low half of address space is reserved for userspace, that means I have to create a special kind of process just for this.
  3. Writing my trampoline into that page then jumping there
  4. Entering compatibility mode. Since my OS does not have 32- or 16-bit segments in the GDT normally, I would have to add them. But since I need to update the segments anyway, might as well load a new GDT in the trampoline.
  5. Disabling paging
  6. jumping into 16-bit mode. This also means I have to update the GDT with the correct segment base for the new CS.
  7. Disabling protected mode
  8. Performing the BIOS call
  9. And then undoing all of that
I don't think the original BIOS spec took the possibility of multiprocessing into consideration, so I have no idea what other hardware accesses are even possible while the BIOS call is going on. Indeed, I need to restore an entire BIOS compatible environment, and that means loading the IDTR with its reset values (and restoring the right values on the way home), and setting the PIC pair to their interrupt bases of 0x8 and 0x70. And to disable APIC.

And all of this to save on device drivers! Write drivers for ATA, ATAPI, AHCI, USB, and the floppy and you have most of the storage devices captured. Actually, write a SCSI driver that can be used with different transports, and you have 90% of ATAPI and USB done.

Your idea of hooking interrupt 15 would not help my case, since then the interrupt 15 hook could not return to normal operating procedure for my OS, since we need to remain in BIOS compatible 16 bit mode until the disk ready interrupt. Also, I just checked and SeaBIOS is not implementing that hook. So it would not help me on the one class of machines I know the BIOS source code for.

Meanwhile, if I just write an ATA driver, then it can just sleep until the disk ready interrupt, and the sleep can just schedule another process. And most importantly, I don't need to mess with my interrupt setup.
Carpe diem!
Octocontrabass
Member
Member
Posts: 5568
Joined: Mon Mar 25, 2013 7:01 pm

Re: Non-blocking I/O using standard BIOS calls

Post by Octocontrabass »

Smoppi wrote:- BIOS does not support over 2 TB hard drives
I wouldn't be surprised if some BIOSes are limited to 32-bit LBA (2 TiB). There are certainly BIOSes out there limited to 28-bit LBA (128 GiB), translated CHS (8 GiB), and untranslated CHS (504 MiB). But, yeah, this particular claim is probably from less-technical users misunderstanding the link between UEFI and the PC industry switching to GPT.
Smoppi wrote:- BIOS prevents the operating system from doing 32-bit or 64-bit stuff
You're right that it doesn't prevent you from switching the CPU mode, but it does make some things difficult or impossible thanks to its inflexible dependence on legacy compatibility. I think nullplan covered the major pain points already.
Smoppi wrote:- With BIOS you cannot have more than four partitions/filesystems on a hard disk (wtf?)
This is probably from less-technical users misunderstanding how MBR partitions work.
Smoppi wrote:- BIOS is slow because all BIOS calls are synchronous
As you've already noticed, many BIOSes are buggy and require strange workarounds for asynchronous calls. I'm surprised you were able to make it work on all of the ones you tried; I found one that doesn't call INT 0x15 at all, which forces synchronous disk access. (And it wasn't SeaBIOS!)
Smoppi
Posts: 3
Joined: Mon Oct 17, 2022 4:44 am
Libera.chat IRC: Sompi

Re: Non-blocking I/O using standard BIOS calls

Post by Smoppi »

Octocontrabass wrote: I wouldn't be surprised if some BIOSes are limited to 32-bit LBA (2 TiB). There are certainly BIOSes out there limited to 28-bit LBA (128 GiB), translated CHS (8 GiB), and untranslated CHS (504 MiB).
This is only an implementation-specific problem. Surely there are also many UEFI implementations that don't use the high bits of LBA sector indexing.
Octocontrabass wrote: You're right that it doesn't prevent you from switching the CPU mode, but it does make some things difficult or impossible thanks to its inflexible dependence on legacy compatibility. I think nullplan covered the major pain points already.
BIOS calls are still usable if you chain every hardware interrupt to their original 16-bit handlers. Of course chaining a 64-bit interrupt handler to a 16-bit interrupt handler is much harder to do than chaining a 16-bit interrupt handler to another 16-bit interrupt handler, but it is still less work than having to write drivers for every device, and then you can be sure that your operating system is usable on every PC-compatible computer.
Octocontrabass wrote: But, yeah, this particular claim is probably from less-technical users misunderstanding the link between UEFI and the PC industry switching to GPT.
Octocontrabass wrote:This is probably from less-technical users misunderstanding how MBR partitions work.
Sadly that has been official UEFI marketing propaganda from the start: https://uefi.org/sites/default/files/re ... _Sheet.pdf

And in reality BIOS works and has always worked with all possible types of partitioning schemes. It is UEFI that is limiting things and forcing everyone into using GPT partitioning, because using GPT and FAT32 as the boot partition has been hardcoded into the UEFI specification.
Octocontrabass wrote: As you've already noticed, many BIOSes are buggy and require strange workarounds for asynchronous calls. I'm surprised you were able to make it work on all of the ones you tried; I found one that doesn't call INT 0x15 at all, which forces synchronous disk access. (And it wasn't SeaBIOS!)
It often seems that firmware writers often don't take their work seriously. This problem also doesn't only affect BIOS.
rdos
Member
Member
Posts: 3297
Joined: Wed Oct 01, 2008 1:55 pm

Re: Non-blocking I/O using standard BIOS calls

Post by rdos »

It's perfectly possible for a protected mode OS to issue BIOS calls from V86-mode. From long mode, there is a need to switch the processor back to real mode as V86 mode is not supported in long mode. Switching back to real mode means nothing else works. In V86 mode, the rest of the OS still works, and BIOS can be called at any time. I used this for video mode switching (and still support it, although nowadays I usually setup the native video mode instead).

As for which BIOS functions that works and which doesn't, a rule of thumb is that anything that Windows use (or used) will work, and the rest can be broken without anybody complaining. I don't think Windows ever used non-blocking I/O, and so a few BIOSes are likely not to implement it at all while a few are likely to have bugs. This is not sonething I would rely on. It's a bit similar to using VBE in protected mode. Many VBE implementations will not support this or will have serious bugs in it, and so it cannot be relied on.

Even if BIOS have some nice drivers, they often depend on legacy settings, and some cease to function if you reprogram hardware. I don't think a serious OS can rely on BIOS for hardware support. I don't think a serious UEFI-based OS can rely on UEFI device support either. Both BIOS & UEFI are tools to load an OS, and are not aimed to provide useful device-drivers for an OS.
Octocontrabass
Member
Member
Posts: 5568
Joined: Mon Mar 25, 2013 7:01 pm

Re: Non-blocking I/O using standard BIOS calls

Post by Octocontrabass »

Smoppi wrote:BIOS calls are still usable if you chain every hardware interrupt to their original 16-bit handlers.
Sure, but once you get into all the hardware that needs to be configured for legacy compatibility while that 16-bit code is running - especially timers and interrupt controllers - you'll quickly see it's more trouble than it's worth.
Smoppi wrote:It often seems that firmware writers often don't take their work seriously. This problem also doesn't only affect BIOS.
Firmware development costs money, and no one wants to spend money improving firmware that already works perfectly fine with Windows.
User avatar
eekee
Member
Member
Posts: 891
Joined: Mon May 22, 2017 5:56 am
Location: Kerbin
Discord: eekee
Contact:

Re: Non-blocking I/O using standard BIOS calls

Post by eekee »

Octocontrabass wrote:Sure, but once you get into all the hardware that needs to be configured for legacy compatibility while that 16-bit code is running - especially timers and interrupt controllers - you'll quickly see it's more trouble than it's worth.
How much separation is there between cores? I had this idea today of keeping the boot core (core 0) in real mode to work with the BIOS, and putting core 1 into long mode. All application code and the scheduler would run on core 1 (and other cores later), while core 0 had its own interrupt-driven code.
Kaph — a modular OS intended to be easy and fun to administer and code for.
"May wisdom, fun, and the greater good shine forth in all your work." — Leo Brodie
Octocontrabass
Member
Member
Posts: 5568
Joined: Mon Mar 25, 2013 7:01 pm

Re: Non-blocking I/O using standard BIOS calls

Post by Octocontrabass »

eekee wrote:How much separation is there between cores?
Probably not enough. You need APICs to use multiple cores, but BIOS calls expect to interact with legacy PICs. You could catch and emulate PIC accesses using virtual 8086 mode, but that won't help you if any occur in SMM and it might interfere with BIOS code that doesn't expect to run in ring 3. You could route the PICs through the APICs, but there's no guarantee firmware running in SMM will know how to handle that gracefully and it forces you to dispatch non-BIOS-related IRQs on the core that's running BIOS code.
User avatar
eekee
Member
Member
Posts: 891
Joined: Mon May 22, 2017 5:56 am
Location: Kerbin
Discord: eekee
Contact:

Re: Non-blocking I/O using standard BIOS calls

Post by eekee »

Ugh, yeah. I thought i was all right with the boot/bios core getting all the IRQs, but if core 1 didn't receive any interrupts after being brought up, communicating only by polling, it couldn't use HLT resulting in high power consumption & maybe shorter core life. Nor could it catch a runaway process.
Kaph — a modular OS intended to be easy and fun to administer and code for.
"May wisdom, fun, and the greater good shine forth in all your work." — Leo Brodie
Octocontrabass
Member
Member
Posts: 5568
Joined: Mon Mar 25, 2013 7:01 pm

Re: Non-blocking I/O using standard BIOS calls

Post by Octocontrabass »

eekee wrote:the boot/bios core getting all the IRQs
Each core has its own LAPIC timer to generate interrupts, and message-signaled interrupts can be directed to any core.
eekee wrote:Nor could it catch a runaway process.
It can still send IPIs to core 0. Not sure it'll help you if the core gets stuck in SMM, but you can break out of any other misbehavior that way as long as it doesn't corrupt memory.
Post Reply