Ok, so we are now in protected mode. We will see how to prepare and switch to long mode.
As we have talked about in previous lectures, long mode consists of two sub modes 64-bit mode and compatibility mode
which is not used in our system.
When we switch to long mode,
we are actually running in 64-bit mode.
So in this course, when we talk about long mode, we are in fact referring to 64-bit mode.
Our kernel runs in this mode as well as the application programs. When we jump to the kernel entry, we will stay there
and never leave this mode until we shut down the system.
So all the projects in the following lectures are supposed to be running in long mode.
As we have discussed in the last lecture, protected mode is only used to prepare for long mode.
Immediately after jumping to protected mode,
we prepare and switch to long mode which is a little bit complex than preparing for protected mode
.
Let's take a look.
Preparing for long mode requires several steps
including load gdt and idt, enable paging.
Note that we have to enable paging before entering long mode. Since segmentation is disabled in 64-bit mode,
we use paging to perform memory protection.
Our memory manager relies on the paging mechanism which is discussed in the section memory management.
For now, you can think of it as a technique of memory management, then we can enable Long mode by setting
the specific registers and load the new code segment descriptor in cs register using jump instruction.
And we are good to go.
Gdt is still used in 64-bit mode and the fields of gdt entries are slightly different.
Regarding to segment registers, cs register is used,
however, the contents of ds es and ss registers are all ignored,
the contents in hidden parts of those registers are also ignored.
The memory segment referenced by those segment registers are treated as if the base address is 0
and the size of the segment is the maximum size the processor can address. Actually cpu doesn’t perform segment limit checks
as we saw in protected mode.
So generally we don’t need data segment descriptors. But there is one special case
where we need the data segment descriptors,
that is, when we try to get from ring0 to ring3 to run user applications.
Because getting from ring0 to ring3 requires us to add a valid data segment descriptor to load into ss register.
So code and data segments are used in our operating system.
Another thing I need to mention is that although the contents of ds es and ss registers are ignored,
When loading descriptors into these registers,
the processor still performs checks to see if the descriptor is valid. If it is an invalid descriptor
,
the exception is generated. So we don’t mess with segment registers.
As you can see, the content in ds register is ignored
and the base address is always 0. And then add it to the offset.
The address we get is 1000.
So the process is simplified compared to that in protected mode.
As for the interrupt processing,
the interrupt descriptor table is still used in 64-bit mode. Each entry takes up 16 bytes, whereas in protected mode
,
the idt entry is 8 bytes long.
The process of interrupting handling is the same as in protected mode.
In this example, the interrupt request number 3 is fired, cpu gets the corresponding item in the IDT.
The idt entry also references the code segment the interrupt handler is at.
The base address of the code segment is always 0
and add it to the offset which is specified in idt entry, 5000 in this example.
Then we get the address of interrupt handler.
The privilege levels are the same as what we have discussed in the previous lecture.
Our kernel and user programs run in ring0 and ring3 respectively and leaves ring1 and ring2 unused
.
The structure of the segment selectors is not changed.
If we want to load a descriptor into segment register, the index should point to a valid descriptor
and cpl rpl will compare against dpl in the descriptor. In 64-bit mode,
it’s really rare to load segment descriptors by ourselves. In our system, we load code segment descriptors when we jump to 64-bit mode
.
And load data segment descriptor in ss register
when we get to ring3 for the first time, which are the only two scenarios
where we load the segment descriptors by ourselves.
Anyway, let’s see the descriptor formats in detail.
As you see, the fields in dark grey are ignored which leaves us only few bits in attributes field to set
.
The fields which are set 1 indicate that the descriptor is code segment descriptor.
C is conforming bit, as we have talked about in the last lecture, we only use non-conforming code segment in the system
.
The DPL indicates privilege level of the segment. We will set it to 0. After we load the code segment
into cs register
and jump to 64-bit mode,
the cpl will be 0 meaning that we are at ring0.
Move to the next bit which is P present bit which should be 1. Otherwise the cpu exception
is generated when we load the descriptor.
L stands for long which is new attribute bit. If the bit is set to 1,
it means that we are running in 64-bit mode.
If it is 0, we are running in compatibility mode. As for the D bit,
the only valid value is 0 when we set the long bit to 1.
The data segment descriptor also has very few bits we can set. The field 10 means that
the descriptor is data segment descriptor. W stands for writeable.
We should set it 1.
If we load the descriptor in ss register with w bit set to 0,
then we will get cpu exception.
P and dpl has the same meaning as those in code segment descriptor.
What we are going to talk about next is memory address.
There are two terms virtual address and physical address. The virtual address is basically we see in our code
and these addresses do not directly go to the memory bus.
The virtual addresses need to be mapped to physical addresses by using memory management unit or MMU.
The physical address is the address put on the memory bus and then the data in that memory location is accessed
.
There are only few cases where we use physical address in our code directly.
We will see one of these cases in this lecture.
In 64-bit mode,
we have 64 bits of virtual address space. But not all the virtual addresses are available to us.
The canonical address is the address with the bits 63 through to the most-significant implemented bit
set to either all ones or all zeros. For example, suppose we have a 48-bit address here,
from bit 0 to bit 47.
Bit 47 here is the most-significant implemented bit,
so we can only set the upper 16 bits which is from bit 48 to bit 63 to all 1s.
Other value in the upper 16 bits in this case will be considered as an invalid address or non-canonical address.
If we, for example, have 0 in bit 47
then we can only set the upper 16bits to all zeros.
Generally, when we use non-canonical address, the cpu exception is generated.
Also, note that intel has added support for 57-bit virtual address in recent processors
which are able to address 128 petabyte memory space.
But in this course, our code is based upon the assumption that
the processors only support 48 bit virtual address so that our system can run on older machines.
Now let’s see the memory map setup for our system in 64-bit mode.
The user space and kernel space are the address space available to us. As you see, the addresses in red
are the address range where all the addresses are non-canonical address which cannot be used.
You can check out the bit 47 and the upper 16 bits of these address yourself
and you will find out they doesn’t conform to the canonical address.
and our kernel will be placed at the higher part of the memory space.
Ok we start from protected mode and remove the code of printing characters.
The first thing we are going to do is we are going to set up paging. Enable long mode requires us
to setup and enable paging.
Paging is another major subject we will discuss later in this course. For the moment, we just write the code as is here
.
So we just copy and paste the code here.
This block of code basically finds a free memory area and initialize the paging structure
which is used to translate the virtual address to physical address.
Then we load the gdt that we will use in 64-bit mode. Just like we did in protected mode,
we define a gdt pointer.
So we call it
gdt 64 pointer
The first two bytes are the length of gdt
and the next four bytes are the address of gdt.
Let’s define the global descriptor table.
We call it gdt64
The first entry of it is empty so we simply assign 0 to it.
The second entry is used for code segment. Setting it up is relatively simple because most of the fields are ignored
.
There are only some of attributes left to us.
As you can see, the conforming bit is set to 0 since we only use non-conforming code segment in our system.
The next 1s means that the descriptor is code segment descriptor. The DPL indicates privilege level of the segment.
We set dpl to 0, when we load the descriptor to cs register and jump to 64-bit mode.
the cpl will be 0 indicating that we are at ring0.
Present bit is set to 1, otherwise the cpu exception is generated if we try to load the descriptor.
The Long bit should be 1 indicating that the code segment runs in 64-bit mode.
If it is 0, then it runs in compatibility mode.
The D bit can only be set to 0 if the long bit is set.
With all the fields set, we assign the value to the second entry.
That’s all we need for running in 64-bit mode for the moment. If we have no plan to jump to ring3,
we don’t need data segment for accessing data in memory.
In the kernel file, we will sort out the data we collect here and reload the gdt and idt.
So here in the loader file, there is no need to have data segment in gdt,
since we are running in ring0.
Alright, let’s continue. We define a constant
gdt64length
and assign to gdt64 pointer.
OK, now we can use the instruction lgdt to load gdt pointer.
The next thing we are going to do is we are going to enable 64-bit mode by setting the necessary bits
in some registers.
The first register is register cr4. The bit 5 in cr4 is called physical address extension or PAE bit.
We have to set it to 1 before activating 64-bit mode.
So we copy the content of cr4 to eax
and set bit 5 using or instruction.
then move it back to cr4 register.
The next register is related to cr3 register, we copy the address of the page structure we just set up,
80000 in this case to cr3 register.
The address loaded to cr3 are physical address.
So this is one of few cases we need to use physical address in our code.
By the way, all the addresses we have seen so far are all physical addresses because we don’t enable paging.
After we enable paging and enter long mode, the addresses in our code need to be mapped to physical addresses first
and then sent to the bus.
But if we load new address in cr3 register, we still need physical address instead of virtual address
.
OK, let's move on.
Then we can enable long mode. There is a model specific register called extended feature enable register
,
and the bit 8 is long mode enable bit which should be set. To read and write a model specific register,
we move the index of the register to ecx,
c0000080 in this case
and execute read msr.
The return value is in eax register. So we set the bit 8 in eax using or instruction.
Then write it back to the same register by using writer msr.
Ok the last thing we will do is enable paging by setting the bit 31 in register cr0.
So we copy the content of cr0 to eax
and set the bit 31
and write it back to cr0.
The long mode is activated now.
Let’s load the new code segment descriptor by jumping to the long mode entry.
Here we specify the segment selector 8, since each entry is 8 bytes and the code segment selector is
the second entry and then the offset long mode entry
We haven’t defined the label long mode entry. Let’s do it now.
don’t forget the directive bits
to indicate that we are now running at 64-bit mode.
So let's define
the label long mode entry
We don’t need to initialize ds es and ss registers.
So here we only initialize the stack pointer. We still set it to 7c00. We move rsp
7c00. Just like we did when we enter protected mode, we print a character L on the screen
to let us know we jump to long mode successfully.
After printing the character, we end our system by jumping to the infinite loop.
We defined the label Lend.
Ok let’s build the project and test it in the virtual machine. We save the file.
And in the terminal we run the script.
Let's open the boot folder and run bochs.
OK, as you can see, L is printed on screen.
This is the message printed in the real machine, so we successfully enter the long mode.
That's it for this lecture.
See you in the next video.