- 초기 컴퓨터는 스택과 힙 없이도 함수 호출을 구현해야 했고, 컴파일러는 매개변수·반환 주소·지역 변수에 대응하는 숨은 전역 변수로 호출 상태를 관리함
- 호출자는 인자를 저장하고 반환 주소 변수에 복귀 위치를 넣은 뒤 함수 시작점으로 점프했으며, 함수는 계산 후 저장된 주소로 다시 점프함
- 논리적 지역 변수도 실제로는 전역 저장 공간을 썼기 때문에, 겉보기에는 함수처럼 보여도 내부 동작은 고정된 메모리와
goto에 가까웠음 - 일부 ABI와 프로세서는 인자 전달과 복귀 주소 처리를 레지스터나
branch with link로 최적화했지만, 기본 제약은 그대로 남아 있었음 - 같은 함수의 반환 주소가 새 호출로 덮어써지기 때문에 재귀 호출은 불가능했고, 당시 언어들은 재귀를 금지하거나 명시적으로만 허용하는 방식으로 대응함
스택 없이 함수 호출을 구성하는 방식
- 초기 컴퓨터 환경에서는 오늘날 당연하게 여기는 스택이나 힙이 없었음
- 힙이 없는 동적 메모리 할당은 고정 크기 버퍼로 대체할 수 있었음
- 변수 크기 데이터를 처리할 때도 충분히 큰 고정 버퍼를 미리 예약함
- 요청 데이터가 버퍼 용량을 넘으면 치명적 오류로 프로그램을 종료함
- 더 친절한 구현은 컴파일 시 최대 용량을 설정할 수 있게 함
- 더 정교한 구현은 고정 버퍼 위에 커스텀 할당자를 두고
allocate와free처럼 사용 가능하게 함
숨은 전역 변수 기반 호출 규약
- 스택 없이 함수 호출을 구현하기 위해 컴파일러는 각 함수마다 여러 숨은 전역 변수를 정의함
- 입력 매개변수별 전역 변수
- 함수의 반환 주소를 담는 전역 변수
- 지역 변수에 대응하는 전역 변수
- 호출 코드는 다음 순서로 실행됨
- 매개변수 값을 해당 숨은 전역 변수에 저장함
- 복귀할 위치를 함수의 반환 주소 변수에 기록함
- 함수 시작 위치로
goto점프함
- 함수는 매개변수와 지역 변수를 모두 숨은 전역 변수에서 읽고 씀
- 실행이 끝나면 반환 값을 반환 값 레지스터에 넣고, 함수의 반환 주소 변수에 저장된 주소로 점프함
C 비슷한 코드가 goto 기반 코드로 바뀌는 예
- 예시 함수
add_two_values(int a, int b)는 스택 없이 다음과 같은 저장 공간으로 변환될 수 있음a2v_a,a2v_b는 인자 저장용 전역 변수a2v_c는 지역 변수c에 대응하는 전역 변수a2v_retaddr는 복귀 주소 저장용 전역 변수
- 호출자
sample()은31415와2718을 각각 인자 전역 변수에 저장함 - 이어서
a2v_retaddr에resume위치를 넣고add_two_values로 점프함 add_two_values는 계산 결과를return_value_register에 저장한 뒤a2v_retaddr로 복귀함resume위치로 돌아온 호출자는 반환 값 레지스터의 값을sample_x에 저장함
레지스터와 branch with link를 이용한 최적화
- 같은 구조는 ABI 차원에서 레지스터 전달로 더 빠르게 만들 수 있음
- 많은 프로세서는 특별한
link register와branch with link명령을 제공했음branch with link는 분기 명령 다음 명령어의 주소를 자동으로 link register에 저장함- 호출자는 첫 두 인자를
argument_register_1,argument_register_2에 넣을 수 있음 - 피호출 함수는 이 레지스터 값을 자신의 숨은 전역 변수로 옮겨 사용 가능함
- 반환 주소도
link_register에서 함수의 반환 주소 변수로 저장될 수 있음 - 이 최적화는 스택 없이도 호출과 반환이 가능하다는 기본 구조를 유지함
재귀가 막히는 이유
- 이 호출 방식의 핵심 제약은 재귀 호출 불가임
- 재귀 호출이 발생하면 같은 함수의 반환 주소 변수가 새 호출의 반환 주소로 덮어써짐
- 바깥 호출이 끝날 때 원래 복귀해야 할 위치가 사라져 잘못된 위치로 점프하게 됨
- 당시 프로그래밍 언어들은 재귀를 지원하지 않는 방식으로 이 문제를 피함
- FORTRAN은 처음에는 서브루틴도 지원하지 않았고, 서브루틴은 1958년에 추가됨
- FORTRAN에서 재귀 지원이 표준이 된 것은 1991년이며, 그때도 서브루틴을
RECURSIVE로 명시해야 했음
자기 수정 코드와 초기 프로세서의 서브루틴 명령
- 일부 컴파일러는 더 교묘하게 자기 수정 코드를 사용함
- 함수 끝의 점프 명령어 안에 있는 주소 필드가 사실상 반환 주소 변수 역할을 함
- 이런 방식은 단순한 트릭이 아니라 실용적 필요일 수 있었음
- 일부 프로세서는 간접 점프를 지원하지 않을 수도 있었음
- 서브루틴의 실용성이 인정된 뒤 여러 프로세서는 전용 호출 명령을 추가함
- 반환 주소를 서브루틴의 첫 번째 워드에 저장함
- 실제 실행은 두 번째 워드에서 시작함
- 반환할 때는 서브루틴 시작 라벨을 통한 간접 점프를 실행함
- 예시 어셈블리에서
bsr add_two_values는add_two_values의 첫 워드에 반환 주소를 저장하고, 희생용nop다음의 실제 명령어부터 실행함