함수와 스택
함수는 두 번 이상 사용할 수 있는 이름이 붙은 명령 블록입니다. 그곳으로 점프하면 작업을 수행하고, 호출한 곳으로 제어를 돌려줍니다. 이렇게 하면 한 번의 논리를 작성해 프로그램 전체에서 같은 줄을 반복하는 대신 여러 곳에서 사용할 수 있습니다.
을 사용해 함수를 호출합니다. 이는 레이블로 점프하고 반환 위치를 ra에 저장합니다. 함수는 로 끝나며, ra에 있는 주소로 다시 점프합니다. 과 는 의사 명령입니다. 즉, 어셈블러가 실제 명령인 과 으로 확장하는 편리한 이름입니다.
인수는 a0, a1 및 그 뒤의 레지스터에 배치됩니다. 반환 값은 a0에 배치됩니다. 이 배치는 호출 규약으로, 서로 다른 코드 조각이 함께 작동할 수 있게 해 주는 공통된 약속입니다.
li a0, 6 # 인자call square # 호출, 그리고 ra는 돌아올 위치를 기억한다li a7, 1 # square는 결과를 a0에 남긴다ecallli a7, 10ecallsquare:mul a0, a0, a0 # a0 = a0 * a0ret # ra로 돌아간다
호출을 따라가 보세요. 은 인수 6을 a0에 넣습니다. 는 square 레이블로 점프하고 반환 주소를 ra에 저장합니다. 함수 내부에서 은 6 * 6 = 36을 계산하고 결과를 반환 값에 사용되는 레지스터인 a0에 남깁니다. 는 호출 바로 다음 명령으로 돌아가며, 그곳에서 과 이 36을 출력하고 프로그램이 종료됩니다.
호출자가 A를 호출하려고 합니다.
caller가 를 실행하면 반환 지점이 ra 레지스터의 caller에 자동으로 저장됩니다. A가 를 실행하기 전에 문제가 발생합니다. 명령이 ra를 새로운 반환 주소로 덮어쓰려 하여 A가 원래 위치로 돌아가는 데 필요한 주소를 파괴하게 됩니다. 이를 해결하기 위해 A는 스택에 공간을 확보하고() 원래의 ra를 그곳에 저장합니다(). 이제 안전하게 를 실행할 수 있습니다. B가 실행되고 일반적인 로 반환하여 A 내부로 돌아옵니다. 마지막으로 A는 스택에서 원래의 ra를 복원하고(), 스택 포인터를 정리한 뒤(), 자신의 가 복원된 값을 사용하여 caller로 성공적으로 점프합니다. 만약 A가 이 단계를 건너뛰었다면, 명령이 ra를 영구적으로 덮어썼을 것이고, A의 마지막 는 원래 위치로 돌아가는 대신 잘못된 곳으로 점프했을 것입니다.스택
레지스터는 극히 소수만 존재하므로, 다른 함수를 호출하는 함수들은 임시 저장 공간이 필요합니다. 스택은 스택 포인터 sp를 통해 빌리는 메모리입니다: 공간을 확보하려면 값을 빼고, 유지해야 할 값은 저장한 뒤, 전에 원래대로 복원해야 합니다.
스택은 후입선출(LIFO) 구조입니다. 가장 최근에 저장한 값이 가장 먼저 검색됩니다. RISC-V에서 스택은 메모리를 따라 아래쪽으로 자랍니다. sp에서 빼면 안전하게 사용할 수 있는 새 공간이 확보됩니다. 값을 다시 꺼낼 때는 저장한 순서의 역순으로 해야 하며, 그렇지 않으면 프로그램의 실행 흐름이 손상됩니다.
addi sp, sp, -4 # 한 워드만큼 공간을 만든다sw ra, 0(sp) # 복귀 주소를 저장한다# ... 여기서 다른 함수를 호출한다 ...lw ra, 0(sp) # 복원한다addi sp, sp, 4 # 그 공간을 반환한다ret
모든 /은 ra를 덮어쓰므로, 함수가 다른 함수를 호출하면 내부 호출이 반환 주소를 파괴하고 자신의 가 잘못된 곳으로 점프합니다. 이를 방지하려면 로 스택에 4바이트를 예약하고 로 ra를 거기에 저장합니다. 내부 호출이 끝난 뒤에는 로 반환 주소를 복원하고, 로 전에 공간을 돌려줍니다.
ra를 복원하고, 항상 sp를 시작 시점의 값으로 되돌리세요. 시작 상태와 일치하지 않는 스택은 어셈블리 프로그램에서 충돌이 발생하는 흔한 원인입니다.