指令系统

指令集

指令集(ISA)包含:

  • 指令格式、寻址方式、操作类型,以及操作数的种类、个数与约束
  • 操作数的数据类型,大端/小端存放
  • 可访问的寄存器和存储空间
  • 可见的状态特征,比如程序寄存器,条件码

一条指令包含操作码与地址码,其中操作码是执行指令的地址、地址码是被执行操作数相关的地址。通过操作码/地址码寻找下一条操作/本条操作数的行为称为寻址

常见的指令格式有零地址指令、一地址指令、二地址指令、三地址指令。

  • 零地址指令不需要面向操作数就可以执行,通常用于计算机的底层操作,如空操作、停机等
  • 一地址指令作用于单个对象,比如对对象自加,平移
  • 二地址指令可以应用于单个对象迁移或者两个对象的操作。比如两个操作数的加法,将一个操作数移动到另外一个地址
  • 三地址指令通常将第三个地址作为结果地址,将第一个操作数和第二个操作数以指令处理后存入第三个地址中

指令的类型

  • 数据传送指令: 传送(MOV)、 从内存读取数据到CPU寄存器(LOAD)、写数据(STORE)、进出栈(PUSHPOP)
  • 算术与逻辑运算: 加(ADD)、减(SUB)、异或(XOR)
  • 移位操作: 算术移位(SALSAR)、逻辑移位(SHLSHR)、循环移位(ROLROR)
  • 顺序控制指令: 调用指令、转移指令(无条件转移、条件转移)、返回指令、陷阱指令
  • IO指令
  • CPU控制,通常只能在操作系统内核使用

寻址

寻址分为指令寻址与数据寻址。

指令的地址通常是定长连续的,通过程序计数器(PC)顺序访问下一个指令地址称为顺序寻址

通过转移指令访问其他指令地址的方式称为跳跃寻址。其中跳跃寻址也分为绝对跳跃与相对跳跃,分别为确定指令地址的跳跃与确定指令的相对偏移量的跳跃,分别对应数据寻址的直接寻址与基址寻址

数据寻址方式分为

  • 隐含寻址 - 不显式写出操作数地址,蕴含在上一轮操作中或者特定寄存器中,比如累加器结构的其中一个操作数默认来自于累加器
  • 立即数寻址 - 形式地址直接存操作数本身,不需要再次访存就执行
  • 直接寻址 - 形式地址存操作数的真实地址,访问一次内存提取操作数
  • 间接寻址 - 形式地址指向一个主存的单元,主存单元的地址指向操作数的真实地址
  • 寄存器寻址 - 形式地址指向寄存器中操作数的真实地址
  • 寄存器间接寻址 - 形式地址指向的是寄存器的块,指向操作数主存真实地址
  • 相对寻址 - 操作数的真实地址对于程序寄存器值的偏移地址
  • 基址寻址 - 相对基址寄存器的偏移,面向对象场景下提高空间局部性
  • 变址寻址 - 相当变址寄存器偏移,变址寄存器可以由用户修改也可以根据程序变化,适用于线性表等索引型数据对象
  • 堆栈寻址 - 执行堆栈指针指向的出栈地址的操作数。通过指令顺序寻址和数据堆栈寻址可以实现对局部性好的程序的高效执行

机器代码表示

汇编语言是指令的人类语言实现。对于不同的芯片,比如x86_64、MIPS、ARM,他们的指令集与对应的汇编都不相同。

在x86 CPU中,通常有8个32位通用寄存器。分别为

  • EAX 累加器
  • EBX 基地址寄存器
  • ECX 计数寄存器
  • EDX 数据寄存器
  • ESI/EDI 变址寄存器
  • EBP 堆栈基指针
  • ESP 堆栈顶指针

其中A/B/C/DX 寄存器可以分为一般,将其中的16位作为独立的寄存器使用

EBP与ESP共同维护栈寄存器中的栈帧

基础运算指令

使用x86 Intel格式指令举例

数据传输指令

  • mov指令 -- 将后一个地址的内容移动到第一个地址指向的内存或者寄存器
  • push指令 -- 将操作数压入栈
  • pop指令 -- 将操作数出栈

算术/逻辑运算指令

  • add/sub 加减法,执行后的结果存入第一个操作数
  • inc/dec 自增1与自减1
  • imul/mul 前者为有符号数乘法,后者为无符号数乘法
  • idiv/div 有/无符号数除法
  • and/or/xor/not 与/或/异或/非运算
  • neg 变为负数,相当于求补码
  • shl/shr 逻辑左移/逻辑右移
  • sal/sar 算术左移/算术右移

控制流指令

  • jmp 无条件转移
  • jcondition 条件转移
  • cmp/test cmp运算执行减法运算但只更新标志位, test执行与运算但只更新标记位
  • call/ret call实现子程序的调用,将下一条指令的返回地址压入栈,ret将下一条的返回地址弹出栈

汇编格式

对于x86平台的汇编语言,通常使用AT&T格式与Intel格式。

  • AT&T格式的指令名必须为小写字母,但是Intel格式的指令同时支持大小写
  • 二者的操作数位置是相反的,AT&T格式: 源,目的; Intel格式: 目的:源
  • AT&T格式与Intel格式对于寄存器、立即数的区分方式是不同的。AT&T表示寄存器为%eax, Intel直接暴露寄存器eax; AT&T表示立即数为$0x10, Intel直接写为0x10Intel添加[]以访问地址的内存数据,比如[0x1001], 这与C语言的*x访问指针的值类似,AT&T则使用圆括号()

逻辑语句的机器表示

选择语句

选择语句逻辑为if-else或者if-then-else结构

循环语句

循环语句的逻辑为do-whilewhilefor结构

过程调用

call/ret指令加栈寄存器实现。call指令将下一条指令的地址压入栈;ret指令将栈顶的地址弹出并转移到该地址继续执行

整体的流程为

%%{init: {"flowchart": {"nodeSpacing": 14, "rankSpacing": 18, "diagramPadding": 6}, "themeVariables": {"fontSize": "12px"}}}%%
graph TD
    Start([P 调用 Q])
    Start --> Call[调用准备<br>放置实参 / 执行 call / 返回地址入栈]
    Call --> Execute[Q 执行<br>建立栈帧 / 分配空间 / 保存寄存器 / 执行函数体]
    Execute --> Return[返回准备<br>放置返回值 / 释放栈帧 / 恢复寄存器]
    Return --> Back[执行 ret<br>弹出返回地址 / 回到 P 继续执行]

RISC与CISC

RISC和CISC代表指令系统的两个发展方向,其中 CISC代表指令系统向大型化、复杂化发展,增强原有指令的功能并引入更加复杂的指令,这类计算机称为复杂指令系统计算机(CISC); RISC代表通过精简指令集、简化指令功能,以提升指令的执行效率,这类计算机称为精简指令系统计算机(RISC)

RISC的指令系统复杂且庞大,指令数目多,且指令字长是可变长不固定的,通过微程序进行指令的控制。由于指令复杂,难以通过编译器进行编译优化。

CISC的指令集精简、定长,只能通过LOAD/STORE进行访存,指令执行事件通常约束在一个时间周期内完成,且必须通过指令流水线实现指令串行执行。由于指令集精简,能够采用优化的编译程序。