안녕하세요. IT 엘도라도 에 오신 것을 환영합니다.
글을 쓰는 것은 귀찮지만 다시 찾아보는 것은 더 귀찮습니다.
완전한 나만의 것으로 만들기 위해 지식을 차곡차곡 저장해 보아요.   포스팅 둘러보기 ▼

컴퓨터 구조 (Architecture)/컴퓨터의 개념 및 실습

[Chapter 14] Functions - 함수

피그브라더 2020. 2. 23. 03:13

우리는 앞서 어셈블리어 수준에서 서브 루틴이라는 것이 무엇이고, 어떻게 구현하는지 알아본 바 있다. 그러한 서브 루틴을 프로그래머가 쉽게 구현하고 사용할 수 있도록 고급 언어가 제공하는 기능이 바로 함수(Function)이다. 함수는 이전 포스팅에서 말했듯, 빈번히 사용되는 코드들의 뭉치를 추상화하여 그 활용성을 높인 것이다. 따라서 프로그래머는 함수라는 고급 언어의 기능을 활용하면 더욱더 직관적이고 효율적으로 프로그래밍을 할 수 있다. 그리고 고급 언어의 함수는 컴파일러에 의해 어셈블리어 수준의 서브 루틴으로 번역이 되는데, 그 구체적인 번역 과정에 대해서도 이번 포스팅에서 다룰 것이다. 고급 언어의 함수가 내부적으로 어떻게 구현이 되는지를 이해하는 것은 상당히 중요하므로 이번 포스팅은 꼼꼼히 읽어보기를 권장한다.

 

1. 함수 (Function)

1-1. 함수의 활용 가치

서론에서 간단히 얘기했듯, 함수의 가장 큰 가치는 바로 '추상화(Abstraction)'이다. 물론 고급 언어가 제공하는 함수는 기본적으로 어셈블리어 수준의 서브 루틴 구현을 추상화한다는 것만으로도 상당한 의미를 가진다. 프로그래머가 해당 고급 언어가 정한 규칙대로 함수를 정의하고 사용하기만 하면, 컴파일러가 그것을 어셈블리어 수준의 서브 루틴으로 알아서 바꿔주기 때문이다. 하지만 더 중요한 가치는 따로 있다. 그것은 바로 특정 기능을 수행하는 코드들의 뭉치를 재활용이 가능한 형태로 구성함으로써, 사용하는 사람 입장에서는 그 함수의 내부 구현을 모르더라도 기능만 알면 가져다 쓸 수 있다는 것이다. 이러한 원리로 프로그램의 개발을 독립적으로 할 수 있게 되었고, 프로그래머 입장에서는 모든 기능을 일일이 직접 개발할 필요 없이 누군가 개발한 것을 가져다 씀으로써 한층 더 프로그래밍의 효율을 향상시킬 수 있는 것이다. 또 무엇보다 큰 이점은 프로그램의 전체적인 흐름을 한눈에 파악하기 쉬워진다는 점이다.


1-2. C 언어의 함수

C 언어가 제공하는 함수의 형태는 크게 두 부분으로 이루어져 있다. 첫째는 매개변수로 전달되는 0개 또는 여러 개의 인자(Argument), 둘째는 0개 또는 1개의 반환 값(Return Value)이다. 각 매개변수로 전달되는 인자들과 반환 값은 반드시 특정 자료형을 지녀야 한다. 참고로 C 언어가 아닌 다른 고급 언어에서는 함수를 프로시저(Procedure) 또는 단순히 서브 루틴(Sub Routine)이라고 부르기도 한다. 용어의 차이일 뿐이다.

 

2. C 언어 함수의 선언, 호출, 정의

2-1. 선언문 (Declaration, Prototype)

C 언어에서는 특정 함수를 사용(호출)하기 전에 반드시 그 함수에 대한 선언문(Declaration 또는 Prototype)이 있어야 한다. 함수의 선언문은 크게 세 부분으로 구성된다. 첫째, 반환 값의 자료형에 해당하는 반환형이다. 둘째, 함수의 이름이다. 셋째, 매개변수로 전달되는 각 인자들의 자료형이다. 예를 들어 int 형 값을 반환하고, 함수의 이름은 Factorial이고, 매개변수로는 int 형 값이 1개 전달되는 함수의 경우 다음과 같이 선언문을 작성할 수 있다.

 

EX) int Factorial(int n);


2-2. 호출 문 (Function Call)

앞선 포스팅에서 C 언어의 Expression이라는 것이 무엇인지 알아보았다. 함수를 호출하는 문장, 즉 함수 호출 문(Function Call)도 Expression을 구성하는 하나의 요소이다. 예를 들어 다음과 같은 Expression은 함수 호출 문을 구성 요소로 가지고 있다.

 

EX) a = x + Factorial(f + g);


2-3. 정의문 (Definition)

함수의 선언문을 포함하여, 함수의 기능에 해당하는 코드를 정의하는 부분이다. 단 선언문에서는 필요 없었던 각 매개변수의 이름까지도 정확히 정의해줘야 한다. 만약 다른 곳에 함수의 선언문이 존재한다면, 그 선언문과 정의문은 반드시 반환형, 함수의 이름, 각 매개변수의 자료형이 일치해야 한다. 예를 들어 Factorial 함수는 다음과 같이 정의할 수 있다. 위에서 예로 보여준 Factorial 함수의 선언문과 반환형, 함수의 이름, 매개변수의 자료형이 일치한다는 것에 주목하자.

 


2-4. 선언문이 따로 존재하는 이유

위의 설명을 읽으면서 의아한 점이 생겼을 수도 있다. 바로 정의문에 포함되어 있는 선언문이 굳이 왜 따로 존재해야 하는가이다. 그 이유는 대략 두 가지로 요약할 수 있다.

 

첫째, 함수가 정의되기 전에 사용(호출)될 수도 있기 때문이다. 이러한 경우, 컴파일러가 그 함수를 올바르게 기계어로 번역을 하려면 최소한 그 함수가 무슨 자료형의 값을 반환하는지, 그리고 무슨 자료형의 값들을 매개변수로 전달받는지 알아야 한다. 이를 위해서는 함수의 선언문을 함수의 호출 문보다 먼저 작성해줘야 하는 것이다.

 

둘째, 사용(호출)하고자 하는 함수의 정의문이 다른 파일에 존재할 수도 있기 때문이다. 이는 다른 프로그래머가 작성한 파일 혹은 라이브러리 파일일 수도 있다. 이러한 경우, 해당 함수의 선언문만 작성되어 있는 헤더 파일을 include 함으로써 그 함수를 사용(호출)할 수 있게 된다. 그리고 각 파일은 독립적으로 컴파일되고, 나중에 링커에 의해 적절히 링킹 되어 하나의 실행 가능한 파일이 만들어지게 된다.

 

3. C 언어 함수의 내부 구현

그렇다면 위에서 설명한 방식과 같이 정의하고 호출하여 사용할 수 있는 C 언어의 함수 메커니즘은 어셈블리어 수준에서 어떻게 구현이 되는 것일까? 함수의 호출 과정에서 메모리 상태가 어떻게 바뀌어 가는지에 초점을 맞춰서 이를 한 번 알아보도록 하자.


3-1. 스택 프레임 (Stack Frame, Activation Record)

앞선 포스팅에서 다뤘던 스택 프레임과 동일한 것이다. 지역 변수가 할당되는 곳으로, 새로운 중괄호 블록을 마주칠 때마다 새로운 스택 프레임이 메모리의 런타임 스택이라는 영역에 쌓인다고 하였다. 함수가 호출될 때도 마찬가지이다. 함수가 호출되면 그 함수를 위한 스택 프레임이 런타임 스택에 푸시(Push)되고, 함수가 실행을 마치고 리턴할 때는 해당 스택 프레임을 팝(Pop)하게 된다. 해당 스택 프레임에는 그 함수를 위한 몇 가지 정보들이 저장된다. 우선 가장 먼저 떠올릴 수 있는 건 해당 함수 내에서 선언되는 지역 변수들이다. 함수의 몸체도 중괄호로 둘러싸인 하나의 블록 영역이기 때문이다. 그 이외에도 매개변수로 전달되는 인자들, 반환되는 값 등등이 저장된다.

 

또한 R5는 현재 스택 프레임, 즉 현재 실행 중인 함수의 스택 프레임의 밑바닥 주소를 저장하고 있기 때문에, 함수가 호출되어 새로운 스택 프레임이 런타임 스택에 쌓이면 R5의 값도 바뀌게 된다.

 

런타임 스택에 스택 프레임이 쌓이는 구조를 그림으로 보이면 다음과 같다. 여기서 하나 의문이 생길 수 있는데, R5가 왜 현재 스택 프레임의 정확히 밑바닥이 아닌 약간 위쪽의 주소를 가리키는가이다. 그것은 바로 R5가 현재 스택 프레임에서 지역 변수를 저장하는 부분의 밑바닥 주소를 가리키기 때문이다. 앞선 포스팅에서는 지역 변수만을 위한 스택 프레임을 상정했기 때문에 스택 프레임의 밑바닥 주소를 가리킨다고 설명해도 문제가 없었다. 하지만 함수의 스택 프레임에는 지역 변수뿐 아니라 매개변수로 전달되는 인자나 반환 값 등 다른 정보들도 저장되기 때문에 R5의 의미를 조금 더 정확히 짚고 넘어갈 필요가 있는 것이다.

 


3-2. 스택 프레임에 쌓이는 정보

그렇다면 구체적으로 함수의 스택 프레임에는 어떤 정보들이 저장될까? 결론부터 얘기하자면 총 다섯 가지의 정보가 저장된다. 스택 프레임에 먼저 쌓이는 순서대로 차례대로 설명하겠다. 첫째, 매개변수로 전달되는 인자들(Arguments)이다. 둘째, 함수의 반환 값(Return Value)이다. 셋째, 함수 리턴 후 돌아가야 하는 복귀 주소(Return Address)이다. 넷째, 이전 스택 프레임의 R5 값이다. 다섯째, 해당 함수 내에서 선언되는 지역 변수들(Local Variables)이다. 그림으로 살펴보면 다음과 같다.

 

 

3-2-1. 매개변수로 전달되는 인자들 (Arguments)

매개변수로 넘어가는 인자들 저장하는 곳이다. 여기서 주목할 만한 점은 인자들이 선언되어 있는 순서와 반대의 순서로 스택 프레임에 쌓인다는 점이다. 이는 가변 인자(Variable Argument)의 개념과 밀접한 관련이 있는데, 본 포스팅에선 다루지 않을 것이니 관심 있는 분은 직접 한 번 검색해보는 것을 권장한다.

 

3-2-2. 반환 값 (Return Value)

함수에 의해 반환되는 값이 저장될 곳이다. 반환 값이 없는 함수라 할지라도 구현의 일관성을 위해 반환 값을 위한 공간이 똑같이 할당된다는 것을 기억하자.

 

3-2-3. 복귀 주소 (Return Address)

Caller(새로운 함수를 호출한 기존 함수)의 다음 명령어 주소, 즉 돌아가야 하는 주소를 저장하는 곳이다. 뒤에서 살펴보겠지만 함수의 호출은 어셈블리어 수준에서(LC-3 기준) JSR 명령어에 의해 구현되는데, JSR 명령어는 그 자체로 복귀 주소를 R7에 저장한다. 하지만 서브 루틴 내에서 또 다른 서브 루틴을 호출할 수 있다는 가정 하에(이 경우 R7을 특별히 백업하고 복원하는 코드가 해당 서브 루틴에 없다면 돌아갈 주소를 잃게 됨), 함수 호출은 내부적으로 R7에 저장된 복귀 주소를 호출된 함수의 스택 프레임에 안전하게 저장하는 방식으로 구현이 된다.

 

3-2-4. 이전 스택 프레임의 R5 값 (Dynamic Link)

말 그대로 이전 스택 프레임, 즉 Caller에 해당하는 스택 프레임의 R5 값을 백업해두는 곳이다. 나중에 여기에 저장한 R5 값을 복원하면 그것은 곧 현재 실행 중이었던 함수에 해당하는 스택 프레임을 팝 하는 셈이 되는 것이다.

 

3-2-5. 지역 변수들 (Local Variables)

해당 함수가 내부적으로 선언한 지역 변수들이 할당되는 곳이다.

 

참고로 이 중에서 그림 상에 빨간색으로 표시된 반환 값, 복귀 주소, 이전 스택 프레임의 R5 값은 묶어서 북키핑(Bookkeeping) 정보라고도 부른다. 매개변수로 전달되는 인자들과 지역 변수들이 아닌 그 외의 정보들을 뜻하는 거라고 생각하면 될 것이다.


3-3. 인자와 지역 변수 접근을 위한 심볼 테이블

앞선 포스팅에서 살펴봤듯이, 컴파일러가 지역 변수를 접근하기 위한 기계어 코드를 만들어 내기 위해서는 각 지역 변수의 스택 프레임 내 오프셋 정보를 파악해야 한다. 매개변수로 전달되는 인자도 마찬가지이다. 컴파일러는 함수의 호출 문을 분석하면서 그 함수의 인자들과 지역 변수들을 위한 심볼 테이블을 구성하게 된다. 위에서 살펴본 예시 함수의 심볼 테이블은 다음과 같이 구성될 것이다.

 


3-4. 함수 호출의 구현

함수의 스택 프레임에 저장되는 정보들에 대해 알아보았으니, 이제는 본격적으로 함수 호출이 어떻게 어셈블리어 수준에서 구현되는지 알아볼 차례이다. 함수 호출은 내부적으로 다음과 같은 순서로 구현된다. Caller는 새로운 함수를 호출하는 기존 함수, Callee는 호출되는 함수를 의미한다.

 

주체 동작
Caller ① 매개변수로 전달되는 인자들을 위한 공간을 할당하고, 그 값들을 저장
② JSR 명령어를 이용하여 해당 서브 루틴을 호출
Callee ③ 반환값을 위한 공간을 할당하고, 현재 R7의 값과 R5의 값을 차례로 푸쉬
④ 지역 변수들을 위한 공간을 할당
⑤ 함수 코드 실행
⑥ (반환값이 있다면) 반환값을 위해 할당했던 공간에 반환값을 저장
⑦ 지역 변수들, 이전 스택 프레임의 R5 값, 복귀 주소에 해당하는 R7 값을 차례로 팝
⑧ RET(= JMP R7) 명령어를 이용하여 리턴
Caller ⑨ 반환값과 인자들을 차례로 팝
⑩ 이후 코드 실행

3-5. 함수 호출 문 번역 예시 (Example)

 

위의 C 언어 코드에서 박스 친 부분의 함수 호출 문을 LC-3 기준 어셈블리어로 번역하면 다음과 같다.