LOADING

加载过慢请开启缓存 浏览器默认开启

IA-32/Linux内存管理

2024/10/26

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对齐。

总结: