计组学习06——RISC-V Functions

有点发烧,唔,不过还是坚持学下来了,新冠让人静心.

计组学习——RISC-V Functions

Loading Sign Extension

假如内存里的字节是这样:

0b1111 1110(-2)

当我们使用load byte指令时:众所周知,我们需要进行符号拓展

我们此时的序列 0b XXXX XXXX XXXX XXXX XXXX XXXX 1111 1110

就会变成:0b1111 1111 1111 1111 1111 1111 1111 1110

可以发现,进行符号拓展之后,获得的数字不变,依旧是-2

举例:假如s0内的地址为0x00008011

  • lb t0,0(s0) -> loading 0b00010001

    0b0000 0000 0000 0000 0000 0000 0001 0001

  • lb t0,1(s0)-> loading 0b10000000

    0b1111 1111 1111 1111 1111 1111 1000 0000

如果我实际上就想要一个无符号数怎么办呢?我该怎么进行拓展呢?

  • 不进行符号拓展,只要添加前导0就可以了!

  • 用上述例子距离:lbu t0,1(s0)-> unsigned loading 0b10000000

    0b0000 0000 0000 0000 0000 0000 1000 0000

Pseudo-Instructions 伪指令

伪指令只是更容易懂的指令,所有伪指令都可以拆解成为基础的语句,但是有时使用基础语句对于程序员来说有些麻烦。

同时,为了适配多种芯片,伪指令的概念也是很有必要的。

基础的指令就是硬件能够执行的指令,能在物理层面运行的指令。

  • move

    mv dst,reg1

    将一个寄存器里的值写入到另一个寄存器

    等同于 addi dst,reg1 0

  • load immediate(li)

    li dst, imm

    把一个32位的数字加载到dst里,但事实上我们没有访问内存

    等同于,先addi,在左移lui

    lui是将一个20位的数左移12位,比如lui x5,0x12135

    这是因为addi内存在一些限制,我们并不能加一个32位的数,稍后会提到

  • load address(la)

    la dst, label

    将label标签内的地址加载到dst里

    本质是我们把label的地址转化为物理上的地址(用label相对于pc的地址加上pc的绝对地址),

    之后算出来之后放到dst里

    等同于,auipc dst, <offset to label>

    auipc是讲一个数字imm左移十二位之后加上一个pc的偏移值

    pc指针是program controller,指向了目前运行的指令的地址

    同样的这里的imm只能是20位的数

    auipc是唯一一条能让我们知道当前PC在哪的指令

  • No Operation(nop) 空指令

    nop

    等同于,add x0,x0,0

就连jump这样的指令也都是伪指令,伪指令真的非常有用!

C to RISC-V Practice

  • Fast String Copy

    char *p, *q;
    while( (*q++ = *p++)!= '\0' );
    

虽然代码很简洁,但是我们最好写下每一步!这样方便进行转换!养成好习惯

p->s0,q->s1;
t0=*p;
*q=t0;
p=p+1;
q=q+1;
if *p==0,go to Exit
go to Loop

汇编代码:

Loop: lb    t0,0(s0)
      sb    t0,0(s1)
      addi  s0,s0,1
      addi  s1,s1,1
      beq   t0,x0,Exit
      j Loop
Exit:

可以发现我们在load byte的时候,获取了一个字节,而前面的24位是进行符号拓展的。

但是这不重要!因为我们在sb操作的时候,忽略了前面24位,所以完全没有影响!

  • 可以发现我们在上述过程中,一次循环执行了六条指令,但是我们可以简化!
Loop: lb    t0,0(s0)
      sb    t0,0(s1)
      addi  s0,s0,1
      addi  s1,s1,1
      bne   t0,x0,Loop
Exit:

这样在一次循环里只运行了五次指令!

汇编语言程序(重点!)

调用程序的六个步骤:

  1. 将参数放入到函数可以访问的位置
  2. 将C语言程序拆分为流程
  3. 考虑堆栈空间,给程序一些内存!
  4. 程序按照指令运行
  5. 在将值返回到某些位置之后,记住“clean up”
  6. 结束调用,返回调用前的指令

重点在于:什么时候调用什么寄存器!

  • 步骤1和步骤5:我们应该把参数和返回值放在哪?
    • 因为寄存器比内存快得多,所以把这些值都放在寄存器里
      • a0-a7:八个参数寄存器(argument registers),用来传递参数
      • a0-a1:两个参数寄存器,也被用于传递返回值
    • 如果需要多余的空间!请使用的内存!
    • 参数的顺序很重要!
      • 因为在返回值的时候,我们已经不再需要参数了,所以需要覆盖掉原本的参数

例子: c语言代码:

int add(int a,int b){
    return a+b;
}
void main(void){
    a=3;
    b=a+1;
    a=add(a,b);
}

汇编语言代码:

main:
	addi a0,x0,3
    addi a1,a0,1
    jal ra,add   #jump and link将执行的下一条指令地址(其实就是+4字节,因为riscv每条指令占有4个字节)				    存入ra,并且跳转到add
add:
	add a0,a0,a1
    jr ra        #这里不使用j是因为j跳转的是标签,而我们把下一条指令的地址存到了ra里。同时,ra也叫x1,专				   门用来储存函数的返回地址
  • 步骤2和步骤6:我们如何进行流程控制?

还是上述例子,我们可以看到,jal ra,add我们把下一条指令的地址存入ra(其实就是jal这一条指令的地址+4),之后我们在add函数里jr ra 但是我们都知道!,jr本身是一个伪指令,那么它的本质指令是什么呢?

jr ra =jalr x0,ra,0 我们把ra寄存器的地址加上偏移值(立即数0),那么就是ra的地址,之后我们把这个新地址储存到x0中,这并不会改变x0,x0永远不会改变!

变量的本地存储

本地存储的本质就是sp指针,sp指向栈空间的底部,拓展肯定是向下拓展的,拓展4个字节相当于sp-4

addi sp,sp,-4
sw   t0,0(sp)
#将t0存入栈里

使用寄存器的规范

例如,如下的c++程序

int sumSquare(int x,int y){
    return mult(x,y)+y;
}
  • 我们需要储存什么?
    • 因为调用mult会覆盖掉ra,所以我们应该先存下来
    • 而a0和a1本来存储x和y,如果调用mult,那么我们a1里面y的值就会被x覆盖掉!

调用规范!

  • calleR:在调用别人的函数
  • calleE:被调用的函数

Saved Registers(CalleE Saved)

  • 这些寄存器在CalleE调用前后保持不变
    • 如果calleE使用了他们,他们必须在返回之前重新储存值
    • 这意味着把旧的值存下来(存到栈里),调用寄存器,然后重新把旧的值读入寄存器里
  • s0-s11(saved registers)
  • sp(stack pointer)

Volatile Registers(CalleR Saved)

  • 这些寄存器可以被CalleE自由的改变
    • 如果calleR需要他们,他必须在程序调用之前把值存储起来
  • t0-t6(temporary registers)
  • a0-a7(return address and arguments)
  • ra(return address)

综上所述,寄存器就分为两种:

  • Caller saved
  • CalleE saved

存储register

当然是栈了!

image

那么...回到我们最开始的c++程序:

int sumSquare(int x,int y){
    return mult(x,y)+y;
}

可以把它转换为汇编程序:

sumSquare:
	addi  sp,sp,-8    #在栈里开辟两个空间
    sw    ra,4(sp)    #存储ra
    sw    a1,0(sp)    #存储y的值
    add   a1,a0,x0    #把a1的值改变为x(因为我们调用的是mult(x,x))
    jal   mult        #跳转执行mult
    lw    a1,0(sp)    #执行之后,重新加载a1(其实就是让a1=y)
    add   a0,a0,a1    #计算mul(x,y)+ y
    lw    ra,4(sp)    #加载回ra
    addi  sp,sp,8     #栈指针回溯
    jr    ra          #结束函数调用
mult:
	...               #执行x*x的功能

函数的基本结构:

Prologue
    func_label:
		addi sp,sp,-framsize
        sw ra,<framsize-4>(sp)
        #store other callee saved registers,such as:s0-s11
        #save other registers if needed,such as:a0-a7,t0-t6
Body (call other functions...)
    ...
Epilogue
     #restore other regs if needed
     #restore other callee saved regosisters
     lw ra,<framsize-4>(sp)
     addi sp.sp.framsize
     jr ra

image

posted @   ZzTzZ  阅读(214)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示