从零开始的操作系统生活04-GDT

本节代码地址github

1、从实模式到保护模式

实模式(Real mode)是x86处理器中的一种16位的寻址方式,但是x86处理器的物理地址有20位,为了实现20位的寻址,x86处理器采用了基地址(base)+偏移量(offset)的方法进行寻址,具体的寻址方式 base<< 4 + offset,寻址空间有$$2^{20}$$一共1MB的地址空间,这对于早期的处理器来说足够使用了,但随着技术的发展,逐渐出现了两个问题:

  • 地址空间逐渐不足
  • 非法访问内存问题

地址空间不足问题很好理解,程序主键变得复杂,需要的空间会逐渐增加。对于非法访问内存,实模式下的内存访问方式可以访问任何一块内存,但是实际上很多内容比如内核代码和数据等是不允许访问或者修改的,所以就需要对内存进行规划,于是就出现了保护模式(Protected Mode)

保护模式采用32位的寻址方式,理论的寻址空间有4GB,这样就可以解决地址空间不足的问题。对于内存非法访问的问题,保护模式将内存划分为不同的段(Segment),比如代码段、数据段、栈段等等,不同的段有不同的访问权限。保护模式下的寻址方式依旧采用基地址+偏移的方式,不过基地址的获取改成了通过GDT来获取,通过GDT就可以对内存访问进行限制,完美的解决非法访问内存问题。

2、访问GDT

Global Descriptor Table,简称GDT,中文名为全局描述符表。

首先,GDT是一项数据结构,从名称table就可以知道GDT是一张表,表中肯定存储很多数据,每条数据就是一个描述符(descriptor)。

然后,这张表肯定要存储到某块内存,如何寻址到这块内存,就需要访问GDTR,R是register的意思,这个寄存器专门存储GDT表的内存地址以便访问。我们可以使用汇编语句lgdt对GDTR进行加载。

GDTR

GDTR是一个48位的数据结构,高32位存储GDT的地址,低16位存储表的尺寸,记录了表的存储项,同时也规定了最大的表的数量

其次,具体的访问方式。操作系统中指定下一条指令的寄存器为cs+eip,cs存储基地址,eip存储偏移。cs的访问方式转换为段选择子(Segment Selector)的访问方式:

SegmentSelector

cs是16位的寄存器,最低的两位RPL,全称是Requested Privilege Level,表示访问权限,用两位数据表示说明最多有4个访问权限。

接下来的TI位表示需要访问GDT还是LDT,Index表示需要访问GDT的表项,即GDT中的第Index项。这样,通过访问GDT得到基地址即可。

3、柳暗花明,终于开始访问GDT了

GDT中的表项叫做段描述符(Segment Descriptor),我们所访问的内容就是第Index个描述符,下面是描述符的数据结构:

SegmentDescriptor

段描述符是一个64位的数据结构,从具体的表项可以看出,我们可以拼出一个32位的基地址Base,这样就可以与偏移Offset结合得到访问地址。

其他的表项内容请大家自行学习,而且是必须学会的,本人也在学习过程中,所以这里不做展开。

4、串联一下

首先我们将GDT表填好并放在某块内存,通过lgdt指令设定GDTR,以便访问GDT。

设定好cs+eip寄存器的内容,cs通过段选择子(Segment Selector)找出需要访问GDT的哪一项。

然后我们根据GDTR找到相对应的段描述符(Segment Descriptor),通过描述符的内容可以找出基地址(BASE)

这里面基地址直接与偏移offset相加即可。

这样我们就能跳转到下一条指令了。

5、该写代码了

新建文件gdt.hgdt.cpptypes.h

首先是types.h,这里面主要将一些常用的数据结构设定为操作系统中常用的形式,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#ifndef __TYPES_H
#define __TYPES_H

typedef char int8_t;
typedef unsigned char uint8_t;

typedef short int16_t;
typedef unsigned short uint16_t;

typedef int int32_t;
typedef unsigned int uint32_t;

typedef long long int int64_t;
typedef unsigned long long int uint64_t;

#endif

然后是gdt.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#ifndef __GDT_H
#define __GDT_H

#include "types.h"
class GlobalDescriptorTable{
public:
class SegementDescriptor{
public:
SegementDescriptor(uint32_t base, uint32_t limit, uint8_t type);

uint32_t Base();

uint32_t Limit();

private:
uint16_t limit_lo;
uint16_t base_lo;
uint8_t base_hi;
uint8_t type;
uint8_t flags_limit_hi;
uint8_t base_vhi;
} __attribute__((packed));

SegementDescriptor nullSegmentDescriptor;
SegementDescriptor unusedSegmentDescriptor;
SegementDescriptor codeSegmentDescriptor;
SegementDescriptor dataSegmentDescriptor;

public:
GlobalDescriptorTable();
~GlobalDescriptorTable();

uint16_t CodeSegmentSelector();
uint16_t DataSegmentSelector();
};


#endif

GDT的基地址、limit以及type数据外部数据可能会访问,所以设定为public

其他的位的数据则设定位private,__attribute__((packed))作用是告诉编译器取消结构在编译过程中的优化对齐,内存对齐参考

其他的还有一些空描述符(GDT的第0项就是空描述符),不可用描述符(暂时未接触到),数据段描述符、代码段描述符(这两部分可以参考S位以及type位的类型,就可以知道不同的段代表不同的内容)。

接下来使用gdt.cpp进行实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include "gdt.h"

GlobalDescriptorTable::GlobalDescriptorTable()
: nullSegmentDescriptor(0, 0, 0),
unusedSegmentDescriptor(0, 0, 0),
codeSegmentDescriptor(0, 64 * 1024 * 1024, 0x9a),
dataSegmentDescriptor(0, 64 * 1024 * 1024, 0x92){
uint32_t i[2];
i[1] = (uint32_t)this;
i[0] = sizeof(GlobalDescriptorTable) << 16;
asm volatile("lgdt (%0)": :"p" (((uint8_t *)i) + 2));
}

GlobalDescriptorTable::~GlobalDescriptorTable() {}

uint16_t GlobalDescriptorTable::DataSegmentDescriptor() {
return (uint8_t*)&dataSegmentDescriptor - (uint8_t*)this;
}

uint16_t GlobalDescriptorTable::CodeSegmentDescriptor(){
return (uint8_t*)&codeSegmentDescriptor - (uint8_t*)this;
}

GlobalDescriptorTable::SegementDescriptor::SegementDescriptor(uint32_t base, uint32_t limit, uint8_t type){
uint8_t* target = (uint8_t*)this;

if(limit < 65536){
target[6] = 0x40;
}else{
if((limit & 0xfff) != 0xfff){
limit = (limit >> 12) - 1;
}else{
limit = limit >> 12;
}
target[6] = 0xC0;
}

//limit
target[0] = limit & 0xff;
target[1] = (limit >> 8) & 0xff;
target[6] = (limit >> 16) & 0xf;

//base
target[2] = base & 0xff;
target[3] = (base >> 8) & 0xff;
target[4] = (base >> 16) & 0xff;
target[7] = (base >> 24) & 0xff;

//type
target[5] = type;
}

uint32_t GlobalDescriptorTable::SegementDescriptor::Base(){
uint8_t* target = (uint8_t*)this;
uint32_t result = target[7];
result = (result << 8) + target[4];
result = (result << 8) + target[3];
result = (result << 8) + target[2];
return result;
}

uint32_t GlobalDescriptorTable::SegementDescriptor::Limit(){
uint8_t* target = (uint8_t*)this;
uint32_t result = target[6] & 0xf;
result = (result << 8) + target[1];
result = (result << 8) + target[0];

if((target[6] & 0xC0) == 0xC0){
result = (result << 12) | 0xfff;
}

return result;
}

可以看到,下面的部分是在设定GDTR,GDTR前32位表示GDT的基地址,后16位代表GDT中表项的内容

1
2
3
4
uint32_t i[2];
i[1] = (uint32_t)this;
i[0] = sizeof(GlobalDescriptorTable) << 16;
asm volatile("lgdt (%0)": :"p" (((uint8_t *)i) + 2));

asm是cpp中使用汇编语句,volatile是防止汇编语句被编译器优化掉,内嵌汇编的具体格式为:

(“指令列表”:”输出列表”:”输入列表”:”破坏描述部分”);注意,括号中的冒号缺一不可,没有输入或者输出可以空着

%0 %1代表出现的第一个第二个寄存器名,p表示下面的操作数是一个合法的地址。

下面的函数都是通过GDT表的具体表项,将需要的数据拼接出来,比如Base()函数就是将段描述符中的三部分拼接到一起得到的。

其他的内容也是如此,段描述符函数SegementDescriptor需要参考段描述符每一位代表的内容,所以需要进行判断是否大于65536等内容。

6、还有Makefile

1
objects = loader.o kernel.o gdt.o

在objects后面增加gdt.o即可,然后进行编译即可、

7、运行结果

由于我们只是将GDT建立起来而没有使用,所以运行结果不会有任何区别,只要不报错就算成功!

8、补充

从实模式到保护模式是操作系统中非常精彩的内容,请大家多查阅一些资料学习一下!


从零开始的操作系统生活04-GDT
http://example.com/2022/09/26/wyoos004/
作者
Anhongzhan
发布于
2022年9月26日
许可协议