栈
先进后出
栈帧
栈帧存放该函数的局部变量及相关调用信息
为何要有 函数调用约定
函数调用约定是为了保证在函数调用期间,被调用函数和调用者之间的通信和数据传递能够有效地进行。它规定了寄存器的使用方式、参数传递方式、返回值传递方式以及堆栈的管理方式等。函数调用约定的存在可以提供以下好处:
-
一致性和可移植性:函数调用约定定义了通用的规则和标准,确保不同函数之间的交互是一致和可预测的。这使得编译器可以生成遵循相同约定的代码,使得代码可以在不同的编译器和平台上移植和执行。
-
寄存器的分配和管理:函数调用约定规定了在函数调用期间哪些寄存器应该由被调用函数保存和恢复,以及哪些寄存器可被调用者使用。这可以确保函数之间对寄存器的使用不会相互干扰,以及在函数调用结束后寄存器的正确恢复。
-
参数传递:函数调用约定定义了如何传递函数参数。通过约定参数的传递方式,编译器和汇编器可以有效地处理函数参数的传递,并生成正确的汇编和机器代码。常见的参数传递方式包括将参数值存储在特定的寄存器中、通过栈传递参数等。
-
返回值传递:函数调用约定规定了函数如何返回值给调用者。通过定义返回值的传递方式,编译器和汇编器可以生成正确的代码,并确保函数返回值正确地返回给调用者。
-
堆栈的管理:函数调用约定定义了在函数调用期间如何管理堆栈。包括如何分配和释放栈帧、栈指针的管理等。这可以确保在函数调用期间栈的使用是安全和可靠的。
寄存器约定
ra存放函数返回地址
a0和a1存放函数返回值
N/A:无关,不考虑谁维护
跳转和返回指令约定
尾调用的具体用处到后面自然会用到,目前先记着就行
实际调用
被调用函数约定
如果被调用函数还会调用其他函数,还需保存ra寄存器(当前函数的返回地址):因为调用其他函数会改变此时的ra
练习
# Calling Convention
# Demo to create a leaf routine
#
# void _start()
# {
# // calling leaf routine
# square(3);
# }
#
# int square(int num)
# {
# return num * num;
# }
.text # Define beginning of text section
.global _start # Define entry _start
_start:
la sp, stack_end # prepare stack for calling functions
li a0, 3
call square
# the time return here, a0 should stores the result
stop:
j stop # Infinite loop to stop execution
# int square(int num)
square:
# prologue
addi sp, sp, -8
sw s0, 0(sp)
sw s1, 4(sp)
# `mul a0, a0, a0` should be fine,
# programing as below just to demo we can contine use the stack
mv s0, a0
mul s1, s0, s0
mv a0, s1
# epilogue
lw s0, 0(sp)
lw s1, 4(sp)
addi sp, sp, 8
ret
# add nop here just for demo in gdb
nop
# allocate stack space
stack_start:
.rept 12
.word 0
.endr
stack_end:
.end # End of file
# Calling Convention
# Demo how to write nested routines
#
# void _start()
# {
# // calling nested routine
# aa_bb(3, 4);
# }
#
# int aa_bb(int a, int b)
# {
# return square(a) + square(b);
# }
#
# int square(int num)
# {
# return num * num;
# }
.text # Define beginning of text section
.global _start # Define entry _start
_start:
la sp, stack_end # prepare stack for calling functions
# aa_bb(3, 4);
li a0, 3
li a1, 4
call aa_bb
stop:
j stop # Infinite loop to stop execution
# int aa_bb(int a, int b)
# return a^2 + b^2
aa_bb:
# prologue
addi sp, sp, -16
sw s0, 0(sp)
sw s1, 4(sp)
sw s2, 8(sp)
sw ra, 12(sp)
# cp and store the input params
mv s0, a0
mv s1, a1
# sum will be stored in s2 and is initialized as zero
li s2, 0
mv a0, s0
jal square
add s2, s2, a0
mv a0, s1
jal square
add s2, s2, a0
mv a0, s2
# epilogue
lw s0, 0(sp)
lw s1, 4(sp)
lw s2, 8(sp)
lw ra, 12(sp)
addi sp, sp, 16
ret
# int square(int num)
square:
# prologue
addi sp, sp, -8
sw s0, 0(sp)
sw s1, 4(sp)
# `mul a0, a0, a0` should be fine,
# programing as below just to demo we can contine use the stack
mv s0, a0
mul s1, s0, s0
mv a0, s1
# epilogue
lw s0, 0(sp)
lw s1, 4(sp)
addi sp, sp, 8
ret
# add nop here just for demo in gdb
nop
# allocate stack space
stack_start:
.rept 12
.word 0
.endr
stack_end:
.end # End of file
char array[] = {'h', 'e', 'l', 'l', 'o', ',', 'w', 'o', 'r', 'l', 'd', '!', '\0'};
int len = 0;
while (array[len] != '\0') {
len++;
}
.section .text
.globl square
.type square, @function
square:
li t0, 0 # 设置寄存器 t0 为 0 (sum)
mul a0, a0, a0 # 计算 a0 * a0,并将结果保存在 a0 中
mv a1, a0 # 将计算结果保存在 a1 中
jr ra # 返回到调用者
.globl sum_squares
.type sum_squares, @function
sum_squares:
li t0, 0 # 设置寄存器 t0 为 0 (sum)
li t1, 1 # 设置寄存器 t1 为 1 (j)
loop:
bgt t1, a0, end_loop # 如果 t1 大于 a0,则跳转到 end_loop 标签处
mv a0, t1 # 将 j 的值保存在 a0 中
jal ra, square # 调用 square 函数,计算平方并返回结果放置在 a0 中
add t0, t0, a0 # 将计算结果加到 sum 中
addi t1, t1, 1 # j++
j loop # 继续循环
end_loop:
mv a0, t0 # 将 sum 的值保存在 a0 中
jr ra # 返回到调用者
.globl _start
.type _start, @function
_start:
li a0, 3 # 设置 a0 为 3
jal ra, sum_squares # 调用 sum_squares 函数
C与汇编混合编程
汇编中嵌入C
# ASM call C
.text # Define beginning of text section
.global _start # Define entry _start
.global foo # foo is a C function defined in test.c
_start:
la sp, stack_end # prepare stack for calling functions
# RISC-V uses a0 ~ a7 to transfer parameters
li a0, 1
li a1, 2
call foo #
# RISC-V uses a0 & a1 to transfer return value
# check value of a0
stop:
j stop # Infinite loop to stop execution
nop # just for demo effect
stack_start:
.rept 12
.word 0
.endr
stack_end:
.end # End of file
/*
* a0 --> a
* a1 --> b
* c <-- a0
*/
int foo(int a, int b)
{
int c = a + b;
return c;
}
最后make将二者一起编译链接,汇编中能够正常调用在c中定义的foo函数,调用形式就是直接call 函数名
c中嵌入汇编
volatile:不需要优化
以下两种格式均可
嵌入RISC-V汇编语言到C代码中需要使用内嵌汇编(inline assembly)。内嵌汇编语法和使用方法可能会因编译器而异,以下是一个通用的示例来说明如何嵌入RISC-V汇编语言到C代码中。
核心语法:
asm("assembly code"
: output operands
: input operands
: clobbered registers);
- assembly code:这里是要嵌入的RISC-V汇编代码。
- output operands:表示输出的操作数,可以是变量名、寄存器或者内存地址。
- input operands:表示输入的操作数,同样可以是变量名、寄存器或者内存地址。
- clobbered registers:指定被修改的寄存器。
- 注意:每个项后面都用冒号(:)分隔,但最后一个项后面没有冒号。
- 操作数约束:
在内嵌汇编中,需要使用操作数约束(operand constraints)来指定嵌入的汇编代码中的寄存器约束。
例如,寄存器约束包含以下字符:r(通用寄存器)、a(参数寄存器)和f(浮点寄存器)。数字表示特定的寄存器。例如,%0表示第一个操作数,%1表示第二个操作数,以此类推。
-
输入输出操作数:
在内嵌汇编中,可以使用输出和输入操作数约束来指定C代码中的变量名作为汇编代码的输入和输出。 -
输出:使用=符号和约束来指定输出操作数。例如,“=r”(output_var)表示将结果输出到output_var变量中。
输入:使用输入操作数的约束指定输入操作数。例如,“r”(input_var)表示将input_var变量作为输入操作数。 -
修改寄存器:
在内嵌汇编中,可以指定被修改的寄存器,这样编译器将会在嵌入汇编之前将其保存,并在嵌入汇编之后还原。
例如,"cc"表示所有可能被修改的寄存器;"memory"表示嵌入汇编可能会修改内存。这些约束可以确保嵌入汇编代码不会对其他代码产生副作用。
下面是一个使用RISC-V汇编嵌入C代码的示例:
int main() {
int a = 10, b = 20, sum;
asm volatile (
"add %0, %1, %2\n\t" // 使用RISC-V汇编的加法指令
: "=r"(sum) // 输出操作数(结果)使用寄存器约束,保存在sum变量中
: "r"(a), "r"(b) // 输入操作数使用寄存器约束,对应a和b变量
: "cc" // 修改寄存器
);
return 0;
}
在这个例子中,我们使用RISC-V汇编的add指令完成a和b的加法,并将结果保存在sum变量中。
练习
int foo(int a, int b)
{
int c;
c = a * a + b * b;
return c;
}
int foo(int a, int b)
{
int c;
asm volatile(
"mul %[result_a], %[a], %[a]\n"
"mul %[result_b], %[b], %[b]\n"
"add %[c], %[result_a], %[result_b]\n"
: [c] "=r" (c)
: [a] "r" (a), [b] "r" (b), [result_a] "r" (0), [result_b] "r" (0)
);
return c;
}
通过嵌入asm volatile指令,我们可以在C函数中嵌入RISC-V汇编代码。在汇编代码中,使用mul指令将a的平方保存在result_a寄存器中,将b的平方保存在result_b寄存器中,然后使用add指令将result_a和result_b的值相加后保存到c寄存器中。最后,使用约束操作数([c] “=r” ©)将c寄存器的值存储到变量c中,并将变量a和b的值存储到对应的寄存器中。