一 计算机实模式和保护模式
实模式
在实模式下,内存被限制为仅有1M字节(220 字节)。有效的地址从00000到FFFFF (十六进制)。
这些地址需要用20位的数来表示。一个20位的数不适合任何一个8086的16位寄存器。
Intel通过利用两个16位数值来决定一个地址的方法来解决这个问题。开始的16位值称为段地址(selector)。
段地址的值必须存储在段寄存器中。第二个16位值称为偏移地址(offset)。
16位保护模式
在实模式下,一个段地址的值是物理内存里的一节的首地址。在保护模式下,一个段地址的值是一个指向描述符表的指针
。两种模式下,程序都是被分成段。在实模式下,这些段在物理内存的固定位置而且段地址的值表示段开始处所在节的首
地址。在保护模式下,这些段不是在物理内存的固定的地址。事实上,它们不一定需要在内存中。
保护模式使用了一种叫做虚拟内存的技术。虚拟内存的基本思想是仅仅保存程序现在正在使用的代码和数据到内存中。其
它数据和代码暂时储存在硬盘中直到它们再次需要时。
在保护模式下,每一段都分配了一条描述符表里的条目。这个条目拥有系统想知道的关于这段的所有信息。这些信息包括
:现在是否在内存中;如果在内存中,在哪;访问权限(例如: 只读)。段的条目的指针是储存在段寄存器里的段地址值
。
32位保护模式
80386引入了32位保护模式。
386 32位保护模式和286 16位保护模式之间最主要的区别是:
1 偏移地址扩展成了32位。这就允许偏移地址范围升至4G。因此,段的大小也升至4G。
2 段可以分成较小的4K大小的单元,称为内存页。虚拟内存系统工作在页的方式下,代替了段方式。这就意味着一段
在任何一个时刻只有部分可能在内存中。在28616位保护模式下,要么整个段在内存中,要么整个不在。
在Windows 3.x系统中,标准模式为286 16位保护模式而增强模式为32位保护模式。Windows 9X,Windows
NT/2000/XP,OS/2和Linux都运行在分页管理的32位保护模式下。
二 全局描述符表GDT
GDT,Global Descriptor Table。
IA32允许将一个段的基地址设置为32bit所能表示的任何值,limit(段大小)则可以设置成以2^12为倍数的任何值。
在保护模式下,对一个段的描述包括以下三个方面:[Base Address,Limit,Access],他们加在一起被放在一个64bit
长的数据结构中,被成为段描述符。
在这种情况下,如果我们直接通过一个64bit段描述符来引用一个段的时候,就必须使用一个64bit长的段寄存器装入
这个段描述符。
把这些长度为64bit的段描述符放入一个数组中,而将段寄存器中的值作为下标索引来间接引用(事实上就是将段寄
存器中的高13bit的内容作为索引)。这个全局数据就是GDT。
三 Linux中GDT和IDT的结构定义
GDT,IDT都是全局的。LDT是局部的(在GDT中有它的描述符);
GDT用来存储描述符(门或非门);系统中几个CPU,就有几个GDT
struct gdt_page {
struct desc_struct gdt[GDT_ENTRIES];
} __attribute__((aligned(PAGE_SIZE)));
DECLARE_PER_CPU_PAGE_ALIGNED(struct gdt_page, gdt_page);
IDT整个系统只有一个;
系统启动时候需要初始化GDT和IDT;
描述符结构定义,在<arch/x86/include/asm/desc_defs.h>
struct desc_struct {
union {
struct {
unsigned int a;
unsigned int b;
};
struct {
u16 limit0;
u16 base0;
unsigned base1: 8, type: 4, s: 1, dpl: 2, p: 1;
unsigned limit: 4, avl: 1, l: 1, d: 1, g: 1, base2: 8;
};
};
} __attribute__((packed));
上面第一个匿名结构体用来作为成员访问取值的出口,下面第二个结构体对真实的成员设置值的入口。
字段:
limit:段长度
base:段的首字节的线性地址,有base0,base1,base2三部分构成
type:段的类型和存取权限
s:系统标志。1-系统段;0-普通段
dpl:描述符特权级
p:segment-Present。linux下总是1
avl:linux不用
d:区分代码段还是数据段
g:段大小粒度。以4K倍数计算
在32位机器上,这就是所有描述符的数据结构;
typedef struct desc_struct gate_desc;
typedef struct desc_struct ldt_desc;
typedef struct desc_struct tss_desc;
由于三类描述符都是一个结构类型,从而一律使用下面宏初始化在GDT中表项
#define GDT_ENTRY_INIT(flags, base, limit) { { { \
.a = ((limit) & 0xffff) | (((base) & 0xffff) << 16), \
.b = (((base) & 0xff0000) >> 16) | (((flags) & 0xf0ff) << 8) | \
((limit) & 0xf0000) | ((base) & 0xff000000), \
} } }
但是在64位机器上,Linux则进行了细致划分;
无论是32位还是64位机器上,都使用typedef重新定义,以提供给系统其他使用此描述符的部分一致的类型名;
区分描述符的枚举量
enum {
GATE_INTERRUPT = 0xE,
GATE_TRAP = 0xF,
GATE_CALL = 0xC,
GATE_TASK = 0x5,
};
enum {
DESC_TSS = 0x9,
DESC_LDT = 0x2,
DESCTYPE_S = 0x10, /* !system */
};
系统GDT,IDT指针描述结构
struct desc_ptr {
unsigned short size;
unsigned long address;
} __attribute__((packed)) ;
这个结构记录了系统的GDT或者IDT的大小以及在系统中的线性基址;
Reference:
<arch/x86/include/asm/desc_defs.h>
四 如何通过段描述符访问内存
当我们要访问某个段中的一个地址时候:
1 从GDTR中拿到GDT在内存中的基地址,得到段描述符表;
2 从段选择子中的前13位得到我们要访问的段的描述符在段描述符表中的索引;
3 从段描述符表中得到要访问的段的描述符,得到其基地址;
4 基地址加上偏移地址就是我们要访问的内存地址(这里是虚拟地址,接下来是分页机制的功能将虚地址转换为物理
地址,)
看内核相关源码涉及到 AT&T汇编,和Intel汇编语法不同;先学部分AT&T汇编语法;
操作数排列是从源(左)到目的(右),如"movl %eax(源), %ebx(目的)";
符号常数直接引用 如:
value: .long 0x12a3f2de
movl value , %ebx
会见到如下的指令:push,pushl,pushfl;
操作数的长度用加在指令后的符号表示b(byte, 8-bit), w(word, 16-bits), l(long, 32-bits);
如
"movb %al, %bl",
"movw %ax, %bx",
"movl %eax, %ebx",
目前还不知道pushfl是啥;
如果没有指定操作数长度的话,编译器将按照目标操作数的长度来设置。
参阅
https://blog.csdn.net/cwcmcw/article/details/21640363
http://blog.chinaunix.net/uid-29113598-id-5210949.html