首先要搞清楚一点,gdtr中存放的是线性地址,而不是物理地址。除了cr3以外,开启了分页所有地址都要通过页表的转换,这是硬件决定的,绕不过去的。为何除了cr3,这显然很好理解,线性地址是先通过cr3找到页目录表的基址,然后才能开始转换,如果cr3中的基址也是线性地址,将会造成死循环。
我们以Orange's中的代码为例,首先看gdtr第一次载入的代码:
//Loader.asm
org 0100h
jmp LABEL_START ; Start
; 下面是 FAT12 磁盘的头, 之所以包含它是因为下面用到了磁盘的一些信息
%include "fat12hdr.inc"
%include "load.inc"
%include "pm.inc"
; GDT ------------------------------------------------------------------------------------------------------------------------------------------------------------
; 段基址 段界限 , 属性
LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符
LABEL_DESC_FLAT_C: Descriptor 0, 0fffffh, DA_CR | DA_32 | DA_LIMIT_4K ; 0 ~ 4G
LABEL_DESC_FLAT_RW: Descriptor 0, 0fffffh, DA_DRW | DA_32 | DA_LIMIT_4K ; 0 ~ 4G
LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW | DA_DPL3 ; 显存首地址
; GDT ------------------------------------------------------------------------------------------------------------------------------------------------------------
GdtLen equ $ - LABEL_GDT
GdtPtr dw GdtLen - 1 ; 段界限
dd BaseOfLoaderPhyAddr + LABEL_GDT ; 基地址
; GDT 选择子 ----------------------------------------------------------------------------------
SelectorFlatC equ LABEL_DESC_FLAT_C - LABEL_GDT
SelectorFlatRW equ LABEL_DESC_FLAT_RW - LABEL_GDT
SelectorVideo equ LABEL_DESC_VIDEO - LABEL_GDT + SA_RPL3
; GDT 选择子 ----------------------------------------------------------------------------------
BaseOfStack equ 0100h
LABEL_START:
...
; 下面准备跳入保护模式 -------------------------------------------
; 加载 GDTR
lgdt [GdtPtr]
; 关中断
cli
; 打开地址线A20
in al, 92h
or al, 00000010b
out 92h, al
; 准备切换到保护模式
mov eax, cr0
or eax, 1
mov cr0, eax
; 真正进入保护模式
jmp dword SelectorFlatC:(BaseOfLoaderPhyAddr+LABEL_PM_START)
LABEL_START是从boot中跳过来后开始执行的地方,可以看到,加载GDTR的那一句是在进入保护模式之前,也就是说此时GDTR中存储的地址是物理地址,即BaseOfLoaderPhyAddr + LABEL_GDT。
下面是Loader开启分页机制以后跳入内核执行的代码。
//Loader.asm
; 从此以后的代码在保护模式下执行 ----------------------------------------------------
; 32 位代码段. 由实模式跳入 ---------------------------------------------------------
[SECTION .s32]
ALIGN 32
[BITS 32]
LABEL_PM_START:
mov ax, SelectorVideo
mov gs, ax
mov ax, SelectorFlatRW
mov ds, ax
mov es, ax
mov fs, ax
mov ss, ax
mov esp, TopOfStack
push szMemChkTitle
call DispStr
add esp, 4
call DispMemInfo
call SetupPaging
;mov ah, 0Fh ; 0000: 黑底 1111: 白字
;mov al, 'P'
;mov [gs:((80 * 0 + 39) * 2)], ax ; 屏幕第 0 行, 第 39 列。
call InitKernel
;jmp $
;***************************************************************
jmp SelectorFlatC:KernelEntryPointPhyAddr ; 正式进入内核
开启分页机制以后,GDTR中的地址值是没有发生改变的,但该地址的实际意义已经发生变化,原先由于分页机制没有开启是直接寻址的,现在分页机制已经开启,这也就意味着现在该地址需要被通过cr3指向的页目录表及相应的页表转换成物理地址进行定位。
显然,为了在开启分页机制后仍能使用原先的GDT表,至少需要BaseOfLoaderPhyAddr + LABEL_GDT到BaseOfLoaderPhyAddr + LABEL_GDT + GDT_LEN - 1这段线性地址映射到同样的物理地址。
下面我们来看GDT在Kernel中的转换:
//Kernel.asm
[section .text] ; 代码在此
global _start ; 导出 _start
_start
...
; 把 esp 从 LOADER 挪到 KERNEL
mov esp, StackTop ; 堆栈在 bss 段中
sgdt [gdt_ptr] ; cstart() 中将会用到 gdt_ptr
call cstart ; 在此函数中改变了gdt_ptr,让它指向新的GDT
lgdt [gdt_ptr] ; 使用新的GDT
;lidt [idt_ptr]
jmp SELECTOR_KERNEL_CS:csini
(在这里插一句,有人可能不太理解最后一句,解释一下:cs的值在保护模式是下不能直接更改的,此处利用这个方法,将cs强制赋值为8,相当于LABEL_DESC_FLAT_C的选择子,由于此值为0,0:csinit也就跳到了csinit执行。)
这里用了cstart这个函数来改变GDT,它在start.c中,让我们来看一下:
//start.c
#include "type.h"
#include "const.h"
#include "protect.h"
PUBLIC void* memcpy(void* pDst, void* pSrc, int iSize);
PUBLIC void disp_str(char * pszInfo);
PUBLIC u8 gdt_ptr[6]; /* 0~15:Limit 16~47:Base */
PUBLIC DESCRIPTOR gdt[GDT_SIZE];
PUBLIC void cstart()
{
disp_str("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"
"-----\"cstart\" begins-----\n");
/* 将 LOADER 中的 GDT 复制到新的 GDT 中 */
memcpy(&gdt, /* New GDT */
(void*)(*((u32*)(&gdt_ptr[2]))), /* Base of Old GDT */
*((u16*)(&gdt_ptr[0])) + 1 /* Limit of Old GDT */
);
/* gdt_ptr[6] 共 6 个字节:0~15:Limit 16~47:Base。用作 sgdt/lgdt 的参数。*/
u16* p_gdt_limit = (u16*)(&gdt_ptr[0]);
u32* p_gdt_base = (u32*)(&gdt_ptr[2]);
*p_gdt_limit = GDT_SIZE * sizeof(DESCRIPTOR) - 1;
*p_gdt_base = (u32)&gdt;
}
此处的gdt即是新的GDT表了,由于现在是在保护模式中,分页机制也已经开启,现在只需要重新定义它的值,把它的地址(此处即是线性地址)和长度放到gdt_ptr中,然后用lgdt将其载入GDTR中即可完成GDT的转换了。因为这就是我们需要的线性地址。
GDT的切换到此已完结,下面是分析Orange's的实现方式,由于它的实现为求简单,分页机制是全部地址直接对应物理地址的,不具备通用性,因此只留给在看Orange's的朋友看看。
---------------------------------------------------------------------------------------------------------------------------------------------------------
Orange's中并没有重新定义GDT表,而是沿用了老GDT表,重点在memcpy这个函数,Kernel.asm中的
sgdt [gdt_ptr]一句已经将原先GDTR中的值赋值给了gdt_ptr,这个值的低16位是界限,即GDT的长度,高32位是GDT的基址。
参数中的&gdt是新GDT表的基址,此值为虚拟地址,第二个参数是将gdt_ptr中的基址拿出来,该值也是虚拟地址,我们前面已经说过开启分页机制时保证了线性地址与物理地址相同,此处是通过将ds、es等数据段值赋值0实现了 逻辑地址 = 线性地址 = 物理地址,第三个参数是将gdt_ptr中的长度拿出来。
这个函数所做的事情是将从老GDT的基址开始复制老GDT的长度个字节到新GDT的基址开始的地方,即将老GDT赋值给新GDT。该函数是用汇编实现的,在string.asm中:
//string.asm
; ------------------------------------------------------------------------
; void* memcpy(void* es:pDest, void* ds:pSrc, int iSize);
; ------------------------------------------------------------------------
memcpy:
push ebp
mov ebp, esp
push esi
push edi
push ecx
mov edi, [ebp + 8] ; Destination
mov esi, [ebp + 12] ; Source
mov ecx, [ebp + 16] ; Counter
.1:
cmp ecx, 0 ; 判断计数器
jz .2 ; 计数器为零时跳出
mov al, [ds:esi] ; ┓
inc esi ; ┃
; ┣ 逐字节移动
mov byte [es:edi], al ; ┃
inc edi ; ┛
dec ecx ; 计数器减一
jmp .1 ; 循环
.2:
mov eax, [ebp + 8] ; 返回值
pop ecx
pop edi
pop esi
mov esp, ebp
pop ebp
ret ; 函数结束,返回
; memcpy 结束------------------------------------------------------------
函数很简单,不做更进一步的说明了。注意这个函数默认ds和es值是相等的且同时为0的。