BIOS execution in QEMU: first I/O interaction

In the previous articles of this series, we analyzed how QEMU starts executing the BIOS firmware (see here) and how addresses are translated from the emulated physical space to the host memory (see here). The proposal now is to discuss what these first instructions are doing and how the first I/O interaction with virtual hardware looks like.

After the initial long jump at the entry point, the BIOS firmware has the following i8086 instructions:

The first comparison is to check whether the VM is resuming or rebooting. Assuming the latter, the stack segment selector is set to 0x0 and the stack pointer to 0x7000. According to the Memory Regions map, the stack will be located in ram-below-4g. An address value, which belongs to the pc.bios region, is finally loaded into the EDX register and a jump to a different block occurs. Source code is available here and here.

The previous instructions, from a higher point of view, are preparing the CPU to jump from 16 to 32 bits protected mode. The value loaded in EDX is the address to continue execution once the transition succeeds. As specified by the i386 architecture, other steps such as disabling NMI interruptions and enabling the A20 addressing line are required. These steps are visible in transition32 source code or dumping the next instructions from the host memory:

The out (Output to Port) instruction is the first I/O interaction with virtual hardware. According to the x86 documentation, the source operand (AL register in this case, loaded with 0x8F) is copied to the I/O port specified by the destination operand (0x70).

This is the Translation Block for the previous instructions:

The out instruction was transformed into a call where the first argument is a pointer to the CPUX86State, the second is the port 0x70 and the third is the value 0x8F. The function called is helper_outb, which ends up calling address_space_stb.

Address_space_stb, in addition to receiving the port and value, gets a pointer to the “I/O” AddressSpace as its first argument. The last two arguments (attrs and result) are 0x0. With the I/O Address Space and a port number, the corresponding MemoryRegion can be obtained. Based on the Memory Map here, that region is rtc-index (subregion of rtc).

The ops member of the rtc-index Memory Region points to cmos_ioport_read and cmos_ioport_write functions (for read and write operations respectively). This is different from the pc.bios region analyzed before, where operations were directly performed over a RAM block. The device we are interacting with in this case is the MC146818 RTC/CMOS.

We will set a breakpoint in cmos_ioport_write, used for the out instruction. Once there, we notice that the opaque argument -coming from the Memory Region opaque member- is a pointer to the RTCState and addr is 0x0. The reason why addr is 0x0 instead of 0x70 is because rtc (located at 0x70) has its rtc-index subregion at 0x0. Other arguments are data (0x8F) and size (1 byte). In summary, the value 0x8F (with a mask of 0x7F) will be written to rtc-index.

Writing to rtc-index means setting an index-value for a following read or write operation into the RTC memory array. This array is pointed by the cmos_data member of the RTCState. The index-value is kept in the cmos_index member. In this case, the RTC memory index of interest is 0xF (CMOS_RESET_CODE); and the next in instruction from port 0x71 indicates that the operation will be a read.

Finally, we will set a breakpoint in cmos_ioport_read and verify how the value is read. Once again, opaque points to the same RTCState. addr this time is 0x1 and size 1 byte. The value read from cmos_data at index 0xF is 0x0.

One Reply to “BIOS execution in QEMU: first I/O interaction”

Leave a Reply

Your email address will not be published.