指令系统
指令格式、寻址方式、CISC与RISC、指令类型、高级语言与机器代码
1. 指令格式
指令是计算机执行某种操作的命令,指令格式是指令字用二进制代码表示的结构形式。一条指令通常由操作码字段和地址码字段两部分组成。
操作码:指出指令要执行什么操作(如加、减、传送等)
地址码:指出操作数的地址、结果的地址、下一条指令的地址等
1.1 按地址码数量分类
(A1) OP (A2) → A3
(A1) OP (A2) → A1 (目的操作数兼存结果)
(A1) OP (ACC) → ACC 或 OP(A) → A(单目运算)
不需要操作数(如NOP、HLT)或操作数隐含(如堆栈指令)
1.2 定长操作码与扩展操作码
- 定长操作码:所有指令操作码长度相同,译码简单,但操作码空间利用率低
- 扩展操作码:短操作码对应常用指令,长操作码对应不常用指令,提高编码效率
1.3 数据的对齐方式
在计算机系统中,数据的存储位置需要满足一定的对齐要求,这涉及到数据存取的效率和硬件实现的复杂度。
按边界对齐的概念
按边界对齐是指数据在内存中的起始地址必须是其长度的整数倍:
- 半字(2字节):起始地址应为 2 的倍数(地址最低位为 0)
- 字(4字节):起始地址应为 4 的倍数(地址最低 2 位为 00)
- 双字(8字节):起始地址应为 8 的倍数(地址最低 3 位为 000)
优点:单次访存即可完成,速度快
缺点:需要两次访存,再拼接数据,速度慢
示例:4字节字数据 0x12345678,地址 0x0004 对齐,地址 0x0005 非对齐
为什么需要对齐?
- 硬件设计简化:内存按字边界组织,对齐访问只需一次总线操作
- 性能提升:非对齐访问可能需要多次存储周期,严重影响性能
- 原子操作支持:某些架构仅支持对齐数据的原子操作
- 缓存效率:对齐数据不会跨越缓存行边界,提高缓存命中率
1.4 大/小端存放方式
当数据宽度大于一个字节时(如半字、字、双字),字节在内存中的存放顺序有两种方式:大端模式和小端模式。
示例数据:32位整数 0x12345678 存储在起始地址 0x1000
高字节存放在低地址
低字节存放在低地址
大端 vs 小端对比
| 特性 | 大端模式 (Big-Endian) | 小端模式 (Little-Endian) |
|---|---|---|
| 存储规则 | 高位字节存放在低地址 | 低位字节存放在低地址 |
| 阅读习惯 | 符合人类阅读顺序 | 与阅读顺序相反 |
| 类型转换 | 需要调整字节顺序 | 直接截取低地址字节 |
| 典型应用 | 网络传输、PowerPC、SPARC | x86、x64、ARM(默认) |
| 优点 | 网络协议统一,便于调试 | 类型转换高效,整数运算简单 |
大端:"大"在前——高字节在"前"(低地址)
小端:"小"在前——低字节在"前"(低地址)
2. 寻址方式
寻址方式是指寻找指令或操作数有效地址的方式,即确定本条指令的数据地址,以及下一条将要执行的指令地址的方法。
2.1 常见寻址方式交互演示
选择不同的寻址方式,查看有效地址的计算过程和数据流向。
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)。
- 指令数量多,通常 200 条以上
- 指令长度不固定,格式多样
- 寻址方式多且复杂
- 各种指令都可以访存
- 以微程序控制为主
- 功能强但实现复杂
- 代表:x86 架构
- 强调软件兼容
- 指令数量少,通常几十条
- 指令长度固定,格式少
- 寻址方式简单
- 只有 Load/Store 能访存
- 以硬布线控制为主
- 优化编译器,流水线效率高
- 代表:ARM、MIPS、RISC-V
- 强调性能与并行
4.1 RISC 为什么更快?
- 简单指令:指令简单意味着译码快、执行快,单周期可以完成
- 寄存器多:减少访存次数,大部分操作在寄存器间进行
- 流水线:规整的指令格式使流水线设计更简单高效
- Load/Store 结构:统一的访存指令,简化控制逻辑
- 硬布线:不用读微指令,控制信号产生更快
5. 高级语言程序与机器级代码的对应
理解高级语言程序如何转换为机器可执行的代码,是掌握计算机底层工作原理的关键。本节将介绍编译过程中的核心工具,以及选择结构、循环结构和函数调用在机器级的实现方式。
5.1 编译器、汇编器和链接器
高级语言程序转换为可执行文件需要经过多个阶段的处理,每个阶段由不同的工具完成。
🔧 编译器 (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)通过条件跳转指令实现,根据条件判断结果决定程序的执行路径。
常见条件跳转指令
| 指令 | 含义 | 跳转条件 | 用途 |
|---|---|---|---|
| je/jz | Jump if Equal/Zero | ZF=1 | 相等/零时跳转 |
| jne/jnz | Jump if Not Equal/Not Zero | ZF=0 | 不等/非零时跳转 |
| jg/jnle | Jump if Greater | ZF=0 且 SF=OF | 有符号大于 |
| jge/jnl | Jump if Greater or Equal | SF=OF | 有符号大于等于 |
| jl/jnge | Jump if Less | SF≠OF | 有符号小于 |
| jle/jng | Jump if Less or Equal | ZF=1 或 SF≠OF | 有符号小于等于 |
| ja/jnbe | Jump if Above | CF=0 且 ZF=0 | 无符号大于 |
| jb/jnae | Jump if Below | CF=1 | 无符号小于 |
5.3 循环结构语句的机器级表示
循环结构(for/while)通过条件跳转和无条件跳转配合实现,控制循环的进入、继续和退出。
while循环示例
5.4 过程(函数)调用的机器级表示
函数调用涉及栈帧管理、参数传递、返回地址保存等机制,理解这些是掌握程序运行时行为的关键。
CALL/RET指令的工作原理
📞 CALL 指令
- 将返回地址(下一条指令地址)压栈
PUSH %rip- 跳转到目标函数地址
JMP target
🔙 RET 指令
- 从栈顶弹出返回地址
POP %rip- 跳转回调用者继续执行
- 恢复调用者的执行上下文
参数传递方式
| 传递方式 | 说明 | 优点 | 缺点 |
|---|---|---|---|
| 寄存器传递 | 参数通过寄存器传递(x86-64: %rdi, %rsi, %rdx, %rcx, %r8, %r9) | 速度快,无需访存 | 寄存器数量有限 |
| 栈传递 | 参数压入调用者的栈帧 | 参数数量无限制 | 需要访存,速度较慢 |
| 混合传递 | 前几个参数用寄存器,超出部分用栈 | 兼顾速度和灵活性 | 调用约定复杂 |
- 函数调用时,返回地址由CALL指令自动压栈
- 被调用函数负责保存和恢复%rbp(栈帧指针)
- 局部变量通常在栈上分配,通过%rbp的负偏移访问
- x86-64使用"红区"优化:叶子函数可不用栈帧
- 递归调用会创建多层栈帧,深度过大导致栈溢出