jal func)jr ra)a0 - a7a0함수 호출에는 두 가지 역할이 존재한다. 함수를 호출하는 Caller 함수와 호출되는 Callee 함수이다.
int main() // Caller
{
simple(); // function call
a = b + c;
/* ... */
}
void simple() // Callee
{
return;
}
0x00000300 main : jal simple # call
0x00000304 add s0, s1, s2 # a = b + c
... ...
0x0000051c simple : jr ra # return
jal simple
jump 전에 ra 레지스터에 PC+4를 저장한다.
PC = 0x00000300, PC+4 = 0x00000304 = 다음 명령어 주소.
이후 simple 주소로 jump
PC = 0x0000051c
jr ra
PC = ra = 0x00000304
caller 함수는 callee 함수에게 인자(arguments)를 넘겨주고 calle로 점프한다.
callee 함수는 내부 기능을 수행한 후 caller 함수에게 결과를 return한 후 return address로 점프한다. 이 과정에서 callee는 caller가 사용하는 레지스터나 메모리를 덮어써서는 안되기 때문에 백업이 필요하다.
스택은 함수 내부에서 변수 값 등을 저장해두기 위한 메모리의 가상 공간(scratch space)이다.
LIFO의 규칙을 따르며 필요에 따라 확장되거나 줄어든다.
stack은 높은 주소에서 시작하여 아래로 확장되는데, stack의 꼭대기를 sp로 가리킨다.

호출된 함수는 주 목적 이외에 side effect(의도하지 않은 부수적인 결과 발생)를 일으키면 안된다.
따라서 덮어 쓰게 될 레지스터의 값들을 미리 stack에 백업해 두었다가 함수의 기능이 끝나면 caller 함수로 돌아가기 전에 레지스터의 값들을 복원한 후 return 한다.
int diffofsums(int f, int g, int h, int i)
{
int result;
result = (f+g)-(h+i);
return result;
}
# s3 = result
diffofsums :
addi sp, sp, -12 # make space on stack
sw s3, 8(sp) # save s3 on stack
sw t0, 4(sp) # save t0 on stack
sw t1, 0(sp) # save t1 on stack
add t0, a0, a1
add t1, a2, a3
sub s3, t0, t1
add a0, s3, zero # result
lw s3, 8(sp) # restore s3 from stack
lw t0, 4(sp) # restore t0 from stack
lw t1, 0(sp) # restore t1 from stack
addi sp, sp, 12 # deallocate stack space
jr ra # return to caller


레지스터의 보존 책임은 다음과 같다.
좌측은 callee가 책임을 지고, 우측은 caller가 책임을 진다.
이 책임 정책에 따라서 위 diffofsums 함수를 최적화 하면 아래와 같이 작성할 수 있다.
diffofsums :
add t0, a0, a1
add t1, a2, a3
sub a0 t0, t1
jr ra
보존 책임이 없는 레지스터만 사용하여 stack을 건드리지 않고 컴파일한 것이다.
다른 함수를 호출하는 함수를 Non-leaf function이라고 한다.
# f1 (non-leaf function) uses s4-s5 and needs a0-a1 after call to f2
f1:
addi sp, sp, -20 # make space on stack for 5 words
sw a0, 16(sp)
sw a1, 12(sp)
sw ra, 8(sp) # save ra on stack
sw s4, 4(sp)
sw s5, 0(sp)
jal func2
...
lw ra, 8(sp) # restore ra (and other regs) from stack
...
addi sp, sp, 20 # deallocate stack space
jr ra # return to caller
# f2 (leaf function) only uses s4 and calls no functions
f2:
addi sp, sp, -4 # make space on stack for 1 word
sw s4, 0(sp)
...
lw s4, 0(sp)
addi sp, sp, 4 # deallocate stack space
jr ra # return to caller
위 함수의 stack 변화를 살펴보자.

return 시에는 이 과정의 반대 순서로 값들이 복원되고, stack 공간이 deallocate 된다.
Caller
ra, 혹은 t0-t6/a0-a7)a0-a7에 인자를 담는다.jal callee(label)a0에 있다.Callee
s0-s11)a0에 담는다.jr ra스스로를 호출하는 함수를 재귀함수(Recursive function)라고 한다.
재귀호출을 어셈블리로 변환할 때는 다음과 같은 단계를 거친다.
int factorial(int n) {
if (n <= 1) return 1;
else return (n*factorial(n-1));
}
factorial :
addi sp, sp, -8 # make space for a0, ra
sw a0, 4(sp)
sw ra, 0(sp)
addi t0, zero, 1 # t0 = 1
bgt a0, t0, else # if a0>=t0 goto else
addi a0, zero, 1 # else a0 = 1 ; == return 1
addi sp, sp, 8 # deallocate space
jr ra # return
else :
addi a0, a0, -1 # n = n - 1
jal factorial # call factorial(n-1)
lw t1, 4(sp) # restore n into t1
lw ra, 0(sp) # restore ra
addi sp, sp, 8 # deallocate
mul a0, t1, a0 # a0 = n * factorial(n-1)
jr ra # return


함수 호출 중 stack의 변화이다.
RISC-V는 두 타입의 무조건 jump가 있다.
jal rd, imm_{20:0})rd = PC+4; PC = PC + immjalr rd, rs, imm_{11:0})rd = PC+4; PC = rs + imm이 명령어 사용을 조금 더 편하게 만들기 위해 의사명령어(pseudo instruction)이 존재한다.
j imm = jal x0, immjal imm = jal ra, immjr rs = jalr x0, rs, 0ret = jr ra위 내용에서 우리는 함수 콜에 label을 사용했다.
label은 다음과 같이 다뤄진다.
jal simple = jal ra, 0x21C
jal의 imm size는 20bits, jalr의 imm size는 12bits이다. 따라서 먼 거리의 주소로 점프하려면 별도의 방법이 필요하다.
그 해결책이 auipc 명령이다.
auipc rd, imm : PC에 상위 20bit를 더한다.의사명령어 : call imm_{31:0}
내부 구현
auipc ra, imm_{31:12}
jalr ra, ra, imm_{11:0}

<참고자료>
Harris & Harris, Digital Design and Computer Architecture, RISC-V Edition, 2022.