x86架构的每个历史决策就像地质层一样累积在现代编程实践中

x86架构的历史演变

  1. 早期阶段:
    • 8086/8088(16位):x86架构的诞生
    • 80186/80188:集成外设控制器
    • 80286:引入保护模式
  2. 32位时代:
    • 80386:首个32位x86处理器,引入虚拟内存支持
    • 80486:集成浮点运算单元
    • Pentium系列:引入超标量架构
  3. 64位扩展:
    • AMD64/x86-64:AMD率先推出64位扩展
    • 多核时代:Core架构、Nehalem、Sandy Bridge等微架构演进
  4. 现代扩展:
    • SIMD指令集(MMX、SSE、AVX)
    • 虚拟化支持(VT-x)
    • 安全扩展(SGX、TXT)

x86汇编语言的核心设计哲学

  1. 向后兼容性:
    • 设计原则:新处理器必须能够运行为旧处理器编写的代码
    • 实现方式:保留所有旧指令,通过模式切换(实模式、保护模式、长模式)支持新特性
    • 代价:指令集日益复杂,解码器负担加重
  2. CISC(复杂指令集计算机)哲学:
    • 丰富的指令集:从简单数据移动到复杂字符串操作
    • 内存操作数:允许指令直接操作内存(不同于RISC的load/store架构)
    • 变长指令:指令长度从1到15字节不等
  3. 寄存器-内存架构:
    • 有限的通用寄存器(早期只有8个)
    • 寄存器特殊化(如EAX用于累加,ECX用于计数)
    • 现代x86-64扩展到16个通用寄存器
  4. 渐进式演进:
    • 通过扩展而非革命性改变引入新特性
    • 例子:从16位到32位到64位的平滑过渡
    • 新指令集的层叠式添加(MMX→SSE→AVX)

x86微架构的哲学

外CISC,内RISC:保持兼容性,但内部高度优化

  • 简单指令(如ADD eax, ebx)可能对应1个μop
  • 复杂指令(如PUSHAD)可能被拆解为多个μops(如8个PUSH操作)
  • 内存操作指令(如ADD [mem], eax)会被拆解为LOAD→ALU→STORE三个μops

并行至上:乱序执行、超标量、多核

◉ 动态指令调度

顺序编程模型,但通过乱序执行提升并行度:

  • 重排序缓冲区(ROB, ReOrder Buffer):记录所有未完成的μops
  • 保留站(RS, Reservation Station):等待操作数就绪的μops队列
  • 寄存器重命名(Register Renaming):解决WAW(写后写)和WAR(写后读)冒险

◉ 推测执行

  • 分支预测(Branch Prediction):预测跳转方向,提前执行代码
  • 错误预测恢复:通过检查点(Checkpoint)机制回滚错误路径

◉ 多级并行化:超标量与多核

超标量流水线

CPU每周期可发射多条μops:

  • 多端口执行单元:如4个ALU、2个LOAD/STORE单元
  • 宏融合(Macro-Op Fusion):合并CMP+JCC为单个μop

多核与超线程(SMT)

  • 物理核心(P-core):完整乱序执行能力(如Golden Cove)
  • 效率核心(E-core):简化流水线(如Gracemont)
  • 超线程(Hyper-Threading):共享执行单元,提高利用率

能效平衡:动态频率、混合架构

◉ 动态频率调整

  • Turbo Boost:短时超频(如5GHz)
  • DVFS(动态电压频率调整):低负载时降频

◉ 混合架构

  • Intel :P-core(性能核)+ E-core(能效核)
  • AMD Zen:统一架构,但优化能效比

安全增强:硬件虚拟化、内存隔离

◉ 硬件虚拟化

  • VT-x(Intel) / AMD-V:减少虚拟机监控(VMM)开销
  • EPT(Extended Page Tables):加速地址转换

◉ 安全扩展

  • SGX(Software Guard Extensions):可信执行环境
  • CET(Control-Flow Enforcement):防御ROP攻击

汇编语法与汇编器概览

汇编语言是程序员与底层硬件之间最直接的桥梁。掌握汇编语法和寄存器的使用,是理解操作系统、编译器原理、嵌入式系统乃至 BIOS、bootloader 的关键

汇编语言有两种主要语法风格:Intel 语法AT&T 语法。不同的汇编器工具链采用的语法不同

汇编器 默认语法 支持平台 特点
MASM Intel Windows (x86/x64) 微软官方汇编器,集成于 Visual Studio,适合 Windows 内核驱动、DLL 编写,支持丰富宏
NASM Intel 跨平台 免费开源,强调可读性、结构清晰,适合裸机开发、Bootloader、UEFI 编程
GNU GAS AT&T(默认) Linux/Unix GNU Binutils 的一部分,Linux 内核使用的汇编器,可通过 .intel_syntax 切换为 Intel 风格

推荐选择

  • 编写 Bootloader 或操作系统:NASM
  • Linux 内核模块或内核源码分析:GAS
  • Windows 内核驱动或反汇编分析:MASM

Intel 与 AT&T 语法对比

特性 Intel 语法 AT&T 语法
操作数顺序 目标, 源(如 mov eax, 1 源, 目标(如 movl $1, %eax
寄存器前缀 无前缀:eax, ebx % 前缀:%eax, %ebx
立即数前缀 无前缀:1, 100h $ 前缀:$1, $0x100
内存访问 使用方括号:[eax+4] 使用圆括号:4(%eax)
指令大小标识 隐式(根据寄存器大小推断) 显式后缀:b(byte)、w(word)、l(long)、q(quad)

《x86汇编语言-从实模式到保护模式》 从零实现字符输出、读盘、BIOS 编程,非常适合系统编程爱好者

王爽《汇编语言》:经典教材,讲解清晰,但基于 MASM,部分 DOS 实验不再适用,仅作入门参考

段机制

8086是16位CPU,16位地址/寻址空间,216=65535字节,也就是64KB

◉ 8086 地址空间需求

8086设计目标是支持 1 MB(220 = 1,048,576 字节) 内存地址空间,需要 20 位地址总线才满足需求

8086 寄存器(AX、BX、CX、DX 等)单个寄存器只能存储 16 位地址,为了突破最大寻址范围 64 KB限制,引入 分段内存管理机制

段寻址的思想就是利用一个寄存器作为段的选择器,往这个寄存器中写值就是在选择段,最终的地址为段选择器的值左移4位(乘以16)得到20位的段基址,加上指令中给定的16位物理内存地址,最终得到20位的内存地址,即1MB的寻址空间

8086 之前的 4 位和 8 位机(如 Intel 8008、8080 或 Z80)时代,程序直接操作物理内存,缺乏现代内存管理机制

通过具体示例和解释说明:

◉ 实模式的“裸奔”内存访问

  • 问题:8086 下程序直接操作物理地址(CS:IPDS:SI 等),无任何硬件级隔离

  • 后果:

    ; 8086 实模式下,程序可随意破坏系统内存
    mov ax, 0x0000
    mov ds, ax
    mov [0x0000], 0x1234  ; 覆盖中断向量表!

    程序必须知道数据的确切物理地址,如果硬件布局改变(如内存芯片地址变化),程序无法运行

◉ 多任务无法实现

  • 若两个程序同时使用 0x1000:0x2000 物理地址,必然冲突
  • 程序员需手动分配内存段,毫无扩展性

◉ 重定位困难

  • 程序若加载到不同内存区域,需修改所有硬编码地址(如 jmp 0x1234

◉ 结构丑陋

代码示例对比

  • 8位机(8080)的丑陋结构:
ORG 0x1000      ; 必须指定程序加载的物理地址
Main:
    LDA 0x1234  ; 硬编码地址
    CALL 0x2000 ; 硬编码子程序地址
    HLT

ORG 0x2000      ; 子程序必须固定在 0x2000
Subroutine:
    RET

8086的段结构改进:

; 代码段和数据段可独立重定位
MOV AX, [DATA_OFFSET] ; 偏移地址由段寄存器动态计算
JMP NEAR LABEL        ; 相对跳转,支持重定位

问题总结

  • 固定布局:代码、数据、栈必须预先分配固定地址
  • 无抽象:程序员需手动管理物理内存,代码难以复用

◉ 总结

8086 引入的段机制(CS、DS 等段寄存器)通过将物理地址计算为 段基址 << 4 + 偏移量,实现了:

  1. 重定位:程序只需修改段寄存器即可运行在不同内存区域
  2. 隔离性:不同程序可独占段空间
  3. 结构化:代码、数据、栈分段管理,逻辑清晰

而 4/8 位机的直接物理地址访问方式,导致代码僵硬、不安全且难以扩展

◉ 后续发展

80286 引入 保护模式

80286 地址总线扩展到 24 位,80386 32 位

现代 CPU 采用 平坦内存模型,不再需要分段机制,用分页机制管理内存

寄存器

x86 架构寄存器包含三大类:通用寄存器段寄存器控制/状态寄存器(如 EFLAGS)

◉ 通用寄存器

数据寄存器

名称 宽度 含义 子寄存器 用途
RAX 64 位 通用累加器 EAX, AX, AH, AL 返回值、算术乘除默认寄存器
RBX 64 位 通用基址寄存器 EBX, BX, BH, BL 数据段地址指针
RCX 64 位 循环计数器 ECX, CX, CH, CL 循环次数(LOOP)、REP 指令
RDX 64 位 数据寄存器 EDX, DX, DH, DL 与 EAX 联用于乘除法等

✅ 各子寄存器共享物理空间。例如 ALAX 的低 8 位,写入 AL 会影响 AXEAX

指针/变址寄存器

名称 含义 用途
ESI 源索引寄存器(Source) 串指令源地址
EDI 目标索引寄存器(Dest) 串指令目标地址
EBP 基址指针寄存器 函数参数、局部变量访问(栈帧)
ESP 堆栈指针寄存器 指向栈顶

✅ 在函数调用中,EBP 用于固定函数栈帧结构,而 ESP 自动增长/缩减

◉ 段寄存器

名称 说明 默认用途
CS 代码段 指向当前指令所在代码段
DS 数据段 常规数据存储的默认段
SS 栈段 栈操作默认使用的段
ES 附加段 串操作中的目标段(配合 EDI
FS 附加段 Windows/线程局部存储使用
GS 附加段 Linux/内核线程使用,如 GS:0x0

现代 x86-64 处理器中,段寄存器本身依然是 16 位的没有变。但需要注意的是:它们的作用和意义在保护模式与长模式(64 位模式)下已经发生了巨大变化

实模式(16位)

段寄存器(如 CS, DS, SS, ES, FS, GS)是 16 位值

和 16 位的偏移地址组合,形成一个 20 位的物理地址:

物理地址 = 段值 « 4 + 偏移

保护模式(32 位)

段寄存器仍是 16 位,但它不再表示直接的段基地址

它表示一个段选择子(Segment Selector),用于在 GDT(全局描述符表)或 LDT(局部描述符表)中查找一个段描述符

段描述符中才包含段的真正基地址、段限长、权限等信息

长模式(x86-64,64 位)

现代的 64 位处理器(进入 long mode 后):

  • 大多数段寄存器(CS, DS, ES, SS)仍是 16 位的选择子,但:
    • 它们的作用基本被“平坦内存模型”取代
    • 除了 FSGS 外,其他段寄存器的基地址通常被忽略或默认为 0
  • 地址寻址不再依赖段寄存器,直接使用 64 位线性地址(分页机制主导地址转换)
  1. FSGS 在现代 CPU 中的作用
  • 在 64 位模式下,FSGS 被保留用于访问 线程局部存储(TLS)或内核结构体
  • 它们的基地址不再来自 GDT/LDT,而是从 MSR(Model Specific Register)中读取
    • 例如 Linux 下的 GS base 用于 per-cpu data
    • Windows 下的 FS base 指向 TEB(线程环境块)

◉ 状态寄存器(FLAGS)

标志位 含义
ZF 零标志(Zero)
CF 进位标志(Carry)
SF 符号标志(Sign)
OF 溢出标志(Overflow)
PF 奇偶标志(Parity)
DF 方向标志(Direction)
IF 中断标志(Interrupt)

✅ 这些标志常用于跳转判断、字符串操作自动增减方向等控制

CPU 根据指令的执行结果,自己操作这个寄存器

◉ SP 和 BP

SPBP 的段地址默认位于 SS(堆栈段)

SP 指向栈顶元素的地址,具备自动加减的能力;而 BP 没有自动加减功能,但可以用来定位栈中某个具体元素的物理地址,方便访问函数参数和局部变量

变址寄存器 DISI 可以与 BXBP 联用:

  • 当与 BX 联用时,段地址默认在 DS(数据段)
  • 当与 BP 联用时,段地址在 SS(堆栈段)

DISI 也可以单独使用,单独使用时段地址默认在 DS 中。如果需要跨段访问,可以通过加上段前缀来实现

在字符串指令操作中,SIDS 联用,确定源操作地址;DIES(附加段寄存器)联用,确定目标操作地址。简单来说,就是分别寻址数据段和附加段。在这些字符串指令中,SIDI 还具有自动增减的功能,方便顺序访问内存

A20

经典历史问题,涉及硬件设计和软件兼容性

地址翻译后理论最大 物理地址=(0xFFFF×16)+0xFFFF=0xFFFF0+0xFFFF=0x10FFEF

20 位寻址范围 00000hFFFFFh ,计算出的物理地址超过 20 位,高位进位截断,导致地址“回绕”(Wrap-around)

计算出的物理地址为 0x10FFEF,实际访问地址为 0x10FFEF & 0xFFFFF = 0x0FFEF

通过设置段寄存器和偏移寄存器访问超出 1 MB 的地址空间(100000h10FFEFh)。会被自动回绕到 00000h0FFEFh 范围内


80286可寻址 224=16MB 内存空间。然而为保持与 8086 兼容性,需要解决地址回绕的问题

  • 8086 上,访问 0x100000 会回绕到 0x00000
  • 80286 上,访问 0x100000 应该访问实际的 0x100000,而不是回绕到 0x00000

解决方案:引入 A20 地址线(第 21 根地址线),控制是否启用地址回绕

  • A20 禁用(A20 = 0),地址回绕行为与 8086 一致
  • A20 启用(A20 = 1),地址不回绕,可以访问超过 1 MB 的内存空间

意义体现

  1. 兼容性:实模式下禁用 A20 可模拟 8086 地址回绕行为,确保旧软件正常运行
  2. 扩展性:保护模式下启用 A20 可访问超过 1 MB 内存空间,充分利用 80286 及后续处理器的地址总线扩展

保护模式不打开A20

21位恒置0,可得到地址线宽度所决定的地址空间范围内任意的奇数兆段的地址,如1M(00000hFFFFFh),3M(200000h2FFFFFh),5M(400000h~4FFFFFh),但却得不到偶数兆段的地址

所以进保护模式之前都要习惯性的开启A20,A20的相关电路虽然不是在CPU内部,但与CPU关系密切

16 位向 32 位过渡关键设计

◉ 保护模式

  • 32 位地址空间: 232=4GB内存地址空间
  • 分段和分页机制:更灵活的内存管理方式
  • 内存保护:Ring 0 ~ 3和段描述符,防止程序访问非法内存区域
  • 多任务支持:任务状态段(TSS)实现任务切换

◉ 32 位寄存器和指令集扩展

  • 32 位寄存器和扩展指令集

  • 新的寻址模式:如基址加变址加偏移量寻址,增强内存访问灵活性

◉ 分页机制

  • 虚拟内存:物理内存划分固定页(通常 4 KB),通过页表映射到虚拟地址空间
  • 地址转换:通过页目录和页表实现虚拟地址到物理地址的转换
  • 页面保护:通过页表项中的权限位控制内存访问权限

内存隔离、共享和保护,现代操作系统的核心特性之一

◉ 虚拟 8086 模式(Virtual 8086 Mode)

  • 兼容性:32 位保护模式下运行 16 位实模式程序
  • 内存隔离:每个虚拟 8086 任务运行在独立的地址空间中,避免相互干扰
  • 中断和异常处理:通过保护模式的中断机制处理虚拟 8086 任务的中断和异常

为过渡期的软件提供了平滑的迁移路径

◉ 任务状态段(Task State Segment, TSS)

  • 任务切换:通过 TSS 保存和恢复任务的上下文(如寄存器状态、段选择子等)
  • 权限控制:TSS 中包含任务的权限级别和堆栈指针,支持任务间的隔离和保护

为操作系统的多任务调度提供了硬件支持,是实现多任务操作系统的关键机制

◉ 中断描述符表(Interrupt Descriptor Table, IDT)

管理中断和异常处理

  • 保护模式中断:通过 IDT 定义中断和异常的处理程序
  • 权限控制:中断处理程序的权限级别由 IDT 中的描述符指定

为操作系统提供了统一的中断和异常处理机制,增强了系统的稳定性和安全性

◉ 浮点运算单元(FPU)

80387 是 80386 的浮点协处理器,用于加速浮点运算

  • 浮点指令集:支持单精度和双精度浮点运算
  • 扩展精度:支持 80 位扩展精度浮点数

FPU 显著提升了处理器的科学计算能力,为图形处理、工程计算等应用提供了支持

◉ 高速缓存(Cache)

80386 开始引入高速缓存机制,用于加速内存访问

  • 一级缓存(L1 Cache):集成在处理器内部,提供高速数据访问
  • 缓存一致性:通过缓存一致性协议(如 MESI)保证多处理器系统中的数据一致性