1.背景
有了前面主存的分段的设计原理的背景知识,对于这里提到的分段和分页会更好理解。
X86处理器有三种不同内存地址:
- 逻辑地址
- 线性地址
- 物理地址
X86 地址转换
![](https://img.haomeiwen.com/i9949611/c6909e75889766f1.png)
MMU(内存管理单元)
- 一种硬件,包含分段管理单元电路和分页管理单元电路
- 通过分段管理单元电路,负责将CPU 产生的逻辑地址转换成实际的物理内存地址
- 通过分页管理单元电路,负责将线性地址转换成物理地址。
2.分段的实现
![](https://img.haomeiwen.com/i9949611/55bac0b466eb74de.png)
分段概念
- 将整个进程地址空间划分成单独的逻辑地址空间
代码段,数据段,堆段,栈段
分段的实现
- 虚拟地址:{段号,段内偏移}
- 段的表示
段基地址 + 段长度
检查访问的偏移与段长度,提供保护 - 段描述符表
映射段号到段的基地址和段长度
分段在80x86的实现
- 逻辑地址表示:{段描述符, 段偏移}
-
段标识符(段选择符): 16 bit
image.png
字段 | 位数 | 用途 |
---|---|---|
index | 13 bit | 段选择 |
TI | 1bit | GDT or LDT 指示器 |
RPL | 2bit | 特权级别选择 |
-
段描述符
image.png
![](https://img.haomeiwen.com/i9949611/4b0f8f33a6e8d570.png)
-
段表
1)段描述符要么存在全局描述符表(GDT),要么局部述符表(LDT)
2)存放在内存中的地址寄存器:gdtr or ldtr -
转换过程如图
1)检查段选择符的TI,确定段描述符在哪个描述符表(LDT or GDT)
2)计算段描述符的地址: 据1)确定的位置,计算地址=gdt or ldtr 的值 + index * 8;
3)得到线性地址:线性地址 = 2)确定的段描述符的地址的base + offset;
image.png
-
处于性能考虑
-
段选择符寄存器
1)为了更容易更快速获取段选择符
2)六个:cs, ds, ss, es, fs, gs -
不可编程的寄存器
1)6个寄存器,对应6个段描述符
2)每次fork和上下文切换时,设置段描述符寄存器到对应的不可编程CPU寄存器中,此时逻辑地址转换需要访问GDT或者LDT
3)后续地址转换,逻辑地址转换不需访问GDT或者LDT,直接引用包含段描述符的CPU寄存器的值
image.png
-
段选择符寄存器
3.分段在Linux 中的实现
3.1 段结构
- 2.6 的Linux 只在x86 下才使用分段
-
段描述符
1)所有段地址从0x0 开始,Linux 下,逻辑地址和虚拟地址相同
2)用户进程段选择符,通过__USER_CS、__USER_DS
3)内核进程段选择符,通过__KERNEL_CS、__KERNEL_DS
image.png
- CPL(CPU当前特权级别)
1)由cs的段选择符的RPL 确定
2)当CPL 改变时对于的段寄存器需要更新
如CPL=3(用户 mode)时,ds必须包含用户数据段的段选择符
如CPL=0(内核 mode)时,ds必须包含内核数据段的段选择符
3.2 GDT
- 每个CPU 对应一个GDT
- 所有GDT 存放在cpu_gdt_table里
- 所有的GDT地址和大小被存放在 cpu_gdt_descr数组
- 这些符号定义在 文件arch/i386/kernel/head.S.
DEFINE_PER_CPU(struct desc_struct, cpu_gdt_table[GDT_ENTRIES]);
EXPORT_PER_CPU_SYMBOL(cpu_gdt_table);
void __init cpu_init (void)
{
//省略部分代码
memcpy(&per_cpu(cpu_gdt_table, cpu), cpu_gdt_table,
GDT_SIZE);
cpu_gdt_descr[cpu].size = GDT_SIZE - 1;
cpu_gdt_descr[cpu].address =
(unsigned long)&per_cpu(cpu_gdt_table, cpu);
//省略部分代码
}
-
GDT layout
18个 段描述符
image.png
网友评论