Page 1 of 1
Leaving 64-bit mode
Posted: Sat Dec 17, 2022 6:57 pm
by IanSeyler
I'm doing some testing on my UEFI boot up for BareMetal and have run into an issue with it running on VirtualBox. QEMU is working fine but it is always more forgiving.
UEFI is running in 64-bit mode and I want to switch back to 32-bit mode to make use of my existing loader (Pure64).
My entire UEFI code is
here.
Relevant section:
Code: Select all
; Switch to 32-bit mode
lgdt [GDTR32] ; Load a 32-bit GDT
xor eax, eax ; Clear the segment registers
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
mov rax, cr0
btc rax, 31 ; Clear PG (Bit 31)
mov cr0, rax
BITS 32
; Call Pure64
jmp 8:0x8000 ; 32-bit jump to set CS
VirtualBox hangs on that last instruction. Is that not the correct method?
Thanks,
-Ian
Re: Leaving 64-bit mode
Posted: Sat Dec 17, 2022 7:21 pm
by Octocontrabass
IanSeyler wrote:I want to switch back to 32-bit mode to make use of my existing loader (Pure64).
Which part of your existing loader is both 32-bit and useful on a 64-bit UEFI system?
Code: Select all
xor eax, eax ; Clear the segment registers
mov ss, ax
SS cannot contain a null selector in compatibility or protected mode. You must load an appropriate segment into SS before switching to compatibility mode.
Code: Select all
mov rax, cr0
btc rax, 31 ; Clear PG (Bit 31)
mov cr0, rax
CR0.PG cannot be cleared in 64-bit mode. You must switch to compatibility mode first.
Re: Leaving 64-bit mode
Posted: Sat Dec 17, 2022 11:34 pm
by nullplan
Basically, what Octo said. In order to leave 64-bit mode, you must first switch to compatibility mode, and then switch off paging. Note that switching off paging can only be done in identity mapped memory, although coming from a UEFI loader, this should be easy to do.
Re: Leaving 64-bit mode
Posted: Sun Dec 18, 2022 7:53 am
by rdos
As others have wrote, the switch must happen in compatibility mode.
Here is my code to switch from compatibility mode to protected mode: (ebx contains CR3 for protected mode paging)
Code: Select all
switch_to_protected_mode Proc far
push eax
push ebx
push ecx
push edx
pushf
;
mov ebx,eax
cli
;
mov eax,cr0
and eax,7FFFFFFFh
mov cr0,eax
;
mov ecx,IA32_EFER
rdmsr
and eax,0FFFFFEFFh
wrmsr
;
mov cr3,ebx
;
mov eax,cr0
or eax,80000000h
mov cr0,eax
;
lidt fword ptr cs:prot_idt_size
;
popf
pop edx
pop ecx
pop ebx
pop eax
ret
switch_to_protected_mode Endp
When coming from long mode with unknown GDT there is a need to load a new GDT with a flat code segment that must be jumped to with a far jump in long mode to get to compatibility mode.5
Re: Leaving 64-bit mode
Posted: Sun Dec 18, 2022 1:15 pm
by IanSeyler
Octocontrabass wrote:Which part of your existing loader is both 32-bit and useful on a 64-bit UEFI system?
Just the bits that put the proper GDT and PML4 in place. It quickly switches to 64-bit itself to process the ACPI data and start up the AP's.
Ok so I build up a temporary 64-bit GDT that has CS.L (bit 21) set to 0 and CS.D (bit 22) to 1:
Code: Select all
align 16
GDTR64: ; Global Descriptors Table Register
dw gdt64_end - gdt64 - 1 ; limit of GDT (size minus one)
dq gdt64 ; linear address of GDT
align 16
gdt64:
dq 0x0000000000000000
dq 0x0040980000000000 ; D(22), P(15), S(12), Type(11)
dq 0x0000900000000000 ; P(15), S(12)
gdt64_end:
How does this look? Again, QEMU works fine (CS is updated) but VirtualBox hangs on the 'call far' based on the log when I shut the VM down.
Code: Select all
; Stop interrupts
cli
; Switch to 64-bit compatibility mode
lgdt [GDTR64]
mov eax, 8
push rax
lea rax, [compatmode]
push rax
call far [rsp]
BITS 32
compatmode:
; Switch to 32-bit mode
lgdt [GDTR32] ; Load a 32-bit GDT
mov eax, cr0
btc eax, 31 ; Clear PG (Bit 31)
mov cr0, eax
; Call Pure64
jmp 8:0x8000 ; 32-bit jump to set CS
Thanks,
-Ian
Re: Leaving 64-bit mode
Posted: Sun Dec 18, 2022 1:24 pm
by iansjack
Why don’t you just put the proper GDT and Page Table in place whilst in 64-bit mode? It seems silly to switch to 32 bits just to do this and then switch back to 64 bits.
Re: Leaving 64-bit mode
Posted: Sun Dec 18, 2022 1:44 pm
by IanSeyler
iansjack wrote:Why don’t you just put the proper GDT and Page Table in place whilst in 64-bit mode? It seems silly to switch to 32 bits just to do this and then switch back to 64 bits.
Agreed. Once I know I can set CS properly in 64-bit mode after loading a new GDT I think I'll attempt that. I can add code to my BIOS MBR to get things into a minimal 64-bit environment and build the rest later.
Re: Leaving 64-bit mode
Posted: Sun Dec 18, 2022 1:57 pm
by rdos
You are switching to protected mode so why do you think you should setup a long mode code segment? You should setup a 32-bit flat code selector and switch to it.
Also note that GDT has the same format in 32-bit and 64-bit. The only difference is that in 64-bit mode the adress can be above 4G. No need to load two different GDTs. Instead you should build the 32-bit code selector as you would in protected mode.
Re: Leaving 64-bit mode
Posted: Sun Dec 18, 2022 2:51 pm
by IanSeyler
rdos wrote:You are switching to protected mode so why do you think you should setup a long mode code segment? You should setup a 32-bit flat code selector and switch to it.
I tried that initially. The CPU needs to be put in compatibility mode (which is a subset I guess of 64-bit) first before you can go to 32-bit.
Re: Leaving 64-bit mode
Posted: Sun Dec 18, 2022 2:59 pm
by kzinti
IanSeyler wrote:Once I know I can set CS properly in 64-bit mode after loading a new GDT I think I'll attempt that.
Code: Select all
void Cpu::LoadGdt()
{
const mtl::GdtPtr gdtPtr{sizeof(m_gdt) - 1, m_gdt};
mtl::x86_lgdt(gdtPtr);
asm volatile("pushq %0\n"
"pushq $1f\n"
"lretq\n"
"1:\n"
:
: "i"(Selector::KernelCode)
: "memory");
asm volatile("movl %0, %%ds\n"
"movl %0, %%es\n"
"movl %1, %%fs\n"
"movl %1, %%gs\n"
"movl %0, %%ss\n"
:
: "r"(Selector::KernelData), "r"(Selector::Null)
: "memory");
}
Seriously... Going to 32 bits is not saving you any work, it just makes thing so much more complicated for no good reason.
Re: Leaving 64-bit mode
Posted: Sun Dec 18, 2022 5:01 pm
by rdos
IanSeyler wrote:rdos wrote:You are switching to protected mode so why do you think you should setup a long mode code segment? You should setup a 32-bit flat code selector and switch to it.
I tried that initially. The CPU needs to be put in compatibility mode (which is a subset I guess of 64-bit) first before you can go to 32-bit.
Compatibility mode basically is the same as protected mode in regards to selectors, and so you can just turn off paging and everything is still valid. Therefore, there is no need to load another GDT or do another far jump. The CS load when switching to compatibility mode is enough.
I notice in my code when I switch from EFI long mode to 32-bit protected mode, that I reset bit 5 in CR4 too, as well as the bit in the EFER register that changes the meaning of paging. However, if you don't plan to use protected mode paging, then that isn't necessary,
A further complication is that the image can be loaded anywhere by UEFI, but I assume I can load it at a fixed address. The load point is important since protected mode doesn't have RIP-relative addressing and the image needs to be bound to the correct address. My code is mostly byte-coded since my assembler cannot handle long mode code and cannot mix 32-bit and 64-bit code.
Re: Leaving 64-bit mode
Posted: Sun Dec 18, 2022 9:52 pm
by Octocontrabass
IanSeyler wrote:Just the bits that put the proper GDT and PML4 in place.
It doesn't make any sense to switch to protected mode to do either of these things.
Code: Select all
dq 0x0040980000000000 ; D(22), P(15), S(12), Type(11)
dq 0x0000900000000000 ; P(15), S(12)
All those bits that were ignored in 64-bit mode are not ignored in compatibility mode. At the very least, you need to set the limit high enough to access your code and data.
IanSeyler wrote:How does this look?
You don't need a separate GDT for each mode. You can load a single GDT that contains all of the segment descriptors you need.
You can just write "push 8". The immediate operand can be any value that can be sign-extended from 32 bits.
Does your assembler default to RIP-relative addressing? If not, this may cause relocation failures if the firmware loads your binary above 2GiB.
On AMD CPUs, far CALL doesn't support 64-bit offsets. Are you sure you didn't want to use RETFQ here instead?
Code: Select all
jmp 8:0x8000 ; 32-bit jump to set CS
UEFI doesn't guarantee fixed addresses will be usable memory. All code that runs before you switch to your own page tables must be relocatable.
Re: Leaving 64-bit mode
Posted: Mon Dec 19, 2022 1:44 am
by rdos
Octocontrabass wrote:IanSeyler wrote:Just the bits that put the proper GDT and PML4 in place.
It doesn't make any sense to switch to protected mode to do either of these things.
Code: Select all
dq 0x0040980000000000 ; D(22), P(15), S(12), Type(11)
dq 0x0000900000000000 ; P(15), S(12)
All those bits that were ignored in 64-bit mode are not ignored in compatibility mode. At the very least, you need to set the limit high enough to access your code and data.
I'd go a step further. I don't think long mode descriptors are valid for compatibility mode, or at least doesn't make any sense since base & limits cannot be higher than 4G anyway. The compatibility mode CS descriptor should be an ordinary 32-bit code descriptor.
My long mode loader actually sets the base to the load point, which means the code can run from any position without needing relocation.
Re: Leaving 64-bit mode
Posted: Thu Dec 22, 2022 11:25 am
by IanSeyler
Thanks for the hints everyone. I was able to get VirtualBox going with switching to 32-bit mode temporarily so I didn't need to make any changes to my second stage loader.
Code: Select all
; Stop interrupts
cli
; Build a 32-bit memory table
mov rdi, 0x200000
mov rax, 0x00000083
mov rcx, 1024
again:
stosd
add rax, 0x400000
dec rcx
cmp rcx, 0
jne again
; Load the custom GDT
lgdt [gdtr]
; Switch to compatibility mode
mov rax, SYS32_CODE_SEL ; Compatibility mode
push rax
lea rax, [compatmode]
push rax
retfq
BITS 32
compatmode:
; Set the segment registers
mov eax, SYS32_DATA_SEL
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
; Deactivate IA-32e mode by clearing CR0.PG
mov eax, cr0
btc eax, 31 ; Clear PG (Bit 31)
mov cr0, eax
; Load CR3
mov eax, 0x200000
mov cr3, eax
; Disable IA-32e mode by setting IA32_EFER.LME = 0
mov ecx, 0xC0000080 ; EFER MSR number
rdmsr ; Read EFER
and eax, 0xFFFFFEFF ; Clear LME (Bit 8)
wrmsr ; Write EFER
mov eax, 0x00000010 ; Set PSE (Bit 4)
mov cr4, eax
; Enable legacy paged-protected mode by setting CR0.PG
mov eax, 0x00000001 ; Set PM (Bit 0)
mov cr0, eax
jmp SYS32_CODE_SEL:0x8000 ; 32-bit jump to set CS
....
align 16
gdtr: ; Global Descriptors Table Register
dw gdt_end - gdt - 1 ; limit of GDT (size minus one)
dq gdt ; linear address of GDT
align 16
gdt:
SYS64_NULL_SEL equ $-gdt ; Null Segment
dq 0x0000000000000000
SYS32_CODE_SEL equ $-gdt ; 32-bit code descriptor
dq 0x00CF9A000000FFFF ; 55 Granularity 4KiB, 54 Size 32bit, 47 Present, 44 Code/Data, 43 Executable, 41 Readable
SYS32_DATA_SEL equ $-gdt ; 32-bit data descriptor
dq 0x00CF92000000FFFF ; 55 Granularity 4KiB, 54 Size 32bit, 47 Present, 44 Code/Data, 41 Writeable
SYS64_CODE_SEL equ $-gdt ; 64-bit code segment, read/execute, nonconforming
dq 0x00209A0000000000 ; 53 Long mode code, 47 Present, 44 Code/Data, 43 Executable, 41 Readable
SYS64_DATA_SEL equ $-gdt ; 64-bit data segment, read/write, expand down
dq 0x0000920000000000 ; 47 Present, 44 Code/Data, 41 Writable
gdt_end:
Re: Leaving 64-bit mode
Posted: Thu Dec 22, 2022 11:29 am
by Octocontrabass
There's no need for a separate 64-bit data segment. In the few cases where you can't use a null data segment in 64-bit mode, you can use your 32-bit data segment.