企业建设营销型网站的目的有,wordpress 水印插件,好的装修效果图网站,常用外贸网站写在前面添油加醋系列第二弹——剖析GDT话说C语言的话除了刷刷OJ外#xff0c;就是用来实现操作系统这个大头了。C语言比C少了很多很多臃肿的语法特性#xff0c;写起来非常优美(至少写操作系统是这样的)。虽说C有许多的奇技淫巧#xff0c;一个算法有N种实现方法#xff0…写在前面添油加醋系列第二弹——剖析GDT话说C语言的话除了刷刷OJ外就是用来实现操作系统这个大头了。C语言比C少了很多很多臃肿的语法特性写起来非常优美(至少写操作系统是这样的)。虽说C有许多的奇技淫巧一个算法有N种实现方法但这会让选择恐惧症患者(比如我)难堪比如说一个类要怎样写啊等等抛开其他不谈假如一个语言的语法特性越少学起来可能越简单(刚试过lua语法很简单)。OK废话不多说进入本章主题(涉及OS的资料很杂很偏如有错误望海涵)。GDT的构成首先根据网上资料GDT(全局描述符表)又叫段描述符表暂且就这样认为吧如有异议可以提出来。在代码中它又是这样// 全局描述符表结构 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(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; // 设置保留位为1gdt[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告诉给CPU后CPU就知道了操作系统中段的设置从而可以通过段选择子得到线性地址在后面实现分页管理后可进一步将线性地址转换为物理地址(不过当前连物理 址有多大都没法知道呢在后面会解决)。段选择子段选择子包括三部分描述符索引(index)、TI(指示从GDT还是LDT中找)、请求特权级(RPL)。index部分表示所需要的段的描述符在描述符表的位置由这个位置再根据在GDTR中存储的描述符表基址就可以找到相应的描述符gdt_entry。然后用描述符gdt_entry中的段基址SEL加上逻辑地址OFFSET就可以转换成线性地址SEL:OFFSET(看下面给的例子应该就是它们的和SELOFFSET)段选择子中的TI值只有一位0或10代表选择子是在GDT选择1代表选择子是在LDT选择。请求特权级(RPL)则代表选择子的特权级共有4个特权级(0级、1级、2级、3级)0级最高。关于特权级的说明任务中的每一个段都有一个特定的级别。每当一个程序试图访问某一个段时就将该程序所拥有的特权级与要访问的特权级进行比较以决定能否访问该段。系统约定CPU只能访问同一特权级或级别较低特权级的段。例如给出逻辑地址21h:12345678h需要将其转换为线性地址a. 选择子SEL21h0000000000100 0 01b他代表的意思是选择子的index4即100b选择GDT中的第4个描述符TI0代表选择子是在GDT选择左后的01b代表特权级RPL1(因此有SELn3n是索引号)b. OFFSET12345678h若此时GDT第四个描述符中描述的段基址(Base)为11111111h则线性地址11111111h12345678h23456789h任务状态段TSS任务寄存器(TR)用于寻址一个特殊的任务状态段(Task State SegmentTSS)。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;}// 装载TSSvoid tss_install() {__asm__ volatile(ltr %%ax : : a((SEL_TSS 3)));}// 设置TSSvoid tss_set(uint16_t ss0, uint32_t esp0) {// 清空TSSmemset((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。在任务内发生特权级变换时堆栈也随着自动切换外层堆栈指针保存在内层堆栈中而内层堆栈指针存放在当前任务的TSS中。所以在从外层向内层变换时要访问TSS(从内层向外层转移时不需要访问TSS而只需访问内层栈中保存的栈指针)。LTR指令是专门用于装载任务状态段寄存器TR的指令。该指令的操作数是对应TSS段描述符的选择子。LTR指令从GDT中取出相应的TSS段描述符把TSS段描述符的基地址和界限等信息装入TR的高速缓冲寄存器中。// 任务状态段 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 kernels 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 dont plan to use this io-bitmap further (according to mystran in http://forum.osdev.org/viewtopic.php?t13678)// 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)段寄存器(ESCSSSDSFSGS)状态寄存器(EFLAGS)指令指针(EIP)前一个执行的任务的TSS段的选择子(只有当要返回时才更新)2、 静态字段(处理器读取但从不更改)任务的LDT选择子页目录基址寄存器(PDBR)(当启用分页时只读)内层堆栈指针特权级0-2T-位指示了处理器在任务切换时是否引发一个调试异常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的内容很杂很多所以也只能挑一些重点的讲讲了不可能面面俱到在后面的编写/借鉴中还是要以查资料为主给源码附上参考文章的地址方便阅读。