Custom QEMU BIOS: how to best deal with segments and aliasing memory regions?
Posted: 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.
My current problem is: QEMU maps the BIOS ROM to two locations:
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:
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:
... 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:
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:
... but that is still quite nasty.
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.
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
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
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'
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
}
====================
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
}