Rocket Chip 代码注释已上传至github,持续更新中。
Return Address Stack
RAS(Return Address Stack), 即返回地址预测栈,该模块针对函数调用进行优化,在取指令前端维护了一个硬件实现的返回地址预测栈
- 当执行函数调用(call)时,将函数返回地址(Return Address)入栈
- 当函数返回(return)时,直接从栈中弹出地址进行取指令
RAS更新相关的指令
call和ret都是RISC-V中的伪指令,查阅riscv-spec中伪指令的定义,可以看到call和return指令主要与JAL、JALR指令相关。
call与return伪指令但是,从上表可以看出,call和ret指令在关于JALR这条指令上有所重叠,同样是JALR指令,有些是call指令,有些是ret指令,为了区分两者,必须仔细查阅一下手册。
Unconditional Jumps
JALR指令的语义是:
- 跳转到base+offset地址
- 将返回/链接地址写入dest寄存器
- 若返回/链接地址无意义,则dest寄存器置为x0(针对ret指令)
The standard software calling convention uses:
- x1 as the return address register
- x5 as an alternate link register
根据指令集手册中的说明,我们可以进而知道标准的软件调用过程中:
- 函数调用时,我们希望写入RAS栈中的地址在dest字段(即rd字段),约定使用x1与x5寄存器;
- 相应地,当函数返回时,返回地址通常也在x1与x5寄存器, 位于base字段(即rs1字段),rd字段通常为x0。
了解了上述细节,可以查看Rocket Core中的实现来验证我们的理解(显然,我已经验证过了hh)
// 见 Frontend.scala
val rviJump = rviBits(6,0) === Instructions.JAL.value.asUInt()(6,0)
val rviJALR = rviBits(6,0) === Instructions.JALR.value.asUInt()(6,0)
val rviReturn = rviJALR && !rviBits(7) && BitPat("b00?01") === rviBits(19,15)
val rviCall = (rviJALR || rviJump) && rviBits(7)
Rocket Core中的实现细节
了解了相关指令之后,我们下面关注RAS的具体实现细节,riscv-spec中还明确了RAS的push/pop条件与规范。如下表所示
RAS Push/Pop规范link: 代表JAL/JALR指令的rs1或rd字段是否使用x1或x5寄存器
表中前三项为常规操作
- 无关指令: 空操作
- Call指令: rd寄存器内容入栈(push)
- ret指令: 出栈(pop)并预测跳转
下面两项比较特别,下面简单说明一下
1. 支持协程实现
一般情况下,函数调用过程中Call与Return指令是配对出现的。(不考虑程序抛出异常)
但是协程与一般函数调用过程不同
(TODO:还没弄清楚orz)
2. macro-op fusion
TODO
Chisel代码实现
Rocket Chip中用面向对象的方式将RAS封装成了一个类(而不是RTL代码中常见的module的写法),定义了相应的push、pop、peek等接口,为了方便理解,我绘制了一个示意图。
RAS示意图
基于示意图,代码还是比较好理解的。
// Return Address stack
// 主要针对函数调用进行优化:
// - Call相关指令入栈, 记录返回地址
// - Return相关指令出栈, 根据记录的返回地址预测跳转
class RAS(nras: Int) {
// 函数调用时入栈顶
def push(addr: UInt): Unit = {
when (count < nras) { count := count + 1 }
val nextPos = Mux(Bool(isPow2(nras)) || pos < nras-1, pos+1, UInt(0))
stack(nextPos) := addr
pos := nextPos
}
// 读出栈顶元素
def peek: UInt = stack(pos)
// 函数返回出栈
def pop(): Unit = when (!isEmpty) {
count := count - 1
pos := Mux(Bool(isPow2(nras)) || pos > 0, pos-1, UInt(nras-1))
}
def clear(): Unit = count := UInt(0)
def isEmpty: Bool = count === UInt(0)
// Bug!!! 这里count和pos应该用RegInit,复位初始化为0
// 当前栈中返回地址个数
private val count = Reg(UInt(width = log2Up(nras+1)))
// 栈顶指针
private val pos = Reg(UInt(width = log2Up(nras)))
// 栈本体 (:-P)
private val stack = Reg(Vec(nras, UInt()))
}
一些注意事项
1. 未实现RAS pop then push 操作
截取RAS实例化后的代码
if (btbParams.nRAS > 0) {
val ras = new RAS(btbParams.nRAS)
//
val doPeek = (idxHit & cfiType.map(_ === CFIType.ret).asUInt).orR
io.ras_head.valid := !ras.isEmpty
io.ras_head.bits := ras.peek
when (!ras.isEmpty && doPeek) {
io.resp.bits.target := ras.peek
}
when (io.ras_update.valid) {
when (io.ras_update.bits.cfiType === CFIType.call) {
ras.push(io.ras_update.bits.returnAddr)
}.elsewhen (io.ras_update.bits.cfiType === CFIType.ret) {
ras.pop()
}
}
}
可以发现,这里并没有实现指令集手册中的pop then push的操作。
[未完待续,不定期补充]
网友评论