IA-32/Linux中的存储管理
存储地址采用逻辑地址、线性地址和物理地址来进行描述,其中,逻辑地址和线性地址是虚拟地址的两种不同表示形式,描述的都是4GB虚拟地址空间中的一个存储地址
- 逻辑地址由48位组成,包含16位段选择符和32位段内偏移量(即有效地址)
- 线性地址32位(其位数由虚拟地址空间大小决定)
- 物理地址32位(其位数由存储器总线中的地址线条数决定)
分段过程实现将逻辑地址转换为线性地址(早期的,现在兼容,但是不用)
分页过程实现将线性地址转换为物理地址
逻辑地址到线性地址的变换
最早的处理器——8086
而,8086这种早期的处理器,寄存器都是16位的。
这些寄存器支持的寻址方式:
A:地址段偏移量
B:基址寄存器
I:变址寄存器(除SP)
S:比例因子
EA:有效地址
| 寻址方式 | 说明 |
|---|---|
| 位移 | EA = A |
| 基址寻址 | EA = (B) |
| 基址加位移 | EA = (B) + A |
| 比例变址加位移 | EA = (I) * S + A |
| 基址加变址加位移 | EA = (B) + (I) + A |
| 基址加比例变址加位移 | EA = (B) + (I) * S + A |
此时,支持的寻址空间为2^16,共64K。
这是非常小的,所以要想一个办法,在16位的情况下,扩充地址空间
实模式
引入段寄存器开辟更大的寻址空间
在现在的内存中,都可以将一个物理内存看成一个数组,以这种方式组织内存
但是,在最早的处理器——8086时期,科技人员却不是这样设计内存的
在物理内存中开辟4片空间
- [0…N-1],存储全部的代码
- [0…N-1],存储所有的栈
- 存储所有的数据(变量等)
- 存储其他数据,如字符串常量等
他们分别属于不同的地址空间,称为“段”
这是一种二维的地址空间
其中cpu中有一些段寄存器存储的就是这些地址的基地址,然后如果要访问对应段的内容,只需要一个偏移量即可。
其中,CS为代码段寄存器,SS为栈寄存器,DS为数据段寄存器,ES为其他寄存器(16位段寄存器)
物理访存地址 = (段寄存器 << 4) + 有效地址
| 寻址方式 | 说明 |
|---|---|
| 位移 | LA = (SR<<4) + A |
| 基址寻址 | LA = (SR<<4) + (B) |
| 基址加位移 | LA = (SR<<4) + (B) + A |
| 比例变址加位移 | LA = (SR<<4) + (I) * S + A |
| 基址加变址加位移 | LA = (SR<<4) + (B) + (I) + A |
| 基址加比例变址加位移 | LA = (SR<<4) + (B) + (I) * S + A |
寻址空间变为:2^20 = 1MB,此时物理地址称为线性地址
1MB在当时,足够了。
当然,这种分段机制仅限很早期的cpu,因为16位的寄存器已经变成了历史,而32位的寄存器已经差不多够了,更何况现在都是64位,这就非常足够了。
在这种分段模式下工作,叫“实模式”
在现在的虚拟地址模式下工作,叫“保护模式”
不过现在的处理器依然兼容实模式。
IA—32
IA-32,既80386已经使用了32位寄存器,既32位的计算机。
可是,虽然32位的寻址空间达到了4GB但是依然要保留段寄存器,为了兼容。(不支持兼容的产品注定会被市场淘汰)
因此IA-32支持两种工作模式
实模式:IA-32处理器加电或复位时处于这一模式,此时相当于8086/8088处理器,32位地址线中的A31~A20不起作用,所有访存地址都是物理地址(实地址)。上电后,首先执行bios的代码,此时,就是实模式。
保护模式:完成系统初始化后,进入该模式,此时32位地址线全部起作用,访存地址为逻辑地址(虚拟地址),进入虚拟存储器管理方式。
这两种工作模式切换的“开关”,在CR0寄存器里面的一个二进制位

计算机加电或复位时,PE = 0,IA-32处理器处于实模式。当PE = 1时,IA-32处理器进入保护模式,而且不能切换回实模式
进入保护模式之后,如何利用这些段寄存器?
IA-32分段机制
保护模式下,段寄存器的使用:
进入IA-32时代,我们希望段地址也是32位的,还可以灵活设置各种段属性,但段寄存器只有16位,连32位段基址也放不下,怎么办?
采用一个数据结构:段描述符,用来描述一个段所有属性的数据结构,每个段对应一个段描述符
段描述符
这是一个8字节的数据结构
段基址:在线性地址下的基地址(共32位)
限界:段的总长度(共16位)
DPL:表示一个段所在的特权级别(后面介绍)
段基址 + 限界 = 描述符的最后一个位置
段描述符占64位,段寄存器根本放不下,怎么办?——放到主存中
怎么在主存中找到一个描述符?——利用指针
IA-32中指针是32位的,段寄存器还是放不下即使能放下,如果想切换到其他段的时候,如何知道描述符在什么地方?
——把内存中的某一连续空间解释成一个数组,称为段描述符表,数组中每个元素对应一个段描述符,段寄存器存放数组下标,段描述符表基地址用另一个寄存器
段描述符表
简称段表,由OS负责填写,包括三种类型:
- 全局描述符表(GDT):只有一个,用来保存系统中每个任务都可以访问的段描述符,如内核代码段、数据段,用户代码段、数据段等
- 局部描述符表(LDT):存放某一用户进程专用的描述符,保存在GDT中(Linux已经不用了,因为有分页)
- 中断描述符表(IDT):独立于GDT的段表,包含中断门、陷阱门等描述符(写NEMU不用管)
GDT的首地址由全局描述符表寄存器(GDTR)提供
通过索引(数组下标)可找到所需的描述符,该索引保存在段寄存器中,称为段选择符
全局描述符表寄存器

GDTR由基地址和限界两部分组成,共48位
GDTR中保存的首地址是线性地址,为什么不是逻辑地址?(因为要翻译成线性地址,否则就循环了)
GDTR对用户进程不可见,仅可由OS内核通过一条特权指令(lgdt m16&32)将GDT的首地址和限界装载到GDTR中,启动分段机制
段选择符和段寄存器
段寄存器(16位),用于存放段选择符,其绑定规则:
- CS(代码段):程序代码所在段
- SS(栈段):栈区所在段
- DS(数据段):全局静态数据区所在段
- 其他3个段寄存器ES、GS和FS可指向任意数据段
段选择符各字段含义:

TI=0,选择全局描述符表(GDT),TI=1,选择局部描述符表(LDT)
RPL表示请求特权级(后面介绍)
高13位索引用来确定当前使用的段描述符在描述表中的位置
GDT最多可容纳多少段描述符?2^13 = 8192 = 8K
可见性
虚线框内的对用户不可见,仅可通过OS内核调用特权指令对GDTR,LDTR和IDTR进行读写
地址翻译
TI=0
1.通过段寄存器中的段选择符TI位决定在哪个表中查找。
2.根据GDTR读出段描述符表的首地址。
3.根据段寄存器中的段选择符index位在表中进行索引,找到一个段描述符。
4.在段描述符中读出段的基地址,和逻辑地址相加,得到线性地址。
在计算线性地址的过程中,可根据段描述符中的限界和访问权限判断是否“地址越界”或“访问越权”,以实现存储保护
TI=1
局部描述符表存在全局描述符表中
总结:
逻辑地址到线性地址的变换
在CR0的PE位为“1”时,进入保护模式,启动分段机制;
通过段寄存器中段选择符的TI位决定在哪个段表中查找;
通过GDTR读出段表的首地址;
根据段寄存器中段选择符的索引,找到一个段描述符;
在段描述符中读出段基址,和有效地址相加得到线性地址
NEMU操作系统中设置GDT并启动分段
#define GDT_ENTRY(n) ((n) << 3)
#define MAKE_NULL_SEG_DESC \
.word 0, 0; \
.byte 0, 0, 0, 0
#define MAKE_SEG_DESC(type,base,lim) \
.word (((lim) >> 12) & 0xffff), ((base) & 0xffff); \
.byte (((base) >> 16) & 0xff), (0x90 | (type)), \
(0xC0 | (((lim) >> 28) & 0xf)), (((base) >> 24) & 0xff)
.globl start
start:
lgdt va_to_pa(gdtdesc)
movl %cr0, %eax
orl $0x1, %eax #或运算,将cr0的最低位置一,开启保护模式
movl %eax, %cr0
.p2align 2
gdt:
MAKE_NULL_SEG_DESC # empty segment
MAKE_SEG_DESC(0xA, 0x0, 0xffffffff) # code
MAKE_SEG_DESC(0x2, 0x0, 0xffffffff) # data
gdtdesc:
.word (gdtdesc - gdt - 1) # limit = sizeof(gdt) - 1
.long va_to_pa(gdt) # address of GDT
上面是用汇编语言写的。
其中,gdt里面在一个物理空间里面将code和data段重叠了(基地址设为0,限界设为最大值),所以偏移量就是实际地址。
而LINUX就是这么实现的,因为它认为段已经没有用了。而且此时描述符cache也不会进行替换,性能可以认为没有损失。这种模式叫做“扁平模式”
存储保护
环保护机制
内核工作在“0”环
用户工作在“3”环
其它环留给中间件
特权级由高到低:0~3
内环可以访问外环,但外环不能进入内环
大多数操作系统,如Linux仅用第0环和第3环
在80386中,存在0~3四个特权级别,0特权级最高,3特权级最低。特权级n所能访问的资源,在特权级0~n也能访问。不同特权级之前的关系就形成了一个环
权限检查(基于环保护机制)
DPL:位于段描述符中,表示一个段所在的特权级别。
RPL:位于段选择符中,表示请求者所在的特权级别。
CPL:表示当前进程的特权级别,一般与CS寄存器指向的段描述符的DPL 字段相同。
1.假如你到银行找工作人员办理取款业务,你就相当于“请求者”,你的账户相当于“目标段”,工作人员相当于“当前进程”。业务办理成功是因为:
a. 你有权访问自己的账户(target_descriptor.DPL >= requestor.RPL)
b. 工作人员也有权对你的账户进行操作(target_descriptor.DPL >= CPL)
2.如果你想从别人的账户取钱,虽然工作人员有权访问别人的账户(target_descriptor.DPL >= CPL),但你却没有权利访问(target_descriptor.DPL < requestor.RPL),业务办理失败
3.如果你打算亲自操作银行系统来取款,虽然账户是你的(target_descriptor.DPL >= requestor.RPL),但你却没有权限直接对你的账户金额进行操作(target_descriptor.DPL < CPL)这样你会被保安抓起来。
计算机中也有类似的情况:用户进程(请求者)想对自己拥有的数据(段描述符所描述的段)进行一些没有权限的操作(比如,输入),它就要请求有权的进程(通常是操作系统)来完成这个操作,于是就出现了“内核代表用户进程进行操作”的场景。当然,在真正进行操作前,也要检查这些数据是不是真的是用户进程有权使用的数据。
const char str[] = "Hello, world!\n";
int main() {
asm volatile ( "movl $4, %eax;" // system call ID, 4 = SYS_write
"movl $1, %ebx;" // file descriptor, 1 = stdout
"movl $str, %ecx;" // buffer address
"movl $14, %edx;" // length
"int $0x80");
return 0;
}
通常,内核运行在ring 0, CPL = 0
用户进程运行在ring 3,CPL = 3
只要操作系统将GDT,页表等重要信息放在ring 0段中,恶意程序永远无法篡改它们(除非恶意程序获得操作系统权限)
IA-32中的环保护和特权级可有效辨别非法操作,让恶意程序无所遁形,维护计算机的“和谐社会”
线性地址到物理地址的变换
IA-32中的控制寄存器
保存机器的各种控制和状态信息。
操作系统进行任务控制或存储管理时使用控制寄存器
CR0:控制寄存器(开启分页机制的开关)
① PE: 1为保护模式。一旦在保护模式,不能再将PE清0,只能重启系统以回到实模式。
② PG:1-启用分页;0-禁止分页,此时线性地址被直接作为物理地址使用。若启用分页机制,则PE和PG都要置1
CR2:页故障(page fault)线性地址寄存器
存放引起页故障的线性地址。只有在CR0中的PG=1时,CR2才有效
CR3:页目录基址寄存器
保存页目录表的起始地址。只有当CR0中的PG=1时,CR3才有效。
Linux中线性地址空间划分
4GB=1K个子空间 × 1K个页面/子空间 × 4KB/页
采用两级页表

线性地址由3个字段组成,分别是10位页目录索引,10位页表索引,12位页内偏移
根据控制寄存器CR3中给出的页目录表首地址找到页目录表
由DIR字段提供的10位页目录索引找到对应的页目录项,每个页目录项大小为4B
根据页目录项中20位基地址指出页表首地址找到对应的页表,在根据线性地址中间的页表索引(PAGE字段)找到页表中的页表项
将页表项中的20位基地址和线性地址中的12位页内偏移组合成32位物理地址。
在这个转换过程中,页目录索引和页表索引都要乘以4,因为每个页目录项和页表项都是32位,占4个字节。
页目录项和页表项的格式

P:1表示页表或页在主存中;P=0表示页表或页不在主存,即缺页,此时需将页故障线性地址保存到CR2。
R/W:0表示页表或页只能读不能写;1表示可读可写。
U/S:0表示用户进程不能访问;1表示允许访问。
PWT:控制页表或页的cache写策略是写直通还是回写(Write Back)。
PCD:控制页表或页能否被缓存到cache中。
A:1表示指定页表或页被访问过,初始化时OS将其清0。利用该标志,OS可清楚了解哪些页表或页正在使用,一般选择长期未用的页或近来最少使用的页调出主存。由MMU在进行地址转换时将该位置1。
D:修改位(脏位dirty bit)。页目录项中无意义,只在页表项中有意义。初始化时OS将其清0,由MMU在进行写操作的地址转换时将该位置1。
高20位是页表或页在主存中的首地址对应的页框号,即首地址的高20位。每个页表的起始位置都按4KB对齐。
总结:
