IA-32/Linux下逻辑地址-线性地址-物理地址

发布 : 2020-12-26 分类 : NEMU 浏览 :

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位段基址也放不下,怎么办?

段描述符

段描述符是用来描述一个段所有属性的数据结构。

每个段对应一个段描述符。

image-20201225173007530

一个段描述符占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中

image-20201225200258987

总之,逻辑地址转线性地址的过程就是这样:

  1. 逻辑地址高16位是段选择符,低32位是段内偏移。
  2. 通过段寄存器中的段选择符TI位决定在哪个表中查找。
  3. 根据GDTR读出段描述符表的首地址。
  4. 根据段寄存器中的段选择符index位在表中进行索引,找到一个段描述符。
  5. 在段描述符中读出段的基地址,和逻辑地址相加,得到线性地址。

在计算线性地址的过程中,可根据段描述符中的限界和访问权限判断是否“地址越界”或“访问越权”,以实现存储保护。

Q:索引 为什么乘以8?

P:1个段描述符占8个字节。

Q:被选中的段描述符存放在什么地方?

P:描述符cache里。

总结

  1. 逻辑地址起作用的前提:CR0的PE位为1,进入保护模式,启动分段机制。
  2. 根据段寄存器中的段选择符的TI位决定查GDT还是LDT
  3. 在GDTR中读出GDT首地址
  4. GDT首地址+段选择符中索引*8 — 得到段描述符首地址
  5. 在段描述符中读出段基址,和有效地址(逻辑地址后32位,即段内偏移)相加,得到线性地址

注意那个前提——否则实模式下有效地址就是线性地址,段寄存器也就用来左移4位加上有效地址得到物理地址了。

IA-32/Linux中的存储保护

IA-32的权限检查(基于环保护机制)

DPL:位于段描述符中,表示一个段所在的特权级别。例如,DPL为3说明该段可能是一个用户段,为0可能是内核段。

RPL:位于段选择符中,表示请求者所在的特权级别。

CPL:表示当前进程的特权级别,一般与CS寄存器指向的段描述符的DPL字段相同。

同时满足以下两个条件:

1
2
target_descriptor.DPL >= requestor.RPL   //请求者有权访问目标段
target_descriptor.DPL >= CPL //当前进程有权访问目标段

只要OS将GDT、页表等重要信息放在ring0段中,恶意程序将永远无法篡改它们,除非恶意程序获得了OS权限。

IA-32/Linux中的分段机制

为能被移植到绝大多数流行处理器平台,Linux简化了分段机制。

RISC对分段支持非常有限,因此Linux仅使用IA-32的分页机制,而对于分段,则通过在初始化时将所有段描述符的基址设为”0”来简化

每个段都被初始化在 0~4GB 的线性地址空间中。

PA2中的用户程序都是从 kernel/src/start.S 开始的。PA3也是如此。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include "common.h"

#ifndef IA32_SEG

.globl start
start:
# Set up a stack for C code.
movl $0, %ebp
movl $(128 << 20), %esp
jmp init # never return

#else

# To understand macros here, see i386 manual.
#define GDT_ENTRY(n) ((n) << 3)

#define MAKE_NULL_SEG_DESC \
.word 0, 0; \
.byte 0, 0, 0, 0

# The 0xC0 means the limit is in 4096-byte units
# and (for executable segments) 32-bit mode.
#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)

#ifdef IA32_PAGE
# define KOFFSET 0xc0000000
# define va_to_pa(x) (x - KOFFSET)
#else
# define va_to_pa(x) (x)
#endif

.globl start
start:
lgdt va_to_pa(gdtdesc) # See i386 manual for more information
movl %cr0, %eax # %CR0 |= PROTECT_ENABLE_BIT
orl $0x1, %eax
movl %eax, %cr0

# Complete transition to 32-bit protected mode by using long jmp
# to reload %CS and %EIP. The segment descriptors are set up with no
# translation, so that the mapping is still the identity mapping.
ljmp $GDT_ENTRY(1), $va_to_pa(start_cond)

start_cond:
# Set up the protected-mode data segment registers
movw $GDT_ENTRY(2), %ax
movw %ax, %ds # %DS = %AX
movw %ax, %es # %ES = %AX
movw %ax, %ss # %SS = %AX

# Set up a stack for C code.
movl $0, %ebp
movl $(128 << 20), %esp
jmp init # never return

# GDT
.p2align 2 # force 4 byte alignment
gdt:
MAKE_NULL_SEG_DESC # empty segment
MAKE_SEG_DESC(0xA, 0x0, 0xffffffff) # code
MAKE_SEG_DESC(0x2, 0x0, 0xffffffff) # data

gdtdesc: # descriptor
.word (gdtdesc - gdt - 1) # limit = sizeof(gdt) - 1
.long va_to_pa(gdt) # address of GDT

# end of IA32_SEG
#endif

现在来逐段分析:

1
#ifndef IA32_SEG

这个是分段的宏,PA2里没考虑分段,也就没用到,所以直接跳到内核里去了。但是在PA3中,这个宏就要起作用了。在 kernel/include/common.h 中设置,把 define IA32_SEG 前的注释符号去掉。

除了 #define 中的 # 外,其他的 # 都是注释!

1
2
3
4
5
6
.globl start
start:
# Set up a stack for C code.
movl $0, %ebp
movl $(128 << 20), %esp
jmp init # never return

PA2没用分段(即没用 IA32_SEG 宏,not define,满足 #ifndef),就执行上面的部分;

PA3用了分段,就执行 #else 的部分.

1
2
3
#define MAKE_NULL_SEG_DESC   \
.word 0, 0; \
.byte 0, 0, 0, 0

这个宏是将 MAKE_NULL_SEG_DESC 替换为下面那两个东西…

.word.byte 就是汇编里的声明数据。声明为 .word 就是说那两个0都是 .word 类型,也就是16位宽,所以这里声明了两个16位的0。.byte 声明的是字节,相当于C语言中的 char 类型。2个16位的加上4个8位的,一共就是8个字节。所以——

这就是一个段描述符!只不过这是一个空的段描述符,因为都是0.

1
2
3
4
#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)

这个宏还带参数的啊—— typebaselim ——这不就是段描述符里的信息吗?

1
2
3
4
5
6
#ifdef IA32_PAGE
# define KOFFSET 0xc0000000
# define va_to_pa(x) (x - KOFFSET)
#else
# define va_to_pa(x) (x)
#endif

定没定义PAGE,自己决定用哪个函数。

最核心的部分:

1
2
3
4
5
6
.globl start
start:
lgdt va_to_pa(gdtdesc) # See i386 manual for more information
movl %cr0, %eax # %CR0 |= PROTECT_ENABLE_BIT
orl $0x1, %eax
movl %eax, %cr0

再来看这个——lgdt ,将全局描述符表的首地址和大小加载到了GDTR里。这里 va_to_pa 只有1个参数 gdtdesc ,这是一个地址,而由 #define va_to_pa(x) (x) 得知,这不就是 gdtdesc 本身吗。那这到底是什么东西呢,看下面:

1
2
3
gdtdesc:                      # descriptor
.word (gdtdesc - gdt - 1) # limit = sizeof(gdt) - 1
.long va_to_pa(gdt) # address of GDT

嗯,不就是把 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/页

image-20201226101431856

每一个页表就是一个子空间,页表中有1K项,每一项对应一个物理页;

页目录表只有1张,里面有1K个页目录项,每一项对应一个页表。

每1K个页面用一个页表进行组织,每个页表有1K个项,每项对应一个物理页(页框)

线性地址由3个字段组成,分别是10位页目录索引,10位页表索引,12位页内偏移

CR3中存放的就是页目录表的基地址,拿到基地址后加上DIR*4(为什么乘以4?因为一个页目录项是32位,即4字节),得到相应页表项的首地址。再加上PAGE*4,得到物理页的首地址。物理页首地址再加上页内偏移量,就是真正的物理地址。

页目录项和页表项的格式

image-20201226105427190

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 许可协议。转载请注明出处!
留下足迹

博客已萌萌哒运行(●'◡'●)ノ♥
Theme - BMW | Made With 💗 | Powered by GodBMW