写在前面

添油加醋系列第二弹——剖析GDT

头文件:https://github.com/bajdcc/MiniOS/blob/master/include/gdt.h

实现:https://github.com/bajdcc/MiniOS/blob/master/src/kernel/gdt.c

话说C语言的话除了刷刷OJ外,就是用来实现操作系统这个大头了。C语言比C++少了很多很多臃肿的语法特性,写起来非常优美(至少写操作系统是这样的)。虽说C++有许多的奇技淫巧,一个算法有N种实现方法,但这会让选择恐惧症患者(比如我)难堪,比如说一个类要怎样写啊等等,,抛开其他不谈,假如一个语言的语法特性越少,学起来可能越简单(刚试过lua语法很简单)。OK废话不多说,进入本章主题(涉及OS的资料很杂很偏,如有错误望海涵)。

GDT的构成

这个网址不错(英文的):Global Descriptor Table

首先,根据网上资料,GDT(全局描述符表)又叫段描述符表,暂且就这样认为吧,如有异议可以提出来。

一个GDT可能是这样的(GDT与LDT - Lan'Sir - 博客频道 - CSDN.NET):

同样也是这样的(Global Descriptor Table):

在代码中它又是这样:

// 全局描述符表结构 http://www.cnblogs.com/hicjiajia/archive/2012/05/25/2518684.html
// base: 基址(注意,base的byte是分散开的)
// limit: 寻址最大范围 tells the maximum addressable unit
// flags: 标志位 见上面的AC_AC等
// access: 访问权限
struct gdt_entry {
uint16_t limit_low;
uint16_t base_low;
uint8_t base_middle;
uint8_t access;
unsigned limit_high: 4;
unsigned flags: 4;
uint8_t base_high;
} __attribute__((packed));

这时你的内心OS:

答案是——它们都是GDT。。

关于C语言的问题:首先,可能有些童鞋不知道struct里那些冒号是神马意思。(C语言 struct结构体的变量声明加冒号)这里叫作“位域”,就是占几个二进制位。同时,它又涉及内存对齐的概念(C语言 结构体的内存对齐问题与位域)。涉及__attribute__((packed))的概念(__attribute__ 你知多少?)它是手动设置对齐大小。

众所周知,一个字节byte是八个bit,那么结构体中有两个4bit的成员,不可能用16bit去容纳它们吧~让它们互相挤挤,节省空间,何乐而不为。

可能看到这里,已经花了好多时间了……没办法,OS的内容非常多,同时GCC的一些怪异偏僻用法又不得不去领会,所以只能一步步来,慢慢理解,急不得。

至于GDT为什么这样描述呢,我自创行不行?一个字——标准,你想改,可能你电脑里的硬件设施不答应……

GDT的存在意义

GDT 与 LDT - hicjiajia - 博客园)描述得很清楚。

全局描述符表GDT(Global Descriptor Table)在整个系统中,全局描述符表GDT只有一张(一个处理器对应一个GDT),GDT可以被放在内存的任何位置,但CPU必须知道GDT的入口,也就是基地址放在哪里,Intel的设计者门提供了一个寄存器GDTR用来存放GDT的入口地址,程序员将GDT设定在内存中某个位置之后,可以通过LGDT指令将GDT的入口地址装入此寄存器,从此以后,CPU就根据此寄存器中的内容作为GDT的入口来访问GDT了。GDTR中存放的是GDT在内存中的基地址和其表长界限

也就是说,GDT是全局的,存放在内存中的某个位置,而这个位置是由你来指定给CPU的,换句话说,你来钦定!

设置GDT

现在知道了GDT的struct构成(就是一个个数组元素),那么我们要给CPU的就是一个gdt_entry数组地址啦~

那么设置gdt_entry的方法如下:

void gdt_install(uint8_t num, uint32_t base, uint32_t limit, uint8_t access, uint8_t flags) {

    /* Setup the descriptor base address */
gdt[num].base_low = (base & 0xffff);
gdt[num].base_middle = (base >> 16) & 0xff;
gdt[num].base_high = (base >> 24) & 0xff; /* Setup the descriptor limits */
gdt[num].limit_low = (limit & 0xffff);
gdt[num].limit_high = ((limit >> 16) & 0x0f); /* Finally, set up the granularity and access flags */
gdt[num].flags = flags; access |= AC_RE; // 设置保留位为1
gdt[num].access = access;
} 通过实例认识它:
// 宏定义 #define AC_AC 0x1 // 可访问 access
#define AC_RW 0x2 // [代码]可读;[数据]可写 readable for code selector & writeable for data selector
#define AC_DC 0x4 // 方向位 direction
#define AC_EX 0x8 // 可执行 executable, code segment
#define AC_RE 0x10 // 保留位 reserve
#define AC_PR 0x80 // 有效位 persent in memory // 特权位: 01100000b
#define AC_DPL_KERN 0x0 // RING 0 kernel level
#define AC_DPL_USER 0x60 // RING 3 user level #define GDT_GR 0x8 // 页面粒度 page granularity, limit in 4k blocks
#define GDT_SZ 0x4 // 大小位 size bt, 32 bit protect mode // gdt selector 选择子
#define SEL_KCODE 0x1 // 内核代码段
#define SEL_KDATA 0x2 // 内核数据段
#define SEL_UCODE 0x3 // 用户代码段
#define SEL_UDATA 0x4 // 用户数据段
#define SEL_TSS 0x5 // 任务状态段 task state segment http://wiki.osdev.org/TSS // RPL 请求特权等级 request privilege level
#define RPL_KERN 0x0
#define RPL_USER 0x3 // CPL 当前特权等级 current privilege level
#define CPL_KERN 0x0
#define CPL_USER 0x3 ======================================================== /* Setup the GDT pointer and limit */
gp.limit = (sizeof(struct gdt_entry) * NGDT) - 1;
gp.base = (uint32_t)&gdt; /* null descriptor */
gdt_install(0, 0, 0, 0, 0);
/* kernel code segment type: code addr: 0 limit: 4G gran: 4KB sz: 32bit */
gdt_install(SEL_KCODE, 0, 0xfffff, AC_RW|AC_EX|AC_DPL_KERN|AC_PR, GDT_GR|GDT_SZ);
/* kernel data segment type: data addr: 0 limit: 4G gran: 4KB sz: bit 32bit */
gdt_install(SEL_KDATA, 0, 0xfffff, AC_RW|AC_DPL_KERN|AC_PR, GDT_GR|GDT_SZ);
/* user code segment type: code addr: 0 limit: 4G gran: 4KB sz: 32bit */
gdt_install(SEL_UCODE, 0, 0xfffff, AC_RW|AC_EX|AC_DPL_USER|AC_PR, GDT_GR|GDT_SZ);
/* user code segment type: data addr: 0 limit: 4G gran: 4KB sz: 32bit */
gdt_install(SEL_UDATA, 0, 0xfffff, AC_RW|AC_DPL_USER|AC_PR, GDT_GR|GDT_SZ);

我的理解是,gdt_install的参数:(段选择子索引号/见题图,基址起始,长度,访问权限,GDT flags)。虽然上述例子中基址起始地址和长度都是一样的(原项目https://github.com/SilverRainZ/OS677是这样写的,可能有点问题),但是访问权限中有AC_EX和AC_DPL_KERN(ring0)/AC_DPL_USER(ring3)的变化,说明每个段的权限是不同的。这些段管理的是同一片内存,只是由于当前索引号的不同,访问/修改内存的权限也不同。

GDT 与 LDT - hicjiajia - 博客园)讲述了分段管理和分页管理:

分段管理可以把虚拟地址转换成线性地址,而分页管理可以进一步将线性地址转换成物理地址。

(根据段选择子找到)段基指 + 偏移地址 => 线性地址

线性地址 (通过页表) => 物理地址

通过将GDT告诉给CPU后,CPU就知道了操作系统中段的设置,从而可以通过段选择子得到线性地址,在后面实现分页管理后,可进一步将线性地址转换为物理地址(不过当前连物理 址有多大都没法知道呢,在后面会解决)。

段选择子

GDT 与 LDT - hicjiajia - 博客园)介绍:

段选择子包括三部分:描述符索引(index)、TI(指示从GDT还是LDT中找)、请求特权级(RPL)。

  1. index部分表示所需要的段的描述符在描述符表的位置,由这个位置再根据在GDTR中存储的描述符表基址就可以找到相应的描述符gdt_entry。然后用描述符gdt_entry中的段基址SEL加上逻辑地址OFFSET就可以转换成线性地址SEL:OFFSET(看下面给的例子应该就是它们的和SEL+OFFSET)
  2. 段选择子中的TI值只有一位0或1,0代表选择子是在GDT选择,1代表选择子是在LDT选择。
  3. 请求特权级(RPL)则代表选择子的特权级,共有4个特权级(0级、1级、2级、3级),0级最高。关于特权级的说明:任务中的每一个段都有一个特定的级别。每当一个程序试图访问某一个段时,就将该程序所拥有的特权级与要访问的特权级进行比较,以决定能否访问该段。系统约定,CPU只能访问同一特权级或级别较低特权级的段

例如:

给出逻辑地址:21h:12345678h,需要将其转换为线性地址
a. 选择子SEL=21h=0000000000100 0 01b,他代表的意思是:选择子的index=4即100b,选择GDT中的第4个描述符;TI=0代表选择子是在GDT选择;左后的01b代表特权级RPL=1(因此有SEL=n<<3,n是索引号)
b. OFFSET=12345678h,若此时GDT第四个描述符中描述的段基址(Base)为11111111h,则线性地址=11111111h+12345678h=23456789h

任务状态段TSS

任务寄存器(TR)用于寻址一个特殊的任务状态段(Task State Segment,TSS)。TSS中包含着当前执行任务的重要信息。

TR寄存器用于存放当前任务TSS段的16位段选择符、32位基地址、16位段长度和描述符属性值。它引用GDT表中的一个TSS类型的描述符。指令LTR和STR分别用于加载和保存TR寄存器的段选择符部分。当使用LTR指令把选择符加载进任务寄存器时,TSS描述符中的段基地址、段限长度以及描述符属性会被自动加载到任务寄存器中。当执行任务切换时,处理器会把新任务的TSS的段选择符和段描述符自动加载进任务寄存器TR中。

它的初始化和设置:

void tss_init() {
gdt_install(SEL_TSS, (uint32_t)&tss, sizeof(tss),AC_PR|AC_AC|AC_EX, GDT_GR);
/* for tss, access_reverse bit is 1 */
gdt[5].access &= ~AC_RE;
} // 装载TSS
void tss_install() {
__asm__ volatile("ltr %%ax" : : "a"((SEL_TSS << 3)));
} // 设置TSS
void tss_set(uint16_t ss0, uint32_t esp0) {
// 清空TSS
memset((void *)&tss, 0, sizeof(tss));
tss.ss0 = ss0;
tss.esp0 = esp0;
tss.iopb_off = sizeof(tss);
}

跟GDT也差不了多少,只是GDT_SZ没有了,也指定了tss的地址,并设置gdt_entry的保留位为1(至于为啥我没有仔细查)。至于__asm__ volatile的GCC在C语言中内嵌汇编 asm __volatile__我也没全部搞明白怎么用。SEL_TSS << 3的话要参考选择子的构成,它高13位是索引,所以要乘8。

关于ltr指令(设置TSS结构中堆栈信息的 ltr 指令):

在任务内发生特权级变换时堆栈也随着自动切换,外层堆栈指针保存在内层堆栈中,而内层堆栈指针存放在当前任务的TSS中。所以,在从外层向内层变换时,要访问TSS(从内层向外层转移时不需要访问TSS,而只需访问内层栈中保存的栈指针)。
LTR指令是专门用于装载任务状态段寄存器TR的指令。该指令的操作数是对应TSS段描述符的选择子。LTR指令从GDT中取出相应的TSS段描述符,把TSS段描述符的基地址和界限等信息装入TR的高速缓冲寄存器中。

TSS的构成在https://github.com/bajdcc/MiniOS/blob/master/include/idt.h中(看下面的英文注释/Task State Segment,就是说SS0、ESP0比较重要)。

// 任务状态段 task state segment http://wiki.osdev.org/TSS
// The only interesting fields are SS0 and ESP0.
// SS0 gets the kernel datasegment descriptor (e.g. 0x10 if the third entry in your GDT describes your kernel's data)
// ESP0 gets the value the stack-pointer shall get at a system call
// IOPB may get the value sizeof(TSS) (which is 104) if you don't plan to use this io-bitmap further (according to mystran in http://forum.osdev.org/viewtopic.php?t=13678) // http://blog.csdn.net/huybin_wang/article/details/2161886
// TSS的使用是为了解决调用门中特权级变换时堆栈发生的变化 // http://www.kancloud.cn/wizardforcel/intel-80386-ref-manual/123838
/*
TSS 状态段由两部分组成:
1、 动态部分(处理器在每次任务切换时会设置这些字段值)
通用寄存器(EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI)
段寄存器(ES,CS,SS,DS,FS,GS)
状态寄存器(EFLAGS)
指令指针(EIP)
前一个执行的任务的TSS段的选择子(只有当要返回时才更新)
2、 静态字段(处理器读取,但从不更改)
任务的LDT选择子
页目录基址寄存器(PDBR)(当启用分页时,只读)
内层堆栈指针,特权级0-2
T-位,指示了处理器在任务切换时是否引发一个调试异常
I/O 位图基址
*/
struct tss_entry {
uint32_t link;
uint32_t esp0;
uint32_t ss0;
uint32_t esp1;
uint32_t ss1;
uint32_t esp2;
uint32_t ss2;
uint32_t cr3;
uint32_t eip;
uint32_t eflags;
uint32_t eax;
uint32_t ecx;
uint32_t edx;
uint32_t ebx;
uint32_t esp;
uint32_t ebp;
uint32_t esi;
uint32_t edi;
uint32_t es;
uint32_t cs;
uint32_t ss;
uint32_t ds;
uint32_t fs;
uint32_t gs;
uint32_t ldtr;
uint16_t padding1;
uint16_t iopb_off;
} __attribute__ ((packed));

阶段性总结

涉及OS的内容真是庞大,单单一个GDT就涉及巨量的知识,包括结构体定义、汇编指令、GCC黑魔法、参数的使用等,还涉及了TSS,目标仅仅是实现分段管理。而后面还有中断管理、物理内存管理、虚拟内存管理等一系列内容,篇幅绝对不比本文少,真令人望洋兴叹。

原始项目OS67中也存在着一些错误,有些错误像是单词拼写等我已经纠正了,还有些如软盘访问我去参考了网上的资料,与OS67的不一致,但我没采用OS67的。毕竟OS67也是其作者自己摸索出来的,让我跳过了许多坑。。不过我想后面的进程管理还是得自己写才能体会更深。

既然OS的内容很杂很多,所以也只能挑一些重点的讲讲了,不可能面面俱到,在后面的编写/借鉴中,还是要以查资料为主,给源码附上参考文章的地址,方便阅读。

https://zhuanlan.zhihu.com/p/25867829备份。

全局描述符表GDT的更多相关文章

  1. linux内核学习之全局描述符表(GDT)(二)

    来源:https://www.cnblogs.com/longintchar/p/5224406.html 在进入保护模式之前,我们先要学习一些基础知识.今天我们看一下全局描述符表(Global De ...

  2. Bran的内核开发教程(bkerndev)-06 全局描述符表(GDT)

    全局描述符表(GDT)   在386平台各种保护措施中最重要的就是全局描述符表(GDT).GDT为内存的某些部分定义了基本的访问权限.我们可以使用GDT中的一个索引来生成段冲突异常, 让内核终止执行异 ...

  3. 获取全局描述符表GDT的内容

    /stdfx.h文件 //Ring0环的程序 //测试环境VS2005 #ifndef _WIN32_WINNT // Allow use of features specific to Window ...

  4. GDT全局描述符表

    GDT全局描述符表 什么是GDT全局描述符表 GDT全称为Global Descriptor Table,全局描述符表. 保护模式的寻址方式不在使用寄存器分段的方式直接寻址方式了.而采用的是使用GDT ...

  5. 全局描述符表(GDT)——《x86汇编语言:从实模式到保护模式》读书笔记09

    在进入保护模式之前,我们先要学习一些基础知识.今天我们看一下全局描述符表(Global Descriptor Table, 简称GDT). 同实模式一样,在保护模式下,对内存的访问仍然使用段地址加偏移 ...

  6. 保护模式下GDTR,LDTR,全局描述符表,局部描述符表和选择器的关系

    这张图要注意:右边两个0-15,其中上面的是LDTR,  下面的是选择子. 图下第五个标线,是两个线交叉的,实际上第五个线是指向右边水平的那个线. 没有箭头的两组线分别表示GDT的区间,LDT的区间 ...

  7. 段描述符表(GDT+LDT)的有感

    [0]写在前面 要知道,在汇编中,代码的装入顺序决定了在内存中的地址位置.所有的代码或者数据都在硬盘上,当调试或者启动的时候,加载到内存:当需要对数据进行处理的时候,我们通过将数据从内存载入到regi ...

  8. LGDT/LIDT-加载全局/中断描述符表寄存器

    将源操作数中的值加载到全局描述符表寄存器 (GDTR) 或中断描述符表寄存器 (IDTR).源操作数指定 6 字节内存位置,它包含全局描述符表 (GDT) 或中断描述符表 (IDT) 的基址(线性地址 ...

  9. GDT,LDT,GDTR,LDTR 详解,包你理解透彻(转)

    引自:http://www.techbulo.com/708.html 一.引入 保护模式下的段寄存器 由 16位的选择器 与 64位的段描述符寄存器 构成 段描述符寄存器: 存储段描述符 选择器:存 ...

随机推荐

  1. iOS网络编程解析协议二:XML数据传输解析

    XML两种解析方式,一种是SAX,NSXMLParser是SAX方法解析,另一种是DOM(Document Object Model); 区别: SAX: 只能读,不能修改,只能顺序访问,适合解析大型 ...

  2. ASP.Net生成静态HTML页

    动态网页开发技术中,为了降低网站维护的工作量,常常用到动态页面技术.目前因特网上流行的做法是将网站中需要经常更新的数据存放到数据库中,当客户端浏览器向服务器发出HTTP请求时,服务器通过执行.解释某个 ...

  3. java中 HashMap和Hashtable,list、set和map 的区别

    摘自: http://blog.chinaunix.net/uid-7374279-id-2057584.html HashMap是Hashtable的轻量级实现(非线程安全的实现),他们都完成了Ma ...

  4. html5-video视频播放

    watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/ ...

  5. Sn.exe(强名称工具)

    Sn.exe(强名称工具) .NET Framework 4.5   强名称工具 (Sn.exe) 有助于使用强名称对程序集进行签名. Sn.exe 提供了用于密钥管理.签名生成和签名验证的选项. 强 ...

  6. ASCII对比表

    ASCII控制字符和ASCII可显示字符 ASCII控制字符 二进制 十进制 十六进制 缩写 能够显示的表示法 名称/意义 0000 0000 0 00 NUL ␀ 空字符(Null) 0000 00 ...

  7. Android DataBinding库(MVVM设计模式)

    什么是MVVM 说到DataBinding,就有必要先提起MVVM设计模式.Model–View–ViewModel(MVVM) 是一个软件架构设计模式,相比MVVM,大家对MVC或MVP可能会更加熟 ...

  8. Drag & drop a button widget

    In the following example, we will demonstrate how to drag & drop a button widget. #!/usr/bin/pyt ...

  9. Matlab interpgui

    function interpgui(arg1,arg2) %INTERPGUI Behavior of interpolating functions. % Demonstrates interpo ...

  10. 双系统 ubuntu装完系统后 丢失原win系统启动项

    sudo update-grub 可以尝试以上命令 注意这里是先装的win 后装的ubuntu