指令:计算机的语言(1)

目标:展示一个遵循硬件简单性的指令系统,展示其硬件表示及与高级语言的关系。

参考书籍:《计算机组成与设计:软/硬件接口 RISC-V》(原书第2版)

image-20240914170240860

操作数

处理器的功能就是处理数据,而数据的来源主要是寄存器和内存。

寄存器:RISC-V中有32个寄存器,使处理器可以快速定位数据,进行算术运算

内存:供数据传输、指令访问,同时保存寄存器中无法保存的数据结构(数组、链表……)

算术运算的操作数必须由寄存器提供,而寄存器很有可能数量不够或无法存储一些数据结构

因此,内存需要通过数据传输指令向寄存器传输数据

lw x9, 8(x22) // 将内存中取出的数据放到x9寄存器中 
              // 取数据的地址 = x22寄存器中存储的地址 + 8
              // x9是要放入的寄存器,8是偏移量,x22是基址寄存器
sw x5, 40(x6) // 将x5中的数据放到内存(存放地址 = x6 + 40)中

我们由这两条指令发现,指令均为3个操作数【偏移量,基址寄存器,目标寄存器】

这是因为处理固定的操作数会降低硬件的复杂程度。原则1:简单源于规整

寄存器的数量为什么是32呢?

和程序变量相比,寄存器肯定是有限的。而过多的寄存器会导致时钟周期变长,运行速度变慢。而过少的寄存器又无法保存数据。经过权衡,得出32这个值。原则2:更少则更快

为什么算术指令的操作数由寄存器提供呢?

  1. 访问寄存器的时间比访问内存短

  2. 算术指令1次即可读取两个寄存器,而数据传输指令中1次只能传一个操作数(寄存器吞吐率高)

几乎所有的体系结构都是按照字节来寻址的,RISC-V使用的是32位电脑,即1字 = 4字节 = 32位
所以内存中连续字地址相差4

image-20240914173214173

通过上图,我们可以发现按字取值时,字节中的值并不重要。比如我们取0字地址中的数据,只要取出的数据是1就可以,而0字节、1字节、2字节、3字节地址中的值并不值得我们关注。

事实上,每个字内存中如何存储值分为两个模式:大端模式和小端模式

假设现有16进制数0x1234

大端模式下,内存是按如下格式存储的:

地址
10x34
00x12

小端模式:

地址
10x12
00x34

RISC-V属于小端模式,按字取时一个字中字节存储顺序并不重要,除非访问单个字节。

立即数操作

addi x22, x22, 4    // x22寄存器的值 = x22寄存器的值 + 4

使用立即数从而避免从内存中取值。

x0寄存器中硬接线至0,因此总是为0

因为0在指令系统中经常用到,设置寄存器始终为0是加速经常性事件思想的体现

符号扩展:假设寄存器中的位数大于要存的二进制数,那么按照符号位填充二进制数的左端

而对于无符号数而言,二进制数左侧填充0即可

lbu x6, 30(x22) // 无符号数载入

指令表示

RISC-V中指令位数均为32位。

image-20240914180449637

如上图所示

R型用于两个源操作数寄存器、目的操作数寄存器完整的指令(add)

I型用于一个源操作数寄存器的指令(lw)

S型用于没有目的操作数寄存器的指令(sw)

可以看到,即使指令类型不同,相同功能的模块仍然处于相同位置,S型甚至将立即数分成两部分,前7位保存立即数的前7位值,后5位保存立即数的后5位值。简单源于规整

计算机的两个关键原则

  1. 指令由数字表示

  2. 程序和数据一样保存在存储器中进行读写

计算器:输入数据,人来操作,得出结果

计算机:输入数据和程序,运行程序,处理数据,得出结果

程序的执行是计算机自动处理数据的关键所在。

逻辑操作

目的:用于简化打包和拆包。

sll x5, x6, x7 // x5 = x6 << x7 逻辑左移
srl x5, x6, x7 // x5 = x6 >> x7 逻辑右移
and x5, x6, x7 // x5 = x6 & x7  按位与
or x5, x6, x7  // x5 = x6 | x7  按位或
xor x5, x6, x7 // x5 = x6 ^ x7  按位异或

由于NOT只有一个操作数,因此RISC-V中用XOR异或来代替(3个操作数=>简单源于规整)

AND操作:只取需要的位上的值,其余位置均为0

用于决策的指令

beq rs1, rs2, L1  // if rs1 == rs2, go to L1
bne rs1, rs2, L2  // if rs1 != rs2, go to L2
blt rs1, rs2, L3  // if rs1 < rs2, go to L3
bge rs1, rs2, L4  // if rs1 >= rs2, go to L4

L1代表语句前的显示标签

L1: sub x19, x20, x21

边界检查

当检查arr[i]中的i是否越界时,一般采用0 ≤ i < arr.length

假设arr.length放在x11中,i放在x20中,指令中可以用:

bgeu x20, x11, IndexOutOfBounds

若i为负数,则在无符号的情况必然大于arr.length,因为负数补码第1位为1;若i超出范围,也会大于arr.length

计算机硬件对函数的支持

  1. 放置参数到寄存器中

  2. 将控制权交给函数

  3. 获取函数所需的资源

  4. 执行任务

  5. 放置返回值到寄存器中

  6. 将控制权交给初始点

RISC-V将x10 ~ x17用于传递参数和返回值;x1作为返回地址寄存器,用于返回初始点

jal x1, ProcedureAddress // 跳转至ProcedureAddress,x1 = PC + 4即x1保存初始点的下一行函数地址
                         // 用于调用函数
jalr x0, 0(x1) // 跳转至初始点的下一行函数,因为x0常为0,丢弃PC + 4
               // 函数使用完毕后,返回原过程

用栈来存储数值

  1. 函数需要超过8个参数寄存器时

  2. 换出寄存器中的值到内存中

  3. 以栈的形式存储

  4. 运算结束

  5. 弹出栈的值到寄存器

  6. 返回调用点

x2寄存器:栈指针寄存器,又称sp

翻译程序

// 编译以下函数,g放在x10, h放在x11, i放在x12, j放在x13:
int leaf-example(int g, int h, int i, int j) {
    int f;
    f = (g + h) - (i + j);
    return f;
}
addi sp, sp, -12        // 压栈
sw x5, 8(sp)
sw x6, 4(sp)
sw x20, 0(sp)
​
add x5, x10, x11        // 计算f
add x6, x12, x13
sub x20, x5, x6
​
addi x10, x20, 0        // 保留返回值
​
lw x20, 0(sp)           // 弹栈
lw x6, 4(sp)
lw x5, 8(sp)
addi sp, sp, 12
​
jalr x0, 0(x1)          // 返回初始点

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值