首页 / 第3章 指令系统

指令系统

指令格式、寻址方式、CISC与RISC、指令类型、高级语言与机器代码

1. 指令格式

指令是计算机执行某种操作的命令,指令格式是指令字用二进制代码表示的结构形式。一条指令通常由操作码字段和地址码字段两部分组成。

通用指令格式
操作码 OP
7位
地址码 A
25位

操作码:指出指令要执行什么操作(如加、减、传送等)
地址码:指出操作数的地址、结果的地址、下一条指令的地址等

1.1 按地址码数量分类

三地址指令
OP
操作码
A1
第一操作数
A2
第二操作数
A3
结果地址

(A1) OP (A2) → A3

二地址指令(最常用)
OP
操作码
A1
源/目的操作数
A2
源操作数

(A1) OP (A2) → A1 (目的操作数兼存结果)

一地址指令
OP
操作码
A1
操作数/累加器

(A1) OP (ACC) → ACC 或 OP(A) → A(单目运算)

零地址指令
OP
只有操作码

不需要操作数(如NOP、HLT)或操作数隐含(如堆栈指令)

1.2 定长操作码与扩展操作码

  • 定长操作码:所有指令操作码长度相同,译码简单,但操作码空间利用率低
  • 扩展操作码:短操作码对应常用指令,长操作码对应不常用指令,提高编码效率
📌
扩展操作码设计原则 短码不能是长码的前缀(保证唯一解码);使用频率高的指令用短操作码,使用频率低的用长操作码

1.3 数据的对齐方式

在计算机系统中,数据的存储位置需要满足一定的对齐要求,这涉及到数据存取的效率和硬件实现的复杂度。

按边界对齐的概念

按边界对齐是指数据在内存中的起始地址必须是其长度的整数倍:

  • 半字(2字节):起始地址应为 2 的倍数(地址最低位为 0)
  • 字(4字节):起始地址应为 4 的倍数(地址最低 2 位为 00)
  • 双字(8字节):起始地址应为 8 的倍数(地址最低 3 位为 000)
对齐与非对齐存储对比
对齐存储(推荐)

优点:单次访存即可完成,速度快

非对齐存储(不推荐)

缺点:需要两次访存,再拼接数据,速度慢

示例:4字节字数据 0x12345678,地址 0x0004 对齐,地址 0x0005 非对齐

为什么需要对齐?

  1. 硬件设计简化:内存按字边界组织,对齐访问只需一次总线操作
  2. 性能提升:非对齐访问可能需要多次存储周期,严重影响性能
  3. 原子操作支持:某些架构仅支持对齐数据的原子操作
  4. 缓存效率:对齐数据不会跨越缓存行边界,提高缓存命中率
⚠️
考试要点 某些 CPU(如 ARM、MIPS)不支持非对齐访问,会触发异常;而 x86 架构支持非对齐访问但性能会下降。编译器通常会自动进行数据对齐。

1.4 大/小端存放方式

当数据宽度大于一个字节时(如半字、字、双字),字节在内存中的存放顺序有两种方式:大端模式和小端模式。

大端与小端存储方式对比

示例数据:32位整数 0x12345678 存储在起始地址 0x1000

大端模式 (Big-Endian)

高字节存放在低地址

符合人类阅读习惯
小端模式 (Little-Endian)

低字节存放在低地址

利于类型转换
低地址
地址增长方向
高地址

大端 vs 小端对比

特性 大端模式 (Big-Endian) 小端模式 (Little-Endian)
存储规则 高位字节存放在低地址 低位字节存放在低地址
阅读习惯 符合人类阅读顺序 与阅读顺序相反
类型转换 需要调整字节顺序 直接截取低地址字节
典型应用 网络传输、PowerPC、SPARC x86、x64、ARM(默认)
优点 网络协议统一,便于调试 类型转换高效,整数运算简单
🌐
网络字节序 网络传输统一使用大端模式,称为"网络字节序"。在发送数据前,主机需要将本地字节序转换为网络字节序(htonl/htons),接收后再转换回来(ntohl/ntohs)。
💡
记忆口诀
大端:"大"在前——高字节在"前"(低地址)
小端:"小"在前——低字节在"前"(低地址)

2. 寻址方式

寻址方式是指寻找指令或操作数有效地址的方式,即确定本条指令的数据地址,以及下一条将要执行的指令地址的方法。

2.1 常见寻址方式交互演示

选择不同的寻址方式,查看有效地址的计算过程和数据流向。

寻址方式动态演示
指令寄存器 IR
OP LOAD
A 0x0050
相关寄存器
R1 0x0000
有效地址 EA 计算
有效地址 EA =
--
主存储器
立即寻址:指令的地址字段直接给出操作数本身,而不是操作数的地址。特点:指令执行时间短,不需访存;但A的位数限制了立即数范围。

2.2 各种寻址方式比较

寻址方式 有效地址 访存次数 特点
立即寻址 EA 不存在,A 即操作数 0次 最快,但数的范围受限
直接寻址 EA = A 1次 简单,寻址空间受A位数限制
间接寻址 EA = (A) 2次及以上 寻址空间大,但速度慢
寄存器寻址 操作数在寄存器中 0次 速度快,寄存器数量少
寄存器间接 EA = (Ri) 1次 比间接寻址少一次访存
相对寻址 EA = (PC) + A 1次 程序浮动,A是位移量
基址寻址 EA = (BR) + A 1次 多道程序,扩大寻址空间
变址寻址 EA = (IX) + A 1次 数组处理,IX用户可控

3. 指令类型

📐 算术运算指令

  • 加法指令 ADD
  • 减法指令 SUB
  • 乘法指令 MUL
  • 除法指令 DIV
  • 加1 INC、减1 DEC
  • 求补 NEG、比较 CMP

🔧 逻辑运算指令

  • 与 AND、或 OR、非 NOT
  • 异或 XOR
  • 测试 TEST
  • 算术左移 SAL
  • 算术右移 SAR
  • 逻辑左/右移 SHL/SHR
  • 循环左/右移 ROL/ROR

📦 数据传送指令

  • 通用传送 MOV
  • 堆栈操作 PUSH、POP
  • 数据交换 XCHG
  • 输入输出 IN、OUT
  • 取有效地址 LEA

🔀 控制转移指令

  • 无条件转移 JMP
  • 条件转移 JZ/JC/JO 等
  • 调用与返回 CALL/RET
  • 陷阱与中断 INT/IRET
  • 循环 LOOP

4. CISC 与 RISC

根据指令系统的复杂程度,可将计算机分为复杂指令系统计算机(CISC)和精简指令系统计算机(RISC)。

🏛️ CISC 复杂指令系统
  • 指令数量多,通常 200 条以上
  • 指令长度不固定,格式多样
  • 寻址方式多且复杂
  • 各种指令都可以访存
  • 以微程序控制为主
  • 功能强但实现复杂
  • 代表:x86 架构
  • 强调软件兼容
RISC 精简指令系统
  • 指令数量少,通常几十条
  • 指令长度固定,格式少
  • 寻址方式简单
  • 只有 Load/Store 能访存
  • 以硬布线控制为主
  • 优化编译器,流水线效率高
  • 代表:ARM、MIPS、RISC-V
  • 强调性能与并行
💡
RISC 的主要特点(必背) 选取使用频率最高的简单指令;指令长度固定、格式少;只有 Load/Store 访存;CPU 中通用寄存器多;硬布线控制为主;采用流水线技术;优化编译支持

4.1 RISC 为什么更快?

  1. 简单指令:指令简单意味着译码快、执行快,单周期可以完成
  2. 寄存器多:减少访存次数,大部分操作在寄存器间进行
  3. 流水线:规整的指令格式使流水线设计更简单高效
  4. Load/Store 结构:统一的访存指令,简化控制逻辑
  5. 硬布线:不用读微指令,控制信号产生更快

5. 高级语言程序与机器级代码的对应

理解高级语言程序如何转换为机器可执行的代码,是掌握计算机底层工作原理的关键。本节将介绍编译过程中的核心工具,以及选择结构、循环结构和函数调用在机器级的实现方式。

5.1 编译器、汇编器和链接器

高级语言程序转换为可执行文件需要经过多个阶段的处理,每个阶段由不同的工具完成。

程序编译流程
源程序
hello.c C语言源代码
编译器
预处理 → 词法分析 → 语法分析 → 语义分析 → 中间代码 → 目标代码生成
汇编代码
hello.s 汇编语言(人类可读)
汇编器
将汇编指令翻译成机器指令,生成可重定位目标文件
目标文件
hello.o 机器代码(二进制,地址未解析)
链接器
合并多个目标文件,解析符号引用,生成可执行文件
可执行文件
hello.exe / a.out 完整的可执行程序

🔧 编译器 (Compiler)

高级语言(C、Java等)翻译成汇编语言。进行词法分析、语法分析、语义分析、代码优化和目标代码生成。

⚙️ 汇编器 (Assembler)

汇编语言翻译成机器语言(目标文件)。处理伪指令、符号表,生成可重定位的目标文件。

🔗 链接器 (Linker)

多个目标文件库文件合并成一个可执行文件。解析外部符号引用,进行地址重定位。

💡
常考考点 源程序 → 预处理 → 编译 → 汇编 → 链接 → 可执行文件。
gcc -E hello.c -o hello.i (预处理)
gcc -S hello.c -o hello.s (编译)
gcc -c hello.c -o hello.o (汇编)
gcc hello.o -o hello (链接)

5.2 选择结构语句的机器级表示

选择结构(if-else)通过条件跳转指令实现,根据条件判断结果决定程序的执行路径。

if-else 语句的机器级实现
📝C语言源代码
int
max(int a, int b) {
if (a > b) {
return a;
} else {
return b;
}
}
⚙️x86汇编代码(AT&T语法)
max:
# 比较a和b
cmpl %esi, %edi # a在%edi, b在%esi
# 如果a <= b,跳转到else分支
jle .L2 # 条件跳转
# if分支:返回a
movl %edi, %eax # 返回值放在%eax
ret
.L2: # else分支:返回b
movl %esi, %eax
ret
📊 执行流程分析
1
条件判断:cmpl比较%edi(a)和%esi(b),设置条件码(EFLAGS寄存器)
2
条件跳转:jle指令检测SF≠OF或ZF=1,若成立则跳转到.L2标签
3
分支执行:根据跳转结果,执行if分支(返回a)或else分支(返回b)

常见条件跳转指令

指令 含义 跳转条件 用途
je/jzJump if Equal/ZeroZF=1相等/零时跳转
jne/jnzJump if Not Equal/Not ZeroZF=0不等/非零时跳转
jg/jnleJump if GreaterZF=0 且 SF=OF有符号大于
jge/jnlJump if Greater or EqualSF=OF有符号大于等于
jl/jngeJump if LessSF≠OF有符号小于
jle/jngJump if Less or EqualZF=1 或 SF≠OF有符号小于等于
ja/jnbeJump if AboveCF=0 且 ZF=0无符号大于
jb/jnaeJump if BelowCF=1无符号小于

5.3 循环结构语句的机器级表示

循环结构(for/while)通过条件跳转无条件跳转配合实现,控制循环的进入、继续和退出。

for 循环的机器级实现
📝C语言源代码
int sum(int n) {
int s = 0;
for (int i = 1; i <= n; i++) {
s += i;
}
return s;
}
⚙️x86汇编代码
sum:
movl $0, %eax # s = 0 (返回值)
movl $1, %ecx # i = 1
.L3: # 循环起始
cmpl %edi, %ecx # 比较 i 和 n
jg .L4 # i > n 则退出
addl %ecx, %eax # s += i
incl %ecx # i++
jmp .L3 # 无条件跳转回循环头
.L4: # 循环结束
ret
📊 循环执行流程
初始化 i=1, s=0
i <= n ?
s += i; i++
跳回判断

while循环示例

📝 C语言
while (i < n) {
s += i;
i++;
}
⚙️ 汇编结构
.L5: # 循环入口
cmpl %edi, %ecx
jge .L6 # i >= n 退出
addl %ecx, %eax
incl %ecx
jmp .L5
⚠️
循环优化技巧 编译器常将for循环转换为"do-while"形式(先执行后判断),减少跳转次数。在某些情况下使用"循环展开"技术,减少循环控制开销。

5.4 过程(函数)调用的机器级表示

函数调用涉及栈帧管理、参数传递、返回地址保存等机制,理解这些是掌握程序运行时行为的关键。

函数调用栈帧结构
调用者栈帧 (Caller)
局部变量
caller的局部数据
参数n
第n个参数
参数1
第1个参数
↓ 栈向低地址增长 ↓
被调用者栈帧 (Callee)
返回地址
CALL指令压入
保存的%rbp
旧的栈帧指针
局部变量
callee的局部数据
临时空间
可选
%rbp → 指向栈帧底部
%rsp → 指向栈顶

CALL/RET指令的工作原理

📞 CALL 指令

  1. 将返回地址(下一条指令地址)压栈
  2. PUSH %rip
  3. 跳转到目标函数地址
  4. JMP target

🔙 RET 指令

  1. 从栈顶弹出返回地址
  2. POP %rip
  3. 跳转回调用者继续执行
  4. 恢复调用者的执行上下文

参数传递方式

传递方式 说明 优点 缺点
寄存器传递 参数通过寄存器传递(x86-64: %rdi, %rsi, %rdx, %rcx, %r8, %r9) 速度快,无需访存 寄存器数量有限
栈传递 参数压入调用者的栈帧 参数数量无限制 需要访存,速度较慢
混合传递 前几个参数用寄存器,超出部分用栈 兼顾速度和灵活性 调用约定复杂
函数调用示例:交换两个数
📝C语言源代码
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int x = 1, y = 2;
swap(&x, &y);
return 0;
}
⚙️x86-64汇编代码
swap:
movl (%rdi), %eax # temp = *a
movl (%rsi), %edx # %edx = *b
movl %edx, (%rdi) # *a = *b
movl %eax, (%rsi) # *b = temp
ret
main:
subq $16, %rsp # 分配栈空间
movl $1, -4(%rbp) # x = 1
movl $2, -8(%rbp) # y = 2
leaq -8(%rbp), %rsi # &y → %rsi (第2个参数)
leaq -4(%rbp), %rdi # &x → %rdi (第1个参数)
call swap # 调用函数
movl $0, %eax # 返回 0
addq $16, %rsp
ret
📌 关键点解析
参数传递
x86-64: 前6个整数参数用%rdi, %rsi, %rdx, %rcx, %r8, %r9传递
返回值
整数/指针返回值放在%eax(32位)或%rax(64位)
栈帧指针
%rbp指向当前栈帧底部,%rsp指向栈顶
栈对齐
x86-64要求调用前%rsp为16字节对齐
💡
常考知识点总结
  • 函数调用时,返回地址由CALL指令自动压栈
  • 被调用函数负责保存和恢复%rbp(栈帧指针)
  • 局部变量通常在栈上分配,通过%rbp的负偏移访问
  • x86-64使用"红区"优化:叶子函数可不用栈帧
  • 递归调用会创建多层栈帧,深度过大导致栈溢出