본문 바로가기
BackEnd/Computer Architecture

1.2.4 어셈블리어로 프로시저 구현

by 12312121 2022. 1. 18.

프로시저 : 특정 업무 수행을 위한 절차들의 기술
함수 : 각 절차의 구현

(프로시저와 함수는 코드의 가독성과 재사용성을 높이는 추상화 방법 중 하나다)

인수 : 입력 겸, 데이터와 프로시저를 잇는 인터페이스

프로시저의 실행 과정
1. 프로시저에 인수 입력
2. 프로시저에 제어 넘기기
3. 프로시저는 필요한만큼 메모리를 가져가서 작업을 수행한다
4. 프로시저는 리턴
5. 사용한 프로시저를 원래 위치에 되돌려 놓기

이때 쓰이는 레지스터들의 역할

x0 : 0만 담음

x1 : 다음 실행될 명령어의 주소를 담음

x2 : 다른 레지스터들의 값을 보존 (=sp, stack pointer)

x5~7 : 보존 X

x8(=fp), x9 : 보존 O

x10~17 : 인수용

x18~27 : 보존 O

x28~31 : 보존 X

 

프로시저용 명령어
jal (= jump and link )

기능 = jal의 다음 명령어 주소를 rd(여기선 x1)에 저장하고 지정된 주소로 이동한다

사용예 = jal x1, ProcedureAddress

또한, 복귀를 위해 아래 명령어를 사용한다

jalr x0, 0(x1)

하지만 이름만 복귀주소고 실제로는 다음 주소를 넣는다. (다음 주소 = 복귀주소 + 4)

컴파일러의 프로시저 명령어 번역시에 인수 레지스터 8개만으로 부족한 경우

위의 해결책 = 레지스터 스필링

그것이 필요로 하는 것 = 스택

그것이 필요로 하는 것 = 스택 포인터 (x2(=sp) 사용)

(ArrayStack의 NowPosition과 같다)

(자료구조 카테고리의 Stack 게시글 참고)

 

f = ( g + h ) - ( i + j );를 C프로시저로 쓰면 아래와 같다

long long int leaf_example ( long long int g, long long int h, long long int i, long long int j ){

    long long int f;

    f = ( g + h ) - ( i + j );

    return f;

}

_____________________________________

f    ,g, h, i, j  =  x20,    x10, x11, x12, x13

 

위 C프로시저는 아래 명령어들을 쓰게 된다

addi sp, sp, -24    // 스택포인터에 3바이트 할당

sd x5, 16(sp)    // 스택포인터[2]에 x5 저장

sd x6, 8(sp)    // 스택포인터[1]에 x6 저장

sd x20, 0(sp)    // 스택포인터[0]에 x20 저장

 

add x5, x10, x11    //  ( g + h )를 x5에 저장

add x6, x12, x13    //  ( i + j )를 x6에 저장

sub x20, x5, x6  //  ( g + h ) - ( i + j )를 x20에 저장

addi x10, x20, 0    // x20을 x10에 저장

 

ld x20, 0(sp) // x20의 값 복구

ld x6, 8(sp) // x6의 값 복구

ld x5, 16(sp) // x5의 값 복구

addi sp, sp, 24 // 스택포인터 복구

jalr x0, 0(x1) // 복귀주소로 돌아가기

 

그런데 쓰지도 않는 프로시저에 위 작업을 하는 비효율이 발생할 수 있다. 그래서 이를 위해 아래 관례를 사용한다.

 

x5 ~ x7, x28 ~ x31 = 값 보존 안되는 임시 레지스터

x8 ~ x9, x18 ~ x27 = 값 보존이 되는 임시 레지스터

 

중첩 레지스터

말단 레지스터 : 다른 프로시저를 안호출하는 프로시저

그런데 대부분은 말단 레지스터가 아니라서 같은 레지스터를 중복해서 사용할 수도 있다. 이를 방지하기 위해 보존되어야 모든 레지스터는 아까처럼 스택포인터를 이용해서 보존한다

 

어셈블리어로 중첩 프로시저의 구현 예

long long int Get_factorial (long long int n){

    if (n < 1)

        return 1;

    else

        return n * Get_factorial(n - 1);

}// n = x10

_____________________________________

factorial:

    addi sp, sp, -16    // 기존 데이터 유지를 위해 2바이트의 스택포인터 생성

    sd x1, 8(sp)   // 스택포인터[2]에 복귀주소 저장

    sd x10, 0(sp) // 스택포인터[1]에 n의 값 저장

 

    addi x5, x10, -1  // if (n < 1)의 변형식 if (n - 1 < 0)의 구현을 위해 n - 1을 만들고 x5에 저장

    bge x5, x0, L1     // if (n - 1 < 0)가 else 되는 경우 (n - 1 >= 0)에 else구문으로 이동

 

    addi x10, x0, 1    // if (n < 1)가 true인 경우에 실행될 내용  ( n = 1 )

    addi sp, sp, 16     // 스택포인터 삭제

    jalr x0, 0(x1)        // 복귀

 

L1:

    addi x10, x10, -1 // n에서 1 빼기                              

    jal x1, Get_factorial // 빼진 n으로 함수 재실행

    addi x6, x10, 0 // 빼진 n을 x6에 저장

    ld x10, 0(sp) // n 복구

    ld x1, 8(sp) // 복귀주소 복구

    addi sp, sp, 16 // 스택포인터 삭제

    mul x10, x10, x6 // n * (n - 1)

    jalr x0, 0(x1) // 복귀

 

프로시저를 위한 스택의 공간 할당

프로시저 프레임(=activition record) : 프로시저의 보존 레지스터와 지역변수를 가진 스택 영역

프레임 포인터(=fp) : 프로시저 프레임의 내부 값의 위치를 표시하는 값

 

RISC-V의 메모리 할당방식

스택의 할당 방식 : 최상위 주소에서 시작해서 최하위쪽으로 추가

기계어 코드의 할당방식 : 스택 다음에 할당 (이 할당할 곳을 text segment라고 부른다)

(그 중 위쪽에 있는 것은 static data segment로, 상수와 기타 정적 변수, 배열이 들어간다)

(아래에 있는 것은 static data segment (=heap)으로​

C의 할당 방식 : 위 기계어 코드의 할당을 구현하기 위해 malloc()을 이용해서 힙에 공간을 할당하고

tree()를 이용해서 공간을 반납한다. 반납을 계속 하지 않으면 메모리 부족으로 운영체제가 붕괴할 수 있다.

자바는 가비지 컬렉터, 자동 메모리 할당이 대신한다

 

어셈블리어로 합을 누적시키는 프로시저의 구현 예

long long int sum (long long int n, long long int acc) {

    if (n > 0)

        return sum( n - 1, acc + n );

    else

        return acc;

} // x10 = n, x11 = acc, x12 = return용 값

____________________________________________________________

sum: ble x10, x0, sum_exit // { if (n > 0)

       add x11, x11, x10 // acc + n

       addi x10, x10, -1 // n - 1

       jal x0, sum // sum(n - 1, acc + n)

sum_exit: // else

       addi x12, x11, 0 // return acc;

       jalr x0, 0(x1) // }

댓글