The BIOS is the famous non-volatile firmware of an IBM compatible PC. The name comes from a part of the CP/M operating system of IBM (1975). But actual bios look more like the IBM PC XT one (1983).
In a school project, the goal was to do a full 64-bit x86 bootloader. As a bonus, a student could implement a tiny bios which is both a simple and interesting task as it’s virtualized under Qemu/KVM. Indeed, at least two services really need to be implemented: a serial and a floppy service (floppy was mandatory for the project, but one could also use virtio stuff). Also, the POST routines can be omitted for a limited implementation. Thus, it’s perfectly known that the project is quite tiny and very unsufficiant to meet the goal of a real bios.
Boot as a bios !
Contrary to other architectures, firmware is not at the beginning of the memory, as there is the interrupt vector, neither at the (real) top of the memory, as the whole memory is not addressable in x86 16-bit real mode. Quite logically, the top of the bios is mapped at the top of the addressable memory. The problem is that the base of the bios image is not really known. A quick way to pass over that is to use real mode segmentation to put the code segment base (the actual linear address 0) at the base of the bios. It means that the last 16 bytes of the addressable memory (and thus, the bios) could be a long jump to the start of the code inside the bios ROM and with a code segment equal to the base of the ROM.
.text .org 0xf000 ENTRY(bios_entry) movw $0xf000, %ax movw %ax, %ds movw $0x0, %ax movw %ax, %ss movw $0x8000, %sp call set_vectors # ... boot code # ... interrupt services .org 0xfff0 ENTRY(reset_vector) ljmpw $0xf000, $bios_entry # just for padding, but should be done in ld script .org 0xffff .byte 0
As the addressable memory in real mode is 1MB (without a20), the upper 16 bytes of the memory got the address 0xffff0. The bios image is loaded at 0xf000 (note the .org) but with a code segment equal to 0xf000. The linear address is 0xf000, but the physical address is 0xff000. Thus the bios ROM is 4kB (one page). In the bios_entry routine, the segment are updated and the routine set_vectors is called which will be explained later.
The POST procedure is used to identify, initialize and check all the components of the system. As our system doesn’t use that much components, I don’t think that it’s really mandatory for us. Moreover, the system is virtualized under Qemu/KVM and thus, all the memory initialization can be bypassed. Nevertheless, firmware tables (like e820) should really be implemented in the future in this project.
In x86 real mode, the interrupt vector is at the address 0 and the entries are composed of a code selector and a jump address. At the end of an interrupt routine, there is an instruction iretw which returns from interrupt.
# set int_fake for each interrupt # # IVT entry: # +-----------+-----------+ # | Segment | Offset | # +-----------+-----------+ # 4 2 0 # movw $0x0, %di 1: movw $int_fake, 0x0(%di) movw $0xf000, 0x2(%di) addw $0x4, %di cmp $0x400, %di jbe 1b
This first routine part set up a fake interrupt routine to all the services. This one just set the carry flag to say that this service fails (not implemented actually).
# add disk interrupt movw $(0x13 << 2), %di movw $int_13, 0x0(%di) # add console interrupt movw $(0x14 << 2), %di movw $int_14, 0x0(%di) # add a20 interrupt movw $(0x15 << 2), %di movw $int_15, 0x0(%di) popw %ds ret
This part adds known interrupts that should be handled in our project. Let’s see them closer.
Serial stuff (Int 14)
Floppy magic (Int 13)
And .. MBR boot, let’s give the control to another firmware !
Normally, to boot an OS, err, a payload I mean (a bootloader, another firmware…), one should implement interrupt 19 which is the actual bios boot command (also used for reboot). But I am quite lazy I didn’t Implement it as a bios service, but as a normal assembly routine.
movw $1, %cx movw $1, %ax movw $0x7c00, %bx movw $0x0000, %dx movw %dx, %es call floppy_read ljmpw $0x0000, $0x7c00
Basically, it just uses the floppy read routine explained previously and ljmp to the entry which is 0x7c00. Indeed, on IBM PC compatible machine, the bios copy the first sector from the device into physical memory at the address 0x7c00.
This micro bios was very fun to do and very educative. But, in today’s computers, this firmware just doesn’t make any sense, especially in virtualized machines. Maybe it can be used for educative purpose though !