第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的内存管理,那才是真正让人头疼的地方。