Page 1 of 1

Custom QEMU BIOS: how to best deal with segments and aliasing memory regions?

Posted: Sun Oct 12, 2025 1:40 pm
by Demindiro
I'm creating a custom BIOS for QEMU, but I'm having a difficult time doing it "properly", i.e. without hacky solutions.

My current problem is: QEMU maps the BIOS ROM to two locations:
  • Everything to 0xffff0000 (if the BIOS is 64KiB, not sure if larger).
  • The last 64KiB to 0xf0000.
At machine startup, the state right after the first instruction is:

Code: Select all

Trace 0: 0x7f87d4000100 [ffff0000/00000000fffffff0/00000040/ff020e01] 
EAX=00000000 EBX=00000000 ECX=00000000 EDX=00060fb1
ESI=00000000 EDI=00000000 EBP=00000000 ESP=00000000
EIP=0000fff0 EFL=00000002 [-------] CPL=0 II=0 A20=1 SMM=0 HLT=0
ES =0000 00000000 0000ffff 00009300
CS =f000 ffff0000 0000ffff 00009b00
SS =0000 00000000 0000ffff 00009300
DS =0000 00000000 0000ffff 00009300
FS =0000 00000000 0000ffff 00009300
GS =0000 00000000 0000ffff 00009300
LDT=0000 00000000 0000ffff 00008200
TR =0000 00000000 0000ffff 00008b00
GDT=     00000000 0000ffff
IDT=     00000000 0000ffff
CR0=60000010 CR2=00000000 CR3=00000000 CR4=00000000
DR0=0000000000000000 DR1=0000000000000000 DR2=0000000000000000 DR3=0000000000000000 
DR6=00000000ffff0ff0 DR7=0000000000000400
CCS=00000000 CCD=00000000 CCO=EFLAGS
EFER=0000000000000000
So the CS segment has as base 0xffff0000. As I switch to long mode, which has no segments, I need to fix that up first, which I do with:

Code: Select all

.equ PML4_BASE, 0x0000
.equ PDPT_BASE, 0x1000
.equ PD_BASE  , 0x2000
.equ PT_BASE  , 0x3000
.equ GDT_BASE , 0x4000
.equ CODE64_BASE, 0x5000
.equ CODE32_BASE, 0xf000

.equ REAL_BIOS_BASE, 0xffff0000

...

. = GDT_BASE
gdt:
segm 0, 0, 0, 0 # null
segm 0, 0xfffff, 0b10011011, 0b1010 # code64 (access: P, S, E, RW, A) (flags: G, L)
segm 0, 0xfffff, 0b10010011, 0b1010 # data64 (access: P, S, RW, A) (flags: G, L)
segm 0, 0xfffff, 0b10011011, 0b1100 # code32 (access: P, S, E, RW, A) (flags: G, DS)
segm 0, 0xfffff, 0b10010011, 0b1100 # data32 (access: P, S, RW, A) (flags: G, DS)
segm 0xffff0000, 16, 0b10011011, 0b1100 # code32 (access: P, S, E, RW, A) (flags: G, DS)
gdt_end:
gdtr:
.word gdt_end - gdt - 1
.long 0xf0000 + GDT_BASE

...

. = CODE32_BASE
.code32
main32:
jmp (3*8):REAL_BIOS_BASE + CODE32_BASE + (main32_s - main32)
main32_s:
# https://wiki.osdev.org/Entering_Long_Mode_Directly
mov eax, REAL_BIOS_BASE + PML4_BASE
mov cr3, eax
mov eax, 0b10100000 # PAE, PGE
mov cr4, eax
mov ecx, 0xc0000080 # IA32_EFER
rdmsr
or eax, (1 << 11) | (1 << 8) | (1 << 0) # NXE, LME, SCE
wrmsr
mov eax, 0x80000001 # PG, PE
mov cr0, eax
jmp (1*8):REAL_BIOS_BASE + CODE64_BASE

. = 0xfff0
.code16
_start:
lgdt cs:[gdtr]
mov al, 1 # PE
mov cr0, eax
jmp (5*8):CODE32_BASE
Needless to say, this is horrendous. Worse: it doesn't work as soon as I have more than one section which I'll need when using a higher level language, so I'm trying to do it properly.

Issue now is:

Code: Select all

...

.section .init32, "x"
main32_trampoline:
jmp (3*8):main32

.section .init, "x"
.code16
_start:
lgdt cs:[gdtr]
mov al, 1 # PE
mov cr0, eax
jmp (5*8):main32_trampoline

Code: Select all

/tmp/qemubios.o: in function `_start':
(.init+0x4): relocation truncated to fit: R_X86_64_16 against `.text'+4030
(.init+0xc): relocation truncated to fit: R_X86_64_16 against `.init32'
... which is expected since the linker thinks .init is at 0xfffffff0, which is correct, but it is also at 0xffff0. Or actually 0xfff0, accounting for the CS segment.

My current linker script is:

Code: Select all

OUTPUT_FORMAT("binary")

MEMORY {
        rom (rx) : org = 0xffff0000, l = 0x10000
        init32 (x) : org = 0xffffffe0, l = 0x10
        init (x) : org = 0xfffffff0, l = 0x10
}

SECTIONS {
        /DISCARD/ : { *(.note .note*) }
        .text : AT(0xffff0000) {
                *(.text .text*)
        } > rom =0
        .init32 : AT(0xffffffe0) {
                *(.init32 .init32*)
        } > init32 =0
        .init : AT(0xfffffff0) {
                KEEP(*(.init .init*))
        } > init =0
}
My thought is to somehow tell the linker that the last 32 bytes are aliased to 0xffe0, but I've no idea how to do that.

====================

In the meantime, I got things to work with:

Code: Select all

.section .gdt, "a"
gdt:
segm 0, 0, 0, 0 # null
segm 0, 0xfffff, 0b10011011, 0b1010 # code64 (access: P, S, E, RW, A) (flags: G, L)
segm 0, 0xfffff, 0b10010011, 0b1010 # data64 (access: P, S, RW, A) (flags: G, L)
segm 0, 0xfffff, 0b10011011, 0b1100 # code32 (access: P, S, E, RW, A) (flags: G, DS)
segm 0, 0xfffff, 0b10010011, 0b1100 # data32 (access: P, S, RW, A) (flags: G, DS)
segm 0xffff0000, 16, 0b10011011, 0b1100 # code32 (access: P, S, E, RW, A) (flags: G, DS)
gdt_end:

.section .gdtr, "a"
gdtr:
.word gdt_end - gdt - 1
.long LDFIX_gdt
.byte 0xcc

.section .init32, "ax"
.code32
main32_trampoline:
jmp (3*8):main32

.section .init, "ax"
.code16
_start:
lgdt cs:[LDFIX_gdtr]
mov al, 1 # PE
mov cr0, eax
jmp (5*8):LDFIX_main32_trampoline

Code: Select all

OUTPUT_FORMAT("binary")

MEMORY {
        rom    (rx) : org = 0xffff0000, l = 0x10000
        gdt    (r)  : org = 0xffffffb0, l = 8*6
        gdtr   (r)  : org = 0xffffffe0, l = 8
        init32 (x)  : org = 0xffffffe8, l = 8
        init   (x)  : org = 0xfffffff0, l = 16
}

LDFIX_gdt = 0xfffb0;
LDFIX_gdtr = 0xffe0;
LDFIX_main32_trampoline = 0xffe8;

SECTIONS {
        /DISCARD/ : { *(.note .note*) } /* **** OFF */
        .text : AT(0xffff0000) {
                *(.text .text*)
        } > rom =0
        .gdt : AT(0xffffffb0) {
                KEEP(*(.gdt))
        } > gdt =0
        .gdtr : AT(0xffffffe0) {
                KEEP(*(.gdtr))
        } > gdtr =0
        .init32 : AT(0xffffffe8) {
                KEEP(*(.init32))
        } > init32 =0
        .init : AT(0xfffffff0) {
                KEEP(*(.init))
        } > init =0
}
... but that is still quite nasty.

Re: Custom QEMU BIOS: how to best deal with segments and aliasing memory regions?

Posted: Sun Oct 12, 2025 2:20 pm
by nullplan
Demindiro wrote: ↑Sun Oct 12, 2025 1:40 pm I'm creating a custom BIOS for QEMU, but I'm having a difficult time doing it "properly", i.e. without hacky solutions.
That seems like an oxymoron to me. BIOS is all about hacks.
Demindiro wrote: ↑Sun Oct 12, 2025 1:40 pm At machine startup, the state right after the first instruction is:
Exactly the state I'd expect after reading that part of the CPU documentation. BIOS actually starts out in an unreal mode, with CS.base at a value that it cannot normally attain in real mode.
Demindiro wrote: ↑Sun Oct 12, 2025 1:40 pm So the CS segment has as base 0xffff0000. As I switch to long mode, which has no segments, I need to fix that up first,
And now you've lost me. You know exactly where the ROM gets loaded to, right? You know the linear addresses. So you should be able to generate a constant page table identity mapping the ROM. It is ROM, you can't write to it anyway.

In the alternative, you have pretty much all of address space available, so you can place the writable data section wherever you want. Treat the code that goes from real mode to 64-bit mode as a loader, and put something in there to copy the initial .data image to that place, and Bob's your uncle.

Re: Custom QEMU BIOS: how to best deal with segments and aliasing memory regions?

Posted: Sun Oct 12, 2025 3:03 pm
by Demindiro
nullplan wrote: ↑Sun Oct 12, 2025 2:20 pm And now you've lost me. You know exactly where the ROM gets loaded to, right? You know the linear addresses. So you should be able to generate a constant page table identity mapping the ROM. It is ROM, you can't write to it anyway.
I do have an identity mapping. I thought I observed segments effectively getting disabled as soon as I enabled long mode, but testing again this is apparently not the case. I guess I got confused by another issue.

main32_trampoline is redundant then. 7 bytes saved!

EDIT: I remember, I was trying to go directly from (un)real mode to long mode, but you can't directly jump to either 0xf0000 or 0xffff0000 with a 16-bit far jump.

Re: Custom QEMU BIOS: how to best deal with segments and aliasing memory regions?

Posted: Tue Oct 14, 2025 3:50 pm
by Octocontrabass
Demindiro wrote: ↑Sun Oct 12, 2025 3:03 pmEDIT: I remember, I was trying to go directly from (un)real mode to long mode, but you can't directly jump to either 0xf0000 or 0xffff0000 with a 16-bit far jump.
But you can with a 32-bit far jump.

You can also get rid of your LDFIX hack if you use a 32-bit lgdt instruction. The GDTR still needs to be somewhere within the initial CS segment, but the GDT itself does not.