安装RISCV工具链
riscv-gnu-toolchain工具链和模拟器安装记录 - 知乎 (zhihu.com)
riscv-gnu-toolchain工具链分elf-gcc、linux-gnu-gcc两个版本,以及对应的32位和64位版本。两个版本的主要区别是:
- riscv32-unknown-elf-gcc、riscv64-unknown-elf-gcc使用的是riscv-newlib库(面向嵌入式的C库),只支持静态链接,不支持动态链接。
- riscv32-unknown-linux-gnu-gcc、riscv64-unknown-linux-gnu-gcc使用的是glibc标准库,支持动态链接。
最终选用的是riscv64-unknown-linux-gnu-gcc,支持动态链接
函数调用约定
x86
- 调用约定:
- 常见的调用约定包括
cdecl
、stdcall
和fastcall
。 - 不同的调用约定通常是为了满足特定编程语言或平台的需求。
- cdecl(C Declaration):调用者清理堆栈,支持可变参数,C语言标准推荐使用。
- stdcall:被调用者清理堆栈,不支持可变参数,通常用于 Windows API。
- fastcall:使用寄存器传递前两个参数(ECX 和 EDX),其余参数通过堆栈传递,适用于函数频繁调用的场景。
平台与工具链
不同的编译器和平台可能会选择不同的调用约定。
例如,Microsoft 的 Visual C++ 编译器通常支持 stdcall 和 fastcall,
而 GCC 支持 cdecl 和其他约定。
- 参数传递:参数从右到左入栈。
- 返回值:返回值通常通过 EAX 寄存器返回。
- 通用寄存器:8 个(EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP)
- 指令指针寄存器:1 个(EIP)
- 段寄存器:6 个(CS, DS, SS, ES, FS, GS)
- 总数:大约 15 个(包含所有类型)
x64
- 调用约定:
- 常用的调用约定为 Microsoft 和 System V(Linux 和 macOS)。
- Microsoft x64(Window操作系统,常与WindowsAPI 交互):
-
- 前四个整型或指针参数通过寄存器(RCX、RDX、R8、R9)传递,其余通过堆栈。
- 返回值通过 RAX 寄存器返回。
- System V x64( Linux /Unix操作系统 , 常与 POSIX 标准库和开源项目兼容。 ):
-
- 前六个整型或指针参数通过寄存器(RDI、RSI、RDX、RCX、R8、R9)传递,其余通过堆栈。
- 返回值通过 RAX 寄存器返回。
- 参数传递:同样是从右到左入栈。
- 通用寄存器:16 个(RAX, RBX, RCX, RDX, RSI, RDI, RBP, RSP, R8, R9, R10, R11, R12, R13, R14, R15)
- 指令指针寄存器:1 个(RIP)
- 总数:17 个(包含所有类型)
RV32(RV64类似,但支持更大的寄存器范围)
- 调用约定:
- 使用 RISC-V 的 ABI,函数参数通过寄存器和堆栈传递。
- 参数传递:
- 前八个参数通过寄存器(a0 到 a7)传递。
- 如果参数超过八个,其余参数通过堆栈传递。
- 返回值:
- 返回值通过 a0 寄存器(即 x10)返回。
调用约定
函数调用过程通常分为 6 个阶段 [Patterson and Hennessy 2021]:
- 将参数存放到函数可访问的位置;
- 跳转到函数入口(使用 RV32I 的 jal 指令);
- 获取函数所需局部存储资源,按需保存寄存器;
- 执行函数功能;
- 将返回值存放到调用者可访问的位置,恢复寄存器,释放局部存储资源;
- 由于程序可从多处调用函数,故需将控制权返回到调用点(使用 ret 指令)
为提升性能,应尽量将变量存放在寄存器而不是内存中,但同时也要避免因保存
和恢复寄存器(函数调用和上下文切换)而频繁访问内存。
RISCV架构优势在于
1.足量的寄存器
2.可以减少保存和恢复寄存器的次数
寄存器的分类
1.临时寄存器:不保证其值在函数调用前后的一致性
2.保存寄存器:保证其值在函数调用前后的一致性
叶子函数
不再调用其他函数的函数称为叶子函数。
当一个叶子函数只有少量参数和局部变量时,可将其分配到寄存器, 无需分配到内存。大部分函数调用均如此,此时程序无需将寄存器保存到内存
RISCV寄存器
比较特殊的几个寄存器
- 裸机环境
在裸机(bare-metal)编程中,如果程序没有操作系统,tp 通常不会被使用。
如果程序需要自己实现多线程,则可以定义一个线程控制块,并通过软件设置 tp 指向相应的结构。
在函数入口处和出口处标准的RV32I代码
entry_label:
addi sp,sp,-framesize # 调整栈指针(sp 寄存器)来分配栈帧
sw ra,framesize-4(sp) # 保存返回地址(ra 寄存器)
# 按需保存其他寄存器
... # 函数体
# 按需恢复其他寄存器
lw ra,framesize-4(sp) # 恢复返回地址寄存器
addi sp,sp, framesize # 释放栈帧空间
ret # 返回调用点
汇编器
汇编器的作用
1.面向处理器:生成的目标代码(处理器可以理解的指令)
2.面向程序员和编译器开发者:伪指令(常规指令的巧妙特例)
RISCV中的伪指令分为两类
1.依赖恒为0的x0寄存器(硬连线为0)——————>极大简化了RISCV指令集
电气连接:在电路设计中,硬连线意味着某个信号线直接连接到地(0V),从而使得该信号始终保持在低电平状态(即逻辑 0)。
2.其他
伪指令
1.依赖恒为0的x0寄存器的伪指令(32条)
set not equal to zero
set less than zero
set greater than zero
将rs与x0寄存器的值(0)进行条件比较,满足比较条件将rd置为1,否则为0
看大于0时置位,实则是用slt指令进行比较的,无sgt指令
branch equal to zero
以第一条为例,比较rs寄存器的值是否为0,为0则跳转到当前指令相对偏移offset地址处
jump and link 无条件跳转
1.jal x0, offset
会直接跳转到目标地址
适用于不需要保存返回地址的跳转,offset
:是一个相对地址偏移量,指向要跳转到的目标指令地址
(正常情况下在进行函数调用时,需要将调用函数处的下一条指令的地址保存在寄存器中,此处寄存器为x0,硬连线为0, 这种跳转适用于不需要返回的场景,如错误处理或实现循环)
2.jalr x0, rs, 0
会直接跳转到 rs
中存储的地址,
3. 同上,唯一区别是返回地址寄存器x1:
在 RISC-V 架构中,x1
寄存器通常被称为 ra
(return address)寄存器,专门用于存储返回地址。 此处代表从子过程返回
什么是尾调用?
以C源代码为例,将return func();成为尾调用
用jmp 跳转指令替换掉call+ret组合的指令成为尾调用优化
尾调用优化,用jmp 跳转指令替换掉call+ret组合的指令成为尾调用优化,减少栈帧的维护