Leaving 64-bit mode

Question about which tools to use, bugs, the best way to implement a function, etc should go here. Don't forget to see if your question is answered in the wiki first! When in doubt post here.
Post Reply
User avatar
IanSeyler
Member
Member
Posts: 326
Joined: Mon Jul 28, 2008 9:46 am
Location: Ontario, Canada
Contact:

Leaving 64-bit mode

Post 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
BareMetal OS - http://www.returninfinity.com/
Mono-tasking 64-bit OS for x86-64 based computers, written entirely in Assembly
Octocontrabass
Member
Member
Posts: 5562
Joined: Mon Mar 25, 2013 7:01 pm

Re: Leaving 64-bit mode

Post 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.
nullplan
Member
Member
Posts: 1790
Joined: Wed Aug 30, 2017 8:24 am

Re: Leaving 64-bit mode

Post 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.
Carpe diem!
rdos
Member
Member
Posts: 3296
Joined: Wed Oct 01, 2008 1:55 pm

Re: Leaving 64-bit mode

Post 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
User avatar
IanSeyler
Member
Member
Posts: 326
Joined: Mon Jul 28, 2008 9:46 am
Location: Ontario, Canada
Contact:

Re: Leaving 64-bit mode

Post 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
BareMetal OS - http://www.returninfinity.com/
Mono-tasking 64-bit OS for x86-64 based computers, written entirely in Assembly
User avatar
iansjack
Member
Member
Posts: 4703
Joined: Sat Mar 31, 2012 3:07 am
Location: Chichester, UK

Re: Leaving 64-bit mode

Post 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.
User avatar
IanSeyler
Member
Member
Posts: 326
Joined: Mon Jul 28, 2008 9:46 am
Location: Ontario, Canada
Contact:

Re: Leaving 64-bit mode

Post 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.
BareMetal OS - http://www.returninfinity.com/
Mono-tasking 64-bit OS for x86-64 based computers, written entirely in Assembly
rdos
Member
Member
Posts: 3296
Joined: Wed Oct 01, 2008 1:55 pm

Re: Leaving 64-bit mode

Post 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.
User avatar
IanSeyler
Member
Member
Posts: 326
Joined: Mon Jul 28, 2008 9:46 am
Location: Ontario, Canada
Contact:

Re: Leaving 64-bit mode

Post 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.
BareMetal OS - http://www.returninfinity.com/
Mono-tasking 64-bit OS for x86-64 based computers, written entirely in Assembly
kzinti
Member
Member
Posts: 898
Joined: Mon Feb 02, 2015 7:11 pm

Re: Leaving 64-bit mode

Post 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.
rdos
Member
Member
Posts: 3296
Joined: Wed Oct 01, 2008 1:55 pm

Re: Leaving 64-bit mode

Post 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.
Octocontrabass
Member
Member
Posts: 5562
Joined: Mon Mar 25, 2013 7:01 pm

Re: Leaving 64-bit mode

Post 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.

Code: Select all

	mov eax, 8
	push rax
You can just write "push 8". The immediate operand can be any value that can be sign-extended from 32 bits.

Code: Select all

	lea rax, [compatmode]
	push rax
Does your assembler default to RIP-relative addressing? If not, this may cause relocation failures if the firmware loads your binary above 2GiB.

Code: Select all

	call far [rsp]
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.
rdos
Member
Member
Posts: 3296
Joined: Wed Oct 01, 2008 1:55 pm

Re: Leaving 64-bit mode

Post 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.
User avatar
IanSeyler
Member
Member
Posts: 326
Joined: Mon Jul 28, 2008 9:46 am
Location: Ontario, Canada
Contact:

Re: Leaving 64-bit mode

Post 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:
BareMetal OS - http://www.returninfinity.com/
Mono-tasking 64-bit OS for x86-64 based computers, written entirely in Assembly
Octocontrabass
Member
Member
Posts: 5562
Joined: Mon Mar 25, 2013 7:01 pm

Re: Leaving 64-bit mode

Post 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.
Post Reply