we are finally ready to switch to long mode. Before we enter long mode, we should jump to protected mode first
.
So in the protected mode, the only thing we will do is preparing for long mode.
As you see, we are currently running in the real mode.
Then we prepare and enter protected mode.
What we are going to do in this lecture
is talking about protected mode and write code to switch to it in our project
.
OK, let's see how to do it.
There are a couple of things we need to do.
Loading GDT global descriptor table and IDT interrupt descriptor table.
Enable protected mode by setting specific registers and jump to the start entry.
So what is global descriptor table?
Global descriptor table is a structure in memory created by us and is used by CPU to protect the memory.
Each table entry here describes a block of memory or segment and it takes up 8 bytes
,
and the first entry should be empty.
We can have more than 8000 entries in the GDT.
But we only need at most 5 entries in this course.
When the processor accesses memory, the value in segment register is actually an index within the gdt
.
This is different than we saw in real mode where the value in segment register gets shifted 4 bits
and added to the offset. In protected mode, segment registers
hold the index instead of base address.
Let's take a look at an example.
This instruction will copy the value in ds 1000 to register eax. Suppose ds holds value 16
.
Remember each table entry takes up 8 bytes. So 16 here points to the third entry.
The segment registers also hold other information in the lower 3 bits. We will see that in a moment.
The descriptor entry includes base address of the segment, segment limit and segment attributes.
Suppose the third entry has base address 0.
So the base of this segment we are about to access is 0.
Then we add the offset to the base which produces the address 1000.
The descriptor entry includes the segment attributes. So if we don’t have right to access the memory,
this operation will fail and the exception is generated.
If all the checks are done, we can access the data in that memory location.
So when running in protected mode, setting up different segments with different privilege levels,
we can protect the essential data from accessing by user applications.
What we are going to see next is interrupt descriptor table.
First off, we talk about what is an interrupt. Basically, an interrupt is a signal sent from a device to CPU
and if CPU accept the interrupt, it will stop the current task and process the interrupt event.
Different numbers are assigned to different interrupts. For example,
the interrupt request number of keyboard is 1.
So when we press a key, the interrupt request number we will get is 1.
And also, a handler or interrupt service routine is used so that cpu will call that handler to do the specific task
when an interrupt is fired.
In order to find that handler, we use interrupt descriptor table which could have a total of 256 entries
.
As we have seen the global descriptor table, the interrupt descriptor table is also a structure in memory created by us.
But the fields of IDT entries are different from those of GDT entries.
OK, let's see an example.
Suppose the interrupt request number 3 is fired, cpu will get the corresponding item in the IDT.
In the idt entry, we have the information about which segment the interrupt service routine is at and the offset of the routine.
In this example, the base address of the segment this handler locates at is 0
and the offset is 5000.
Then we get the address of the interrupt handler and the code of the handler is executed.
This is the simplified process of interrupt handling. We will delve into details in the next section.
Now move on to the privilege rings or privilege levels.
As you can see, we have 4 rings from ring 0 to ring 3 in x86 architecture.
The ring 0 is the most privileged level where we can execute all the instructions and access all the hardware
.
Ring 3 is the least privileged level where we can only execute a subset of instructions and accessing hardware
is generally not allowed.
Although cpu offers 4 levels of privileges, we only use 2 of them in this course,
which is ring 0 in which our kernel runs
and ring 3 where user applications execute.
When we learn how the privilege check performs, we will use the term ring0 and ring3 all the time
.
So how do we know what level we are currently running at?
The CPL current privilege level will tell us. It’s stored in the lower 2 bits of cs and ss segment registers.
So when the lower 2 bits of cs and ss segment registers are 0, we are in ring0
.
If they are 3, then we are running in ring3.
We will see the details of segment registers in a minute. . In protected mode, the cpl and rpl requested privilege level
will compare against dpl descriptor privilege level, if the check fails,
the cpu exception is generated.
DPL is stored in descriptor table entries
and rpl is stored in the selectors. Let’s see an example
.
As you see, the segment selector, as its name implies, holds the index of gdt or ldt local descriptor table entries.
TI means table indicator. When TI=0 the GDT is used and when TI=1 the LDT is used.
We always set it to 0,
since we don’t use ldt in our system. RPL is stored in the lower 2 bits. Suppose we load the selector
which points to the third entry of GDT in ds register, the privilege checks is performed.
The cpl which is stored in cs and ss register, rpl specified in this selector are compared
with DPL in the third GDT entry.
If the cpl and rpl is numerically smaller or equal to dpl, the privilege checks pass.
And the descriptor will be loaded in the hidden part of ds register if other checks also pass in this example
.
Ok now let’s see the details of descriptors.
What is showing here is the general structure of data and code segment descriptors.
As you see, we have base address of the segment, and the limit which specifies the size of the segment.
The attribute includes DPL field, writeable, accessed bits etc.
For example, if the DPL of the descriptor is 0, then we are not allowed to load this descriptor in the segment registers
if we are running in ring3.
One more thing worth mentioning is that we don’t go into details about the descriptor in protected mode
because the segmentation is disabled in 64-bit mode.
For example, the base and limits of descriptors are ignored. The memory is like
flat memory starting from 0 and spanning all of the memory.
And also, the segment register we use in 64-bit mode is only cs register.
We don’t care about ds and es registers. ss register is only used in context switch.
What I’m saying is that all you need to know in this course is the general idea of
what the gdt idt and selectors are and how they interact with each other.
Alright.
we open the project and see how to enable protected mode.
The print message is not used here, we just remove it.
The first thing we are going to do is that we disable interrupt when we are doing mode switch
so that the processor will not respond to the interrupt.
After we switch to long mode, we will reenable and process the interrupts.
The instruction we use is clear interrupt flag
.
Then we load the gdt and idt structure. Remember gdt and idt structure are stored in memory
and we need to tell the processor where they are located.
There is a register called global descriptor table register which points to the location of the GDT in memory
and we load the register with the address of gdt and the size of gdt.
The instruction we use is lgdt
which takes one memory operand. So we define a variable called gdt pointer.
Actually, we have two parts in this variable. The first two bytes are the size of gdt minus 1.
The next four bytes are the address of gdt.
We haven’t defined gdt yet. So let’s do it.
The start of gdt is called gdt32 in this example.
Remember the first entry is empty,
so here we define the first entry with 0.
Each entry is 8 bytes, we use directive dq to allocate 8 bytes space. Ok next we can define
a code segment descriptor.
So we can define the label code32.
As we have seen it in the slide, the descriptor is divided into several parts. The first two bytes are
the lower 16 bits of segment size.
We want to define the code segment to the maximum size.
So we set it to ffff.
The next three bytes are the lower 24 bits of base address which we set to 0,
meaning that the code segment starts from 0.
Then we move to the fourth byte which specifies the segment attributes. The s means the segment descriptor is
a system descriptor or not.
Here we set it to 1 meaning that it’s a code or data segment descriptor.
The type field is assigned to the value 10 or 1010 in binary,
which means that the segment is non-conforming readable code segment.
The major difference between conforming and non-conforming code segment is that
the cpl is not changed when the control is transferred to higher privilege conforming code segment from lower one.
Conforming code segment is not used in our system. As for non-conforming code,
in the next section we will see how to transfer the execution from lower privilege non-conforming code segment to the higher one
.
The dpl indicates privilege level of the segment.
Because we want to be running at ring0 when we jump to protected mode. We set dpl to 0
and when we load the descriptor to cs register, the cpl will be 0 indicating that we are at ring0.
Let’s move on. P is present bit which means we need to set it to 1 when we load the descriptor,
otherwise the cpu exception is generated.
So we assign the value
9a.
The next bytes is a combination of segment size and attributes. The lower half is the upper 4 bits of segment size.
We set it to the maximum size.
The available bit can be used by the system software,
we simply ignore it.
The db bit which is default operand size bit.
If this bit is 1, it means that the default operand size is 32bits.
Otherwise, the default operand size is 16bits. We set it to 1 in the protected mode.
G granularity bit. We simply set it to 1 meaning that the size field is scaled by 4kb
which gives us the maximum of 4gb of segment size.
Ok, with all the bits set here, we assign cf to this byte.
The last byte is the upper 8 bits of base address. We simply set to 0.
Alright, this is the code segment descriptor we will use in protected mode. Since we will access data in memory,
the data segment is also needed. The structure of code segment and data segment descriptors is very similar
.
So we just copy and paste here.
Name it data32.
The base address and size of data segment are the same as code segment.
The only change we need to make in this case is the type field.
We set it to 0010 in binary which means the segment is readable and writeable data segment.
If we only set the segment to readable data segment,
when we write data into the memory location referenced by this segment descriptor, the exception will be generated
.
So be sure to set the writeable bit to 1 to make the segment writable.
OK, so we change 9a to 92.
Ok, the code and data segment descriptors are all we need in protected mode
and we can calculate the length of the descriptor table. We define a constant gdt32 length.
Right now we assign the length of the table to the first two bytes and the address of gdt to the next four bytes.
So we load gdt and the address of gdt pointer.
Note that the default operand size of lgdt instruction is 16 bits in 16-bit mode.
If the operand size is 16 bits, the address of gdt pointer is actually 24 bits.
But here we simply define the address to be 32bits. and assgin the address of gdt to the lower 24bits
.
The next thing we are going to do is load interrupt descriptor table. Just like global descriptor table register
,
we also have interrupt descriptor table register and we need to load the register with the address and size of idt
.
The instruction we use is load idt instruction.
Since we don’t want to deal with interrupts until we jump to long mode, we load the register with
invalid address and size zero.
So we defines a variable
idt32 pointer
and assign zero to it.
Now we load this
idt pointer
Note that there is one type of interrupt called non-maskable interrupt which is not disabled by the instruction cli.
So when non-maskable interrupt occurs,
the processor will find the idt in memory.
The cpu exception will be generated because the address and size of idt are invalid in this case
and eventually our system will be reset, which is what we want.
The reason is that non-maskable interrupts indicate that non-recoverable hardware errors such as ram error,
there is no need to boot our system if such error occurs.
The only thing we do is reset the computer.
After we load gdt and idt, then we enable protected mode by setting the protected mode enable bit in cr0 register.
The cr0 is a control register which changes or controls the behavior of the processor.
The bit 0 is the protected mode enable bit.
So we copy the content of cr0 to eax.
And set the bit 0 to 1 using or instruction.
Writing the value in eax back to cr0
and we have enabled the protected mode. The last thing we will do is load the cs segment register
with the new code segment descriptor we just defined in the gdt table.
Loading code segment descriptor to cs register is different from another segment registers.
We cannot use mov instruction to load cs register.
Instead we use jump instruction to do it.
The code segment descriptor in this example is the second entry which is 8 bytes away from the beginning of the gdt.
So the selector we use is index being 8
and TI 0 meaning that we use gdt and the rpl is 0.
Therefore when cpu performs privilege checks, the rpl is 0 and is equal to the dpl of code segment.
The check will pass.
Then we also need to specify the offset,
we want to jump to the protected mode entry which is label we will define.
Before we define the label, we use directive bits to indicate that the following code is running at 32-bit mode
.
Ok now we define protected mode entry
and initialize other segment registers
such as ds es and ss registers.
We load them with data segment descriptors. The data segment descriptor is the third entry.
So we the index is 16 or 10 in hexadecimal. As for these segment registers,
we can use move instruction to initialize them.
Then we set stack pointer esp to 7c00.
In order to see we actually enter the protected mode, we can print a character P to let us know.
So we print P in the beginning of screen. Remember the base address for text mode is b8000
and every character takes up 2 bytes of space.
The first byte is for ascii code P
and the second byte attribute of the character.
The last thing we do is end the program using the infinite loop just as we did before.
So we define a label pend
and halt the processor
Since we were only print character p, so the message here is not used.
This block of code is used before we jump into protected mode, so we should place
this block of code
at the end of the code
in 16-bit mode.
Alright, we save the file and in the terminal
we run the build script.
OK, now let's run the bochs.
as you see, the character p is printed.
So we know that we are entering the protected mode.
Here is testing on the real machine, you can see p is printed. In the video, we will switch to long mode.
See you in the next video.