IA-32/Linux下逻辑地址-线性地址-物理地址
CPU内部的存储器就是寄存器
分层存储器存在的意义:让存储器又快又大
存储器越靠近CPU速度越大,容量越小
局部性原理达到分层的效果
英特尔32位架构(英语:Intel Architecture, 32-bit,缩写为IA-32),常被称为i386、或x86,由英特尔公司于1985年推出的指令集架构。它是8086架构的延伸版本,可支持32位运算,首次应用在Intel 80386芯片中。
IA-32架构属于复杂指令集,由英特尔公司开发,1985年,随着Intel 80386的上市,被公之于世。接下来20年的时间,虽然后继的新型芯片运算速度不断增加,但IA-32架构大体上都没有改变。对许多编程语言来说,IA-32与i386是同义词。
英特尔也是世界上最大的IA-32芯片供应商,AMD则是第二大的供应商。2011年,英特尔与AMD同时采用了新的x86-64架构,但是x86架构仍然被应用在如Intel Atom(N2xx与Z5xx系列)、AMD Geode等芯片上。威盛电子生产的VIA C3/C7,也仍然采用IA-32架构。
IA-32微处理器支持实模式和保护模式。
实模式
相当于高性能的16位8086微处理器,但进行了功能扩充,能够使用8086所没有的寻址方式和32位通用寄存器以及大部分指令。不具有保护机制,不能使用部分特权指令。实模式下只有20条地址线有效,存储空间为1MB。
保护模式
充分发挥IA-32微处理器的存储管理功能和硬件支持的保护机制,为多任务操作系统设计提供支持。该模式下每个任务的存储空间为4GB。
在保护模式下还具有一种子模式——虚拟8086模式(V86模式),可以在保护模式的多任务环境中以类似实模式的方式运行16位8086软件。
按字节编址(通用计算机大都是)
在保护模式下,IA-32采用段页式虚拟存储管理方式
逻辑地址和线性地址是虚拟地址,是编程用的地址;描述的都是4GB虚拟地址空间中的一个存储地址。
物理地址是访问存储器的地址,是真实地址。
逻辑地址:48位,包含16位段选择符和32位段内偏移量(即有效地址)
线性地址:32位(其位数由虚拟地址空间大小决定)
32位处理器,所以虚拟成32位
物理地址:32位(其位数由存储器总线中的地址线条数决定)
对于物理地址:(实际的不知道)假设就是32位,那外面的物理存储器可能就是4GB(2^32B=4GB)
分段过程实现 逻辑地址 —> 线性地址
分页过程实现 线性地址—>物理地址
逻辑地址到线性地址(分段)
8086处理器的所有内部寄存器都是16位(AX, BX, CX, DX, SP, BP, SI, CS, DS)
(CS、DS是段寄存器)
8086处理器支持的存储器寻址方式有哪些?
寻址方式 | 说明 |
---|---|
位移 | EA = A |
基址寻址 | EA = (B) |
基址加位移 | EA = (B) + A |
比例变址加位移 | EA = (I) * S + A |
基址加变址加位移 | EA = (B) + (I) + A |
基址加比例变址加位移 | EA = (B) + (I) * S + A |
A — 地址段偏移量
B — 基址寄存器
I — 变址寄存器(除SP)
S — 比例因子
EA — 有效地址
8086指令(AT&T格式)举例:
1 | movw 8(%bp, %dx, 4), %ax //R[ax] <-- M[R[bp] + R[dx]*4 + 8] |
此时的寻址空间是 2^16B = 64KB,太小了!
于是8086引入段寄存器开辟更大的寻址空间:
16位段寄存器(CS, SS, DS, ES等)
物理访存地址 = (段寄存器 << 4) + 有效地址
于是寻址空间变为 1MB了(2^16<< 4 B = 2^20 B = 1MB)
此时的物理地址称为线性地址。
于是就变成了:
寻址方式 | 说明 |
---|---|
位移 | 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 |
1 | movw [ds:8(%bp, %dx, 4)], %ax //R[ax] <-- M[R[ds]<<4 + R[bp] + R[dx]*4 + 8] |
访问数据段就是和DS绑定的,访问代码段就是和CS绑定的。取指令要用到CS和IP,访问堆栈用到SS,等等。
物理访存地址 = (段寄存器 << 4) + 有效地址—这个访问模式称为实模式!
实模式
8086处理器在实模式下工作:
物理地址直接访存
可访问 1MB 主存空间
需要20根地址线(2^20,因为加上了(SR>>4))
每次访存必须和某个段寄存器绑定
存在的问题:
寻址空间有限
存在安全隐患
IA-32,即80386,标志着32位计算机的时代到来。
所有通用寄存器都是32位,寻址空间达到4GB!
正式支持虚拟存储器的概念,采用虚拟地址访存。
寻址空间已经达到4GB,是否可以去掉段寄存器?
不能。考虑兼容性!
于是IA-32处理器支持两种工作模式:
实模式:IA-32处理器加电或复位时处于这一模式,此时相当于8086/8088处理器,32位地址线中的A31~A20不起作用,所有访存地址都是物理地址(实地址)。
保护模式:完成系统初始化后,进入该模式,此时32位地址线全部起作用,访存地址为逻辑地址(虚拟地址),进入虚拟存储器管理方式。
保护模式
IA-32有一个”开关”,决定处理器处于哪种模式下:
CR0寄存器与通用寄存器不同,是另外一种寄存器。它叫控制状态寄存器。
对实模式和保护模式来说看哪一位呢?
最后一位。(第0位,即PE位)
计算机加电或复位时,PE = 0,IA-32处理器处于实模式。
PE = 1时,处于保护模式,并且一旦进入保护模式就不能再切换回到实模式了,除非…重启(重新开机复位)。
(所以只能从实模式进保护模式)
为什么IA-32分段机制更复杂?
与历史遗留问题有关:
寻址空间有限,仅1MB.
存在安全隐患。
段大小固定(64KB(2^16 B))
不灵活,无法设置访问权限。
……
进入IA-32时代,我们希望段地址也是32位的,还可以灵活设置各种段属性,但段寄存器只有16位,连32位段基址也放不下,怎么办?
段描述符
段描述符是用来描述一个段所有属性的数据结构。
每个段对应一个段描述符。
一个段描述符占8个字节(64位),包括:
B31~B0:32位基地址
L19~L0:20位限界,表示段的大小
G:粒度。G = 1以页(4KB)为单位,G = 0以字节为单位。因为界限为20位,故当G = 0时最大的段为1MB;当G = 1时,最大段为4KB*2^20=4GB。
D:D = 1表示段内偏移量为32位宽,D = 0表示段内偏移量为16位宽。
P:present. P = 1表示段在主存里,P = 0表示段不在主存里。Linux总把P置1,不会以段为单位淘汰,因为Linux以页为单位。
S:S = 0表示系统控制描述符,S = 1表示普通的代码段或数据段描述符。
TYPE:段的访问权限或系统控制描述符类型。
A:A = 1表示已被访问过,A = 0表示未被访问过(TYPE一般是4位,A是其中的1位)
DPL:权限位。
段描述符的组织
(Q represents question, P represents preccrep)
Q:段描述符占64位,段寄存器才16位,根本放不下,怎么办?
P:放到主存中。
Q:可是怎么在主存中找到一个段描述符?
P:利用指针。
Q:IA-32中涉及到地址的,还是32位。所以段描述符的地址也一定是32位的,段寄存器放不下,只能放到主存里。IA-32中的指针也是32位的,段寄存器还是放不下;即使能放下,如果想切换到其他段,怎么知道段描述符在什么地方?
P:把所有的段描述符组织成一个数组啊。
Q:?
P:段描述符只是一个数据结构,一旦找到段描述符,就能把段的基地址读出来,然后就能访问到相应的段了,问题就解决了。所以关键是怎么找到段描述符。没错,段描述符的地址一定是32位地址,因为它们是放在主存里的。在IA-32里,我们把所有段描述符都组成一个数组,数组索引用16位总能放下吧!用数组索引访问!就是——
IA-32把内存中的某一连续空间解释成一个数组,称为段描述符表,简称段表。数组中每个元素对应一个段描述符。
段表由OS负责填写。包括3种类型:
全局描述符表(GDT):只有一个,是所有进程共享的,用来保存系统中每个任务都可以访问的段描述符,如内核代码段、数据段,用户代码段、数据段等。
局部描述符表(LDT):存放某一用户进程专用的描述符(其他用户进程是访问不到的),但LDT不是一个独立的段表,它就保存在GDT中,甚至可以看作是GDT里的一个段描述符。
中断描述符表(IDT):独立于GDT的段表,包含中断门、陷阱门等描述符。例如系统调用的函数入口地址就要从IDT中获取。
GDT的首地址由全局描述符表寄存器(GDTR)提供。
GDTR这个寄存器只存放GDT的入口地址!
还有,段表不是一个大数组吗,我们是通过数组索引找到所需的描述符的,而该索引不就保存在段寄存器中吗,它称为段选择符。
GDTR
GDTR的结构长这样:
BASE ADDR(基地址) | LIMIT(限界)
32-bit | 16-bit
高32位存放GDT的入口地址,低16位存放GDT的大小。
当然,GDTR最大也就64位。这里是48位。
Q:GDTR中保存的地址是线性地址,为什么不是逻辑地址?
P:我们分段机制用它(GDTR)不就是为了把逻辑地址变为线性地址吗。要是GDTR中也是逻辑地址,那这个首地址(GDT的首地址)谁来给它变?
GDTR对用户进程不可见,只有OS才能访问。仅可由OS内核通过一条特权指令(lgdt m16&32)将GDT的首地址和限界装载到GDTR中,启动分段机制。
段选择符和段寄存器
CS(代码段):程序代码所在段
SS(栈段):栈区所在段
DS(数据段):全局静态数据区所在段
其他3个段寄存器ES、GS和FS可指向任意数据段
段选择符各字段含义
15 14 … 3 | 2 | 1 0
INDEX | TI | RPL
TI = 0,选择GDT;TI = 1,选择LDT。
RPL请求特权级。
高13位索引用来确定当前使用的段描述符在描述表中的位置。
——> 由此可知,GDT中最多可容纳 2^13=8192=8K 个段描述符。
那么整个段表多大?(1个段描述符占8个字节)
8K*8 B = 64KB.
而段寄存器是16位,2^16 = 64K个字节——刚好相等呀!
没错,16位就是这么来的。
于是,回归正题,逻辑地址怎么变到线性地址?
先看TI位。若TI = 0,那就先从GDTR里把GDT首地址读出来,然后加上索引——注意,是索引乘上8!乘上字节的宽度!基地址+索引*8.
GDT和IDT只有一个,GDTR和IDTR指向各自起始处。
LDTR 16-bit
GDTR, IDTR 48-bit
每次段寄存器装入新选择符时,新描述符装入描述符cache,在逻辑地址到线性地址转换时,MMU直接用描述符cache中的信息,不必访问主存段表
LDTR存放LDT描述符的段选择符
LDT描述符在GDT中
总之,逻辑地址转线性地址的过程就是这样:
- 逻辑地址高16位是段选择符,低32位是段内偏移。
- 通过段寄存器中的段选择符TI位决定在哪个表中查找。
- 根据GDTR读出段描述符表的首地址。
- 根据段寄存器中的段选择符index位在表中进行索引,找到一个段描述符。
- 在段描述符中读出段的基地址,和逻辑地址相加,得到线性地址。
在计算线性地址的过程中,可根据段描述符中的限界和访问权限判断是否“地址越界”或“访问越权”,以实现存储保护。
Q:索引 为什么乘以8?
P:1个段描述符占8个字节。
Q:被选中的段描述符存放在什么地方?
P:描述符cache里。
总结
- 逻辑地址起作用的前提:CR0的PE位为1,进入保护模式,启动分段机制。
- 根据段寄存器中的段选择符的TI位决定查GDT还是LDT
- 在GDTR中读出GDT首地址
- GDT首地址+段选择符中索引*8 — 得到段描述符首地址
- 在段描述符中读出段基址,和有效地址(逻辑地址后32位,即段内偏移)相加,得到线性地址
注意那个前提——否则实模式下有效地址就是线性地址,段寄存器也就用来左移4位加上有效地址得到物理地址了。
IA-32/Linux中的存储保护
IA-32的权限检查(基于环保护机制)
DPL:位于段描述符中,表示一个段所在的特权级别。例如,DPL为3说明该段可能是一个用户段,为0可能是内核段。
RPL:位于段选择符中,表示请求者所在的特权级别。
CPL:表示当前进程的特权级别,一般与CS寄存器指向的段描述符的DPL字段相同。
同时满足以下两个条件:
1 | target_descriptor.DPL >= requestor.RPL //请求者有权访问目标段 |
只要OS将GDT、页表等重要信息放在ring0段中,恶意程序将永远无法篡改它们,除非恶意程序获得了OS权限。
IA-32/Linux中的分段机制
为能被移植到绝大多数流行处理器平台,Linux简化了分段机制。
RISC对分段支持非常有限,因此Linux仅使用IA-32的分页机制,而对于分段,则通过在初始化时将所有段描述符的基址设为”0”来简化。
每个段都被初始化在 0~4GB 的线性地址空间中。
PA2中的用户程序都是从 kernel/src/start.S
开始的。PA3也是如此。
1 | #include "common.h" |
现在来逐段分析:
1 | #ifndef IA32_SEG |
这个是分段的宏,PA2里没考虑分段,也就没用到,所以直接跳到内核里去了。但是在PA3中,这个宏就要起作用了。在 kernel/include/common.h
中设置,把 define IA32_SEG
前的注释符号去掉。
除了
#define
中的#
外,其他的#
都是注释!
1 | .globl start |
PA2没用分段(即没用 IA32_SEG 宏,not define,满足 #ifndef
),就执行上面的部分;
PA3用了分段,就执行 #else
的部分.
1 | #define MAKE_NULL_SEG_DESC \ |
这个宏是将 MAKE_NULL_SEG_DESC
替换为下面那两个东西…
.word
和 .byte
就是汇编里的声明数据。声明为 .word
就是说那两个0都是 .word
类型,也就是16位宽,所以这里声明了两个16位的0。.byte
声明的是字节,相当于C语言中的 char
类型。2个16位的加上4个8位的,一共就是8个字节。所以——
这就是一个段描述符!只不过这是一个空的段描述符,因为都是0.
1 | #define MAKE_SEG_DESC(type,base,lim) \ |
这个宏还带参数的啊—— type
,base
,lim
——这不就是段描述符里的信息吗?
1 | #ifdef IA32_PAGE |
定没定义PAGE,自己决定用哪个函数。
最核心的部分:
1 | .globl start |
再来看这个——lgdt
,将全局描述符表的首地址和大小加载到了GDTR里。这里 va_to_pa
只有1个参数 gdtdesc
,这是一个地址,而由 #define va_to_pa(x) (x)
得知,这不就是 gdtdesc
本身吗。那这到底是什么东西呢,看下面:
1 | gdtdesc: # descriptor |
嗯,不就是把 gdtdesc
里面的 .word
和 .long
加载过去吗? .word
是16位的, .long
是32位的。它们是什么注释里都写清楚了,要强调的是 gdtdesc - gdt - 1
的这个减一。
当然,再看看这个 gdtdesc
,不就刚好是48位吗?没错,lgdt
就是把48位的逻辑地址加载到GDTR中去。
继续看start的代码。先把CR0的值放到EAX里,然后EAX与1进行OR,再把得到的值放回CR0。我们分析一下,当 “CR0最低位” = 0时,这一番操作使得 “CR0最低位” = 1,这就是实模式 —> 保护模式。
线性地址到物理地址(分页)
IA-32中的控制寄存器
控制寄存器保存机器的各种控制和状态信息,操作系统进行任务控制或存储管理时使用控制寄存器。
CR0:PG位:
1 - 启用分页
0 - 禁止分页,此时线性地址被直接作为物理地址使用。
若启用分页机制,PE和PG位都要置1.
CR2:页故(page fault)障线性地址寄存器
当要访问的页不在主存里时,触发了缺页中断,中断处的线性地址保存在CR2里,等到页面被调入后再继续从这里开始执行。
CR3:页目录基址寄存器
保存页目录表的起始地址。
当然,CR2和CR3有效的前提都是PG = 1。
Linux中线性地址空间划分:4GB = 1K个子空间 1K个页面/子空间 4KB/页
每一个页表就是一个子空间,页表中有1K项,每一项对应一个物理页;
页目录表只有1张,里面有1K个页目录项,每一项对应一个页表。
每1K个页面用一个页表进行组织,每个页表有1K个项,每项对应一个物理页(页框)
线性地址由3个字段组成,分别是10位页目录索引,10位页表索引,12位页内偏移
CR3中存放的就是页目录表的基地址,拿到基地址后加上DIR*4(为什么乘以4?因为一个页目录项是32位,即4字节),得到相应页表项的首地址。再加上PAGE*4,得到物理页的首地址。物理页首地址再加上页内偏移量,就是真正的物理地址。
页目录项和页表项的格式
CPU发出地址先送到MMU,MMU完成分段和分页再送到cache。MMU中的分段解决逻辑地址到物理地址的变换,分页解决线性地址到物理地址的变换。
本文作者 : preccrep
原文链接 : https://preccrep.github.io/2020/12/26/IA-32-Linux%E4%B8%8B%E9%80%BB%E8%BE%91%E5%9C%B0%E5%9D%80-%E7%BA%BF%E6%80%A7%E5%9C%B0%E5%9D%80-%E7%89%A9%E7%90%86%E5%9C%B0%E5%9D%80/
版权声明 : 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!