Page 1 of 1

x86 MBR - int 1Ch triggers in qemu/bochs, not in virtualbox

Posted: Tue Jun 01, 2021 9:24 am
by Fahr
I'm trying to build an MBR that uses a timer to do some display stuff. So far it works fine when running under qemu or bochs, but when I try to run it under VirtualBox, the timer interrupt does not trigger at all.

I've created a very small test program that prints an "x" on the screen every second (ish);

Code: Select all

CPU 386		; Architecture we're using
BITS 16		; MBRs are 16-bit code
org 7C00h	; The location the BIOS loads the MBR

; Offset of int 0x1C - 4 bytes per entry
%define TIMER_IVT_OFFSET 0x1C * 4

main:
	; Perform this jump to make sure cs is set to 0x0000 and ip is set to 0x7c00(ish) - in line with our org.
	; BIOS behaviour on this is unpredictable
	jmp 0x0000:setcs

	print_char db 0		; Flag to see if we need to print or not
	tick_count db 0		; Tick count for 1Ch

setcs:
	; Base setup

	cld			; Set the direction flag forward, used for movs, stos, etc.
				; Setting this forward will increment cx on operations instead of decrementing it.

	xor ax, ax		; Set ES=DS=0 - this can be changed later for rep operations
	mov ds, ax
	mov es, ax

	mov ss, ax		; Stack segment is 0
	mov sp, 0x7c00		; This sets the stack (with segment 0, so ss:sp = 0000:7c00) to grow below the
				; bootloader. Nothing is relocated or loaded there, so this should be fine.


	; Set up the screen for displaying error messages and load the remaining sectors
	; These calls could be defined as callable functions, but since they are only used once, it doesn't
	; make that much sense to add that overhead and complexity.

	; Clear the screen
	mov ah, 0x07	; Function 0x07: scroll window
	mov al, 0x00	; Clear entire window
	mov bh, 0x07	; White on black
	mov cx, 0x00	; Specifies top left of screen as (0,0)
	mov dh, 0x18	; 18h = 24 rows of chars
	mov dl, 0x4f	; 4fh = 79 cols of chars
	int 0x10	; Video interrupt


	; Move cursor to position 0,0
	mov ah, 0x02	; Function 0x02: set cursor position
	mov dh, 0x00	; Row 0
	mov dl, 0x00	; Col 0
	mov bh, 0x00	; Page 0
	int 0x10	; Video interrupt

	; Stop all interrupts while we change an entry
	cli

	; IVT in real mode is at 0x0000:0x0000
	; Each entry is 2 words; offset and address with address first,
	; then offset. We are using offset 0 in this entire MBR and the
	; address is the defined anim_timer_isr below.
	mov word [TIMER_IVT_OFFSET], tick
	mov word [TIMER_IVT_OFFSET+2], 0x0000

	; Enable interrupts again
	sti

main_loop:
	cmp byte [print_char], 1
	je .print_char
	jmp main_loop

.print_char:
	mov byte [print_char], 0
	mov bh, 0
	mov bl, 0
	mov ah, 0Eh
	mov al, 'x'
	int 10h
	jmp main_loop
	

tick:
	; If we're on tick 5
	cmp byte [tick_count], 5
	je .set_print

	; Increase tick count
	inc byte [tick_count]

	jmp .end_tick

.set_print:
	; Set print_char to true and reset tick count
	mov byte [tick_count], 0
	mov byte [print_char], 1

.end_tick:
	iret


; Padding bytes until the partition table.
; $$ = address of start of section, $ = current position, $-$$ = length of section thus far.
; We use 510, so the last 2 bytes can be specifically set (total sector size = 512).
times 510-($-$$) db 0

; MBR signature
db 55h, 0AAh
I compile this with

Code: Select all

nasm -fbin mbr.asm -o mbr.bin
and then run it with qemu like so;

Code: Select all

qemu-system-i386 -drive "format=raw,file=mbr.bin,if=floppy"
This works fine, it boots and I see an x printed on the screen roughly every second.

When I create a VirtualBox disk, like so;

Code: Select all

VBoxManage internalcommands createrawvmdk -filename ./mbr.vmdk -rawdisk ./mbr.bin
and then create a virtual machine of type Other > VirtualBox Bootsector Test, it boots, but nothing is printed.

The actual MBR is far more complex and uses the timer for some animations. When I saw that didn't work, I suspected it might be because VirtualBox does not handle 1Ch. I've searched around for other cases of this issue, but could not find any.

Am I missing something here? Is there a way to fix this, or another way to get a timer working inside VirtualBox?

I also tried int 08h with the same result...

Re: x86 MBR - int 1Ch triggers in qemu/bochs, not in virtual

Posted: Tue Jun 01, 2021 12:35 pm
by nullplan
In the interrupt handler, you cannot assume the values of any segments except CS. This means you must either save DS and restore it on exit, or use CS overrides for all memory accesses in the interrupt handler. This is especially important for a BIOS called interrupt, because you cannot assume a whole lot about what BIOS is doing to its segments. If it was interrupt 8 you were intercepting, this would be a little bit different, since there is no BIOS layer between the main program and the interrupt handler in that case. Still, it is better to reset DS in the interrupt handler, or only reference CS.

You can however assume that CS is 0, because you set it 0 in the IVT. This does not hold for the main bootloader, where after setting DS the opposite is true.

Re: x86 MBR - int 1Ch triggers in qemu/bochs, not in virtual

Posted: Tue Jun 01, 2021 1:08 pm
by rdos
The BIOS used to call int 0x1C as part of handling the timer interrupt, so I suspect this is what you see.

Re: x86 MBR - int 1Ch triggers in qemu/bochs, not in virtual

Posted: Tue Jun 01, 2021 1:27 pm
by Octocontrabass
nullplan wrote:If it was interrupt 8 you were intercepting, this would be a little bit different, since there is no BIOS layer between the main program and the interrupt handler in that case.
Interrupt 8 can occur while the BIOS is servicing another interrupt, so there really is no case where you can trust registers other than CS to hold particular values.

I think some CS overrides would be the easiest way to fix it.

Re: x86 MBR - int 1Ch triggers in qemu/bochs, not in virtual

Posted: Wed Jun 02, 2021 4:42 am
by Fahr
This was exactly the issue! After prefixing all addresses with cs:, or setting ds to 0 in the top of the handler, it works fine in VirtualBox!

Thanks for the insight, everyone. I can continue now :)