Chinaunix首页 | 论坛 | 博客
  • 博客访问: 152961
  • 博文数量: 50
  • 博客积分: 83
  • 博客等级: 民兵
  • 技术积分: 297
  • 用 户 组: 普通用户
  • 注册时间: 2011-03-12 11:47
文章分类
文章存档

2012年(43)

2011年(7)

分类:

2012-03-17 00:22:13

原文地址:GDT简介(翻译中) 作者:sonald

    在intel架构中,更准确的说是保护模式下,大部分内存管理和中断服务例程都通过描述符表来控制。
每个描述符存储了CPU随时可能需要获取的一个单个对象(例如服务例程、任务、一段代码或数据等)
的信息。如果你试图装载一个数据到一个段寄存器中,CPU需要进行安全性和访问控制检查,来确认你
是否获得了访问该内存区域的许可。一旦检查结束,一些有用的信息(例如最低和最高地址)被缓存
在CPU中的几个不可见的寄存器中。
    intel定义了3种类型的描述符表:中断描述符表(用以替换中断向量表IVT)、全局描述符表和局部描述符表。每个表分别通过LIDT、LGDT、LLDT指令以(size, linear address)的形式定义(注:是载入描述符表的指令,这里就是说表包含了大小和基址组成。)。大多数情况下,操作系统中启动期指定这些表的位置,然后通过一个指针直接读写这些表。
 
基本术语
段      一个逻辑上连续的具有一致的属性的内存块。
段寄存器 一个CPU的段寄存器指向一个具有特殊用途的段(例如SS,CS,DS等)
选择子  指向一个描述符的索引(reference),通过它你可以装载一个段寄存器描述符。
描述符  一个内存结构(某个表的一部分),指明一个给定段的许多属性。
 
Segment          a logically contiguous chunk of memory with consistent properties (CPU's speaking)
Segment Register a register of your CPU that refers to a segment for a special use (e.g. SS, CS, DS ...)
Selector         a reference to a descriptor you can load into a segment register
Descriptor       a memory structure (part of a table) that tells the CPU the attributes of a given segment
 
GDT中应该存放什么?
基础
一个完整的GDT中,总是应该存储如下一些项:
  • 空描述符(不可省略)。在GDT中空描述符是强制的。如果没有该描述符,某些模拟器,例如bochs,会抱怨段限长(limit)的异常。
  • 一个代码段描述符(作为内核段,应该有类型值0x9A)
  • 一个数据段描述符(你不可能向一个代码段写数据,用类型值0x92建立该段)
  • 一个TSS段描述符(最少建立一个这样的描述符是有好处的)
  • 如果需要,为其他段准备更多描述符(比如用户级的数据/代码段、LDTs,更多TSS)

Sysenter/Sysexit

如果你使用intel的SYSENTER/SYSEXIT例程,那么GDT必须是如下的结构:

  • 基础描述符(空描述符、内核代码段/数据段描述符等)
  • 一个DPL为0的代码段描述符(SYSENTER使用)
  • 一个DPL为0的数据段描述符(作为SYSENTER堆栈)
  • 一个DPL为3的代码段描述符(为SYSEXIT之后的代码使用)
  • 一个DPL为3的数据段描述符(作为SYSEXIT之后用户模式的堆栈)
  • 更多描述符

参考Intel Instruction Reference关于SYSENTER and SYSEXIT部分获取更多信息。

Flat Setup

一种对4G内存的简单安排:

GDT[0] = {.base=0, .limit=0, .type=0}; // Selector 0 cannot be used

GDT[1] = {.base=0, .limit=0xffffffff, .type=0x9A}; // Selector 8 will be our code

GDT[2] = {.base=0, .limit=0xffffffff, .type=0x92}; // Selector 0x10 will be our data

GDT[3] = {.base=&myTss, .limit=sizeof(myTss), .type=0x89};

在此模式下,因为代码段和数据段重叠,代码还是有被覆写的危险。

Small Kernel Setup

由于某种原因,你想使代码和数据清晰的独立开,可以这样设置:

GDT[0] = {.base=0, .limit=0, .type=0}; // Selector 0 cannot be used

GDT[1] = {.base=0x04000000, .limit=0x03ffffff, .type=0x9A}; // Selector 8 will be our code

GDT[2] = {.base=0x08000000, .limit=0x03ffffff, .type=0x92}; // Selector 0x10 will be our data

GDT[3] = {.base=&myTss, .limit=sizeof(myTss), .type=0x89}; // You can use LTR(0x18)

That means whatever you load at physical address 4 MiB will appear as code at CS:0 and what you load at physical address 8 MiB will appear as data at DS:0. However it might not be the best design.

我们该如何做?

Disable interrupts

如果中断处于打开状态,则禁止它们。

Filling the table

你也许注意到我并没有给出一个GDT的真实结构,这样做是有原因的。由于世界的描述符结构很复杂,基址被拆成3个不同的域(field),并且你不能为限长(limit)指定随意数值。而且如果想一切正常工作,必须为许多标志设置正确的值。

encodeGdtEntry(unsigned char target[8], struct GDT source)
{
    if ((source.limit > 65536) && (source.limit & 0xFFF) != 0xFFF)) {
        kerror("You can't do that!");
    }
    if (source.limit > 65536) {
        // Adjust granularity if required

        source.limit = source.limit >> 12;
        target[6] = 0xC0;
    } else {
        target[6] = 0x40;
    }
    
    target[0] = source.limit & 0xFF;
    target[1] = (source.limit >> 8) & 0xFF;
    target[6] |= (source.limit >> 16) & 0xF;
    
    target[2] = source.base & 0xFF;
    target[3] = (source.base >> 8) & 0xFF;
    target[4] = (source.base >> 16) & 0xFF;
    target[7] = (source.base >> 24) & 0xFF;
    
    target[5] = source.type;
}

Okay, that's rather ugly, but it's the most 'for dummies' i can come with ... hope you know about masking and shifting. You can hard-code that rather than convert it at runtime, of course. It assumes you want 32 bits stuff, too, and it's probably not valid for gates and other things i didn't talk about.

Telling the CPU where the table stands

Some assembly example is required here. While you could use inline assembly, the memory packing expected by LGDT and LIDT makes it much easier to write a small assembly routine instead. As said above, you'll use LGDT instruction to load the base address and the limit of the GDT. Since the base address should be a linear address, you'll need a bit of tweaking depending of your current MMU setup.

From real mode

The linear address should here be computed as segment * 16 + offset. I'll assume GDT and GDT_end are symbols in the current data segment.

gdtr DW 0 ; For limit storage
     DD 0 ; For base storage

setGdt:
   XOR EAX, EAX
   MOV AX, DS
   SHL EAX, 4
   ADD EAX, GDT
   MOV [gdtr + 2], eax
   MOV EAX, GDT_end
   SUB EAX, GDT
   MOV [gdtr], AX
   LGDT [gdtr]
   RET

From flat, protected mode

"Flat" meaning the base of your data segment is 0 (regardless of whether paging is on or off). This is the case if you're just been booted by GRUB, for instance. I'll assume you call setGdt(GDT, sizeof(GDT)).

 

DD 0 ; For base storage

setGdt:
   MOV EAX, [esp + 4]
   MOV [gdtr + 2], EAX
   MOV AX, [ESP + 8]
   MOV [gdtr], AX
   LGDT [gdtr]
   RET

 

From non-flat protected mode

If your data segment has a non-zero base (e.g. you're using a HigherHalfKernel during the segmentation trick), you'll have to "ADD EAX, base_of_your_data_segment_which_you_should_know" between the "MOV EAX, ..." and the "MOV ..., EAX" instructions of the sequence above.

Reload segment registers

Whatever you do with the GDT has no effect on the CPU until you load selectors into segment registers. You can do this using

 

reloadSegments:
   ; Reload CS register containing code selector:
   JMP 0x08:reload_CS ; 0x08 points at the new code selector
.reload_CS:
   ; Reload data segment registers:
   MOV AX, 0x10 ; 0x10 points at the new data selector
   MOV DS, AX
   MOV ES, AX
   MOV FS, AX
   MOV GS, AX
   MOV SS, AX
   RET

 

What's so special about the LDT?

Much like the GDT (global descriptor table), the LDT (local descriptor table) contains descriptors for memory segments description, call gates, etc. The good thing with the LDT is that each task can have its own LDT and that the processor will automatically switch to the right LDT when you use hardware task switching.

Since its content may be different in each task, the LDT is not a suitable place to put system stuff such as TSS or other LDT descriptors: Those are the sole property of the GDT. Since it is meant to change often, the command used for loading an LDT is a bit different from the GDT and IDT loading. Rather than giving directly the LDT's base address and size, those parameters are stored in a descriptor of the GDT (with proper "LDT" type) and the selector of that entry is given.

               GDTR (base + limit)
              +-- GDT ------------+
              |                   |
SELECTOR ---> [LDT descriptor     ]----> LDTR (base + limit)
              |                   |     +-- LDT ------------+
              |                   |     |                   |
             ...                 ...   ...                 ...
              +-------------------+     +-------------------+

Note that with 386+ processors, the paging has made LDT almost obsolete, and there's no longer need for multiple LDT descriptors, so you can almost safely ignore the LDT for OS developing, unless you have by design many different segments to store.

What is the IDT and is it needed?

As said above, the IDT (Interrupt Descriptor Table) loads much the same way as the GDT and its structure is roughly the same except that it only contains gates and not segments. Each gate gives a full reference to a piece of code (code segment, priviledge level and offset to the code in that segment) that is now bound to a number between 0 and 255 (the slot in the IDT).

The IDT will be one of the first things to be enabled in your kernel sequence, so that you can catch hardware exceptions, listen to external events, etc. See Interrupts for dummies for more information about the interrupts of X86 family.

阅读(1177) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~