*컴퓨터 구조 및 설계 5th Edition (저자 David A. Patterson 과 John L. Hennessy)를 기반으로 작성되었습니다.
*MIPS 어셈블리언어 기반으로 작성되었습니다.
프로시저(procedure)와 함수(Function)는 이해하기 쉽고 재사용이 가능하도록 프로그램을 구조화하는 방법 중 하나입니다. 프로시저는 프로그래머가 한 번에 한 부분씩 집중하여 처리할 수 있게 도와줍니다. 그리고 프로시저는 소프트웨어에서 추상화를 하는 구현 방법입니다.
프로그램이 프로시저를 실행하는 방법은 다음과 같습니다.
- 프로시저가 접근할 수 있는 곳에 인수를 넣습니다.
- 프로시저로 제어를 넘깁니다.
- 프로시저가 필요로 하는 메모리 자원을 획득합니다.
- 필요한 작업을 수행합니다
- 호출한 프로그램이 접근할 수 있는 장소에 결과 값을 넣습니다.
- 프로시저는 프로그램 내의 여러 곳에서 호출될 수 있으므로 원래 자리로 되돌려줍니다.
*프로시저(procedure): 제공되는 인수에 따라서 특정 작업을 수행하는 서브루틴.
*인수(parameter)는 프로시저에 값을 보내고 결과를 받아오는 일을 처리하므로, 프로그램의 다른 부분 및 데이터와 프로시저 사이의 인터페이스(interface)역할을 수행합니다.
레지스터는 데이터를 저장하는 가장 빠른 장소이므로 많이 사용하는 것이 바람직합니다. 따라서, MIPS 소프트웨어는 다음의 프로시저 호출 관례에 따라 레지스터 32개를 할당합니다.
이름 | 번호 | 사용 방법 |
$zero | 0 | 상수 0로 사용 |
$at | 1 | 명령 내의 임시 값 |
$v0 - v1 | 2 - 3 | 반환 되는 값을 갖는 레지스터 |
$a0 - a3 | 4 - 7 | 전달할 인수를 가지고 있는 인수 레지스터 |
$t0 - t7 | 8 - 15 | 프로시저 호출 시, 피호출 프로그램이 값을 보존해주지 않는 임시 레지스터 |
$s0 - s7 | 16 - 23 | 프로시저 호출 전과 후의 값이 같게 유지되어야 하는 변수 레지스터 |
$t8 - t9 | 24 - 25 | 프로시저 호출 시, 피 호출 프로그램이 값을 보존해주지 않는 임시 레지스터 |
$k0 - k1 | 26 - 27 | OS 커널을 위해 예약된 레지스터 |
$gp | 28 | 전역 포인터(global pointer) 값을 저장하는 레지스터 |
$sp | 29 | 스택 포인터(stack pointer) 값을 저장하는 레지스터 |
$fp | 30 | 프레임 포인터(frame pointer) 값을 저장하는 레지스터 |
$ra | 31 | 호출한 곳으로 되돌아가기 위한 복귀 주소를 가지고 있는 레지스터 |
MIPS 어셈블리 언어는 레지스터를 할당할 뿐만 아니라 프로시저를 위한 명령어도 제공합니다. 지정된 주소로 점프하면서 동시에 다음 명령어의 주소를 $ra 레지스터에 저장하는 명령으로 jal 명령어(jump-and-link instruction)이라 부른다.
link란, 프로시저 종료 후 올바른 주소로 되돌아올 수 있도록 호출한 곳과 프로시저 사이에 주소 또는 링크를 형성한다는 뜻입니다. 동작 과정을 보면, 먼저 호출 프로그램(caller)은 $a0 - $a3에 전달할 인수 값을 넣은 후 jal X 명령을 이용해서 프로시저 X[피호출 프로그램(callee)라고 부릅니다.] 로 점프 합니다. 그렇게 되면, 피호출 프로그램은 계산을 끝낸 후, 계산 결과를 $v0 - $v1에 넣은 후 jr $ra 명령을 실행하여 복귀합니다.
* 현재 수행 중인 명령어의 주소를 기억하는 레지스터를 프로그램 카운터(program counter)라고 부릅니다. jal 명령은 프로시저에서 복귀할 때 다음 명령어부터 실행하도록 PC+4를 레지스터 $ra에 저장합니다.
컴파일러가 프로시저를 번역하는거에 있어서 인수 레지스터 4개, 결과 값 레지스터 2개만으로는 부족한 경우가 있을 수 있습니다. 프로시저 호출이 다른 부분에 영향을 미쳐서는 안되므로 호출 프로그램이 사용하는 모든 레지스터는 복귀하기 전에 프로시저 호출 전의 상태로 되돌려 놓아야합니다. 데이터 복원을 위해 메모리에 데이터(기존 레지스터 값)를 저장한 후 사용합니다. 즉, '레지스터 스필링'이 필요한 경우입니다. 이를 위해서 자료구조로는 스택(stack)을 사용합니다. 또한 스택을 사용하려면 다음 프로시저가 스필할 레지스터를 저장할 장소나 레지스터의 옛날 값이 저장된 장소를 표시하기 위해 최근에 할당된 주소를 가르키는 포인터가 필요합니다. 이를 스택 포인터(stack pointer)라고 하며, 레지스터 29번을 할당해놓고 있습니다. 스택에 데이터를 넣는 작업을 푸시(push), 꺼내는 작업을(pop)이라고 합니다. 그리고 역사적 선례에 따라서 스택은 높은 주소에서 낮은 주소쪽으로 공간을 할당합니다.
int leaf_example(int g, int h, int i, int j)
{
int f;
f = (g + h) - (i + j);
return f;
}
예를 들어 위와 같은 코드가 있을 때, 임시 레지스터를 통하여 레지스터 스필링을 줄일 수 있습니다.
첫번째로 t0, t1 레지스터를 원상복구해야한다고 가정했을 때 MIPS 어셈블리 코드는 다음과 같습니다.
leaf_example:
$\qquad addi \quad \$sp\,,\$sp\,,-12$ # 향후 사용할 $t0, $t1, $s0가 사용할 스택 공간 할당
$\qquad sw \quad \$t1\,,8(\$sp)$ # 원상 복구 하기 위해 스택에 레지스터 저장
$\qquad sw \quad \$t0\,,4(\$sp)$ # 원상 복구 하기 위해 스택에 레지스터 저장
$\qquad sw \quad \$s0\,,0(\$sp)$ # 원상 복구 하기 위해 스택에 레지스터 저장
$\qquad add \quad \$t0\,,\$a0\,,\$a1 $ # t0 = g + h
$\qquad add \quad \$t1\,,\$a2\,,\$a3 $ # t1 = i + j
$\qquad sub \quad \$s0\,,\$t0\,,\$t1 $ # f = $t0 - $t1 => (g + h) - (i + j)
결과 f를 보내주기 위해 결과 값 레지스터에 f를 복사합니다.
$\qquad add \quad \$v0\,,\$s0\,,\$zero$ # return f
호출 프로그램으로 되돌아가기 전에 저장해 두었던 값을 스택에서 꺼내 레지스터를 원상 복구 합니다.
$\qquad lw \quad \$s0\,,0(\$sp) $ # 레지스터 복구
$\qquad lw \quad \$t0\,,4(\$sp) $ # 레지스터 복구
$\qquad lw \quad \$t1\,,8(\$sp) $ # 레지스터 복구
$\qquad addi \quad \$sp\,,\$sp\,,12$ # stack에 할당한 공간 삭제
프로시저는 복귀 주소를 사용하는 jr 명령으로 끝납니다.
$\qquad jr \quad \$ra$ #복귀 주소를 기반으로 복귀합니다.
하지만 임시 레지스터와 같이 간단한 관례를 정함으로써 레지스터 스필링을 많이 줄일 수 있습니다. 실제로 t0, t1 값은 호출 전후로 같은 값을 유지할 필요가 없기 때문에 저장 명령 두 개와 적재 명령 두 개를 없앨 수 있습니다.
새 데이터를 위한 스택 공간의 할당
레지스터에 들어가지 못할 만큼 큰 배열이나 구조체 같은 지역 변수를 저장하는 데에도 스택이 사용되기 때문에 보다 복잡해집니다. 프로시저의 저장된 레지스터와 지역 변수를 가지고 있는 스택 영역을 프로시저 프레임(procedure frame) 또는 액티베이션 레코드(activation record)라고 부릅니다.
MIPS 소프트웨어 중에는 프레임 포인터(frame pointer, $fp)가 프로시저 프레임의 첫 번째 워드를 가리키도록 하는 것이 있습니다. 즉, 스택을 사용하는데 있어서 베이스 레지스터 역할을 수행하여 지역 변수를 간단하게 참조할 수 있도록 하는 역할을 합니다. (그림 1 참조)
*별도의 프레임 포인터 사용 여부와 상관 없이 액티베이션 레코드는 항상 스택에 존재합니다.
새 데이터를 위한 힙 공간의 할당
C 프로그래머는 프로시저에만 국한되는 자동 변수 외에도 정적 변수와 동적 자료구조를 위한 메모리 공간이 필요합니다. 정적 데이터 세그먼트(static data segment)라는 부분이 있는데, 상수와 기타 정적 변수들이 저장됩니다(그림 1 참조). 배열은 그 크기가 고정되어 있어 정적 세크 먼트에 잘 맞습니다. 다만 링크드 리스트(linked list)와 같은 자료 구조는 늘어났다 줄어들었다 합니다. 이러한 자료구조를 위한 세그먼트를 전통적으로 힙(heap)이라고 합니다.
*스택과 힙이 서로 마주보면서 자라도록 할당하기 때문에 공간을 효율적으로 사용합니다.
*C언어의 경우, 함수를 통해 힙 공간을 할당받고 반환하는데 사용이 끝난 공간을 반납하는 것을 잊어버리면 '메모리 누출(memory leak)'이 발생하여 메모리 부족으로 운영체제가 붕괴될 수 있습니다.
*반대로 공간을 너무 일찍 반납하면 프로그램의 의도와 상관없이 엉뚱한 것을 가리키는 '매달린 포인터(dangling pointer)'가 발생합니다.
*Java에서는 이러한 버그를 피하기 위해 자동 메모리 할당과 가비지 컬렉션(garbage collection)을 사용합니다.
'CS > 컴퓨터구조' 카테고리의 다른 글
[컴퓨터구조] 논리연산 명령어와 판단을 위한 명령어 (1) | 2022.09.30 |
---|---|
[컴퓨터구조] 명령어 형식 (0) | 2022.09.29 |
[컴퓨터구조] 2의 보수 표현법(two's complement) (0) | 2022.09.28 |
[컴퓨터구조] 하드웨어 설계 원칙 및 피연산자 (0) | 2022.09.28 |