banner
Zein

Zein

x_id

An Overview of x86 Architecture and Binary Interface

amd64,intel64,x86-64,IA-32e ,x64 都被归为 64 位 x86 架构;IA-32 是 32 位 x86 架构

64bit os 启用 x64 模式 (长模式):装载 os 后,初始进入传统的页式地址保护模式,然后 PAE 模式被使能。
1)兼容子模式:允许运行 32bit 或 16bit 应用程序,而无需修改或重新编译应用程序。然而虚拟 8086 模式、任务切换和栈参数复制特性在兼容模式下不被支持。因此在运行传统应用程序时有一些限制。
2)64 位子模式:专门运行 64 bit 应用程序,支持 64 位线性地址空间。CPU 中的通用寄存器扩展到 64 位,并新增 8 个额外通用寄存器(R8~R15)。此外,还有新的 SIMD 扩展寄存器(XMM8~XMM15)来提高处理效率。64 位子模式还支持更高效的指令集,扩展的内存寻址,新的中断优先级控制机制,提供更强大的运算能力。

32bit os 启用 x86-32 模式:
1)实模式:逻辑地址直接映射物理地址,没有分页机制,20bit 外地址总线只能访问 1MB 内存空间。BIOS 通常在这个模式下初始化硬件和数据结构。
2)保护模式:保护模式引入的机制是现代操作系统(如 Windows、Linux)运行的基础。该模式下,CPU 可实现内存保护、虚拟内存、任务切换等功能,os 可利用这些功能管理硬件资源。系统可访问更大的物理地址空间,且支持多任务和内存保护。
3)虚拟 8086 模式:允许在保护模式下运行 16bit 程序,这些程序可运行在近似于实模式的环境中。os 通过虚拟化技术将每个 16bit 程序运行在独立的虚拟环境中,从而使其在不退出保护模式的情况下运行。

运行模式运行的子模式被多少位的 os 所启用应用程序是否需重编译默认地址长度默认操作数长度扩展寄存器通用寄存器位宽
x86-32 模式虚拟 8086 模式16bit不需要1616不使用16
实模式16bit 或 32bit不需要1616不使用16
保护模式16bit 或 32bit不需要16 或 3216 或 32不使用16 或 32
x64 模式 (长模式)64 位模式64bit需要6432使用64
兼容模式64bit不需要16 或 3216 或 32不使用16 或 32

下述描述以 x86-32 保护模式和 x64 兼容模式的经典架构为基础脉络,毕竟后续架构改进都以此为基础;并会列出 x64 64 位模式的特殊改进

CPU 架构#

内存架构#

地址空间#

物理地址空间#

内存和其他硬件设备等 CPU 可以使用的资源统一编址为物理地址空间;CPU 用物理地址索引访问。MMU 将 cpu 传递的虚拟地址转换为物理地址后通过外地址总线访问内存;外地址总线位数决定物理地址空间大小,外地址总线位数与内地址总线位数≥内地址总线位数;

内存管理单元 Memory Management Unit 有基址寄存器base:存储进程空间 0 地址 映射的物理地址;MMU 做如下工作:
physical address = virtual address + base;因为进程空间从零开始,而虚拟地址相当于进程空间的 offset

线性地址空间#

image

线性地址空间大小由 CPU 的内地址总线位数决定。内地址总线与 CPU 执行单元相连,内地址总线位数往往与 CPU 位数一致;线性地址空间描述的就是每个程序认为自己独享的虚拟地址空间;线性地址空间会被映射到某一部分物理地址空间或整个物理地址空间。一个硬件平台上可以有多个线性地址空间;

分页机制未启用时,线性地址与物理地址相同;当 CPU 使用分页机制时,需将线性地址转换成物理地址才能访问平台内存或其他硬件设备;

逻辑地址 / 段地址#

image

x86 早期分段机制的历史包袱;逻辑地址是程序直接使用的地址;由 16bit 段索引子 + 32bit 或 64bit offset 构成。下述语句中的指针变量 p 存储的就是变量 a 的逻辑地址的 offset,连接器和加载器会分配该 offset 所在的段索引子,CPU 从段寄存器或根据索引子查询映射表 Descriptor table 获得段对应的基址,段界限、权限等;然后将段基址 + offset 生成虚拟地址

int a = 100;
int *p = &a;

分段机制#

进程数据逻辑上划分为代码段、数据段和堆栈段等部分;程序用 Selector 标识每个逻辑段,用 offset 标识数据在段中的位置;

每个逻辑段映射到虚址空间 段基址 Base + 段界限 Limit 描述的内存块;程序访问内存数据时,要把变量的逻辑地址 Selector+offset 转换为线性地址 base+offset:
段寄存器缓存了 Selector→Descriptor 关系;Descriptor 中有段基址和各种属性,权限标志位;
1)进行属性、访问权限等检查;
2)从 DS 数据段寄存器后半的 Descriptor 部分获得该段 映射的虚址空间基址 base;
3)将变量的 Segment offset 和 该段在线性空间中的基址 base 相加,获得该变量线性地址

Segment register 缓存未命中:
进程加载阶段,在 LDT 的段映射关系数据结构创建好,且 LDT 地址在 GDT 注册后,该进程 LDT 的 Selector 索引 GDT,获得 LDT 的 Descriptor 描述的表地址,并将 Selector→Descriptor 关系缓存到 LDTR 寄存器。此外,该进程的 CS、DS、SS 被加载入相应 Selector ,CPU 根据 Selector 的 TI 字段索引相应 Descriptor table,获得相应 Descriptor ,并将 Selector→Descriptor 关系缓存到 CS、DS、SS 对应的段寄存器

image

Descriptor#

image

Descriptor 描述段的基址、长度及各种属性(如读 / 写、访问权限等):
1) Base 字段描述该段基址
2)Limit 字段描述该段长度
3)DPL 字段表示代码访问此段所需最低权限

当 CPU 获得段对应的 Descriptor 后,用 Descriptor 中各种属性字段对访问进行检查,一旦确认访问合法,CPU 将 Descriptor 中的 base(即 Segment 映射的虚址空间基址)和程序中逻辑地址的 offset 相加。获得逻辑地址所对应线性地址。

Selector & GDT/LDT#

image

Selector :
程序只存储和使用逻辑地址 offset 部分,Selector 是程序可见的逻辑地址组成部分,其修改和分配由连接器和加载器完成,用于索引 Descriptor table 获得段对应 Descriptor :
1)Index:Descriptor table 的索引。
2) TI:指明索引 GDT 还是 LDT:
a. TI=0 时,表示索引 Global Descriptor Table;
b. TI=1 时,索引 Local Descriptor Table
3)RPL:Requested Privilege Level,所要求的权限级别。RPL 存在于段选择寄存器的低 2 位,为程序访问段时增加一级检查。

GDT/LDT:
系统中至少有一个 global Descriptor table 可被所有的进程访问。系统中可有一或多个 local Descriptor table,可被某进程私有,可被多进程共享;它们只是 Selector →Descriptor 的映射表 ,多数是线性表实现:
1)GDT 在内存中的的位置由:基地址 Base 和表界限 Limit 描述。存放需多进程共享的段的映射关系,比如内核代码段、共享库段、内核数据段
2)LDT 存放不需进程间共享访问的私有段的映射关系,比如用户代码段、用户数据段、堆、栈等;LDT 在内存中的的位置由 GDT 中的 Descriptor 描述,系统中有多少个 LDT,GDT 中有多少个对应 Descriptor
3)GDTR 和 LDTR 寄存器:这两个寄存器帮助 cpu 快速找到 GDT/LDT 映射表:
a. GDTR:GDT 基址 Base + 表界限 Limit
b. LDTR:LDT 在内存中的的位置由 GDT 中的 Descriptor 描述;Descriptor 会缓存到 LDTR;所以 LDTR 结构与段寄存器相同(Selector +Descriptor )

通过 Selector 索引 GDT/LDT 的过程如下图所示:
Selector 的 TI 字段指明索引 GDT 还是 LDT;通过使用 LGDT/SGDT 指令对 GDTR 进行读写获取映射表地址;在进行进程切换时,LDTR 的值会被换成新进程对应的 LDTR

image

Segment register#

每次都通过 Selector 索引 GDT/LDT 查表获取逻辑段映射的虚址,这种类型的开销优化:空间换时间,咱已经很熟悉了,x86 提供 6 个 16 位 Segment register,每个 Segment register 就是在 Selector 后加一个 Descriptor register 缓存逻辑段映射虚址 Descriptor:
1) CS(Code-Segment,代码段):存放代码段的 Selector→Descriptor
2)DS(Data-Segment,数据段):存放数据段的 Selector→Descriptor
3)SS(Stack-Segment,栈段):存放栈的 Selector→Descriptor
4~6)ES、FS、GS:可以存放额外三个数据段的 Selector→Descriptor,由程序自由使用。

当 Segment register 加载一个新的 Selector 后,CPU 自动将该 Selector 索引的 Descriptor 缓存到不可见的 Descriptor register 中,也就是说,CPU 只有在更新段寄存器时才索引 Descriptor table (比如切换绑定线程时会更新段寄存器)

image

分页机制#

将虚拟地址空间分为多个页,物理地址空间划分也类似,减少了内存碎片;
分页机制允许在物理内存告急时将不常用的页移到磁盘的 swap space(如 Linux 的 Swap 分区,Windows 的虚拟内存文件),可认为是内存虚拟化机制的一项基础。

32bit 内地址总线的虚址空间为可编址内存$2^{32}B=4\cdot2^{10}\cdot2^{10}\cdot2^{10}B=4GB$;(64bit 内地址总线使用其中 48 位可编址内存 256TB)
x86 架构允许大于 4KB 的页大小(如 2MB、4MB);但 x86-32 页的典型大小为 $4KB=4\cdot2^{10} B$,4GB 内存可划分出 1024×1024 个页。
可见页表项数量与 $[内地址总线 (虚址空间大小) \cdot 进程数]$ 成正比

CPU 访问数据流程:
CPU 访问数据时,要将进程的虚拟地址(虚拟页 VFN + 偏移 offset)转换为实际物理地址(物理页 PFN + 偏移 offset);
1)查找 TLB:CPU 首先在 TLB 中查找该虚拟地址的物理地址。如果 TLB 命中,直接使用此物理地址进行访问。
2)查找 Page Table:如果 TLB Miss,CPU 根据虚拟地址中的各级页表索引,查找相应页表项。页表项存储着该虚拟页对应的物理页基址。
3)如果页表项中没有有效映射(如该页不在物理内存中),触发 Page Fault 中断。os 将页从磁盘的 swap space 加载到物理内存中,并更新 Page Tablex 相应页表项 PTE,设置 P 位为 1,并对其他字段进行相应的设置;最后从缺页错误处理程序中返回;CPU 重新查页表,把对应的映射插入到 TLB 中
4)PFN+offset,得到对应物理地址

image

TLB#

Translation Lookaside Buffer 缓存最近用到的页面映射(VFN→PFN),当 CPU 访问某线性地址时,如果其所在页的映射存在于 TLB 中,无须查页表,即可得到该线性地址对应的 PFN,CPU 再将 PFN 与线性地址的偏移相加,得到物理地址。TLB 未命中则查询 Page Table 并更新 TLB

Page Table#

image

Page Table 数据结构存储 Virtual Frame Number→Physical Frame Number 的映射关系;出于 TLB 命中率和每次缓存的页表大小考虑,Page Table 通常实现为多级页表;

x86-32 保护模式下,Page Table 实现为两级页表;这样每次缓存 TLB 时就不是缓存 1 张页表中的 $2^{20}$ 个页映射关系;而是缓存 $2^{10}$ 张页表中的一张,每张 $2^{10}$ 个页映射关系;CR3 指向一个 $2^{10}$ 个项的的页目录(每项 4B,页目录大小 4KB);每个页目录项又指向 1024 个 4KB 大小的页表。未启用 PAE 的 4KB 大小的页面如图所示。

image

4KB 页大小只需 12bit 索引 page offset;规定 TLB 一次缓存 2102^{10} 210 个页映射关系 PTE,即用 10bit 可编码一次缓存的页映射关系 PTE;剩下 10bit 自然编码 2102^{10} 210 个二级 Page Table;
很显然虚址的高 20bit,恰好编码索引了整个二级页表数据结构;
1)Page Table Entry:存储 VFN→ PFN 映射关系。次 10bit 编码索引了 2102^{10} 210 个 TLB 一次可缓存的页映射关系,存放在二级 Page Table 中,从 PTE 中获取 VFN→ PFN 映射关系,即可确定线性地址 VFN+offset 对应的物理地址 PFN+offset。

2)Page Directory Entry:高 10bit 编码索引了所有二级 Page Table,即 PDE 存储 10 位 2 进制编码 xxxx xxxx xx → 二级 Page Table 基址的映射关系;所有 PDE 存储在一级 Page Table;PDE 大小为 4B,页目录含 1024 个 PDE,占一个 4KB 的物理页。

page fault#

PDE 和 PTE 都包含一个 P(Present)字段
1)P=1,物理页在物理内存中,CPU 完成地址转换后可直接访问该页。
2) P=0 ,物理页不在物理内存,当 CPU 访问该页时,会产生 page fault 中断并跳转缺页处理程序处理,os 通常将存放在 swap space 的页调入物理内存,使访问可继续。由于程序的局部性特点,os 会将该页附近的页一起调入物理内存,方便 CPU 访问。

物理地址扩展 PAE#

启用 PAE 后,各级页表大小仍是 4KB,但页映射关系表数据结构实现为三级页表;各级页表项从 32 位扩为 64 位,以使用附加的地址位。拿出 2bit 编码 1 级页表;9bit 编码 2 级页表;9bit 编码 3 级页表;这样,2,3 级页表都只有 $2^{9}=512$ 个映射数据,是 2 级页表方案的一半:而 CR3 指向 1 级页表基址,1 级页表是一个包含 4 个项的表。

image

PDBR/CR3 Register#

Page-Directory Base Register 或称 CR3 Register 存放着顶级页表的物理地址。一个进程在运行前,必须将其顶级页表基地址存入 CR3,且顶级页表基址必须对齐到 4KB 页边界

64 位子模式内存管理改进#

地址空间:
使用 64 位线性地址,但实际限制了有效虚拟地址空间为 48 位,高 48~63 位是相同的,程序可访问 $2^{48} B=256TB$ 虚拟地址内存空间;对于物理地址空间,实际限制有效地址空间为 52 位,

分段机制未被禁用但作用被弱化:
cpu 不再用段基址进行地址转换,CS、DS、ES、SS 等段寄存器基址置 0。 FS 和 GS 段寄存器,仍存储有段基址,可用于某些特定操作,如本地数据寻址和 os 内部数据结构管理(如线程局部存储)。此外,段长度检查被禁用,即 cpu 不会对段大小进行检查。

分页机制优化:
内存页大小可以是 4KB、2MB、1GB。PAE 必需开启,开启后 os 使用四级页表 —— 附加了一个第四级页面映射表 Page-Map Level 4 Table,简称 PML 4 Table。帮助将将 48 位线性地址转换为 52 位物理地址。PML4 的物理基地址存在 CR3 中;
PML4 项:含一个 PML3 的物理基址、访问权限和内存管理信息。
PML3 项:含一个 PML2 的物理基址、访问权限和内存管理信息。
PML2 项:含一个 PML1 的物理基址、访问权限和内存管理信息。
PML1 项:含一个 VDN→PFN、访问权限和内存管理信息

image

中断 & 异常架构#

子类别原因异步 / 同步返回行为举例
中断来自 I/O 设备的信号异步总是返回到下一条指令外部设备的响应请求,如敲击键盘
异常错误潜在可恢复的错误同步可能返回到当前指令不一定是致命性错误,如缺页错误
陷阱有意的异常同步总是返回到下一条指令请求系统调用,如文件读取
终止不可恢复的错误同步不会返回致命性错误,如奇偶错误

中断架构#

一些异常和中断会打断顺序执行的指令流,转而进入一条完全不同的执行路径。现代计算机架构是由大量的中断事件驱动的。中断机制使外部硬件设备可以打断 CPU 当前的执行任务,使 CPU 为自己提供服务;中断从设备经由 “中断控制器” 转发给 CPU(MSI 除外)。

PIC(Programmable Interrupt Controller)#

可编程中断控制器 PIC 是最早被广泛应用的中断控制器,PIC 只能在 UP(单处理器)平台上工作,无法用于 MP(多处理器)平台。具有 8 个中断管脚(IR0~IR7),连接外部设备;当外部设备需要 CPU 处理时,通过对应中断线发送信号,触发中断。

主要寄存器:
1)IRR(Interrupt Request Register): 记录当前请求的中断。如果外部设备发出 IR 中断请求且该请求没被屏蔽,在对应 IRR 中相应的位置置 1
2)ISR(In Service Register): 记录当前正在处理的中断。当 CPU 响应并开始处理中断时,ISR 相应中断位置 1。
3)IMR(Interrupt Mask Register): 屏蔽特定中断线。如果 IMR 中对应中断位置 1,对应 IR 管脚的中断请求将被屏蔽,CPU 不会响应该中断。

中断处理流程:
1)外部设备发出中断信号,如果该中断没有被屏蔽,IRR 中对应的位被置为 1。
2)PIC 通过 INT 管脚通知 CPU 有中断发生。
3)CPU 通过 INTA(中断应答)管脚响应 PIC,表示已收到中断请求。
4)PIC 在收到 CPU 的应答后,会清除 IRR 中优先级最高的中断请求,并将 ISR 相应的位设置为 1,表示中断正在处理中。
5)CPU 再通过 INTA 发出第二次脉冲,PIC 会根据优先级提供相应的中断向量并将其送到 CPU 的数据总线上。
6)CPU 处理完中断后,通过写 EOI(End of Interrupt)寄存器告知 PIC 中断处理完成,PIC 会清除 ISR 中相应的位。

APIC(Advanced Programmable Interrupt Controller#

image

多处理器(MP)系统中,APIC 系统使得多个 CPU 能够协调工作、共享中断处理的任务。各 CPU 的 LAPIC 可以通过 IPI 机制进行通信和协作。这种中断控制机制不仅支持高效的中断处理,还能优化多核处理器系统的中断平衡。

组成:
1)LAPIC (Local APIC):每个 CPU 内核都配备有一个 LAPIC,负责处理本地中断信号。它通过 IRR(Interrupt Request Register)存储当前中断请求的状态,ISR(Interrupt Service Register)存储已处理的中断标志,CPU 通过写 EOI(End of Interrupt)寄存器来告诉 LAPIC 该中断已经处理完毕,允许其他中断的处理。
2)IOAPIC (I/O APIC):通常位于南桥芯片(或称低速 I/O 控制器中),负责接收外部设备中断请求,并将它们转发给特定的 CPU 的 LAPIC。IOAPIC 通常有多个中断输入管脚(通常为 24 个),每个管脚可以连接外部设备。

处理器间中断(IPI):IPI (Inter-Processor Interrupt) 允许一个 CPU 向其他 CPU 发送中断信号。这对于多核系统中的进程迁移、负载平衡和 TLB 刷新等操作非常重要。通过 LAPIC 的ICR(Interrupt Command Register),系统可以指定目标 CPU 并发送 IPI 中断。

中断传递过程:
1)当外部设备触发中断时,IOAPIC 接收到中断请求信号。IOAPIC 使用 **PRT(Programmable Redirection Table)来查找中断请求的路由信息。
2)根据 PRT 的配置,IOAPIC 将中断信息格式化为
RTE(Redirection Table Entry)** 并传递给系统总线。
3)系统总线将中断消息发送给目标 CPU 的 LAPIC。
4)LAPIC 收到中断消息后,将根据 IRR 寄存器中的信息决定是否进行处理。如果该中断符合处理条件,LAPIC 将触发 ISR 寄存器,最终将中断交给处理器。

异常架构#

中断由外部设备产生,和 CPU 当前执行的指令无关。异常由 CPU 内部产生,其原因是 CPU 当前执行的指令出了问题。

异常产生原因按严重性划分:
1)错误(Fault):由某种错误情况引起,一般可以被错误处理程序纠正。错误发生时,处理器将控制权交由对应的处理程序。前面所讲的缺页错误就属于此类。
2)陷阱(Trap):指在执行了一条特殊指令后引起的异常。陷阱是有意的异常,陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口(即系统调用)。Linux 中用于实现系统调用的 INT 80 指令就属于此类。
3)终止(Abort):指严重的不可恢复的错误,将导致程序终止。典型的是一些硬件错误。

IDT#

CPU 可通过中断描述符表 Interrupt Descriptor Table 查找中断或异常 vector 号对应的处理程序入口线性地址;
IDT 数据结构存储 vector→Descriptor 的映射关系;Descriptor 中有处理例程的所在段的Selector和段内 offset,及各种属性标志位;
IDT 表基址存放在IDTR寄存器中:
1)Base:IDT 基址
2)Limit:IDT 表的最大字节数(通常为 8 字节对齐)

Interrupt Gate & Trap Gate#

Interrupt Gate 是 IDT 中的 Descriptor;描述发生特定中断或异常时,应跳转到哪个中断服务例程 ISR。其格式与 segment Descriptor 类似 (但也有一些特殊属性);为加以区分被称为 System Descriptor,它的 S 位(Segment type)用于区分是 segment Descriptor 还是 System Descriptor
1)Offset:指向中断服务例程 ISR 的地址;两个 offset 分别表示高 16bit 和低 16bit
2)Selector:指明 ISR 所在的代码段
3)Type:Interrupt Gate 类型,决定 Interrupt Gate 如何响应中断
4)DPL(Descriptor Privilege Level):指示 Interrupt Gate 的特权级;中断门和陷阱门的 DPL 只在使用 INT n 指令引起中断或异常的时候才进行检查,硬件产生的中断或异常不检查。
5)S 位(Segment Descriptor):控制该 Descriptor 是 segment Descriptor 还是 System Descriptor;
6)P (present):代表该中断门是否有效,置 0 表示无效

Trap Gate 跟 Interrupt Gate 类似 (如下,格式完全一样),但它用于异常处理。中断门和陷阱门唯一的区别是程序通过中断门跳转后,EFLAGS 寄存器的 IF 位自动清零,中断关闭。而陷阱门没有这样的效果。

I/O 架构#

计算机所处理的任务其实只有两种:CPU 运算和 I/O 操作。I/O 是 CPU 访问外部设备的方法。设备通常通过寄存器和设备 RAM 将自身功能展现给 CPU,CPU 通过读 / 写设备寄存器和 RAM 完成对设备的访问及其他操作。

在现代计算机架构中,Port I/O 逐渐被淘汰,特别是在大多数 x86 系统中,MMIO 成为主流的 I/O 访问方式,几乎所有的外部设备(如显卡、网卡、存储控制器等)都通过 MMIO 进行通信。尽管 MMIO 提供更好的性能,但对于一些简单的 I/O 设备(如低速的串行端口),Port I/O 可能仍有一定优势,故还是介绍

Port I/O#

x86 为Port I/O分配了 65536 个 8bit 的 I/O 端口,Port I/O地址空间从 0x0000 到 0xFFFF,共 64KB。I/O 端口地址空间是独立的,不是线性地址空间或物理地址空间的一部分。cpu 通过 I/O 端口地址访问设备寄存器

访问方法:cpu 执行 IN 指令将数据从指定的 I/O 端口读取到寄存器中,而 OUT 指令则将数据从寄存器写入指定的 I/O 端口。比如,IN AX, 0x60 会将地址为 0x60 的 I/O 端口的数据读入到 AX 寄存器中。

CPU 会通过一个特定的信号(比如 I/O 信号)来区分 I/O 操作和内存操作。2 个或 4 个连续的 8 位 I/O 端口可以组成 16 位或 32 位的 I/O 端口;
限制:端口 I/O 的最大缺点之一是速度相对较慢,因为每个 I/O 端口都要通过一个独立的路径进行访问,不同设备可能需要通过中断或轮询来处理。这使得端口 I/O 在现代计算机系统中的使用逐渐被 MMIO 取代,尤其是在需要高效数据交换的场景中。

MM I/O#

  • *Memory map IO:** 设备寄存器或内存区域被映射到 物理地址空间。通过内存访问指令(如 MOV)访问设备寄存器或设备 RAM ,而不需特殊的 I/O 指令。

由于现代 CPU 和系统都支持高效的内存访问,MMIO 能够提供比端口 I/O 更高的访问速度和更大的带宽。且由于它使用标准的内存访问指令,简化了编程和硬件设计。

访问方法:如某设备的寄存器被映射到内存物理地址 0xA0000,那么执行 MOV AX, [0xA0000] 就可读取该设备寄存器内容。

因 IO 寄存器状态反应的是外部设备状态,MMIO 地址区域通常不通过 CPU 的缓存进行优化,因为可能导致缓存不一致;尤其是不能缓存到 TLB(Translation Lookaside Buffer,地址转换后备缓冲)。这意味着 MMIO 操作的访问可能会受到更高延迟的影响,尤其是在访问频繁的 I/O 设备时。

DMA#

直接内存访问 Direct Memory Access 允许设备绕开 CPU 直接向内存中复制或读取数据。如果设备向内存复制数据都经过 CPU,则 CPU 会有大量中断负载,中断过程中, CPU 对其他任务来讲无法使用,不利于系统性能的提高。通过 DMA,CPU 只负责初始化,而传输动作由 DMA 控制器 DMAC 进行。

DMA 传输过程:
在实现 DMA 传输时,由 DMAC 直接控制总线,在 DMA 传输前,CPU 要把总线控制权交给 DMAC,结束 DMA 传输后,DMAC 立即把总线控制权交回给 CPU。
1)DMA 请求:CPU 对 DMAC 进行初始化,并向 I/O 端口发出操作命令, I/O 端口提出 DMA 请求。
2)DMA 响应:DMAC 对 DMA 请求进行优先级判别和屏蔽判别,然后向总线裁决逻辑提出总线请求。CPU 执行完当前总线周期后释放总线控制权。此时,总线裁决逻辑发出总线应答,表示 DMA 已被响应,并通过 DMAC 通知 I/O 端口开始 DMA 传输。
3)DMA 传输:DMAC 获得总线控制权后,CPU 即可挂起或只执行内部操作,由 DMAC 发出读 / 写命令,直接控制 RAM 与 I/O 端口进行 DMA 传输。
4)DMA 结束:当完成规定的成批数据传送后,DMAC 释放总线控制权,并向 I/O 端口发出结束信号。当 I/O 端口接收到结束信号后,停止 I/O 设备的工作并向 CPU 提出中断请求,使 CPU 执行一段检查本次 DMA 传输操作正确性判断的代码,并从不介入的状态退出。

DMA 无须 CPU 直接控制传输,也没有中断处理方式那样保留现场和恢复现场的过程,通过硬件(DMAC)为 RAM 与 I/O 设备开辟了一条直接传送数据的通路,极大地提高了 CPU 效率。需要注意的是,DMA 操作访问的必须是连续的物理内存

image

时钟#

os 中的很多事件都是由时钟驱动的,如进程调度、定时器等。

1) 周期性时钟 Periodic Timer:最常见,时钟以固定频率产生时钟中断。周期性时钟通常会有一个计数器,要么以固定值递减到 0 产生中断,如 PIT(Programmable Interrupt Timer,可编程中断时钟);要么固定增长,当达到某个阈值时产生中断,同时自动将阈值增加一个固定值,计数器继续递增,如 HPET(High Precision Event Timer,高精度时间时钟)。

2)单次计时时钟 One-shot Timer:大多数时钟都可配置成这种方式,如 PIT、HPET。其工作方式和到达阈值产生中断的周期性时钟类似,只是产生中断后阈值不会自动增加,而是需要时钟中断处理函数等软件来增加该阈值。这为软件提供了动态调整下一次时钟中断到来时间的能力。

x86 提供了多种时钟,包括 PIT、RTC(Real Time Clock,实时时钟)、TSC(Time Stamp Counter,时间戳计数器)、LAPIC Timer 和 HPET 等。os 可根据需要使用其中的一种或多种时钟,但是同时使用多个时钟将带来过多的时钟中断,从而影响系统的性能。在有高精度时钟可用的时候,现代操作系统往往禁用低精度的时钟,并根据需要使用高精度的时钟模拟低精度的时钟。

X64 Register#

general register#

image

兼容的 x86-32 通用寄存器在 函数调用过程 保存临时变量 及操作栈的指针等
eax算术累加器,通常用来执行加法,函数调用的返回值一般也放这
ebx数据存取
ecx通常用作计数器,如 for 循环
edxI/O 指针
esi字符串操作时,用于存放目的地址,和 edi 常搭配使用,执行字符串复制等操作
edi字符串操作时,用于存放数据源地址
esp栈指针寄存器 Stack Pointer:指向栈顶
ebp基址寄存器 Base Pointer:栈底指针,指向栈底,通常用 ebp + 偏移量的形式定位函数存放在栈中的局部变量
r8d~r15dx64 扩张的 8 个寄存器的低位

x64 架构中,上面的通用寄存器扩展为 64 位版本,名字也进行了升级。为兼容 32 位模式程序,使用上面的名字仍是可访问的,相当于访问 64 位寄存器的低 32 位。

64bit general register功能描述
rax通常用于存储函数调用返回值
rsp栈顶指针,指向栈的顶部
rdi第一个入参
rsi第二个入参
rdx第三个入参
rcx第四个入参
r8第五个入参
r9第六个入参
rbx数据存储,遵循 Callee Save 原则
rbp数据存储,遵循 Callee Save 原则
r12~r15数据存储,遵循 Callee Save 原则
r10~r11数据存储,遵循 Caller Save 原则

寄存器传参速度快,可减少对内存的读写次数。在编译生成 CPU 指令时决定用栈还是用寄存器传参数;

32 位时代通用寄存器少,函数调用时,参数大多数时候是通过线程的栈来传递(也有用寄存器传递的,如 C++ this 指针使用 ecx 寄存器传递);

x64 时代,寄存器资源富裕了,参数传递绝大多数都用寄存器来传。

status register/RFLAGS#

有翻译为标志寄存器的;里面有众多标记位,记录了 CPU 执行指令过程中的一系列状态,这些标志大都由 CPU 自动设置和修改;x64 架构下,原来的 eflags 寄存器升级为 64 位的 rflags,不过其高 32 位并没有新增什么功能,保留为将来使用。

image

status register bit功能描述
CF进位标志
PF奇偶标志
ZF零标志
SF符号标志
OF补码溢出标志
TF跟踪标志
IF中断标志

instruction pointer/RIP#

指针 / 指令寄存器的;eip 存放下一条要执行指令地址,CPU 的工作就是不断取出eip指向的指令,然后执行这条指令,同时指令寄存器继续指向下一条指令,如此不断重复。x64 架构下,32 位 eip 升级为 64 位的rip寄存器。
在漏洞攻击中,黑客费尽心机修改指令寄存器存放的下一条指令地址,从而能够执行恶意代码。

这不就是 PC 指针吗?

内存管理寄存器#

描述符表寄存器#

GDTRGDT 地址
LDTRLDT 地址

Segment registers#

Segment registers存储逻辑段 selector→descriptor 映射
cs代码段
ds数据段
ss栈段
es扩展段
fs数据段
gs数据段

控制 Register#

可用于管理和检查 CPU 的状态;这些寄存器决定了 CPU 运行的模式和特征等。x86-64 的 64 位模式引入了 CR8,它被定义为任务优先级寄存器(TPR),操作系统能够基于中断的优先级别使用 TPR 来控制是否允许外部中断来中断处理器。

NamePurpose
CR0Basic CPU operation flags
CR1Reserved
CR2Page-fault linear address
CR3Virtual addressing state
CR4Protected mode operation flags
CR5Reserved
CR6Reserved
CR7Reserved
CR8Task priority register (TPR)
CR9Reserved
CR10Reserved
CR11Reserved
CR12Reserved
CR13Reserved
CR14Reserved
CR15Reserved

Debug registers/DR#

一组用于支持软件调试的寄存器;程序能够被调试,关键在于能够被中断执行和恢复执行,被中断的地方就是我们设置的断点。
解释执行(PHP、Python、JavaScript)或虚拟机执行(Java)的高级语言,这很容易办到,因为它们的执行都在解释器 / 虚拟机的掌控之中。对 C、C++ 这样的 “底层” 语言,代码编译成 CPU 机器指令执行,这就需要 CPU 提供调试支持

任务 Register#

Model Specific Register/MSR#

机器检查寄存器#

性能监控寄存器#

内存区域类型寄存器(MTRR)#

流 SMID 扩张 (SSE) Register#

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。