Does Grub Switch to Protected Mode

Grub run in which mode?

AFAIK, Grub starts in real mode like any other software loaded at boot. It switches to protected mode for its run time (detecting HD, displaying menus etc.) and switches back into real mode before loading and running OS such as Linux that do not support multiboot protocol.

CR0 contains PE/PG flags right upon the Linux Kernel startup

"All Intel 64 and IA-32 processors enter real-address mode following a power-up or reset", and then the firmware starts (using one CPU), then it starts all the other CPUs, and all the CPUs get switched to protected mode or long mode (along with initializing a lot of stuff - memory, MTRRs, ACPI tables, ...); and all the CPUs except one are put back to sleep.

Eventually; if the firmware is BIOS it switches the CPU back to real mode and starts a boot loader; and if the firmware is UEFI it leaves the CPU in long mode and starts a boot loader. Either way GRUB is started.

If the firmware was BIOS then GRUB switches the CPU to protected mode (and then switches it from protected mode to real mode and back to protected mode each time it wants to use BIOS functions); does a bunch of stuff; figures out it's loading a 64-bit Linux kernel and switches to long mode. If the firmware is UEFI it just stays in long mode the whole time. Note that GRUB's code is mostly 32-bit code; and both protected mode and long mode allow you to run 32-bit code; so most of GRUB's code works for both protected mode and long mode.

Even later; after a huge amount of stuff has been done by firmware and GRUB, and possibly after the CPU's mode has been changed a few thousand times; GRUB passes control to Linux.

Now... Linux has several different entry points, where the boot loader uses whichever entry point suits it the most. This changes occasionally - e.g. very old versions of Linux has a floppy disk boot loader built into it (they got rid of a bit later), and modern versions of Linux can have a UEFI boot loader built into it. There's also a real mode entry point and a protected mode entry point (which are older) and a 64-bit entry point (which is newer). Of these GRUB probably just uses the 32-bit entry point if the firmware was BIOS, and the 64-bit entry point if the firmware was UEFI.

Most of these entry points (excluding the UEFI boot loader entry point) work as a chain - e.g. if the real mode entry point is used it switches to protect mode and jumps to the 32-bit entry point; and if the 32-bit entry point is used it sets up paging and long mode and jumps to the 64-bit entry point.

Your breakpoint is at/near the 64-bit entry point; so regardless of which entry point the boot loader used (the 32-bit one or the 64-bit one) you start looking at 64-bit code. 64-bit code requires long mode, and long mode requires paging; so PG and PE must be set.

Of course (for the 64-bit entry point) paging is likely configured so that all physical addresses are mapped to the identical virtual addresses, so even though paging is enabled it doesn't actually do anything. Kernel's 64-bit startup code would change the way paging is configured (map kernel into kernel space, etc).

Keyboard interrupt in x86 protected mode causes processor error

I had a hunch last night as to why loading through GRUB and loading through the Multiboot -kernel feature of QEMU might not work as expected. That is captured in the comments. I have managed to confirm the findings based on more of the source code being released by the OP.

In the Mulitboot Specification there is a note about the GDTR and the GDT with regards to modifying selectors that is relevant:

GDTR

Even though the segment registers are set up as described above, the ‘GDTR’ may be invalid, so the OS image must not load any segment registers (even just reloading the same values!) until it sets up its own ‘GDT’.

An interrupt routine could alter the CS selector causing issues.

There is another concern and most likely the root cause of problems. The Multiboot specification also states this about the selectors it creates in its GDT:

‘CS’
Must be a 32-bit read/execute code segment with an offset of ‘0’ and a
limit of ‘0xFFFFFFFF’. The exact value is undefined.
‘DS’
‘ES’
‘FS’
‘GS’
‘SS’
Must be a 32-bit read/write data segment with an offset of ‘0’ and a limit
of ‘0xFFFFFFFF’. The exact values are all undefined.

Although it says what types of descriptors will be set up it doesn't actually specify that a descriptor has to have a particular index. One Multiboot loader may have a Code segment descriptor at index 0x08 and another bootloader may use 0x10. This is of particular relevance when you look at one line of your code:

load_idt_entry(0x21, (unsigned long) keyboard_handler_int, 0x08, 0x8E);

This creates an IDT descriptor for interrupt 0x21. The third parameter 0x08 is the Code selector the CPU needs to use to access the interrupt handler. I discovered this works on QEMU where the code selector is 0x08, but in GRUB it appears to be 0x10. In GRUB the 0x10 selector points at a non-executable Data segment and this will not work.

To get around all these problems the best thing to do is set up your own GDT shortly after starting up your kernel and before setting up an IDT and enabling interrupts. There is a tutorial on the GDT in the OSDev Wiki if you want more information.

To set up a GDT I'll simply create an assembler routine in lowlevel.asm to do it by adding a load_gdt function and data structures:

global load_gdt

; GDT with a NULL Descriptor, a 32-Bit code Descriptor
; and a 32-bit Data Descriptor
gdt_start:
gdt_null:
dd 0x0
dd 0x0

gdt_code:
dw 0xffff
dw 0x0
db 0x0
db 10011010b
db 11001111b
db 0x0

gdt_data:
dw 0xffff
dw 0x0
db 0x0
db 10010010b
db 11001111b
db 0x0
gdt_end:

; GDT descriptor record
gdt_descriptor:
dw gdt_end - gdt_start - 1
dd gdt_start

CODE_SEG equ gdt_code - gdt_start
DATA_SEG equ gdt_data - gdt_start

; Load GDT and set selectors for a flat memory model
load_gdt:
lgdt [gdt_descriptor]
jmp CODE_SEG:.setcs ; Set CS selector with far JMP
.setcs:
mov eax, DATA_SEG ; Set the Data selectors to defaults
mov ds, eax
mov es, eax
mov fs, eax
mov gs, eax
mov ss, eax
ret

This creates and loads a GDT that has a NULL Descriptor at index 0x00, a 32-bit code descriptor at 0x08, and a 32-bit data descriptor at 0x10. Since we are using 0x08 as the code selector this matches what you specify as a code selector in your IDT entry initialization for interrupt 0x21:

load_idt_entry(0x21, (unsigned long) keyboard_handler_int, 0x08, 0x8E);

The only other thing is that you'll need to amend your kernel.c to call load_gdt. One can do that with something like:

extern void load_gdt(void);

void kernel_main(void)
{
// Initialize basic components
load_gdt();
term_init();
mem_init();
dev_init();
interrupts_init();

// Start the Shell module
shell_init();

// This should be unreachable code
kernel_panic("End of kernel reached!");
}

bootloader - switching processor to protected mode

  1. Some BIOS implementations go into protected mode before entering the bootloader. Most don't. It is possible that BIOS switches to protected mode for a short period and switches back before going to the bootloader, which would allow it to use some of the benefits of protected mode (such as 32 bit being the default address size). The reason that the bootloader should be in real mode is that most BIOS functions only work in real mode, so you need to be in real mode to use them.

  2. ljmp specifies a code segment to switch to in addition to the address to jump to. They are so similar that (at least in GAS) the assembler will switch a jmp with 2 operands to a ljmp for you.

  3. ljmp is one of the only ways to change the cs register. This needs to be done to activate protected mode, as the cs register needs to contain the selector for a code segment in the GDT. (In case you want to know, the other ways to change cs are far call, far return, and interrupt return)

  4. See item 1. Either BIOS switched back to real mode, or this bootloader will not work with this BIOS.

  5. See item 3. It changes cs to specify a 32 bit code segment, so the processor goes into 32 bit mode.

  6. When you looked at the .asm file, the instruction was interpretted as if the address size was 32 bits, but GDB interpretted it as if the address size was 16 bits. The data at the address of the instruction would be 0xEA 32 7C 08 00 66 B8. EA is the long jump opcode. In a 32 bit address space, the address would be specified using the next four bytes, for an address of 0x87C32, but in a 16 bit address space, only 2 bytes are used, for an address of 0x7C32. The 2 bytes after the address specify the requested code segment, which would be 0xB866 in 32 bit mode and 0x0008 in 16 bit mode. The 0x66 B8 is the start of the next instruction, which is moving a 16 bit immediate value into the ax register, probably to set up the data segments for protected mode.

Do modern computers boot in real mode or virtual real mode?

do the current systems load in real mode first and then protected more or directly into virtual real mode because otherwise, we'll have to create a multiboot bootloader starting with real mode then jumping to virtual. Doesn't virtual real mode make it easy?

For obsolete systems (that still use BIOS and not UEFI); the firmware has to assume that the boot loader may:

  • switch to protected mode and use virtual 8086 mode (to access BIOS functions), and/or
  • switch between protected mode and real mode as it loads stuff, and/or
  • use "unreal mode"

Therefore the BIOS can not/must not use protected mode (or virtual 8086 mode) itself, because that may prevent a boot loader from working properly.

Doesn't virtual real mode make it easy?

Virtual 8086 mode ("virtual real mode") is a bit painful to support. For it to work properly; you have to have exception handlers (e.g. "general protection fault" handler) that emulate various privileged instructions. Essentially; you get the "CS:IP" from the exception handler's stack, then do some sanity checks (was problem a segment limit violation or ...?), then decode the raw bytes at "CS:IP" to figure out what the code was trying to do, then emulate every possible case while ensuring "100% correct" behavior for each different case.

The only sane reason to use virtual 8086 mode is when you want to run applications designed for an ancient real mode OS (e.g. MS-DOS) under an ancient multi-tasking 32-bit OS (e.g. Windows 95); and the hassle of emulating all of the privileged instructions is relatively minor compared to the huge hassle of emulating all of the other hardware (virtual PIT chip, virtual keyboard controller, virtual video card, ...).

How to shutdown from protected mode using APM?

GNU style inline assembly requires all registers that are not in the output or clobber list to be preserved, likewise changing to real mode will break code produced by the compiler. Fix this my using a single asm statement, or use a separate assembly file.

You try to enter 32-bit real mode, you need switch to 16-bit mode before switching to real mode.

You need to set CS to a suitable value for real mode using a far jump, otherwise the first return from an interrupt will go to the wrong address.

You can find an example of switching from 32-bit protected mode to real mode in the GRUB source.



Related Topics



Leave a reply



Submit