본문 바로가기
System SW

[3.2] Procedure Call in MIPS II

by Hangii 2022. 10. 2.

  • 이전 글에서도 쓴 사진이지만 추가적인 설명을 위해 다시 살펴보자.
  • 지역변수의 경우 함수가 호출될 때 메모리의 Stack에 변수 공간이 생기며, 함수가 리턴되면 지역변수에 해당하는 공간이 사라진다. 또 하나의 함수에서 다른 함수를 호출할 때 전달하는 argument나 호출한 함수가 끝났을 때 되돌아와야하는 위치 등도 Stack에 저장된다.
  • 이제 어떤 함수 C가 다음과 같이 정의되었다고 가정해보자. 
int procedure_B(int x)
{   static int c=0;
    c++;
    return(x+c);
}
void C(int z)
{   int *p = malloc(sizeof(int));
    *p = procedure_B(3);
    printf("%d", *p);
}
  • 함수 C에서 만든 포인터의 *p의 공간은 함수 C와 운명을 같이 하는 것으로, 함수 C의 작동이 끝나면 지워지는 값이다. 하지만 C 내에서(포인터가 가리키는) malloc을 통해 생성한 동적 메모리 공간은 C함수의 사용이 끝나도 사라지지 않고 계속 남아있다. 즉, 동적 메모리 할당의 경우 프로그래머가 free()를 통해서 직접 해제하지 않으면 계속 살아있다. 
  • 동적 메모리의 공간은 함수와 운명을 같이하지 않기 때문에 Stack의 윗 주소부터 쌓으면서 저장하는 것이 아니라, Stack의 아랫 공간인 heap(=dynamic data영역)에서 쌓아 올리는 형태로 저장한다. 
  • 다시 처음의 사진을 보자. 왼쪽 그림을 보면 몇 개의 레지스터가 표시된 것을 볼 수 있다. 먼저 PCprogram counter로, 실행할 instruction의 주소를 가지고 있는 레지스터이다. $28(gp)global pointer로, 메모리의 Data영역을 가리킨다. ($sp와 $fp에 대해서는 추후 설명하겠다.)
  • 레지스터 $31(ra)에는 함수를 호출한 지점을 저장해둔다. 함수 내에서 다른 함수를 호출하고, 호출한 함수의 실행이 끝나면 $31에 저장된 주소로 돌아오면 된다. 이 때 '돌아온다'는 것의 의미는 PC의 값을 ra 레지스터에 저장된 값으로 바꾸라는 것이다.
  • 그렇다면 '되돌아오기'를 실행하는 데 $31만 있으면 충분한가?
    • main함수에서 A함수를, A함수에서 B함수를 호출하는 경우를 생각해보자. main함수로 성공적으로 돌아오려면 '되돌아오기'를 두 번 진행해야 한다. 하지만 $31에는 되돌아오기를 위한 주소를 하나밖에 저장하지 못한다. 이런 경우, 추가적으로 기억해둬야 할 주소를 Stack에 저장한다. 
  • 위에서 함수를 호출할 때 전달할 argument를 Stack에 저장한다고 설명했는데, 사실 이건 반은 맞고 반은 틀린 이야기다. argument 4개까지는 레지스터에 저장이 가능하기 때문이다. (메모리에 저장해두고 꺼내 쓰는 것보다 레지스터에 저장하는 것이 더 빠름을 잊지 말자.) $4~$7(a0~a3)에는 argument값들이 저장된다. argument의 수가 4개를 넘으면 Stack에 저장한다.
  • $2~$3(v0, v1)에는 함수의 리턴값을 저장한다. 
  • 여기까지 이해했다면 32개의 범용 레지스터 중 상당 부분은 이미 역할 분담이 되어 있음을 알 수 있다. 지역 변수의 경우, 위에서 Stack에 저장이 된다고 설명했다. 그런데 메모리 내의 Stack에 접근하는 것은 레지스터 상에서 접근하는 것보다 느리기 때문에 가능하면 변수를 저장할 공간을 레지스터 내에 mapping해두는 것이 시간 면에서 효율적일 것이다.
    • $8~$25는 변수를 mapping해 필요할 때 쓸 수 있는 레지스터이다. A함수에서 필요한 변수들을 이 레지스터들에 할당한 후 함수 내에서 다른 함수 B를 호출했을 때, 먼저 A의 레지스터 값들을 Stack에 저장해둔 후 레지스터를 비운 상태에서 B가 사용할 변수를 저장해야 한다. B함수의 사용이 끝나면 Stack에 저장해둔 A함수의 변수들을 복원 시켜 레지스터 값들을 이전 상태로 되돌린다. 
      • 그렇지만 호출된 B함수가 레지스터를 1개만 쓴다고 가정하자. 이 경우 B가 사용할 레지스터 하나만을 위해 A의 레지스터들을 모두 Stack에 저장한 후 복원하는 것이 비효율적이다. 대신 B가 사용할 하나의 레지스터가 지정되면 A의 해당 레지스터만 Stack에 저장해두고 추후 복원하는 것이 낫겠다.
      • 결론은, 호출된 함수가 사용할 레지스터에 있던 값들만 Stack에 저장해두면 된다는 것이다.
    • 이 레지스터들은 temporary(t0~t7, t8~t9)saved(s0~s7)로 나뉜다. temporary와 saved 둘 다 변수 값을 할당해 사용 가능한 레지스터다. A함수가 B함수를 호출했다고 가정하자.
      • Temporary 레지스터는, A의 입장에서 자신이 사용한 레지스터를 B가 밀어버리고 사용하는 것을 방지하기 위해 Stack에 미리 저장해 둘 때 사용하는 레지스터이다. 즉, 레지스터를 먼저 사용하고 있던 함수가 다른 함수가 레지스터 위에 값을 덮어쓸 것을 예상하고 미리 옮겨둘 의무를 지는 것이다. 
      • Saved 레지스터는, 레지스터에 저장된 값들을 Stack에 옮길 책임이 B에 있다. saved 레지스터를 B함수가 쓰고 싶다면 이미 저장된 값들을 Stack에 옮긴 후 사용하고, 리턴 시 복원해야 한다. Saved 레지스터는 함수가 호출되는 시점에 값들이 아직 남아있어야 하는 레지스터이다.
  • 함수가 호출될 때, what should be done?
    1. 호출 이후에도 값이 유지되어야 하는 레지스터(saved register 등)의 저장
    2. return address의 저장
    3. argument의 전달
    4. 지역변수를 위한 공간 할당 필요(nested call의 경우 호출될 때마다 같은 지역변수가 반복적으로 만들어져야 함)
      • Stack에 저장

  • 가장 마지막에 쌓인 위치(가장 최근에 정보가 저장된 위치)를 Stack의 top이라고 부른다. $sp(stack pointer)는 Stack의 top을 가리킨다. 
    • Stack에 정보를 저장하고 싶을 때: sp가 현재 위치에서 4byte만큼을 뺀 값을 가리키도록 한다. 이는 새 정보를 저장할 공간을 만들어주는 연산이다. 이후 sw명령어를 사용해 $t0의 값을 $sp가 가리키는 곳에 저장한다.
    • Stack에서 정보를 인출하고 싶을 때: 먼저 $sp에 저장된 값을 $t0에 옮겨 저장한다. 그리고 $sp에 4byte만큼을 더해 Stack에서 정보를 인출한다.

댓글