The Microprocessor and its Architecture¶
约 3761 个字 41 行代码 预计阅读时间 19 分钟
Registers¶
Intel IA-32 的基础执行环境有如下寄存器:
其中,图片下半部分的浮点寄存器和向量寄存器为 x87 新增的。
注意到浮点控制 Flag 比 EFLAGS 对了一个 Tag,这是因为浮点计算相比整型更加复杂,需要标识舍入方案等
演变到如今 64-bit mode,它的执行环境如下:
如下表格更直观的比较了二者的寄存器差异,其中深色部分表示存在不同:
AX, BX, CX, DX, BP, SP, SI, DI 是自 x86 开始即有的 General-Purpose Registers,它们在设计之初有各自的作用:
| Register | Name | Commonly used as |
|---|---|---|
A | Accumulator | Return value, especially the sum of arithmetic operations 80x86 对于 A 系列寄存器保留了单独的操作码来表示是什么运算,从而减少指令的译码长度;而用其它寄存器,则需要指示操作码以及寄存器 |
B | Base Index | Starting point of an array or list structure 通常存储一个对象的偏移地址 |
C | Counter | Used by loops 常用作循环计数器或一些指令的计数器(如 REP) |
D | Data | Extended space for accumulator 常存储溢出的部分结果数据,例如乘法的高位结果以及除法的除数 |
BP | Base Pointer | Pointer to address of current stack frame 尤其是函数调用的时候 |
SP | Stack Pointer | Pointer to address of last bytes PUSHed to memory 指向栈顶的偏移 |
SI | Source Index | Starting point of unbounded stream data, especially a string 通常在字符串操作指令中指向 source string 数据的开头,但也用作一般用处 |
DI | Destination Index | Ending point of unbounded data, especially in slicing operations 通常在字符串操作指令中指向 destination string 数据的开头,但也用作一般用处 |
大部分 x86 寄存器都有多粒度的特性,即可以划分为多个 partial register,对于 A, B, C, D 系列寄存器,其命名规则如下(以 A 为例):
上图同时也说明了该寄存器的修改规则,即只有修改 32-bit 的 partial register eax 时,rax 的 bit 32-63 会被置 0;其余修改时余位不变。
特别说明的是,ah 和 al 这两个高低 8-bit 寄存器只有 A, B, C, D 拥有。
BP, SP, SI, DI 其实也有低八位,格式为 BPL 等,但是这只在 64-bit mode 下可用
Intel,AMD,VIA 的 CPU 都不支持重命名 partial register,因为它会担心这是一个虚假的数据依赖,以防用户要利用 partial 的特性进行编程,以如下为例,假定要连续对两处内存地址进行数据修改,其中右侧的第二次修改的是 16-bit 的数据,如果都用 A 系列的寄存器,右边的 AX 就不能被 CPU 的寄存器重命名所修改,从而导致 stall:
Segment Registers 共有六个:
- CS (code): 代码段,存储程序的代码
- DS (data): 数据段,存储程序大部分数据
- ES (extra): 额外数据段,存储一些 destination data
- SS (stack): 栈
- FS & GS: 80386-Core2 新增的额外段寄存器,可通用
64-bit mode 下,memory 的管理方式从分段管理变为分页管理,DS, ES, SS 被忽视(置 0),即便我们尝试对其修改也可能不起作用:
除了以上的 General-Purpose Registers 和 Segment Registers,我们还有一些 Special-Purpose Registers,包括 RIP, RSP, RFLAGS 等:
- RIP 指向段中下一条指令的地址,即 Instruction Pointer
- RSP 指向栈底,即 Stack Pointer
- RFLAGS 表示微处理器的状态,用来控制 operation 的行为
- 分为状态标志、控制标志以及一些信息
状态标志有:
- C (bit 0): Carry Flag 算术运算或逻辑运算产生进位、借位则为 1
- 两数相加产生进位时 CF 置 1
- 两数相减产生借位时 CF 置 1
- 两数相乘乘积超过被乘数宽度时 CF 置 1
- 移位指令最后移出的那一位保存在 CF 中
- Z (bit 6): Zero Flag 算术运算或逻辑运算的结果为 0 则为 1
- S (bit 7): Sign Flag 算术运算或逻辑运算的结果的符号位
- 即运算结果的最高位
- O (bit 11): Overflow Flag 运算结果超出机器可表示的范围则为 1
- 当两正数相加变负数时 OF 置 1
- 当两负数相加变正数时 OF 置 0
- 当两数相乘乘积宽度超过被乘数宽度时 OF 置 1(此时 CF 也置 1)
- 当仅移动一位,且移位前最高位不等于移位后最高位时 OF 置 1
- P (bit 2): Parity Flag 运算结果低 8 位中二进制 1 的个数为偶数时为 1
- A (bit 4): Auxiliary Flag 保存 BCD 运算中途的 carry 和 borrow
- 若执行加法指令时第 3 位向第 4 位产生进位则 AF 置 1
- 若执行减法指令时第 3 位向第 4 位产生借位则 AF 置 1
控制标志有:
- D (bit 10): Direction Flag 控制字符串操作指令的递增/递减方向
- I (bit 9): Interrupt Flag 控制是否允许硬件中断
- T (bit 8): Trap Flag 设置 CPU 运行模式
- 当
TF=1时,CPU 进行单步模式 - 当
TF=0时,CPU 进行常规模式
- 当
- VM (bit 17): Virtual Mode 在保护模式下,选择 Virtual Mode Operation
- IOPL (bit 12, 13): 在保护模式下,选择 I/O 设备的特权阶级
- NT (bit 14): Nested Task 在保护模式下,当前 Task 是在另一个 Task 的嵌套中则为 1
- AC (bit 18): Alignment Check 如果 AC 置 1,且 CPU 的对齐检查机制开启,则会对访问地址进行对齐检查,失败则触发异常
- 例如,访问
int a(4 Bytes) 时,地址应该为 4 的倍数,例如0x1000,此时访问0x1002就会抛出异常
- 例如,访问
- RF (bit 16): Resume 控制 CPU 在单步调试或断点后如何继续执行
- ID (bit 21): Identification 用于测试 CPU 是否支持
CPUID指令,如果支持,则该位可以被软件读写;如果不行,则写这个位无效 - VIF (bit 19): Virtual Interrupt Flag 在 Virtual-8086 Mode 和多任务环境下的中断标志
- VIP (bit 20): Virtual Interrupt Pending 表示是否有虚拟中断正在等待处理
早期 CPU 可以通过观察 RFLAGS 中哪些位可以被修改来判断 CPU 属于哪一代
Operating Mode¶
Long Mode(在 Intel 中对应 IA-32e)是 Legacy Protected Mode 的拓展,它包含了两个子模式 64-Bit Mode 和 Compatibility Mode:
- 64-Bit Mode 支持 64-bit 架构的所有特性和寄存器拓展
- Compatibility Mode 支持现存 16-bit 和 32-bit 应用的二进制兼容
Long Mode 并不支持 Legacy Real Mode 和 Legacy Virtual-8086 Mode,因此不能简单抛弃 Legacy Mode
除此之外,还有一个特殊的模式 System management mode(SMM),它最初是用来适配平台的固件以及特定的底层硬件驱动的。在 AMD64 架构下,不同 Operating Mode 的切换如下:
RSM 表示退出 SMM,从哪里进入 SMM 的就会退回到哪个模式
Memory Management¶
Memory Mangement 通常需要满足 Relocation,Protection,Sharing 三个要求:
| 要求 | 含义 |
|---|---|
| 重定位(Relocation) | 程序可以在内存中任何位置装入和执行,需要地址映射机制。 |
| 保护(Protection) | 防止一个进程访问或修改另一个进程的内存空间。 |
| 共享(Sharing) | 允许多个进程安全地共享相同的代码或数据。 |
其中,Segmentation 和 Paging 是最主流的两种 Memory Management Schemes,这里简要对二者进行一些对比:
| 对比项目 | 分段(Segmentation) | 分页(Paging) |
|---|---|---|
| 大小(Size) | 段的大小可变,由用户或编译器决定(例如:代码段、数据段、堆栈段)。 | 页的大小固定(例如 4KB、8KB),由硬件决定。 |
| 碎片(Fragmentation) | 容易产生外部碎片(因为不同段大小不一,内存分配不连续)。 | 容易产生内部碎片(因为最后一页可能没被完全用完)。 |
| 表结构(Tables) | 使用段表(Segment Table),记录每个段的基址和长度,查找速度较快。 | 使用页表(Page Table),记录页号对应的物理块号。查找相对较慢,但TLB(Translation Lookaside Buffer)可加速。 |
接下来,我们会使用如下几个 terms 来指代各种不同地址:
- Effective Addresses or Segment Offsets
- 段内偏移地址
- Logical Addresses
- 逻辑地址,表示形式类似
Seg:Offset
- 逻辑地址,表示形式类似
- Linear Addresses
- 线性地址,相当于逻辑地址的转译,是连续的地址
- 如果启用了 Paging,Linear Addresses 相当于虚拟地址 Virtual Addresses
- Physical Addresses
- 物理地址,在 Memory 中的真实内存布局
Real Mode Memory Addressing¶
Real Mode 的地址空间只有 1MB 大小,即 \(2^{20}\) byte。它的 Linear Address 的计算为:
乘上 \(10h\),等价于左移 4 位
Real Mode 中,段寄存器和偏移寄存器都是 16-bit 的,因此偏移后相加得到的地址是 20-bit 的
1 MB Address Wrap-Around 是这个设计很经典的问题。当 Linear Address 计算最高位进位时,物理地址发生 overflow,反而变得很小,这也被称为回环。可以考虑如下左右两个例子:
由于有相当数量的商业软件特意使用了这个特性,后续的 8086/8088/80186 CPU 都保留了这点。
Protected Mode Memory Addressing¶
在保护模式下,段寄存器不再单纯存储段地址,而是存储指向 Descriptor Table 中的某一描述符的 Selector。
例如对于指令 MOV [BX], AX,它的访存流程如下:
Descriptor 描述了一个内存段的地址,长度以及访问权限,它的结构随着架构的升级变更如下:
其中 Base 部分指定了这个段的起始位置,Limit 部分指定了这个段的长度(也可以说是结束部分)。G 代表了 Granularity Bit,它决定了该段 Limit 的缩放程度:
- 当 G=0,段的长度为 \((\text{limit}+1)\ \text{bytes}\)
- 此时 limit 的范围为 0 - \(2^{20}-1\)
- 当 G=1,段的长度为\((\text{limit}+1)\times 4K\ \text{bytes}\)
- 也可以认为将 limit 替换为 \(\text{limit}<<12 + 0xFFF\)
- 此时 limit 的范围为 0FFFH - \(2^{32}-1\)
【Example】 有一个描述符,base address = 1000,0000H, limit = 01FFH,求 G 分别为 0 和 1 时的段起始位置和结束位置:
- 当 G=0
- Starting Location = 1000,0000H
- Ending Location = 1000,0000H + 01FFH = 1000,01FFH
- 当 G=1
- Starting Location = 1000,0000H
- Ending Location = 1000,0000H + 01F,FFFFH = 101F,FFFFH
1 Byte 大小的 Access Rights 指定了这个段的权限控制:
其中 DPL 字段共 2-bit,指定了该段的 Priviledge-Level。特权等级共分 4 级,按照数字大小权限依次降低:
Descriptor Tables 分为 Global-Descriptor Table(GDT)、Local-Descriptor Table(LDT)、Interrupt-Descriptor Table(IDT) 三类,其中 LDT 需要通过访问 GDT 中的 entry 来间接访问,这一点我们会在稍后谈及。
Selector 存储在 16-bit 的段寄存器中,它的结构如下:
TI 指定了是要访问 GDT 还是 LDT;RPL 指定了此次请求的特权阶级。
除此之外,段寄存器 CS 中最低两位不为 RPL,为 Current Privilege-Level(CPL),CPL 的值永远为 CPU 当前的特权阶级。综合下来,关于 Descriptor 和 Selector 中的三种特权阶级表示位的关系与区别如下:
在进行数据访问的时候,我们会对权限进行检查:
为了存储 Descriptor Tables 的基地址,处理器中有 4 种用户不可见的寄存器:
- GDTP and IDTR:存储 GDT 和 IDT 的 Base 和 Limit,在进入保护模式之前就被加载
- LDTR and TR:指向 GDT 中特殊的描述符
- 例如 LDTR 指向 GDT 中的指向 LDT 的描述符
还有一些不可见的寄存器用作 Descriptor Cache
当需要发生进程切换时,LDTR 会加载新任务对应的 selector 值,从而利用 LDT 实现扩展内存空间。
左下角对 index 乘 8 是因为 Descriptor Table 中一个 entry 是 8 Byte
在保护模式下,分段内存模型 Segmented-Memory Models 有两种:
| 模型 | 说明 | 特点 |
|---|---|---|
| 多段模型 (Multi-Segmented Model) | 把内存分成多个独立的段,比如代码段、数据段、堆栈段等。每个段有自己的基址和界限。 | ✅ 每个段可以独立管理、保护与共享。❌ 地址转换更复杂,软件需要管理多个段寄存器。 |
| 平坦模型 (Flat-Memory Model) | 把所有段的基址都设为 0,界限设为整个地址空间长度(例如 4GB)。这样所有内存都连续可寻。 | ✅ 看起来就像“没有分段”,只有一个连续的线性地址空间。✅ 编程更简单,现代 OS(如 Linux、Windows)大多采用这种方式。 |
Memory Paging¶
Paging 将内存划分为统一大小的 Page,通常:
- 在 long mode, physical pages 长度可为 4-KByte, 2-MByte, 1-GByte
- 在 legacy mode,physical pages 长度可为 4-KByte, 2-MByte, 4-MByte
与计组中学到的分页稍有区别的是,现实 CPU 通常会采用 Multi-level Paging,而不是只通过一个 Page Table 就能转换虚拟地址和物理地址。这是因为 Single-level 往往占用太多内存。
例如,对于 64-bit computer,假定我们设置 4KB page size, 4GB physical memory, 4Byte per page table entry,那么 \(\text{Page Offset}= \log_2 4K=12\ bits\),那么 \(\text{Virtual Page Number} = 64-12=52\ bits\),一个 entry 有 4B,那么整个 Page Table 占用内存 \(2^{52}\times 4= 2^{54}\ bytes\),这已经是物理内存 4GB(\(2^{32}\)) 的不知道多少倍了。
现在我们使用 Multi-level Paging 技术。通常,我们设置一个 Page Table 的大小等同于一个 Page 的大小,对于该例即为 4KB。此时一个 Page Table 共有 4KB / 4B = 1024 个 entries。
1024 个 entries 意味着我们只需要 10-bit 就能对一个 Page Table 进行索引,那么层数计算则为 \(\#\text{ of Level}= \frac{52}{10}\ \text{ceiled is } 6\),即共需要 6 层,其索引结构类似如下:
Key Points
- 所有 Page Table 的 entry size 均需相同
- Page Table 由内到外分别称为
- Page Table, PT
- Page Directory, PD
- Page Directory Pointer Table, PDPT
| 🌟 类别 | 🟢 优点 (Pros) | 🔴 缺点 (Cons) |
|---|---|---|
| 内存占用 | 仅为实际使用的虚拟地址空间分配页表,节省大量内存 | 页表结构本身更复杂,需要额外的目录页表,占用少量额外空间 |
| 稀疏地址空间支持 | 对于稀疏内存使用的进程非常高效,不会为空洞地址范围浪费页表项 | 若进程使用地址空间非常密集,多级页表节省的空间优势不明显 |
| 可扩展性 | 适用于大型虚拟地址空间(如 32 位、64 位系统) | 随层数增加,页表查找过程更长、更慢 |
| 访问速度 | —— | 每增加一级页表,就需要多一次内存访问才能完成地址转换 |
| 性能优化 | 可与 TLB(Translation Lookaside Buffer) 配合使用,显著减少查找延迟 | 若 TLB 命中率低,会出现明显性能下降 |
Paging 为 OS 提供了将虚拟地址转换为物理地址(Relocation)的抽象层,因此不同应用可以使用各自隔离的虚拟地址空间(Protection),多个应用还可以通过 Shared Mapping 共享同一个物理页(Sharing)。
与分段模式相同,Paging 也需要做一些权限保护,这些用于维护权限的位处于页表的 Entry 中。例如,Page-Directory Entry 的结构如下:
都是 0 表示高权限等级,1 表示低权限等级(这块能 Write,说明这块权限等级低)
在 Multi-Paging 下,一个物理页的最终权限是导向它的 PDE 和 PTE 权限位的 AND:
Paging 使用 CR0-CR4 几个 Control Registers 来对分页进行控制,它们的结构如下:
- Control Register 0: 主要包括分页相关控制位
- PE(bit 0) 是否启用保护模式。当 PE = 1 时会从 Real Mode 进入 Protected Mode
- PG(bit 31) 是否启用分页。该位在进出 Long Mode 时被设置
- Control Register 2: 保存发生 Page Fault 时的 Linear Address
- Handler 通过读取 CR2 来确定哪个地址导致了错误,从而执行缺页调入、权限检查、进程终止等操作
- Control Register 3: 存储页目录的 Physical Address
- Page-Directory Base: 页目录基地址
- Control Register 4: Pentium 开始新增的控制寄存器,拓展了原有的分页架构控制
- PSE(bit 4) 是否启用大页拓展。开启后允许 PDE 直接映射 4MB 的大页
- 当
CR4.PSE=1andPDE.PS=1时,该 PDE 映射到一个 4MB Page - 当
CR4.PSE=1andPDE.PS=0时,该 PDE 映射到一个 4KB Page
- 当
- PAE(bit 5) 是否启用物理地址拓展。开启后允许物理地址空间超过 4GB
- 例如 36-bit 物理地址,可寻址 64GB 内存;此时 Linear Address 还是 32-bit
- PAE 启用的条件下允许映射 2MB Page,此时 PSE 位被忽略,大页达不到 4MB
- PSE(bit 4) 是否启用大页拓展。开启后允许 PDE 直接映射 4MB 的大页
假定我们要对一个 Page Table 进行访问,由于我们只能通过虚拟内存来与 Memory 交互,我们该如何得到 Page Table 上一个 entry 的虚拟内存?
为了解决这个问题,OS 为 Page Table 设置了一个 self-reference table entry,这个 entry 存储了指向自己所处的 Table 的基地址。Windows 32 bit 将 0x300 entry 作为 self-reference table entry,我们接下来也将以此为例作为讲解。
在 10-10-12 模型下给出一个虚拟地址 \(VA\),我们希望得到对该虚拟地址进行映射时经过的 entry 的虚拟地址,可以通过如下公式计算:
具体原理可见以下示例,其中最后的 0x321 要乘以 4(左移两位)是因为最后的 offset 是直接加在基地址上的,所以要乘上一个 entry 的大小 4B:
Total Meltdown
Windows 64 本来选用固定位置 0x1ED 作为 self-reference entry,但是曾经有人将这个 entry 的权限设置成 User 从而获得了整个物理内存的权限,因此从 Windows 10 开始将该值设置为系统启动时随机选择。
Addressing Modes¶
对于三种不同的 Operation Modes,它们默认的 Address Size 和 Operand Size 如下:
- 16-bit Modes
- 16-bit address & 16-bit operand
- 32-bit Protected Modes
- 32-bit address & 32-bit operand
- 64-bit Modes
- 64-bit address & 32-bit operand
例如,在 64-bit 下,指令 MOV EAX, [RBX] 中 EAX 是 32-bit 操作数,RBX 用来 64-bit 寻址。
数据寻址¶
在 x86 寻址中,一个偏移地址可能由如下四个部分组成:
这些部分不是必须出现,根据组合,我们将 Addressing Mode 划分为以下六种:
- Direct Data Addressing (Disp)
- 与 A 系列寄存器相关的
MOV指令被称为 Direct Addressing,特征是译码得到的字节码长度更短MOV AL, [1234H] ; A0 34 12
- 除此之外的指令被称为 Displacement Addressing
MOV CL, [1234H] ; 8A 0E 34 12
- 与 A 系列寄存器相关的
- Register Indirect Addressing (Base)
- Base-Plus-Index Addressing (Base + Index)
- Register Relative Addressing (Base/Index + Disp)
- Base Relative-Plus-Index Addressing (Base + Index + Disp)
- Scaled-Index Addressing (Base+Scale×Index+Disp)
下面我们以一些编译器的实例来说明:
在 8086-80286 间接寻址中,base 可选 BX 和 BP,index 可选 SI 和 DI,scale 可选 1,2,4,8。
当方括号中选用寄存器 BP/EBP/ESP 作为 base 时,则缺省段址默认为 SS;否则,均为 DS。
从 80386(32-bit)开始,所有 32-bit 寄存器都可以作为 base 和 index
Which addressing mode in the following instruction is invalid ?
- A.
- B.
- C.
- D.
ESP不能作为index
在 x86-64 系统上,Linear Address 的逻辑长度为 64-bit。但是我们做分页时所用的 Effective Address 通常是 48-bit 或 57-bit 的,我们通常使用符号扩展来增大地址空间,这被称为 canonical address,例如:
代码寻址¶
代码寻址通常与指令 JMP, CALL 相关,dst operand 指定了指令即将跳转到的地址。
根据 jump offset 的类型,可分类为以下两种:
- relative offset
- 通常指定为跳转到一个 label,编译时会被编码为相对当前指令 IP 的符号偏移
- absolute offset
- 指定一个 General-Purpose Register 或 Memory Location,将这个值直接加载到 PC
- 绝对偏移以 code segment 作为基
根据 jumps 的类型,可分类为以下四种:
- 短跳(short jump) :跳转距离用一个字节(1 Byte)表示,机器码以
EB开头- jump range 范围为 -128 to +127
- 短跳后面只能接目标偏移地址或标号;而近跳还可以接 16 位寄存器或 16 位变量
- 短跳机器码的 idata 为一个字节,对应大小为目标地址减去下条指令的偏移地址($+2)
- 近跳(near jump) :跳转距离或目标地址用一个字(2 Byte)表示,机器码以
E9开头- 机器码
E9FD1E对应指令1D3E:0100: jmp 2000h即跳转到地址1D3E:2000 - 其中
FD1E含义为1EFD = 2000h - 0103h,即目标地址减去下条指令的偏移地址 - 偏移地址在机器码中也是小端存储
- 机器码
- 远跳(far jump) :目标地址用一个远指针表示(段地址:偏移地址),机器码以
EA开头- 机器码
EA0000FFFF对应指令jmp 0FFFFh:0000h。但是实际上远跳不能直接接常数地址。 - 如果要跳转到不同段,一定是使用远跳;例如不同模式切换时
- 机器码
- Task Switch :跳转到不同 task 中的指令处,只存在于 protection mode
栈寻址¶
与栈内存交互通常使用 PUSH, POP 进行,这两个指令总共有六种形式:
- register, memory, immediate(立即数只能入栈,不能出栈)
- segment register, flags, all registers
The PUSH and POP immediate & PUSHA and POPA (all registers) available 80286 - Core2.
栈寻址相关的操作基地址均为 SS,偏移均为 SP(或 ESP)。
由于是小端寻址,在多字节数据入栈时,高位字节先入栈,低位字节后入栈。
汇编代码中,堆栈端只能创建一次,创建时,SS:[SP] 指向栈顶,SP 指向堆栈段的末尾:
在 8086 - 80286 中,PUSH 指令只能传递 2 Bytes 大小的数据;而 80386 开始,视 PUSH 后接的操作数大小,可以传递 2 Bytes, 4 Bytes, 8 Bytes 大小的数据。
除此之外,还有 PUSHA, PUSHAD, PUSHF 等特殊的 push 指令:
PUSHApush all- 按照
AX,CX,DX,BX,SP,BP,SI,DI顺序入栈 - 操作数大小为 2 Bytes
- 按照
PUSHADpush all double- 操作数大小为 4 Bytes
PUSHFpush flags- 将 FLAG 入栈
POP 相关的同理,此处不再书写。
































