计组学习05——Introduction to Assembly Language
感觉可算到了真正计组重要的部分(毕竟我是因为挖操作系统的时候感觉计组知识严重不足来补的计组。
为了看清楚问题和ppt,倍速减慢截取字幕消失的那几帧....
然后还有一些解释看不懂跟不上,去上网搜索
还遇到了看不懂的单词,有些我也不知道官方翻译是什么,这就是没有基础就学习国外教程的痛苦...
应该是阳了,这几天学习效率也不算高,从下一阶段准备开始加速学习了...现在的效率距离目标还是很远的...
计组学习 —— Introduction to Assembly Language
从高级程序语言(比如C),到汇编语言程序(比如RISC-V),我们逐渐深入计算机的硬件。
在这门课里,我终于知道了为什么
硬件与变量
与高级程序语言不同,汇编语言往往没有变量这个概念,而是使用寄存器来存储值
关于寄存器:
- 寄存器就是小块的固定大小的内存。32位或者64位
- 可以被读写
- 数量有限制,32位的机器只有32给普通寄存器
- 非常快
如果使用的变量多于32个怎么办?
- 把使用最频繁的放入寄存器中,之后把剩下的放到内存里,这个方法也叫溢出到内存(spilling to memory)
为什么不把所有的变量都放到内存呢?
- smaller is faster:寄存器比内存快了100-500倍
- 内存分级:
- 寄存器:32个*32位 = 128字节
- RAM: 4-32GB
- SSD:100-1000GB
寄存器的分类:
-
我们使用x0-x31来为32个寄存器命名
-
同时,寄存器由于功能不同,所以也有自己的名字
- s开头的寄存器,表示他是一个安全(safe)寄存器
- t开头的寄存器,一般用于储存临时变量(tmp)
-
s0-s1 对应 x8-x9
-
s2-s11对应x18-x27
-
t0-t2 对应 x5-x7
-
t3-t6 对应x28-x31
零寄存器
什么是程序中最重要的数字?
- 没错,答案就是零寄存器
- x0表示零寄存器,它的值永远是0,来表示一些错误或者别的,永远不可能被改变!!!
相关寄存器
寄存器 | ABI名 | 用途 | Saver |
---|---|---|---|
x0 | zero | 读取时永远为0,写入起不到任何效果 | - |
x1 | ra | 存储函数返回的地址(return address) | Caller |
x2 | sp | 存放栈指针(stack pointer) | Callee |
x3 | gp | 全局指针 global pointer | - |
x4 | tp | 线程指针 thread point | - |
x5-x7 | t0-t2 | 临时 (temporaries) 寄存器,Callee 可能会使用这些寄存器,所以 Callee 不保证这些寄存器中的值在函数调用过程中保持不变,这意味着对于 Caller 来说,如果需要的话,Caller 需要自己在调用 Callee 之前保存临时寄存器中的值。 | Caller |
x28-x31 | t3-t6 | 临时寄存器,同上 | Caller |
x8-x9 | s0/fp - s1 | 保存 (saved) 寄存器,Callee 需要保证这些寄存器的值在函数返回后仍然维持函数调用之前的原值,所以一旦 Callee 在自己的函数中会用到这些寄存器则需要在栈中备份并在退出函数时进行恢复。 | Callee |
x18-x27 | s2-s11 | 保存寄存器,同上 | Callee |
x10-x11 | a0-a1 | 参数 (argument) 寄存器,用于在函数调用过程中保存第一个和第二个参数,以及在函数返回时传递返回值。 | Caller |
x12-x17 | a2-a7 | 参数 (argument) 寄存器,如果函数调用时需要传递更多的参数,则可以用这些寄存器,但注意用于传递参数的寄存器最多只有 8 个 (a0 ~ a7) ,如果还有更多的参数则要利用栈。 | Caller |
Caller :调用者,Callee : 被调用函数【这里可以视为在 Caller (函数)中调用 Callee (函数)】
Caller 维护的寄存器,在运行被调函数前不会被保存,函数返回时这些寄存器可能会被改变,所以在调用前由 Caller 保存维护。
Callee 维护的寄存器,在运行被调函数前会被保存,函数返回后这些寄存器与运行被调函数前相同。原文链接:https://blog.csdn.net/m0_52132972/article/details/126574474
汇编指令
基本命令格式:
op dst, src1, src2
- op即为操作名
- dst为结果储存的寄存器
- src1为操作的第一个寄存器
- src2为操作的第二个寄存器
基础算数指令
-
整数加法
add s1, s2, s3
-
整数减法
sub s1, s2, s3
-
假设abcde分别对应s0-s5,那么 a=(b+c)-(d+e):
add t1, s3, s4
add t2, s1, s2
sub s0, t2, t1
Immediates
命令格式:
opi dst, src, imm
- 在操作后面加一个i,会把第二个source替换为一个常数,叫做immediate
- immediate最大可以到12位
- immediate可以认识符号
那么问题来了: 假设a为s0,b为s1 , a=(5+b)-3
addi t1, s1, 5
addi s0, t1, -3
为什么第二句话不写成 subi s0 t1 3
呢?
- 因为当我们能用一个命令执行的时候,就不要浪费另一条指令的空间,
- 减少使用的指令的种类和数量,可以降低风险
Data Transfer数据传输指令
数据之间的运输是指寄存器和内存之间的运输
命令格式:
memop reg, off(bAddr)
- memop 对于内存的操作名
- reg 资源最终要储存的寄存器
- bAddr 指向内存的指针寄存器
- off 为地址的偏移量
- 我们最终要到达的地址为: bAddr+off
Load Word(lw)指令:
- 从内存中的bAddr+off获取数据放入到reg中
Store Word(sw)指令:
- 从内存中获取数据储存到reg中
假设我们有一个int类型数组array[],储存到s3中,有一个int类型的数字b储存在s2中,实现: array[10]=array[3]+b:
lw t0,12(s3) # t0=array[3]
add t0,s2,t0 # t0=array[3]+b
sw t0,40(s3) # array[10]=array[3]+b
注意到!我们的off在计算时,需要程序员去考虑元素的大小!在这道题中,假设操作系统的int是4字节的,因为我们默认是按字节寻址的,所以也需要手动把单位都转化为字节
Load Byte(lb),Store Byte(sb)指令:
- 在sb操作中,最高的24位将被忽略
- 在lb操作中,最高的24位将通过符号拓展的方式填充
看实例~,假设 s0 = 0x00000180
lb s1,1(s0) # s1=0x00000001 ,因为偏移量为1位,所以我们获取到0x01(00000001)这8位,前面的24位都被符号位填充为0
lb s2,0(s0) # s2=0xFFFFFF80 ,0x80(10000000),所以前面24位全被1填满,十六进制下全是FF
sb s2,2(s0) # *(s0)=0x00800180,我在s0中偏移两位的储存我在s2中加载的值,因为只取s2最低的8位,所以只有80,之后把这个80写入到s0对应的位置
**Load Half(lh),Store Half(sh) **
无需多言,就是把 lb和sb的命令,从忽略24位/填充24位,变成了忽略16位/填充16位
Unsigned Instruction
在上述操作中,我们如果在lh/lb后面加一个u,变成lhu/lbu,就多加了一个意思,把无符号的东西加载到其中。
如果仔细思考,可以发现没有sbu或者shu同样没有lwu,因为这些指令实际上是没有意义的
lbu或者lhu,拓展的是最高有效位的符号!
流程控制
Branch If Equal(beq)
beq reg1,reg2,label
- 如果reg1寄存器里的值等于reg2寄存器里的值,跳转执行label
- 否则进行下一条指令
Branch If Not Equal(bne)
显然,两个寄存器不等于的话直接跳转label
Jump(j)
j label
无条件跳转到label
Branch Less Than(blt)
blt reg1,reg2,label
- 如果reg1<reg2 那么跳转到label
Branch Greater Than or Equal(bge)
bge reg1,reg2,label
- 如果reg1≥reg2,跳转到label
可以思考发现,没有必要小于等于,因为只需要交换两个分支顺序就可以变成大于等于了,为了精简指令,所以没有判断小于等于
关于流程控制的解释
其实流程控制的本质是控制pc指针,因为一条条指令的本质也是数据,pc指针指向的就是当前执行的指令,而流程控制改变了pc的值,所以我们可以控制语句的执行顺序。
同时我们要控制PC是一个偶数,因为每一条指令都是偶数字节,从0开始,加上或者减去偶数,它永远都是偶数
转换指令
- 众所周知,左移一位比乘2快得多,我们在汇编层面也要支持移位操作,才能最大化速度。
- 但是右移不一定与除法相同
在移位时,可以保留值
Instruction Name | RISCV |
---|---|
Shift Left Logical 逻辑左移 | sll s1,s2,s3 |
Shift Left Logical Imm 逻辑左移数字 | slli s1,s2,imm |
Shift Right Logical 逻辑右移 | srl s1,s2,s3 |
Shift Right Logical Imm逻辑右移数字 | srli s1,s2,imm |
Shift Right Arithmetic算数右移 | sra s1,s21s3 |
Shift Right Arithmetic Imm算数右移数字 | srai s1,s2,imm |
-
只会检测变量的最低五位,这意味着:
- 读取s3这样的数字时,只有最低5位有效
- 使用Immediate时,数值之应该是0-31
-
逻辑位移,会用0填充高位
-
算数位移,会进行符号拓展
其他拓展
Multiplication(mul and mulh)
mul dst, src1, src2
mulh dst, src1, src2
- src1*src2的结果:更低的32位在mul里,更高的32位在mulh里
Division(div)
div dst, src1, src2
rem dst, src1, src2
- src1/src2 的结果商在div里,余数在rem里
Bitwise Instructions位运算
Instruction | C | RISCV |
---|---|---|
And | a = b & c | and s1,s2,s3 |
And Immediate | a = b & 0x1 | andi s1,s2,0x1 |
Or | a = b | c | or s1,s2,s3 |
Or Immediate | a = b | 0x5 | ori s1,s2,0x5 |
Exclusive Or | a = b ^ c | xor s1,s2,s3 |
Exclusive Or Immediate | a = b ^ 0xF | xori s1,s2,0xF |
Compare Instructions比较指令
-
Set Less Than(slt)
slt dst, reg1, reg2
如果reg1<reg2,dst为true,否则为false
-
Set Less Than Imm (slti)
slt dst, reg1, imm
可以使用比较指令写出另一种流程控制语句
最后附上参考并总结的博客~
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了