RISC-V 汇编语言
函数调用的步骤在计算机组成与设计中也有过涉及:
- 指定寄存器存入参数;
- 跳转到函数开始位置(jal);
- 在callee中按需保存寄存器;
- 执行函数;
- 恢复保存的寄存器;把返回值存入指定位置;
- 返回调用函数的位置(ret命令,mips是jr)。
变量尽量存在寄存器里因为速度快嘛。调用函数过程中不用保存值的寄存器(被函数直接覆盖掉值)叫做临时寄存器;需要保存值的寄存器叫做保存寄存器。不再调用其他函数的函数叫做叶函数。叶函数如果参数和局部变量较少,可以直接用寄存器解决;如果过多需要存储到内存中(spilling 溢出),不过溢出情况并不多见。
主要是保存寄存器还有一些涉及底层操作的寄存器比如sp栈指针等值不可改,需要保留。
函数调用示例:
entry_label:
addi sp,sp,-framesize # framesize存储的是要保存的ra,保存寄存器等个数*4,因为都是32位的寄存器
sw ra,framesize-4(sp) # 栈顶存ra
# 按需保存其他寄存器
# 函数体
# 按需返回其他寄存器
lw ra,framesize-4(sp) # 栈顶取ra
addi sp,sp,framesize
ret
汇编器
unix系统中是.s后缀的文件,而ms-dos系统中是.asm文件。
文件中.开头的是指示符。比如下面分析一下 hello world 编译产生的汇编文件:
.text 标识代码段开始。
.align 2 是按32位对齐代码,因为指令都是32位。
.globl main 是全局 main 函数。
字符串是不可修改数据,因此用 .section .rodata 只读数据标识。也要对齐数据。
然后用 .string 标识 \0 结尾的字符串。
汇编器最终产生的文件格式如下,是可链接的ELF文件。
里面8到1c行的所有寄存器传入地址都是0,等到后续链接的时候再设置具体值。
链接器
允许各个文件进行独立的编译和汇编,这样改动部分文件不用全部重新编译,把新代码和已经存在的机器语言模块拼接起来即可。
unix 文件是 .o -> .out 文件,msdos 文件是 .obj .lib -> .exe 文件。
链接器需要分配图中相符的地址空间给程序、数据。如果程序和位置无关(PIC代码)这一步可省略。RV32I的相对转移特性(程序跳转是加相对值)使得更容易实现PIC。
然后需要把编译器中直接跳转到标签的指令翻译为跳转到立即数地址,jalr 的值也修正为跳转到立即数地址 jal,auipc jalr lui addi 这些具体地址值修改。
RV的浮点数传递要看选择哪一种ABI接口规范。ilp32 是浮点数在整型寄存器中传递,ilp32f 是单精度在浮点寄存器中,ilp32d 是双精度也在浮点寄存器中。
RV32I不支持FD扩展,因此只能用ilp32.编译指令:GCC 选项-march=rv32i -mabi=lib32
不过没说支持浮点扩展就一定要用,因此RV32IFD也可以用ilp32或ilp32f。
链接器也会检查是否兼容扩展。编译器可能支持多种ABI ISA,但是电脑上面本身没装那些库的情况下就会不兼容。
静态链接和动态链接
链接库函数时,如果这个库函数当前版本已经链接过就不再链接。多次调用也不重复链接。
而且动态链接跳转地址是存在内存中一张表里的,每次第一次链接时更新函数地址表,调用时只需要三条指令(存根指令)去查表找到对应跳转位置后跳转。
结语
0寄存器使得部分伪代码更容易实现;
lui auipc 不同于以往组合两条16位地址的复杂指令形式,20位左移+12位更快获得地址,减少指令数量,且 auipc 便于处理 PIC;
PIC 降低链接复杂度(位置无关);
大量寄存器使得运算速度加快。