从底层原理到汇编编程 —— 完整学习指南
复杂指令集计算机
核心思想:硬件做更多的事,软件(编译器)做更少的事
特点:
• 指令数量多(x86有数千条指令)
• 单条指令功能强大
• 可变长度指令(1-15字节)
• 可以直接操作内存
代表架构:x86、AMD64 (x86-64)
代表厂商:Intel、AMD
精简指令集计算机
核心思想:硬件只做简单的事,复杂功能由软件(编译器)组合实现
特点:
• 指令数量少(通常几十条核心指令)
• 每条指令只做一件事
• 固定长度指令(ARM64为32位)
• Load/Store架构(运算只能操作寄存器)
代表架构:ARM、ARM64、RISC-V、MIPS、PowerPC
代表厂商:ARM公司、Apple、Qualcomm
CISC厨师(大厨):一个大厨会做复杂菜肴,一条指令"做鱼香肉丝"就能完成,但你得找专门的大厨。
RISC厨师(快餐):每个厨师只会简单操作(切菜、炒菜、装盘),但有很多厨师,通过配合完成复杂菜肴,效率高且成本低。
核心原则:用最少的内存存储程序
早期计算机内存非常昂贵(KB级别),所以设计了复杂的指令,让程序占用的空间更小。
典型特征:
• 可变长度指令:指令可以是1-15字节,节省空间
• 复杂寻址模式:一条指令可以包含计算地址的逻辑
• 内存操作数:指令可以直接读写内存,不需要先加载到寄存器
• 微代码实现:复杂指令内部被翻译成多个微操作(μop)
x86指令格式(可变长度):
┌────────┬────────┬────────┬────────┬────────┬────────┐
│ 前缀 │ 操作码 │ ModR/M │ SIB │ 偏移量 │ 立即数 │
│0-4字节 │1-3字节 │1字节 │1字节 │0-4字节 │0-4字节 │
└────────┴────────┴────────┴────────┴────────┴────────┘
示例:
MOV EAX, [EBX+ECX*4+16] ; 一条指令完成:计算地址 + 加载内存 + 存入寄存器
; x86-64 CISC 典型指令 ; 1. 复杂指令:PUSH - 同时完成 减法 + 存储 PUSH RAX ; RSP -= 8; [RSP] = RAX (一条指令完成两个操作) ; 2. 复杂指令:ENTER - 建立栈帧 ENTER 32, 0 ; 自动完成:PUSH RBP; MOV RBP,RSP; SUB RSP,32 ; 3. 复杂指令:REP MOVSB - 字符串操作 REP MOVSB ; 重复执行MOVSB,直到RCX=0(一条指令完成循环拷贝) ; 4. 内存直接操作 ADD EAX, [RBX] ; 直接从内存加载并相加(RISC不允许这样) ; 5. 复杂寻址 MOV EAX, [RBX+RCX*8+16] ; 一条指令完成:EBX+RCX*8+16 的地址计算 + 内存读取
核心原则:简单指令快速执行,复杂功能由编译器生成多条指令实现
典型特征:
• 固定长度指令:ARM64所有指令都是32位(4字节)
• Load/Store架构:运算指令只能操作寄存器,内存访问只能用专门的LOAD/STORE指令
• 大量寄存器:ARM64有31个通用寄存器(x86-64只有16个)
• 流水线优化:每条指令执行阶段相同,流水线效率高
• 3操作数指令:ADD X0, X1, X2(结果、源1、源2分开)
ARM64指令格式(固定32位):
┌────────┬────────┬────────┬────────┬────────┬────────┬────────┐
│dfsf │ op │ Rm │ │ Rn │ Rd │ │
│31 30 │29 24 │20 16 │15 10 │9 5 │4 0 │ │
└────────┴────────┴────────┴────────┴────────┴────────┴────────┘
所有指令都是32位,解码电路非常简单!
示例:
ADD X0, X1, X2 ; 格式固定,硬件一眼就能识别
; ARM64 RISC 典型指令 ; 1. 简单指令:每条只做一件事 ADD X0, X1, X2 ; X0 = X1 + X2 (3操作数,结果和源分开) ; 2. Load/Store架构:必须先用LOAD加载 LDR X0, [X1] ; X0 = 内存[X1] (从内存加载) ADD X0, X0, #1 ; X0 = X0 + 1 (在寄存器里运算) STR X0, [X1] ; 内存[X1] = X0 (写回内存) ; 注意:ARM64不能直接 ADD [X1], #1 ; 3. 分支指令 CMP X0, #10 ; 比较X0和10 B.EQ loop ; 如果相等,跳转到loop ; 4. 批量加载/存储(RISC的优化) STP X29, X30, [SP, #-16]! ; Store Pair: 一次存两个寄存器 LDP X29, X30, [SP], #16 ; Load Pair: 一次加载两个寄存器 ; 5. 位移和位运算 LSL X0, X1, #3 ; X0 = X1 << 3 (逻辑左移) AND X0, X1, #0xFF ; X0 = X1 & 0xFF(按位与)
| 对比维度 | CISC (x86-64) | RISC (ARM64) |
|---|---|---|
| 指令数量 | 多(x86有1000+条指令) | 少(ARM64核心指令约100+条) |
| 指令长度 | 可变(1-15字节) | 固定(32位/4字节) |
| 指令复杂度 | 高(一条指令可完成复杂操作) | 低(每条指令只做简单操作) |
| 内存访问 | 可以直接操作内存(如 ADD EAX,[EBX]) | 只能通过LOAD/STORE访问内存 |
| 通用寄存器 | 16个(RAX~R15,部分有专用用途) | 31个(X0~X30,全部通用) |
| 指令格式 | 2操作数:ADD dest, src(结果覆盖dest) | 3操作数:ADD rd, rn, rm(结果单独存放) |
| 寻址模式 | 复杂(支持多种寻址方式) | 简单(只有少量寻址模式) |
| 代码密度 | 高(同样功能,指令数少) | 低(同样功能,指令数多) |
| 功耗 | 高(解码电路复杂,晶体管多) | 低(指令简单,解码电路小) |
| 编译器负担 | 轻(复杂指令由硬件实现) | 重(需要编译器生成多条指令) |
| 典型代表 | Intel x86/AMD64、VAX、Motorola 68k | ARM/ARM64、RISC-V、MIPS、PowerPC |
CISC = 自动挡豪华轿车:操作简单(一条指令搞定),但内部结构复杂(油耗高、维修难)。
RISC = 手动挡跑车:操作稍微复杂(需要配合离合器、换挡),但结构简单(省油、效率高、可玩性高)。
x86-64 通用寄存器(64位)
💡 寄存器历史兼容性:
每个64位寄存器都有"小名":
• RAX 的低32位 = EAX,低16位 = AX,低8位 = AL/AH
• 这是为了兼容1978年的8086处理器!
函数调用时参数传递顺序:
第1个参数 → RDI
第2个参数 → RSI
第3个参数 → RDX
第4个参数 → RCX
第5个参数 → R8
第6个参数 → R9
第7个及以后参数 → 压入栈中
返回值:存储在 RAX 中
被调用者保存(callee-saved):RBX, RBP, R12-R15(函数如果用了这些,必须恢复原来的值)
调用者保存(caller-saved):RAX, RCX, RDX, RSI, RDI, R8-R11(函数可以随时覆盖这些)
RFLAGS 标志寄存器(重要位)
ARM64 通用寄存器(64位)
💡 ARM64 寄存器特点:
• X0-X30 都是64位通用寄存器,没有任何专用用途(除X29/X30外)
• W0-W30 是对应的32位视图(操作低32位,高32位清零)
• XZR 是零寄存器(读它得到0,写它无效)—— 这是RISC的巧妙设计!
函数调用时参数传递:
第1-8个参数 → X0-X7
第9个及以后参数 → 压入栈中
返回值:X0(小返回值)、X0+X1(大返回值)
特殊寄存器说明:
• X29 (FP):帧指针(Frame Pointer),指向当前栈帧
• X30 (LR):链接寄存器(Link Register),存储返回地址
• SP:栈指针(Stack Pointer)
• XZR:零寄存器(读取时返回0,写入时忽略)
• PC:程序计数器(不能直接修改,只能通过分支指令改变)
CPSR 条件标志(NZCV)
ARM64的条件执行:B.EQ, B.NE, B.GT, B.LT 等条件分支指令
; C代码: ; int add(int a, int b) { ; return a + b; ; } add: ; 参数:EDI = a, ESI = b ; 返回值:EAX MOV EAX, EDI ; EAX = a ADD EAX, ESI ; EAX = EAX + b RET ; 返回,结果在EAX中 ; 注意:x86是2操作数指令,结果会覆盖第一个操作数
; C代码: ; if (a > b) { ; return a; ; } else { ; return b; ; } max: ; 参数:EDI = a, ESI = b CMP EDI, ESI ; 比较 a 和 b,设置标志位 JG .return_a ; 如果 a > b,跳转到 return_a MOV EAX, ESI ; 否则 EAX = b RET .return_a: MOV EAX, EDI ; EAX = a RET
; C代码: ; int sum(int n) { ; int result = 0; ; for (int i = 1; i <= n; i++) { ; result += i; ; } ; return result; ; } sum: ; 参数:EDI = n XOR EAX, EAX ; result = 0 (EAX = 0 的惯用写法) XOR ECX, ECX ; i = 0 .loop: INC ECX ; i++ ADD EAX, ECX ; result += i CMP ECX, EDI ; 比较 i 和 n JLE .loop ; 如果 i <= n,继续循环 RET ; 返回 result (在EAX中)
; x86-64 函数序言(prologue)和结尾(epilogue) my_function: ; 函数序言:建立栈帧 PUSH RBP ; 保存旧的帧指针 MOV RBP, RSP ; 设置新的帧指针 SUB RSP, 32 ; 为局部变量分配32字节栈空间 ; ... 函数体 ... ; 函数结尾:销毁栈帧 MOV RSP, RBP ; 恢复栈指针 POP RBP ; 恢复旧的帧指针 RET ; 返回
; C代码: ; int add(int a, int b) { ; return a + b; ; } add: ; ARM64 RISC 版本 ; 参数:W0 = a, W1 = b ; 返回值:W0 ADD W0, W0, W1 ; W0 = W0 + W1 (3操作数,但结果可以覆盖源) RET ; 返回,结果在W0中 ; 更好的写法(不覆盖输入参数): ; ADD W2, W0, W1 ; W2 = W0 + W1 ; MOV W0, W2 ; W0 = W2 ; RET
; C代码: ; if (a > b) { ; return a; ; } else { ; return b; ; } max: ; 参数:W0 = a, W1 = b CMP W0, W1 ; 比较 a 和 b,设置标志位 B.GT .return_a ; 如果 a > b,跳转到 return_a MOV W0, W1 ; 否则 W0 = b RET .return_a: RET ; W0 已经是 a,直接返回
; C代码: ; int sum(int n) { ; int result = 0; ; for (int i = 1; i <= n; i++) { ; result += i; ; } ; return result; ; } sum: ; 参数:W0 = n MOV W2, #0 ; result = 0 (W2 = result) MOV W3, #1 ; i = 1 (W3 = i) .loop: CMP W3, W0 ; 比较 i 和 n B.GT .done ; 如果 i > n,结束循环 ADD W2, W2, W3 ; result += i ADD W3, W3, #1 ; i++ B .loop ; 无条件跳转回 loop .done: MOV W0, W2 ; 返回值 = result RET
; ARM64 函数序言和结尾(使用STP/LDP优化) my_function: ; 函数序言:建立栈帧 STP X29, X30, [SP, #-16]! ; 压入帧指针和返回地址(一次存两个!) MOV X29, SP ; 设置帧指针 SUB SP, SP, #32 ; 分配局部变量空间 ; ... 函数体 ... ; 函数结尾:销毁栈帧 ADD SP, X29, #0 ; 恢复栈指针 LDP X29, X30, [SP], #16 ; 弹出帧指针和返回地址 RET ; 返回
; x86-64 数组求和 ; int sum(int* arr, int n) sum_x86: XOR EAX, EAX ; result = 0 XOR ECX, ECX ; i = 0 .loop: CMP ECX, ESI ; 比较 i 和 n JGE .done ; 如果 i >= n,结束 ADD EAX, [RDI+RCX*4] ; result += arr[i] INC ECX ; i++ JMP .loop .done: RET
; ARM64 数组求和 ; int sum(int* arr, int n) sum_arm64: MOV W2, #0 ; result = 0 MOV W3, #0 ; i = 0 .loop: CMP W3, W1 ; 比较 i 和 n B.GE .done ; 如果 i >= n,结束 LDR W4, [X0, X3, LSL #2] ; W4 = arr[i] ADD W2, W2, W4 ; result += W4 ADD W3, W3, #1 ; i++ B .loop .done: MOV W0, W2 ; 返回值 = result RET
1. 内存访问:
• x86-64: ADD EAX, [RDI+RCX*4] —— 可以直接在内存操作数上运算
• ARM64: 必须先 LDR W4, [X0, X3, LSL #2] 加载到寄存器,再运算
2. 寻址模式:
• x86-64: 支持复杂的寻址模式(基址+变址*比例+偏移)
• ARM64: 寻址模式简单,复杂地址计算需要多条指令
3. 代码密度:
• x86-64: 代码更紧凑(同样功能,指令数更少)
• ARM64: 代码更长,但每条指令执行更快
; x86-64 调用有多个参数的函数 ; void foo(int a, int b, int c, ; int d, int e, int f, ; int g); PUSH 7 ; 第7个参数压栈 MOV R9, 6 ; 第6个参数 = 6 MOV R8, 5 ; 第5个参数 = 5 MOV RCX, 4 ; 第4个参数 = 4 MOV RDX, 3 ; 第3个参数 = 3 MOV RSI, 2 ; 第2个参数 = 2 MOV RDI, 1 ; 第1个参数 = 1 CALL foo ADD RSP, 8 ; 清理栈(第7个参数)
; ARM64 调用有多个参数的函数 ; void foo(int a, int b, int c, ; int d, int e, int f, ; int g); MOV W0, #1 ; 第1个参数 = 1 MOV W1, #2 ; 第2个参数 = 2 MOV W2, #3 ; 第3个参数 = 3 MOV W3, #4 ; 第4个参数 = 4 MOV W4, #5 ; 第5个参数 = 5 MOV W5, #6 ; 第6个参数 = 6 MOV W6, #7 ; 第7个参数 = 7 BL foo ; 调用函数 ; ARM64不需要清理栈!所有参数都在寄存器中
测试一下你学了多少!点击选项查看答案。
1️⃣ CISC = 复杂指令集:指令多、功能强、可变长度、可直接操作内存(x86-64)
2️⃣ RISC = 精简指令集:指令少、功能简单、固定长度、Load/Store架构(ARM64)
3️⃣ 寄存器差异:x86-64有16个,ARM64有31个
4️⃣ 参数传递:x86-64前6个用寄存器,后面用栈;ARM64前8个用寄存器
5️⃣ 功耗:ARM64更省电,适合移动设备;x86-64性能强,适合桌面/服务器
6️⃣ 未来趋势:RISC正在崛起(Apple M系列、AWS Graviton、RISC-V)
🎉 恭喜你完成了 CISC vs RISC 的学习!
现在你应该能看懂x86-64和ARM64的汇编代码了!