第2章:x86架构基础

各位同学,今天我们来聊聊x86架构的基础。说实话,x86这个架构已经陪伴我们几十年了,从最初的8086到现在的Zen系列,它一直在进化。我个人觉得,理解x86的根基,是成为嵌入式系统专家的必修课。

2.1 x86指令集发展史

x86的故事要从1978年说起。那一年,Intel推出了8086处理器。你可能觉得它很古老,但它的指令集骨架,至今还在你的电脑里运行。

我刚开始做嵌入式时,总觉得x86太复杂。后来我意识到,这种复杂性是有原因的——它要向后兼容。你想想看,一个1978年的程序,理论上还能跑在2024年的处理器上,这本身就是个奇迹。

来看看几个关键节点:

  • 8086(1978):16位处理器,引入了实模式。我记得第一次在实模式下写汇编,感觉就像在跟硬件直接对话。
  • 80286(1982):引入了保护模式。嗯,这里要注意,保护模式是x86走向多任务的关键一步。
  • 80386(1985):32位处理器,引入了分页机制。我在项目中遇到过一个问题,就是分页表配置错了,导致系统直接崩溃。从那以后,我对页表格外小心。
  • Pentium(1993):引入了MMX指令集,开始玩SIMD了。
  • AMD64(2003):64位扩展,这是AMD的杰作。我建议你重点理解这个,因为现在几乎所有x86处理器都在用。

核心要点:x86指令集最大的特点就是向后兼容。8086的指令,在现代处理器上依然有效。但代价就是指令集变得非常庞大,这也是CISC架构的特点。

2.2 CISC与RISC对比

说到x86,就绕不开CISC和RISC的争论。说白了,这是两种设计哲学。

CISC(复杂指令集计算机)

  • 指令数量多,功能复杂
  • 一条指令可以完成多个操作
  • 代码密度高,但译码复杂
  • 代表:x86

RISC(精简指令集计算机)

  • 指令数量少,功能简单
  • 一条指令只做一件事
  • 译码简单,但代码密度低
  • 代表:ARM、RISC-V

我曾经在一个项目中,需要把ARM代码移植到x86上。当时我天真地以为,指令集不同,但逻辑一样,应该不难。结果呢?x86的复杂指令让我吃了不少苦头。比如x86的rep movsb,一条指令就能完成内存拷贝,而ARM需要循环实现。但反过来,x86的译码器比ARM复杂得多。

我的建议:不要纠结于CISC和RISC谁更好。它们只是不同场景下的最优解。x86适合需要高代码密度的场景,RISC适合需要低功耗、简单设计的场景。在嵌入式领域,两者都有用武之地。

2.3 AMD64位扩展技术

2003年,AMD推出了AMD64,也就是x86-64。说实话,这步棋走得非常漂亮。Intel当时在搞IA-64(安腾),结果市场不买账。AMD的64位扩展,直接兼容32位x86,用户迁移成本极低。

AMD64的关键变化:

  • 64位通用寄存器:从32位扩展到64位,寄存器数量也从8个增加到16个
  • 64位地址空间:理论上支持2^64字节的地址空间,但实际只用了48位
  • 兼容32位模式:可以在64位系统上运行32位程序
  • 新增R8-R15寄存器:8个新的通用寄存器

我在项目中遇到过一个问题:一个老旧的32位驱动,在64位系统上死活跑不起来。后来发现,它把指针截断成了32位。嗯,这种坑,你迟早会遇到。

避坑指南:我曾经在64位系统上写汇编时,忘记给地址加上qword前缀,结果地址被截断。记住:在64位模式下,地址操作数默认是64位的,但如果你用32位寄存器做地址计算,就会出问题。

2.4 寄存器模型

寄存器是处理器的核心。x86的寄存器模型,说实话,有点乱。但理解了它,你就理解了x86的一半。

2.4.1 通用寄存器

在64位模式下,我们有16个通用寄存器:

64位 32位 16位 8位(低) 用途
RAX EAX AX AL 累加器,常用于算术运算
RBX EBX BX BL 基址寄存器
RCX ECX CX CL 计数器,常用于循环
RDX EDX DX DL 数据寄存器
RSI ESI SI SIL 源索引
RDI EDI DI DIL 目标索引
RBP EBP BP BPL 基址指针
RSP ESP SP SPL 栈指针
R8-R15 R8D-R15D R8W-R15W R8B-R15B 新增通用寄存器

我个人习惯,在写汇编时,尽量用RAX、RBX这些通用寄存器做临时变量。但要注意,有些寄存器有特殊用途,比如RSP是栈指针,别乱动它。

2.4.2 段寄存器

段寄存器是x86历史遗留的产物。在实模式下,它们用来计算物理地址。在保护模式下,它们变成了选择子,指向GDT或LDT中的描述符。

常见的段寄存器:

  • CS:代码段
  • DS:数据段
  • SS:栈段
  • ES、FS、GS:附加段

在64位模式下,段寄存器的功能被大大弱化了。除了FS和GS还用于线程本地存储(TLS),其他段寄存器基本被废弃。我建议你重点理解FS和GS,因为它们在操作系统中用得很多。

小技巧:在Linux内核中,current指针就是通过gs段寄存器实现的。你可以用mov rax, qword ptr gs:[0]来获取当前进程的task_struct。

2.4.3 标志寄存器

标志寄存器(RFLAGS)是x86的"状态灯"。它记录了运算结果的各种状态。

常用的标志位:

  • CF(进位标志):无符号运算溢出时置1
  • PF(奇偶标志):结果中1的个数为偶数时置1
  • AF(辅助进位标志):BCD码运算时用
  • ZF(零标志):结果为0时置1
  • SF(符号标志):结果为负数时置1
  • OF(溢出标志):有符号运算溢出时置1
  • IF(中断标志):控制可屏蔽中断的响应
  • DF(方向标志):控制字符串操作的方向

我曾经在调试一个bug时,发现条件跳转总是不对。折腾了半天,才发现是标志寄存器被之前的指令污染了。嗯,这里要注意:很多指令都会修改标志寄存器,但有些指令不会。比如mov指令就不改标志位,而add指令会改。

实战建议:在写汇编时,如果你需要保存标志寄存器的状态,可以用pushfq指令把它压栈,用popfq恢复。但要注意,popfq会修改IF标志位,这在系统编程中可能引发问题。

好了,这一章的内容就到这里。x86的寄存器模型,说白了就是一套"硬件变量"。理解它们,你就能写出更高效的代码。下一章,我们来聊聊x86的内存管理,那才是真正让人头疼的地方。